Add Translate Method to Vertex - Demo 05¶
Purpose¶
Restructure the code towards the model view projection pipeline.
Transforming vertices, such as translating, is one of the core concept of computer graphics.
How to Execute¶
On Linux or on MacOS, in a shell, type “python src/demo05/demo.py”. On Windows, in a command prompt, type “python src\demo05\demo.py”.
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 |
Translation¶
Dealing with the two Paddles the way we did before is not ideal. Both Paddles have the same size, although they are placed in different spots of the screen. We should be able to a set of vertices for the Paddle, relative to the paddle’s center, that is independent of its placement in NDC.
Rather than using values for each vertex relative to NDC, in the Paddle data structure, each vertex will be an offset from the center of the Paddle. The center of the paddle will be considered x=0, y=0. Before rendering, each Paddle’s vertices will need to be translated to its center relative to NDC.
All methods on vertices will be returning new vertices, rather than mutating the instance variables. The author does this on purpose to enable method-chaining the Python methods, which will be useful later on.
Method-chaining is the equivalent of function composition in math.
Code¶
Data Structures¶
106@dataclass
107class Vertex:
108 x: float
109 y: float
110
111 def translate(self: Vertex, rhs: Vertex) -> Vertex:
112 return Vertex(x=self.x + rhs.x, y=self.y + rhs.y)
113
114
We added a translate method to the Vertex class. Given a translation amount, the vertex will be shifted by that amount. This is a primitive that we will be using to transform from one space to another.
If the reader wishes to use the data structures to test them out, import them and try the methods
>>> import src.demo05.demo as demo
>>> a = demo.Vertex(x=1,y=2)
>>> a.translate(demo.Vertex(x=3,y=4))
Vertex(x=4, y=6)
Note the use of “keyword arguments”. Without using keyword arguments, the code might look like this:
>>> import src.demo05.demo as demo
>>> a = demo.Vertex(1,2)
>>> a.translate(demo.Vertex(x=3,y=4))
Vertex(x=4, y=6)
Keyword arguments allow the reader to understand the purpose of the parameters are, at the call-site of the function.
119@dataclass
120class Paddle:
121 vertices: list[Vertex]
122 r: float
123 g: float
124 b: float
125 position: Vertex
126
127
Add a position instance variable to the Paddle class. This position is the center of the paddle, defined relative to NDC. The vertices of the paddle will be defined relative to the center of the paddle.
Instantiation of the Paddles¶
131paddle1: Paddle = Paddle(
132 vertices=[
133 Vertex(x=-0.1, y=-0.3),
134 Vertex(x=0.1, y=-0.3),
135 Vertex(x=0.1, y=0.3),
136 Vertex(x=-0.1, y=0.3),
137 ],
138 r=0.578123,
139 g=0.0,
140 b=1.0,
141 position=Vertex(-0.9, 0.0),
142)
143
144paddle2: Paddle = Paddle(
145 vertices=[
146 Vertex(-0.1, -0.3),
147 Vertex(0.1, -0.3),
148 Vertex(0.1, 0.3),
149 Vertex(-0.1, 0.3),
150 ],
151 r=1.0,
152 g=1.0,
153 b=0.0,
154 position=Vertex(0.9, 0.0),
155)
The vertices are now defined as relative distances from the center of the paddle. The centers of each paddle are placed in positions relative to NDC that preserve the positions of the paddles, as they were in the previous demo.
Handling User Input¶
160def handle_movement_of_paddles() -> None:
161 global paddle1, paddle2
162
163 if glfw.get_key(window, glfw.KEY_S) == glfw.PRESS:
164 paddle1.position.y -= 0.1
165 if glfw.get_key(window, glfw.KEY_W) == glfw.PRESS:
166 paddle1.position.y += 0.1
167 if glfw.get_key(window, glfw.KEY_K) == glfw.PRESS:
168 paddle2.position.y -= 0.1
169 if glfw.get_key(window, glfw.KEY_I) == glfw.PRESS:
170 paddle2.position.y += 0.1
171
172
We put the transformation on the center of the paddle, instead of directly on each vertex. This is because the vertices are defined relative to the center of the paddle.
The Event Loop¶
180while not glfw.window_should_close(window):
181 while (
182 glfw.get_time() < time_at_beginning_of_previous_frame + 1.0 / TARGET_FRAMERATE
183 ):
184 pass
185
186 time_at_beginning_of_previous_frame = glfw.get_time()
187
188 glfw.poll_events()
189
190 width, height = glfw.get_framebuffer_size(window)
191 glViewport(0, 0, width, height)
192 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
193
194 draw_in_square_viewport()
195 handle_movement_of_paddles()
199 glColor3f(paddle1.r, paddle1.g, paddle1.b)
200
201 glBegin(GL_QUADS)
202 for paddle1_vertex_in_model_space in paddle1.vertices:
203 paddle1_vertex_in_ndc_space: Vertex = paddle1_vertex_in_model_space.translate(
204 paddle1.position
205 )
206 glVertex2f(paddle1_vertex_in_ndc_space.x, paddle1_vertex_in_ndc_space.y)
207 glEnd()
Here each of paddle 1’s vertices, which are in their “model-space”, are converted to NDC by calling the translate method on the vertex. This function corresponds to the Cayley graph below, the function from Paddle 1 space to NDC.
211 glColor3f(paddle2.r, paddle2.g, paddle2.b)
212
213 glBegin(GL_QUADS)
214 for paddle2_vertex_model_space in paddle2.vertices:
215 paddle2_vertex_ndc_space: Vertex = paddle2_vertex_model_space.translate(
216 paddle2.position
217 )
218 glVertex2f(paddle2_vertex_ndc_space.x, paddle2_vertex_ndc_space.y)
219 glEnd()
The only part of the diagram that we need to think about right now is the function that converts from paddle1’s space to NDC, and from paddle2’s space to NDC.
These functions in the Python code are the translation of the paddle’s center (i.e. paddle1.position) by the vertex’s offset from the center.
N.B. In the code, I name the vertices by their space. I.e. “modelSpace” instead of “vertex_relative_to_modelspace”. I do this to emphasize that you should view the transformation as happening to the “graph paper”, instead of to each of the points. This will be explained more clearly later.