Matrix Stacks - Demo 19

Purpose

Replace lambda stacks with OpenGL 2.1 matrix stacks, provided by the driver for your graphics card. This is how preshader opengl worked.

The function stack allowed us to aggregate the entirety of the transformations from modelspace to NDC, by creating a context of transformations, and a function to do the conversion. We then needed to push or pop functions from the stack, depending on what space transition we were currently dealing with.

A given matrix in OpenGL2.1 is the equivalent of a function stack; given one matrix, it can perform a sequence of transformations from one space to another, with one matrix multiplication.

OpenGL 2.1 deals with two different types of matrices: 1) the projection matrix, which effectively is the function from camera space to NDC (clip space) and 2) The model-view matrix, which deals with the transformations from model space to camera space.

Given that a matrix can perform a sequence of transformations across multiple spaces in the Cayley graph, it may appear that we no longer need any notion of a stack, as we had in the function stacks. But that is not true. For the Model-View matrix, we still need a stack of matrices, so that we can return to a previous transformation sequence. For instance, if we are at world space, the Model-View matrix at the top of the matrix stack will convert from world space to camera space. But we need to draw two relative child spaces to world space, paddle 1 space and paddle 2 space. So before we do the transformations to paddle 1 space, we “push” a copy of the current model-view matrix to the top of the model-view matrix stack, so that after we draw paddle 1 and the square, we can “pop” that matrix off, leaving us the matrix at the top of the model-view matrix stack that represents world space, so that we can then begin to transform to paddle 2 space.

The concepts behind the function stack, in which the first function added to the stack is the last function applied, hold true for matrices as well. But matrices are a much more efficient representation computationally than the function stack, and instead of adding fns and later having to remove them, we can save onto the current frame of reference with a “glPushStack”, and restore the saved state by “glPopStack”.

Use glPushMatrix and glPopMatrix to save/restore a local coordinate system, that way a tree of objects can be drawn without one child destroying the relative coordinate system of the parent node.

In mvpVisualization/pushmatrix/pushmatrix.py, the grayed out coordinate system is one that has been pushed onto the stack, and it regains its color when it is reactivated by “glPopMatrix”

How to Execute

On Linux or on MacOS, in a shell, type “python src/demo19/demo.py”. On Windows, in a command prompt, type “python src\demo19\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

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

First thing to note is that we are now using OpenGL 2.1’s official transformation procedures, and in the projection transformation, they flip the z axis, making it a left hand coordinate system. The reason for this is long, and I have begun discusses in the “Standard Perspective Matrix” section, but it is an incomplete section for now. But for now, all it means that we have to change how the depth test will be configured, as after the projection transformation, the far z value is 1.0, and the near z value is -1

The clear depth that is set for each fragment each frame is now 1.0, and the test for a given fragment to overwrite the color in the color buffer is changed to be less than or equal to.

Code

src/demo19/demo.py
84glEnable(GL_DEPTH_TEST)
85glClearDepth(1.0)
86glDepthFunc(GL_LEQUAL)

The Event Loop

Set the model, view, and projection matrix to the identity matrix. This just means that the functions (currently) will not transform data. In uni-variate terms, f(x) = x

src/demo19/demo.py
252    glMatrixMode(GL_PROJECTION)
253    glLoadIdentity()
254    glMatrixMode(GL_MODELVIEW)
255    glLoadIdentity()

change the projection matrix to convert the frustum to clip space set the projection matrix to be perspective. Since the viewport is always square, set the aspect ratio to be 1.0. We are now going to clip space instead of to NDC, which we be discussed in the next chapter.

Demo 11

Turn our NDC into Clip Space

src/demo19/demo.py
260    glMatrixMode(GL_PROJECTION)
261    gluPerspective(
262        45.0,  # field_of_view
263        1.0,  # aspect_ration
264        0.1,  # near_z
265        1000.0,  # far_z
266    )

“glMatrixMode” tells the computer which matrix stack should be the active one, against which subsequent matrix operations which affect. In this case, we set the current matrix stack to be the projection one. We then call “gluPerspective” to set the projection transformation, which we covered in previous sections, and we will ignore the implementation of it; it is now a black box.

Now onto camera space!

The camera’s position could be described relative to world space by the following sequence of transformations.

# glTranslate(camera.x, camera.y, camera.z)
# glRotatef(math.degrees(camera.rot_y), 0.0, 1.0, 0.0)
# glRotatef(math.degrees(camera.rot_x), 1.0, 0.0, 0.0)

Therefore, to take the object’s world space coordinates and transform them into camera space, we need to do the inverse operations to the view stack.

src/demo19/demo.py
271    glMatrixMode(GL_MODELVIEW)
272
273    glRotatef(math.degrees(-camera.rot_x), 1.0, 0.0, 0.0)
274    glRotatef(math.degrees(-camera.rot_y), 0.0, 1.0, 0.0)
275    glTranslate(-camera.x, -camera.y, -camera.z)

First thing we did was set the current matrix to be the model-view matrix, instead of the projection matrix. Most of our work will be with the model-view matrix, as looking at the Cayley graph, there’s only one function from camera space to NDC. N.B., OpenGL 2.1 transformations use degrees, not radians, so we need to convert to degrees.

Unlike in the lambda stack demo, in which a new function was added to the top of the stack, without modifying any functions below it on the stack, with OpenGL matrices, each transformation, such as translate, rotate, and scale, actually destructively modifies the matrix at the top of the stack, as matrices can be premultiplied together for efficiency.

In linear algebra terms, the matrix multiplication takes place, but then the resulting values of the matrix replace the values of the matrix at the top of the stack.

|a b|     |e f|         |ae+bg  af+bh|
|c d|  *  |g h|  =      |ce+dg  cf+dh|

This means that rotate_x, rotate_y, translate, etc are destructive operations to the matrix on the top of the stack. Instead of creating new matrix to the top of the stack of matrices, these operations aggregate the transformations, but add no new matrices to the stack, and as such are destructive operations to the current matrix.

But many times we need to hold onto a transformation stack (matrix), so that we can do something else now, and return to this context later, so we have a stack composed of matrices.

This is what glPushMatrix, and glPopMatrix do.

“PushMatrix” describes what the function does, but its purpose is to save onto the current coordinate system for later drawing modelspace data.

The model-view matrix stack is currently the transformation from world space into camera space. Since we now have to draw paddle 1, the square, and paddle 2, save onto the current model-view stack, to hold onto world space, so that after we draw paddle 1 and the square, we can restore the world space, so that paddle 2 can be drawn relative to it.

src/demo19/demo.py
288    glPushMatrix()

draw paddle 1

Unlike in previous demos before the lambda stack, because the transformations are now on a stack, the functions on the model stack can be read forwards, where each operation translates/rotates/scales the current space.

glVertex data is specified in modelspace coordinates, but since we loaded the projection matrix and the modelview matrix into OpenGL, glVertex3f will apply those transformations for us [1]!

src/demo19/demo.py
292    glColor3f(paddle1.r, paddle1.g, paddle1.b)
293
294    glTranslate(
295        paddle1.position[0],
296        paddle1.position[1],
297        0.0,
298    )
299    glRotatef(math.degrees(paddle1.rotation), 0.0, 0.0, 1.0)
300
301    glBegin(GL_QUADS)
302    for paddle1_vertex_in_model_space in paddle1.vertices:
303        glVertex3f(
304            paddle1_vertex_in_model_space[0],
305            paddle1_vertex_in_model_space[1],
306            paddle1_vertex_in_model_space[2],
307        )
308    glEnd()

draw the square relative to paddle 1

Since the modelstack is already in paddle1’s space just add the transformations relative to it before paddle 2 is drawn, we need to remove the square’s 3 model_space transformations

src/demo19/demo.py
314    # draw the square
315    # given that no nodes are defined relative to the square, we do not need
316    # to push a matrix.  Here we will do so anyway, just to clarify what is
317    # happening
318    glPushMatrix()
319    # the current model matrix will be copied and then the copy will be
320    # pushed onto the model stack
321    glColor3f(0.0, 0.0, 1.0)
322
323    # these functions change the current model matrix
324    glTranslate(0.0, 0.0, -1.0)
325    glRotatef(math.degrees(rotation_around_paddle1), 0.0, 0.0, 1.0)
326    glTranslate(2.0, 0.0, 0.0)
327    glRotatef(math.degrees(square_rotation), 0.0, 0.0, 1.0)
328
329    glBegin(GL_QUADS)
330    for model_space in square_vertices:
331        glVertex3f(model_space[0], model_space[1], model_space[2])
332    glEnd()
333    glPopMatrix()
334    # the mode matrix that was on the model stack before the square
335    # was drawn will be restored
336    glPopMatrix()

draw paddle 2

No need to push matrix here, as this is the last object that we are drawing, and upon the next iteration of the event loop, all 3 matrices will be reset to the identity

src/demo19/demo.py
340    # draw paddle 2.  Nothing is defined relative to paddle to, so we don't
341    # need to push matrix, and on the next iteration of the event loop,
342    # all matrices will be cleared to identity, so who cares if we
343    # mutate the values for now.
344    glColor3f(paddle2.r, paddle2.g, paddle2.b)
345
346    glTranslate(
347        paddle2.position[0],
348        paddle2.position[1],
349        0.0,
350    )
351    glRotatef(math.degrees(paddle2.rotation), 0.0, 0.0, 1.0)
352
353    glBegin(GL_QUADS)
354    for paddle2_vertex_model_space in paddle2.vertices:
355        glVertex3f(
356            paddle2_vertex_model_space[0],
357            paddle2_vertex_model_space[1],
358            paddle2_vertex_model_space[2],
359        )
360    glEnd()