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¶

Frustum 1¶

Frustum 2¶

Frustum 3¶

Frustum 4¶

Frustum 5¶

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¶
------------------------------- 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)
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
29@dataclasses.dataclass
30class Vector3D(mu2d.Vector2D):
31 z: float #: The z-component of the 3D Vector
...
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)
203while not glfw.window_should_close(window):
...
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()