Previously, we setup a simple Python script that displayed a static red triangle using OpenGL. This is great and all, but boring. Let's add the model-view-projection matrix so we can move this triangle around the screen. To make it clear what is going on, I will do all matrix operations directly in the script using Numpy.

The Model-View-Projection Matrix

Following the third tutorial on opengl-tutorial.org and some math found here, it's simple enough to write a function to create the perspective matrix using Numpy.

def perspective(fov, aspect, near, far):
    n, f = near, far
    t = np.tan((fov * np.pi / 180) / 2) * near
    b = - t
    r = t * aspect
    l = b * aspect
    assert abs(n - f) > 0
    return np.array((
        ((2*n)/(r-l),           0,           0,  0),
        (          0, (2*n)/(t-b),           0,  0),
        ((r+l)/(r-l), (t+b)/(t-b), (f+n)/(n-f), -1),
        (          0,           0, 2*f*n/(n-f),  0)))

along with the "look-at" view matrix:

def normalized(v):
    norm = linalg.norm(v)
    return v / norm if norm > 0 else v
    
def look_at(eye, target, up):
    zax = normalized(eye - target)
    xax = normalized(np.cross(up, zax))
    yax = np.cross(zax, xax)
    x = - xax.dot(eye)
    y = - yax.dot(eye)
    z = - zax.dot(eye)
    return np.array(((xax[0], yax[0], zax[0], 0),
                     (xax[1], yax[1], zax[1], 0),
                     (xax[2], yax[2], zax[2], 0),
                     (     x,      y,      z, 1)))

The position of the model will not change for now, but in order to be explicit about that we'll use the identity matrix in our calculations. The full model-view-projection matrix is then created:

def create_mvp(program_id, width, height):
    fov, near, far = 45, 0.1, 100
    eye = np.array((4,3,3))
    target, up = np.array((0,0,0)), np.array((0,1,0))
    projection = perspective(fov, width / height, near, far)
    view = look_at(eye, target, up)
    model = np.identity(4)
    mvp = model @ view @ projection
    matrix_id = gl.glGetUniformLocation(program_id, 'MVP')
    return matrix_id, mvp.astype(np.float32)

Uniform Variables

Uniform variables are static values pushed on the graphics card. They do not change over the execution of a shader program - that is, they are independent of the triangle or fragment the shader is working on. In this simple example, there is only one MVP matrix and we could set it outside the main loop, but we'll quickly want to change it within the loop and so we will set the uniform matrix on each pass.

def main_loop(window, mvp_matrix_id, mvp):
    while (
        glfw.get_key(window, glfw.KEY_ESCAPE) != glfw.PRESS and
        not glfw.window_should_close(window)
    ):
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
        # Set the view matrix
        gl.glUniformMatrix4fv(mvp_matrix_id, 1, False, mvp)
        # Draw the triangle
        gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
        glfw.swap_buffers(window)
        glfw.poll_events()

Finally, in the main program, we have to pass in the MVP matrix to the main loop:

if __name__ == '__main__':
    width, height = 500, 400
    with create_main_window(width, height) as window:
        with create_vertex_buffer():
            with load_shaders() as prog_id:
                mvp_matrix_id, mvp = create_mvp(prog_id, width, height)
                main_loop(window, mvp_matrix_id, mvp)

And now our triangle is turned and moved around - or is it that we've turned and moved around? We'll worry about that later perhaps.

tut-3-matrices

The full script for this example can be found on gitlab.com/metamost/learning-opengl-with-python.