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
65@dataclasses.dataclass
66class Vector3D(mu2d.Vector2D):
67    z: float  #: The z-component of the 3D Vector
...
src/modelviewprojection/demo18.py
203def perspective(
204    field_of_view: float, aspect_ratio: float, near_z: float, far_z: float
205) -> mu.InvertibleFunction:
206    # field_of_view, dataclasses.field of view, is angle of y
207    # aspect_ratio is x_width / y_width
208
209    top: float = -near_z * math.tan(math.radians(field_of_view) / 2.0)
210    right: float = top * aspect_ratio
211
212    fn = ortho(
213        left=-right,
214        right=right,
215        bottom=-top,
216        top=top,
217        near=near_z,
218        far=far_z,
219    )
220
221    def f(vector: mu.Vector) -> mu.Vector:
222        assert isinstance(vector, Vector3D)
223        s1d: mu.InvertibleFunction = mu.uniform_scale(near_z / vector.z)
224        x_component: mu.Vector = s1d(mu1d.Vector1D(x=vector.x))
225        y_component: mu.Vector = s1d(mu1d.Vector1D(x=vector.y))
226        assert isinstance(x_component, mu1d.Vector1D)
227        assert isinstance(y_component, mu1d.Vector1D)
228
229        rectangular_prism: Vector3D = Vector3D(
230            x=x_component.x, y=y_component.x, z=vector.z
231        )
232        return fn(rectangular_prism)
233
234    def f_inv(vector: mu.Vector) -> mu.Vector:
235        assert isinstance(vector, Vector3D)
236        rectangular_prism: mu.Vector = mu.inverse(fn)(vector)
237        assert isinstance(rectangular_prism, Vector3D)
238
239        inverse_s1d: mu.InvertibleFunction = mu.inverse(
240            mu.uniform_scale(near_z / vector.z)
241        )
242        x_component: mu.Vector = inverse_s1d(
243            mu1d.Vector1D(x=rectangular_prism.x)
244        )
245        y_component: mu.Vector = inverse_s1d(
246            mu1d.Vector1D(x=rectangular_prism.y)
247        )
248        assert isinstance(x_component, mu1d.Vector1D)
249        assert isinstance(y_component, mu1d.Vector1D)
250
251        return Vector3D(
252            x_component.x,
253            y_component.x,
254            rectangular_prism.z,
255        )
256
257    return mu.InvertibleFunction(f, f_inv)
src/modelviewprojection/demo18.py
202while not glfw.window_should_close(window):
...
src/modelviewprojection/demo18.py
244    # cameraspace to NDC
245    with mu3d.push_transformation(
246        mu3d.perspective(
247            field_of_view=45.0, aspect_ratio=1.0, near_z=-0.1, far_z=-1000.0
248        )
249    ):
250        # world space to camera space, which is mu3d.inverse of camera space to
251        # world space
252        with mu3d.push_transformation(
253            mu3d.inverse(
254                mu3d.compose(
255                    [
256                        mu3d.translate(camera.position_ws),
257                        mu3d.rotate_y(camera.rot_y),
258                        mu3d.rotate_x(camera.rot_x),
259                    ]
260                )
261            )
262        ):
263            # paddle 1 space to world space
264            with mu3d.push_transformation(
265                mu3d.compose(
266                    [
267                        mu3d.translate(paddle1.position),
268                        mu3d.rotate_z(paddle1.rotation),
269                    ]
270                )
271            ):
272                GL.glColor3f(*iter(paddle1.color))
273                GL.glBegin(GL.GL_QUADS)
274                for p1_v_ms in paddle1.vertices:
275                    paddle1_vector_ndc = mu3d.fn_stack.modelspace_to_ndc_fn()(
276                        p1_v_ms
277                    )
278                    GL.glVertex3f(
279                        paddle1_vector_ndc.x,
280                        paddle1_vector_ndc.y,
281                        paddle1_vector_ndc.z,
282                    )
283                GL.glEnd()