Vertex Count Higher in Engine Than in 3D Software
After importing a 3D mesh from a software like Blender, Maya or Max, in Unity or Unreal Engine, you might realize that the number of vertices in the engine is higher than the number of vertices you see in the 3D software.
I was recently writing a demo, where I ran a compute shader per vertex. I noticed that the number of vertices I had in Blender, were way less than the number of vertices Unity showed me. I thought exporting the mesh with smooth shading will solve that, but turned out there were things here I never considered. Researching it gave me scattered answers across different forums, so I decided to write this post and clarify why this happens. This is super obvious for most people that learned it on the job, or spend some time thinking about it, but if you don’t have an experienced mentor next to you, it is surprisingly hard to find a complete answer to this simple question.
What is a Vertex
Understanding what a vertex is, is important for understanding this problem. Vertices are often mistaken for positions. A vertex, is not a position. Position is an attribute of a vertex. I was guilty of this too, and I think it is natural to be confused about it, since in geometry, a vertex is a point where two lines meet. And a point implies a position.
In computer graphics this definition has moved to something more general. A vertex is a data container, a grouped series of attributes, that is worked on by a vertex shader. In a similar way how the term shader has changed from a program that shades, to a program that runs on a graphic card, vertices also were used in all sort of creative ways for general computing, before the time of compute shaders. So you might have seen a vertex without a position attribute.
The paragraph above is a bit of a tangent though, since in games and game engines, a vertex almost always has at the very least a position in it. It is still important to separate the vertex positions and vertices, since two vertices might share the same position but still be different vertices, because they differ in their other vertex attributes, such as normals or UVs.
The stat which your engine, or 3D software might show as number of vertices, is refereeing to different metrics. Before we go over these different points of view, and discussing what number of vertices mean in each case, we have one more important definition to go over.
What is an Index Buffer
As far as your triangles are concerned, they need a vertex for each corner of the triangle. So a mesh with N number if triangles, will need N*3 vertices. If several triangles share the same vertex, there is no reason for us to save that vertex several times in memory. This is why people came up with index buffers.
Instead of providing the graphic pipeline with 3 set of vertices, we provide it with 3 indices (ids) that refer to where these vertices are saved in an array else where in memory, in a vertex buffer. The array which contains the indices of the vertices of the triangles, is called the index buffer.
Like this, we avoided duplicating shared vertices in memory, at the cost of having an extra index buffer. This is a worthwhile trade off, as far as our mesh has less than 65,536 vertices, we can use 16 bit integers for our index buffer. as example a mesh with 20k vertices would have an overhead of 0.04 mb for its index buffer. On the other hand, if this mesh has a lot of shared vertices, and each vertex is huge, aka lots of high precision attributes, we would be saving way more on space.
Now that we established what an index buffer is, let’s look at how many vertices the graphic API needs, for a mesh with N number of triangles.
Graphic API Side
As far as the API is concerned, you need to run 3 vertex shaders per triangle (not quite, I will get to that in a bit). As stated above, a triangle needs its 3 vertices, in order to be drawn, shared or not. So engines have to prepare the data differently for run time usage, than how they might serialize them. Because of the format which the Graphic API requires from them.
If you have 10k triangles on your mesh, you need to run the program 30k times. As stated above, this is not completely true. There is a caching system in place, that lets you skip a vertex shader, if you have already ran a vertex shader on a vertex with the same input, which is currently saved in this cache. The size of this cache varies from hardware to hardware, and there are different ways to optimize for it. I don’t know how the driver decides which vertices to keep in the cache and when to switch it with a different vertex. The summary is, the driver remembers some of the vertices you have transformed in your vertex shader, and if you are about to run the vertex program with the exact same input, it uses the result of the previous runs, instead of re-running the vertex shader.
So your vertex shader count might be less than number of triangles times 3, but more than what you see in your 3D software.
Engine Side
When your engine imports a mesh, it needs to prepare it in a way that the Graphic API can render. Engines might also have a different format for how they save a mesh permanently on the hard drive, more on that in the next section.
The preferred format is the index+vertex buffer for most cases. So how many vertices will you see in the vertex buffer? In my case my vertex buffer contained 23k vertices, whereas Blender showed me 16k vertices. The rules for this is simple, in order for a vertex to be considered truly shared, all its attributes need to be exactly the same. This means, not just having the same position, but also normals, tangents, UVs etc.
Normals is a simple one. Having hard edges with split normals and different smoothing groups, would increase your vertex count on the engine side, since vertex positions shared between different faces, have different normals. To optimize for this, you can do things like having smoothshading, where a vertex normaled is averaged across all the faces that share it and use detailed normal maps to reconstruct the hard edges.
UVs are a bit trickier. Two vertices on the same position, that belong to different faces, won’t have the same UVs, if those faces are on different islands. So if you want to optimize your vertex buffer count on the UV sides, you need to optimize your unwrap. More specifically, you should optimize for less islands.
For other attributes such as Vertex Color or anything else, similar tricks like normal maps can be used. To bake the information in a texture. There is a tipping point, where the latency of reading these info from the texture might be higher than whatever else you are optimizing for. So you should always benchmark to know if you are really optimizing, or making things worse.
Mesh Formats Sides
How formats like FBX and Obj save a mesh, is based on other priorities compared to how the engine unpacks them on the RAM. Some examples are are file size, loading/ decoding time, ease of programming a decoder and a encoder and being readable for humans.
As stated above, the trade-off of having an extra index buffer is a good one. So why not taking it one step further and having an index buffer per attribute. For each vertex, you would save several indices instead of one. Each index points to the location of a different attributes, one for position, one for UVs etc.
The advantage of that is, that you can reduce duplication in the file. Imagine a scenario, where the Vertex Color is mostly black, except few instances, however since other attributes such as position, normals or uvs are not the same, all these vertices can’t be merged in one vertex, if you have only one index buffer. But if you have an index buffer per attribute, you avoid duplicating the same Vertex Color everywhere, and only save as many vertex colors as you have variations.
Other advantage is that you can compress these lists differently. You might want to compress one attribute more than the other, using different algorithm.
3D Software Side
We finally got to the 3D software side, and the reason why Blender was showing way less vertices than Unity.
A 3D software might approach this however it wants. It might use any of the above methods, or even other things, such as using quads in stead of triangles to reduce vertex count. By the time it gets to rendering, it faces similar limitation as a game engine.
There are advantages for a 3D editing software to do something similar to how formats like OBJ save the mesh. For example, if you are moving a vertex around in Blender, you are not changing its normals or UVs. Only the vertex position. A cube has 8 corners. If each corner only consists of a position, you would have 8 vertices in your vertex buffer. Let’s say you move the one corner. The application has to move that one vertex.
Now we unwrap this triangulated cube in the most stupid way, each triangle becomes its own island. If we save the mesh how we do it in the engine, our vertex buffer goes from 8 vertices to 36. 12 triangles total, 3 vertices per triangle, each now has its own UVs. If we move a corner now, depending on which corner it is, the program has to to move a maximum of 6 vertices. Those are the 6 vertices that share a position, but have different UVs.
If we had an index buffer per attribute, you could go back to needing to only edit one instance. As far as you are only moving the positions, and not doing anything with the UVs, you would only edit the vertex position shared between those 8 vertices once.
The other added advantage is that you might be saving memory, by having several index buffers, as stated in the Mesh Formats part.
So when you see a stat like 16k vertices in Blender, it should be more something along the lines of 16k vertex positions. If you want to know if that is the case, try unwrapping your mesh differently. If this doesn’t change the vertex counts, the metric is probably referring to the the number of members in the vertex position buffer. Also make sure you are looking at your triangulated mesh, and not the quad one.
Thanks for reading, you can follow me on my Twitter: IRCSS