3D Perspective - Demo 18

Objective

Implement a perspective projection so that objects further away are smaller than the would be if they were close by

Demo 17

Demo 17

Frustum 1

Frustum 1

Frustum 2

Frustum 2

Frustum 3

Frustum 3

Frustum 4

Frustum 4

Frustum 5

Frustum 5

Frustum 6

Frustum 6

How to Execute

Load src/modelviewprojection/demo18.py in Spyder and hit the play button.

Move the Paddles using the Keyboard

Keyboard Input

Action

w

Move Left Paddle Up

s

Move Left Paddle Down

k

Move Right Paddle Down

i

Move Right Paddle Up

d

Increase Left Paddle’s Rotation

a

Decrease Left Paddle’s Rotation

l

Increase Right Paddle’s Rotation

j

Decrease Right Paddle’s Rotation

UP

Move the camera up, moving the objects down

DOWN

Move the camera down, moving the objects up

LEFT

Move the camera left, moving the objects right

RIGHT

Move the camera right, moving the objects left

q

Rotate the square around its center

e

Rotate the square around paddle 1’s center

Description

Frustum
       ------------------------------- far z
       \              |              /
        \             |             /
         \ (x,z) *----|(0,z)       /
          \      |    |           /
           \     |    |          /
            \    |    |         /
             \   |    |        /
              \  |    |       /
               \ |    |      /
                \---*-------- near z
                 |    |    /
                 |\   |   /
                 | \  |  /
                 |  \ | /
                 |   \|/
-----------------*----*-(0,0)-------------------
                (x,0)

If we draw a straight line between (x,z) and (0,0), we will have a right triangle with vertices (0,0), (0,z), and (x,z).

There also will be a similar right triangle with vertices (0,0), (0,nearZ), and whatever point the line above intersects the line at z=nearZ. Let’s call that point (projX, nearZ)

fov
because right angle and tan(theta) = tan(theta)
x / z = projX / nearZ
projX = x / z * nearZ

So use projX as the transformed x value, and keep the distance z.

                       ----------- far z
                       |          |
                       |          |
        (x / z * nearZ,z) *       |   non-linear -- the transformation of x depends on its z value
                       |          |
                       |          |
                       |          |
                       |          |
                       |          |
                       |          |
                       |          |
                       |          |
                       ------------ near z
                       \    |    /
                        \   |   /
                         \  |  /
                          \ | /
                           \|/
      ----------------------*-(0,0)-------------------

Top calculation based off of vertical field of view

                    /* top
                   / |
                  /  |
                 /   |
                /    |
               /     |
              /      |
             /       |
            /        |
           /         |
          /          |
         /           |
        /            |
 origin/             |
      / fov/2        |
z----*---------------*
     |\              |-nearZ
     | \             |
     |  \            |
     x   \           |
          \          |
           \         |
            \        |
             \       |
              \      |
               \     |
                \    |
                 \   |
                  \  |
                   \ |
                    \|

Right calculation based off of Top and aspect ration

                        top
-------------------------------------------------------
|                                                     |
|                         y                           |
|                         |                           |
|                         |                           |
|                         *----x                      | right =
|                            origin                   |   top * aspectRatio
|                                                     |
|                                                     |  aspect ratio should be the viewport's
|                                                     |    width/height, not necessarily the
-------------------------------------------------------    window's
src/modelviewprojection/demo18.py
29@dataclasses.dataclass
30class Vector3D(mu2d.Vector2D):
31    z: float  #: The z-component of the 3D Vector
...
src/modelviewprojection/demo18.py
221def perspective(
222    field_of_view: float, aspect_ratio: float, near_z: float, far_z: float
223) -> mathutils.InvertibleFunction[Vector3D]:
224    # field_of_view, dataclasses.field of view, is angle of y
225    # aspect_ratio is x_width / y_width
226
227    top: float = -near_z * math.tan(math.radians(field_of_view) / 2.0)
228    right: float = top * aspect_ratio
229
230    fn = ortho(
231        left=-right,
232        right=right,
233        bottom=-top,
234        top=top,
235        near=near_z,
236        far=far_z,
237    )
238
239    def f(vector: Vector3D) -> Vector3D:
240        s1d: mathutils.InvertibleFunction[mu1d.Vector1D] = (
241            mathutils.uniform_scale(near_z / vector.z)
242        )
243        rectangular_prism: Vector3D = Vector3D(
244            s1d(vector.x), s1d(vector.y), vector.z
245        )
246        return fn(rectangular_prism)
247
248    def f_inv(vector: Vector3D) -> Vector3D:
249        rectangular_prism: Vector3D = mathutils.inverse(fn)(vector)
250
251        mathutils.inverse_s1d: mathutils.InvertibleFunction[mu1d.Vector1D] = (
252            mathutils.inverse(mathutils.uniform_scale(near_z / vector.z))
253        )
254        return Vector3D(
255            mathutils.inverse_s1d(rectangular_prism.x),
256            mathutils.inverse_s1d(rectangular_prism.y),
257            rectangular_prism.z,
258        )
259
260    return mathutils.InvertibleFunction[Vector3D](f, f_inv)
src/modelviewprojection/demo18.py
203while not glfw.window_should_close(window):
...
src/modelviewprojection/demo18.py
245    # cameraspace to NDC
246    with mu3d.push_transformation(
247        mu3d.perspective(
248            field_of_view=45.0, aspect_ratio=1.0, near_z=-0.1, far_z=-1000.0
249        )
250    ):
251        # world space to camera space, which is mu.inverse of camera space to
252        # world space
253        with mu3d.push_transformation(
254            mu.inverse(
255                mu.compose(
256                    [
257                        mu.translate(camera.position_ws),
258                        mu3d.rotate_y(camera.rot_y),
259                        mu3d.rotate_x(camera.rot_x),
260                    ]
261                )
262            )
263        ):
264            # paddle 1 space to world space
265            with mu3d.push_transformation(
266                mu.compose(
267                    [
268                        mu.translate(paddle1.position),
269                        mu3d.rotate_z(paddle1.rotation),
270                    ]
271                )
272            ):
273                GL.glColor3f(*iter(paddle1.color))
274                GL.glBegin(GL.GL_QUADS)
275                for p1_v_ms in paddle1.vertices:
276                    paddle1_vector_ndc = mu3d.fn_stack.modelspace_to_ndc_fn()(
277                        p1_v_ms
278                    )
279                    GL.glVertex3f(
280                        paddle1_vector_ndc.x,
281                        paddle1_vector_ndc.y,
282                        paddle1_vector_ndc.z,
283                    )
284                GL.glEnd()