Rendering a cube

We already have all the pieces needed to render a 3D scene but we need first to do the maths.

Projection matrix

We need first to define what do we want to view, that is, we need to define a viewing volume such that any object within the volume (even partially) will be rendered while objects outside won’t. On the image below, the yellow and red spheres are within the volume while the green one is not and does not appear on the projection.

../_images/projection.png

There exist many different ways to project a 3D volume onto a 2D screen but we’ll only use the perspective projection (distant objects appear smaller) and the orthographic projection which is a parallel projection (distant objects have the same size as closer ones) as illustrated on the image above. Until now (previous section), we have been using implicitly an orthographic projection in the z=0 plane.

Note

In older versions of OpenGL, these matrices were available as glFrustum and glOrtho.

Depending on the projection we want, we will use one of the two projection matrices below:

Perspective matrix

../_images/frustum-matrix.png

Orthographic matrix

../_images/ortho-matrix.png

At this point, it is not necessary to understand how these matrices were built. Suffice it to say they are standard matrices in the 3D world. Both suppose the viewer (=camera) is located at position (0,0,0) and is looking in the direction (0,0,1).

There exists a second form of the perpective matrix that might be easier to manipulate. Instead of specifying the right/left/top/bottom planes, we’ll use field of view in the horizontal and vertical direction:

Perspective matrix

../_images/perspective-matrix.png

where fovy specifies the field of view angle, in degrees, in the y direction and aspect specifies the aspect ratio that determines the field of view in the x direction.

Model and view matrices

We are almost done with matrices. You may have guessed that the above matrix requires the viewing volume to be in the z direction. We could design our 3D scene such that all objects are withing this direction but it would not be very convenient. So instead, we’ll use a view matrix that will map the the world space to camera space. This is pretty much as if we were orienting the camera at a given position and look toward a given direction. In the meantime, we can further refine the whole pipeline by providing a model matrix that will maps the object’s local coordinate space into world space. For example, this wil be useful for rotating an object around its center. To sum up, we need:

  • Model matrix maps from an object’s local coordinate space into world space
  • View matrix maps from world space to camera space
  • Projection matrix maps from camera to screen space

Now, we can write out shaders:

vertex = """
uniform mat4   u_model;         // Model matrix
uniform mat4   u_view;          // View matrix
uniform mat4   u_projection;    // Projection matrix
attribute vec3 a_position;      // Vertex position
void main()
{
    gl_Position = u_projection * u_view * u_model * vec4(a_position,1.0);
} """

fragment = """
void main()
{
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
} """

Building a cube

We need to define what we mean by a cube since there is not such thing as as cube in OpenGL. A cube, when seen from the outside has 6 faces, each being a square. We just saw that to render a square, we need two triangles. So, 6 faces, each of them being made of 2 triangles, we need 12 triangles.

How many vertices? 12 triangles × 3 vertices per triangles = 36 vertices might be a reasonable answer. However, we can also notice that each vertex is part of 3 different faces actually. Instead we’ll use no more than 8 vertices and tell explicitly OpenGL what to draw with them:

V = np.zeros(8, [("a_position", np.float32, 3)])
V["a_position"] = [[ 1, 1, 1], [-1, 1, 1], [-1,-1, 1], [ 1,-1, 1],
                   [ 1,-1,-1], [ 1, 1,-1], [-1, 1,-1], [-1,-1,-1]]

These describe vertices of a cube cented on (0,0,0) that goes from (-1,-1,-1) to (+1,+1,+1). Then we compute (mentally) what are the triangles for each face, i.e. we describe triangles in terms of vertices index (relatively to the V array we just defined):

I = np.array([0,1,2, 0,2,3,  0,3,4, 0,4,5,  0,5,6, 0,6,1,
              1,6,7, 1,7,2,  7,4,3, 7,3,2,  4,7,6, 4,6,5], dtype=np.uint32)

We now need to upload these data to the GPU. Using gloo, the easiest way is to use a VertexBuffer for vertices data and an IndexBuffer for indices data:

V = V.view(gloo.VertexBuffer)
I = I.view(gloo.IndexBuffer)

cube = gloo.Program(vertex, fragment)
cube["a_position"] = V

We’ll use the indices buffer when rendering the cube.

Building matrices

Note

Note that the view matrix is a translation along z. We actually move away from the center while looking into the (positive) z direction.

All the common matrix operations can be found in the glumpy.glm module that defines ortho, frustum and perspective matrices as well as rotation, translation and scaling operations. We won’t say much more about these and you might want to read a book about geometry to understand how this work, especially when compositing rotation, translation and scaling (order is important):

view = np.eye(4,dtype=np.float32)
model = np.eye(4,dtype=np.float32)
projection = np.eye(4,dtype=np.float32)
glm.translate(view, 0,0,-5)
cube['u_model'] = model
cube['u_view'] = view
cube['u_projection'] = projection
phi, theta = 0,0

It is now important to update the projection matrix whenever the window is resized (because aspect ratio may have changed):

@window.event
def on_resize(width, height):
   ratio = width / float(height)
   cube['u_projection'] = glm.perspective(45.0, ratio, 2.0, 100.0)

Rendering

../_images/solid-cube.png

Rotating the cube means computing a model matrix such that the cube rotate around its center. We’ll do that in the draw function and rotate the cube around the z axis (theta), then around the y axis (phi):

@window.event
def on_draw(dt):
    global phi, theta
    window.clear()
    cube.draw(gl.GL_TRIANGLES, I)

    # Make cube rotate
    theta += 0.5 # degrees
    phi += 0.5 # degrees
    model = np.eye(4, dtype=np.float32)
    glm.rotate(model, theta, 0, 0, 1)
    glm.rotate(model, phi, 0, 1, 0)
    cube['u_model'] = model

We’re now alsmost ready to render the whole scene but we need first to modify the initialization a little bit to enable depth testing:

@window.event
def on_init():
    gl.glEnable(gl.GL_DEPTH_TEST)

This is needed because we’re now dealing with 3D, meaning some rendered triangles may be behind some others. OpenGL will take care of that provided we declared our context with a depth buffer which is the default in glumpy.

Complete source code is available on github.

But… But… But is’t ugly ! Yes, of course ! We have no color (but red), no texture and no light. What did you expect ?