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
32@dataclass
33class Vector3D:
34 x: float #: The x-component of the 3D Vector
35 y: float #: The y-component of the 3D Vector
36 z: float #: The z-component of the 3D Vector
37
38 def __add__(self, rhs: Vector3D) -> Vector3D:
39 return Vector3D(
40 x=(self.x + rhs.x), y=(self.y + rhs.y), z=(self.z + rhs.z)
41 )
42
43 def __sub__(self, rhs: Vector3D) -> Vector3D:
44 return Vector3D(
45 x=(self.x - rhs.x), y=(self.y - rhs.y), z=(self.z - rhs.z)
46 )
47
48 def __mul__(vector, scalar: float) -> Vector3D:
49 return Vector3D(
50 x=(vector.x * scalar), y=(vector.y * scalar), z=(vector.z * scalar)
51 )
52
53 def __rmul__(vector, scalar: float) -> Vector3D:
54 return vector * scalar
55
56 def __neg__(vector):
57 return -1.0 * vector
58
59
60def translate(translate_amount: Vector3D) -> InvertibleFunction:
61 def f(vector: Vector3D) -> Vector3D:
62 return vector + translate_amount
63
64 def f_inv(vector: Vector3D) -> Vector3D:
65 return vector - translate_amount
66
67 return InvertibleFunction(f, f_inv)
68
69
...
208def perspective(
209 field_of_view: float, aspect_ratio: float, near_z: float, far_z: float
210) -> Vector3D:
211 # field_of_view, field of view, is angle of y
212 # aspect_ratio is x_width / y_width
213
214 top: float = -near_z * math.tan(math.radians(field_of_view) / 2.0)
215 right: float = top * aspect_ratio
216
217 def f(vector: Vector3D) -> Vector3D:
218 scaled_x: float = vector.x / vector.z * near_z
219 scaled_y: float = vector.y / vector.z * near_z
220 rectangular_prism: Vector3D = Vector3D(scaled_x, scaled_y, vector.z)
221
222 fn = ortho(
223 left=-right,
224 right=right,
225 bottom=-top,
226 top=top,
227 near=near_z,
228 far=far_z,
229 )
230 return fn(rectangular_prism)
231
232 def f_inv(vector: Vector3D) -> Vector3D:
233 raise ValueError("Inverse_Inner Perspective not yet implement")
234
235 return InvertibleFunction(f, f_inv)
236
237
236while not glfw.window_should_close(window):
...
278 # cameraspace to NDC
279 with push_transformation(
280 perspective(
281 field_of_view=45.0, aspect_ratio=1.0, near_z=-0.1, far_z=-1000.0
282 )
283 ):
284 # world space to camera space, which is inverse of camera space to
285 # world space
286 with push_transformation(
287 inverse(
288 compose(
289 translate(camera.position_ws),
290 rotate_y(camera.rot_y),
291 rotate_x(camera.rot_x),
292 )
293 )
294 ):
295 # paddle 1 space to world space
296 with push_transformation(
297 compose(translate(paddle1.position), rotate_z(paddle1.rotation))
298 ):
299 glColor3f(*astuple(paddle1.color))
300 glBegin(GL_QUADS)
301 for p1_v_ms in paddle1.vertices:
302 paddle1_vector_ndc = fn_stack.modelspace_to_ndc_fn()(
303 p1_v_ms
304 )
305 glVertex3f(
306 paddle1_vector_ndc.x,
307 paddle1_vector_ndc.y,
308 paddle1_vector_ndc.z,
309 )
310 glEnd()