Adding Depth - Z axis Demo 14¶
Objective¶
Do the same stuff as the previous demo, but use 3D coordinates, where the negative z axis goes into the screen (because of the right hand rule). Positive z comes out of the monitor towards your face.
Things that this demo doesn’t end up doing correctly:
The blue square is always drawn, even when its z-coordinate in world space is less than the paddle’s. The solution will be z-buffering https://en.wikipedia.org/wiki/Z-buffering, and it is implemented in the next demo.

Demo 14¶

Camera Space¶

Camera Space¶
How to Execute¶
Load src/modelviewprojection/demo14.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¶
Vector data will now have an X, Y, and Z component.
Rotations around an angle in 3D space follow the right hand rule. Here’s a link to them in matrix form, which we have not yet covered.

With open palm, fingers on the x axis, rotating the fingers to y axis, means that the positive z axis is in the direction of the thumb. Positive Theta moves in the direction that your fingers did.
starting on the y axis, rotating to z axis, thumb is on the positive x axis.
starting on the z axis, rotating to x axis, thumb is on the positive y axis.
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
Rotate Z¶
Rotate Z is the same rotate that we’ve used so far, but doesn’t affect the z component at all.
110def rotate_z(angle_in_radians: float) -> Vector3D:
111 fn = rotate2D(angle_in_radians)
112
113 def f(vector: Vector3D) -> Vector3D:
114 xy_on_xy: Vector2D = fn(Vector2D(x=vector.x, y=vector.y))
115 return Vector3D(x=xy_on_xy.x, y=xy_on_xy.y, z=vector.z)
116
117 def f_inv(vector: Vector3D) -> Vector3D:
118 xy_on_xy: Vector2D = inverse(fn)(Vector2D(x=vector.x, y=vector.y))
119 return Vector3D(x=xy_on_xy.x, y=xy_on_xy.y, z=vector.z)
120
121 return InvertibleFunction(f, f_inv)
122
123
Rotate X¶
74def rotate_x(angle_in_radians: float) -> Vector3D:
75 fn = rotate2D(angle_in_radians)
76
77 def f(vector: Vector3D) -> Vector3D:
78 yz_on_xy: Vector2D = fn(Vector2D(x=vector.y, y=vector.z))
79 return Vector3D(x=vector.x, y=yz_on_xy.x, z=yz_on_xy.y)
80
81 def f_inv(vector: Vector3D) -> Vector3D:
82 yz_on_xy: Vector2D = inverse(fn)(Vector2D(x=vector.y, y=vector.z))
83 return Vector3D(x=vector.x, y=yz_on_xy.x, z=yz_on_xy.y)
84
85 return InvertibleFunction(f, f_inv)
86
87
Rotate Y¶
92def rotate_y(angle_in_radians: float) -> Vector3D:
93 fn = rotate2D(angle_in_radians)
94
95 def f(vector: Vector3D) -> Vector3D:
96 zx_on_xy: Vector2D = fn(Vector2D(x=vector.z, y=vector.x))
97 return Vector3D(x=zx_on_xy.y, y=vector.y, z=zx_on_xy.x)
98
99 def f_inv(vector: Vector3D) -> Vector3D:
100 zx_on_xy: Vector2D = inverse(fn)(Vector2D(x=vector.z, y=vector.x))
101 return Vector3D(x=zx_on_xy.y, y=vector.y, z=zx_on_xy.x)
102
103 return InvertibleFunction(f, f_inv)
104
105
Scale¶
128def uniform_scale(scalar: float) -> InvertibleFunction:
129 def f(vector: Vector3D) -> Vector3D:
130 return vector * scalar
131
132 def f_inv(vector: Vector3D) -> Vector3D:
133 if scalar == 0:
134 raise ValueError("Not invertible. Scaling factor cannot be zero.")
135
136 return vector / scalar
137
138 return InvertibleFunction(f, f_inv)
139
140
Code¶
The only new aspect of the code below is that the paddles have a z-coordinate of 0 in their modelspace.
122paddle1: Paddle = Paddle(
123 vertices=[
124 Vector3D(x=-1.0, y=-3.0, z=0.0),
125 Vector3D(x=1.0, y=-3.0, z=0.0),
126 Vector3D(x=1.0, y=3.0, z=0.0),
127 Vector3D(x=-1.0, y=3.0, z=0.0),
128 ],
129 color=Color3(r=0.578123, g=0.0, b=1.0),
130 position=Vector3D(x=-9.0, y=0.0, z=0.0),
131)
132
133paddle2: Paddle = Paddle(
134 vertices=[
135 Vector3D(x=-1.0, y=-3.0, z=0.0),
136 Vector3D(x=1.0, y=-3.0, z=0.0),
137 Vector3D(x=1.0, y=3.0, z=0.0),
138 Vector3D(x=-1.0, y=3.0, z=0.0),
139 ],
140 color=Color3(r=1.0, g=1.0, b=0.0),
141 position=Vector3D(x=9.0, y=0.0, z=0.0),
142)
The only new aspect of the square below is that the paddles have a z-coordinate of 0 in their modelspace. N.B that since we do a sequence transformations to the modelspace data to get to world-space coordinates, the X, Y, and Z coordinates are subject to be different.
147@dataclass
148class Camera:
149 position_ws: Vector3D = field(
150 default_factory=lambda: Vector3D(x=0.0, y=0.0, z=0.0)
151 )
152
153
154camera: Camera = Camera()
The camera now has a z-coordinate of 0 also.
158square: list[Vector3D] = [
159 Vector3D(x=-0.5, y=-0.5, z=0.0),
160 Vector3D(x=0.5, y=-0.5, z=0.0),
161 Vector3D(x=0.5, y=0.5, z=0.0),
162 Vector3D(x=-0.5, y=0.5, z=0.0),
163]
Event Loop¶
216while not glfw.window_should_close(window):
...
Draw Paddle 1
238 glColor3f(*astuple(paddle1.color))
239 glBegin(GL_QUADS)
240 for p1_v_ms in paddle1.vertices:
241 ms_to_ndc: InvertibleFunction[Vector3D] = compose(
242 # camera space to NDC
243 uniform_scale(1.0 / 10.0),
244 # world space to camera space
245 inverse(translate(camera.position_ws)),
246 # model space to world space
247 compose(translate(paddle1.position),
248 rotate_z(paddle1.rotation)),
249 )
250
251 paddle1_vector_ndc: Vector3D = ms_to_ndc(p1_v_ms)
252 glVertex3f(paddle1_vector_ndc.x,
253 paddle1_vector_ndc.y,
254 paddle1_vector_ndc.z)
255 glEnd()
The square should not be visible when hidden behind the paddle1, as we do a translate by -1. But in running the demo, you see that the square is always drawn over the paddle.
Draw the Square
259 # draw square
260 glColor3f(0.0, 0.0, 1.0)
261 glBegin(GL_QUADS)
262 for ms in square:
263 ms_to_ndc: InvertibleFunction[Vector3D] = compose(
264 # camera space to NDC
265 uniform_scale(1.0 / 10.0),
266 # world space to camera space
267 inverse(translate(camera.position_ws)),
268 # model space to world space
269 compose(translate(paddle1.position),
270 rotate_z(paddle1.rotation)),
271 # square space to paddle 1 space
272 compose(translate(Vector3D(x=0.0, y=0.0, z=-1.0)),
273 rotate_z(rotation_around_paddle1),
274 translate(Vector3D(x=2.0, y=0.0, z=0.0)),
275 rotate_z(square_rotation)))
276 square_vector_ndc: Vector3D = ms_to_ndc(ms)
277 glVertex3f(square_vector_ndc.x,
278 square_vector_ndc.y,
279 square_vector_ndc.z)
280 glEnd()
This is because without depth buffering, the object drawn last clobbers the color of any previously drawn object at the pixel. Try moving the square drawing code to the beginning, and you will see that the square can be hidden behind the paddle.
Draw Paddle 2
284 # draw paddle 2
285 glColor3f(*astuple(paddle2.color))
286 glBegin(GL_QUADS)
287 for p2_v_ms in paddle2.vertices:
288 ms_to_ndc: InvertibleFunction[Vector3D] = compose(
289 # camera space to NDC
290 uniform_scale(1.0 / 10.0),
291 # world space to camera space
292 inverse(translate(camera.position_ws)),
293 # model space to world space
294 compose(translate(paddle2.position),
295 rotate_z(paddle2.rotation)),
296 )
297
298 paddle2_vector_ndc: Vector3D = ms_to_ndc(p2_v_ms)
299 glVertex3f(paddle2_vector_ndc.x,
300 paddle2_vector_ndc.y,
301 paddle2_vector_ndc.z)
302 glEnd()
Added translate in 3D. Added scale in 3D. These are just like the 2D versions, just with the same process applied to the z axis.
They direction of the rotation is defined by the right hand rule.