Mesh Topology

Thus far, we have seen three different kinds of vertex attributes. We have used positions, colors, and now normals. Before we can continue, we need to discuss the ramifications of having three independent vertex attributes on our meshes.

A mesh's topology defines the connectedness between the vertices. However, each vertex attribute can have its own separate topology. How is this possible?

Consider a simple cube. It has 8 vertex positions. The topology between the positions of the cube is as follows:

Figure 9.12. Cube Position Topology

Cube Position Topology

The topology diagram has the 8 different positions, with each position connected to three neighbors. The connections represent the edges of the cube faces, and the area bounded by connections represent the faces themselves. So each face has four edges and four positions.

Now consider the topology of the normals of a cube. A cube has 6 faces, each of which has a distinct normal. The topology is a bit unorthodox:

Figure 9.13. Cube Normal Topology

Cube Normal Topology

The square points represent distinct normals, not positions in space. The connections between normals all loop back on themselves. That's because each vertex of each face uses the same normal. While the front face and the top face share two vertex positions in common, they share no vertex normals at all. Therefore, there is no topological relation between the front normal and the top normal. Each normal value is connected to itself four times, but it is connected to nothing else.

We have 8 positions and 6 normals. They each have a unique topology over the mesh. How do we turn this into something we can render?

If we knew nothing about OpenGL, this would be simple. We simply use the topologies to build the meshes. Each face is broken down into two triangles. We would have an array of 8 positions and 6 normals. And for each vertex we render, we would have a list of indices that fetch values from these two arrays.

The index list for two faces of the cube might look like this (using a C++-style multidimensional list):

{
  {0, 0}, {1, 0}, {2, 0}, {1, 0}, {3, 0}, {2, 0},
  {0, 1}, {4, 1}, {1, 1}, {4, 1}, {6, 1}, {1, 1},
}

The first index in each element of the list pulls from the position array, and the second index pulls from the normal array. This list explicitly specifies the relationship between faces and topology: the first face (composed of two triangles) contains 4 positions, but uses the same normal for all vertices. The second face shares two positions with the first, but no normals.

This is complicated in OpenGL because of one simple reason: we only get one index list. When we render with an index array, every attribute array uses the same index. Therefore, what we need to do is convert the above index list into a list of unique combinations of vertex attributes. So each pair of indices must refer to a unique index.

Consider the first face. It consists of 4 unique vertices; the index pairs {1, 0} and {2, 0} are repeated, since we must draw triangles. Since these pairs are repeats, we can collapse them; they will refer to the same index. However, the fact that each vertex uses the same normal must be ignored entirely. Therefore, the final index list we use for the first face is:

{ 0, 1, 2, 1, 3, 2 }

The attribute arrays for this one face contain 4 positions and 4 normals. But while the positions are all different, the normals must all be the same value. Even though they are stored in four separate locations. This seems like a waste of memory.

This gets worse once we move on to the next face. Because we can only collapse index pairs that are identical, absolutely none of the vertices in the second face share indices with the first face. The fact that we reuse two positions in the next face is irrelevant: we must have the following index list:

{ 4, 5, 6, 5, 7, 6 }

The attribute array for both faces contains 8 positions and 8 normals. Again, the normals for the second face are all duplicates of each other. And there are two positions that are duplicated. Topologically speaking, our cube vertex topology looks like the following:

Figure 9.14. Full Cube Topology

Full Cube Topology

Each face is entirely distinct topologically from the rest.

In the end, this gives us 24 positions and 24 normals in our arrays. There will only be 6 distinct normal values and 8 distinct position values in the array, but there will be 24 of each. So this represents a significant increase in our data size.

Do not be too concerned about the increase in data however. A cube, or any other faceted object, represents the worst-case scenario for this kind of conversion. Most actual meshes of smooth objects have much more interconnected topologies.

Mesh topology is something to be aware of, as is the ability to convert attribute topologies into forms that OpenGL can directly process. Each attribute has its own topology which affects how the index list is built. Different attributes can share the same topology. For example, we could have colors associated with each face of the cube. The color topology would be identical to the normal topology.

Most of the details are usually hidden behind various command-line tools that are used to generate proper meshes from files exported from modelling programs. Many videogame developers have complex asset conditioning pipelines that process exported files into binary formats suitable for loading and direct upload into buffer objects. But it is important to understand how mesh topologies work.