Sequenced Triangles

How to 3D

Chapter 2: Triangles and Transformations

Sequenced Triangles

Earlier you learned how to render line segments by enumerating pairs of vertices in a vertex buffer and then issuing a call to drawSequence(gl.LINES). Triangle sequences are rendered similarly. You enumerate triplets of vertices in a vertex buffer and then issue a call to drawSequence(gl.TRIANGLES). This code, which is adapted from the hello-cornflower renderer, draws a single triangle:

function initialize() {
  // ...
  const positions = [
    -0.5, -0.5, 0,  // 0
     0.5, -0.5, 0,  // 1
    -0.5,  0.5, 0,  // 2
  ];

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

function render() {
  // ...
  array.drawSequence(gl.TRIANGLES);
  // ...
}
function initialize() {
  // ...
  const positions = [
    -0.5, -0.5, 0,  // 0
     0.5, -0.5, 0,  // 1
    -0.5,  0.5, 0,  // 2
  ];

  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. The vertex and fragment shaders used to render lines can be used without modification to render triangles. The resulting triangle looks like this:

The same vertex buffer can be interpreted using the point and line primitives you learned about earlier.

Backface Culling

Notice how the vertices of the triangle wind around in a particular order. What is that order? What happens when you change the vertex positions so that the triangle winds around in the opposite order?

You should see the triangle disappear. Switch to a line loop to confirm that the triangle is still there. Unlike points and lines, triangles have two faces: a front face and a back face. The front face is considered to be the one around which the vertices appear in counterclockwise order from the viewer's perspective. To speed up rendering, graphics developers enable backface culling. Only triangles whose front faces are facing toward the viewer are rendered, and the back-facing triangles are discarded.

Backface culling is enabled with these two WebGL calls:

gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);

Backface culling is hard to appreciate when you are rendering only a few triangles in a 2D scene. Imagine, however, a rich 3D scene full of solid models, one of which is a simple four-sided building. The four sides of the building are rendered. How many faces might be front-facing? At most, two. The back-facing sides will be occluded by the front-facing sides, so rasterizing them would be wasteful.

We'll revisit culling later on when we bust into the third dimension. For the time being, just make sure your triangles are enumerated in counterclockwise order or leave culling disabled.

Multiple Triangles

Complex models are made by piecing triangles together. A simple but expensive way to render multiple triangles is to add additional triplets to your attributes, like this:

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);

  // ...
}
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.

The two triangles in this list do not touch, and enumerating them as separate triplets is reasonable. However, many models are made of triangles that stitch together to form a solid surface. Consider this wireframe of a triangulated circle:

All of the vertices in this circle are shared amongst several triangles. If you enumerate the triangles separately, you will be wasting storage with duplicate information. Each of the duplicated vertices will also be wastefully passed through the vertex shader, always producing the same result.

If 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 center position is listed first in the positions array. All remaining positions are the vertices on the fan's perimeter.

When you tell the graphics card to draw a triangle fan, it grabs the central vertex and runs it through the vertex shader exactly once. For the first triangle, it grabs the next two vertices from the vertex buffer, runs them through the vertex shader, and combines them with the central vertex. For each remaining triangle, it grabs the next vertex from the buffer, runs it through the vertex shader, and combines it with the central vertex and a vertex from the preceding triangle. No space is wasted on redundant vertices, and the vertex shader runs just once per vertex.

A triangle fan is another geometric primitive supported by WebGL and is drawn just as you draw points, lines, and triangles. You load up a vertex buffer with the vertices and then draw it using drawSequence(gl.TRIANGLE_FAN). This code draws a four-pointed star:

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);
  // ...
}
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);
  // ...
}

This renderer displays the star using this code but lets you change which primitive is used to interpret the vertex buffer:

Triangle fans in WebGL are not implicitly closed to form a radial shape. That is, no triangle is automatically generated between the last vertex and vertices 0 and 1. To close this star, vertex 9 is explicitly added as a duplicate of vertex 1. This adds the triangle (0, 9, 8) into the fan. One duplicated vertex is a small redundancy, but it can be eliminated with indexed geometry.

← TrianglesIndexed Triangles →