Triangles

In which you learn how all of computer graphics pivots around the triangle and extend your tools for rendering points to this new geometric primitive.

Triangles as Atoms

Complex 3D scenes are made of nothing but triangles. Your GPU knows nothing of quadrilaterals, pentagons, hexagons, or any other polygon. It doesn't have to, because any complex shape or surface can be decomposed into a quilt of triangles. Consider these regular polygons:

A square is just two triangles, a pentagon is three triangles, and a hexagon is four:

Even complex 3D shapes are really just triangles. Consider this cylinder:

Cylinder Made of Triangles

Limiting renderers to use just triangles has several advantages. Triangles have been studied for centuries, and their mathematical properties are well-known and relatively simple compared to other polygons. A triangle is necessarily planar, whereas a polygon with four or more vertices may not be flat. GPUs have circuitry optimized for processing triangles.

One Triangle

Drawing one triangle is similar to drawing one point. There are two differences. First, your vertex attributes list must contain properties for three vertices. Second, your draw call must specify that gl.TRIANGLES are to be drawn instead of gl.POINTS.

The code to draw a single triangle might look like this:

function initialize() {
  const positions = [
    -0.5, -0.5, 0,
     0.5, -0.5, 0,
    -0.5,  0.5, 0,
  ];

  attributes = new VertexAttributes();
  attributes.addAttribute('position', 3, 3, positions);

  // ...
}

function render() {
  // ...

  array.drawSequence(gl.TRIANGLES);

  // ...
}

The addAttribute call indicates that there are three vertices, each with a 3-component position.

Back-face Culling

Try swapping the first and second vertices in the positions array. What do you notice?

You should see the triangle disappear. Graphics systems consider triangles to have two faces: a front-face and a back-face. Only triangles whose front-faces are toward the viewer are rendered. This is called back-face culling. A front-face is one whose vertices are listed in counterclockwise order from the viewer's perspective. In the code above, they are listed in counterclockwise order. When you swapped them, you put them in clockwise order.

Distinguishing between front-faces and back-faces probably seems silly at the moment. In general, however, you will be rendering solid models whose back-faces are always on the inside of a 3D model. If the GPU runs across a triangle who back-face is toward the viewer, then it must be on the far side of the model, and front-facing triangles on the near side will necessarily occlude it. Rendering the back-facing triangle would be a waste of computation.

You'll revisit culling later on when you bust into the third dimension. For the time being, just make sure your triangles are enumerated in counterclockwise order.

Multiple Triangles

To render complex surfaces, you need multiple vertices. There are several ways to organize collections of triangles. Some render faster than others. One simple but slow way is to just list additional triplets in your attributes:

function initialize() {
  const positions = [
    // Triangle 0
    -0.5, -0.5, 0,
     0.5, -0.5, 0,
    -0.5,  0.5, 0,

    // Triangle 1
     0.5,  0.5, 0,
     1.0,  0.5, 0,
     0.0,  1.0, 0,
  ];

  attributes = new VertexAttributes();
  attributes.addAttribute('position', 6, 3, positions);

  // ...
}

The addAttribute call indicates that there are six vertices.

Recall that the positions array is flat. There's no inherent grouping of vertices into shapes. Only when you call drawSequence does the GPU know how the vertices are meant to grouped. If you pass gl.POINTS, it will pull out one vertex at a time from the VBO and render a point at the vertex. If you pass gl.LINES, it will pull out two at a time and render a line between the vertices. If you pass gl.TRIANGLES, it will pull out three at a time and render a triangle between the vertices.

The grouping is a property of the draw call, not the VBO or VAO. In fact, you can render the same VAO multiple times with different geometric primitives. What draw calls would you issue to render triangles but also render points at the vertices?

Triangle Fans

A sequential list of triangles can be redundant. Consider the circular cap in the cylinder shown above. There's a vertex at the center of that cylinder, and its position will be enumerated in the positions array 32 times, once for each of the triangles on the cap. It will consume 32 times more VRAM than a single copy, and the vertex shader will be wastefully executed 32 times more than it needs to, always producing the same result.

When you need to render a shape that has a common central vertex that all its triangles share, you are better off using a triangle fan. In a triangle fan, the shared position is listed first in the positions array. All remaining positions are the vertices on the shape's perimeter.

Consider this triangle fan, which is drawn using gl.TRIANGLE_FAN:

function initialize() {
  const positions = [
     0.0,  0.0, 0, // Vertex 0
     1.0,  0.0, 0, // Vertex 1
     0.1,  0.1, 0, // Vertex 2
     0.0,  1.0, 0, // Vertex 3
    -0.1,  0.1, 0, // Vertex 4
    -1.0,  0.0, 0, // Vertex 5
    -0.1, -0.1, 0, // Vertex 6
     0.0, -1.0, 0, // Vertex 7
     0.1, -0.1, 0, // Vertex 8
     1.0,  0.0, 0, // Vertex 9
  ];

  attributes = new VertexAttributes();
  attributes.addAttribute('position', 10, 3, positions);

  // ...
}

function render() {
  // ...

  array.drawSequence(gl.TRIANGLE_FAN);

  // ...
}

On a piece of paper, draw the shape that you expect to be rendered. After you've drawn a prediction, make a copy of hello-orange, name it hello-triangles, and modify it to draw this fan.

When you tell the GPU to draw a triangle fan, it grabs the first vertex, which is at the center of the fan, and computes its properties. Then it grabs the remainder of the vertices from the VBO, two at a time. Each of these pairs is combined with the central vertex to make a complete triangle.

In this particular example, triangles are formed by connecting vertices in these triplets:

0 1 2
0 2 3
0 3 4
0 4 5
0 5 6
0 6 7
0 7 8
0 8 9

After you get the triangle fan working, switch your draw call in turn to gl.POINTS, gl.LINES, and gl.TRIANGLES. Try to make sense of what you are seeing.