Optimizing SkinnedMeshRenderers for Unity5

Posted by
December 14th, 2015 7:01 pm

We made a game where you play as the mayor of a procedurally generated town.

You could just play it here, or read on as I discuss more in depth the issues we faced with draw calls and SkinnedMeshRenderers in Unity (and then you play it).

Staten & Kapitalet Gameplay

“Premature optimization is the root of all evil” – Donald Knuth

In the early stages of development I pay little heed to the cost of any algorithms and whatnots. I tend to go for the simplest solution for any given problem and fix up what needs optimizing later. Doing this speeds up development time immensely, and results in much more clean code. At the end of development we found that the graphical assets were the only bottleneck in our game, and optimizing that bit would prove to be quite difficult. This blog post details the steps I took to optimize the rendering, talking about each iteration from start to finish.

First iteration – Using the non-animated mesh

The most obvious problem is the sheer number of meshes that keeps getting added as the game progresses. Each city block contains roughly 15 meshes each. As the number of city blocks reaches the hundreds, the number of drawcalls becomes too much for most hardware to handle. Unity is able to batch meshes together, resulting in fewer draw calls. But all the meshes are animated, so they use the SkinnedMeshRenderer component rather than MeshRenderer component. Unity can’t batch SkinnedMeshRenderers, so I need to do some work around.

Second Iteration – SkinnedMeshRenderer.BakeMesh()

All the buildings and trees are only animated as they appear and fade away. When they’ve emerged they’re completely still, so what about converting them to a MeshRenderer during that time? My first idea was to use the non-animated mesh with a MeshRenderer and toggle between those two when appropriate, but I quickly found that a few non-animated meshes didn’t match up with the final pose of the SkinnedMeshRenderer, so using this technique was not an option.

Staten_Kapitalet_mesh_vs_pose
Image showing the discrepancy between the final pose of a SkinnedMeshRenderer and the mesh it has been animating.

The SkinnedMeshRenderer has a rather interesting function called BakeMesh(). What it does is it creates a snapshot of the mesh as it is in it’s current state of animation, outputting a new Mesh to be used elsewhere. This would solve the problem splendidly, as I could pass that mesh to the MeshRenderer and watch as I bask in the glory of automatic batching. As Admiral Ackbar so boldly noted it wouldn’t be that easy.

I started by creating a dictionary that would contain each different type of mesh. This is so the MeshRenderers can share a single mesh, allowing Unity to batch them. Whenever a new kind of mesh was found, it’s mesh would be baked and added to the dictionary. Problems were immediately made apparent as the meshes would ever-so-slightly pop up in height as the swap was made. This was due to the scale of the buildings being something other than zero. The scale of the building would affect the scale of the mesh output by the SkinnedMeshRenderer, which would result in a double increase in scale as it was applied to a different object. My solution to this quandary was to temporarily set the scale of the building to 1, do the bake, and then set it back to it’s original value.

Staten_Kapitalet_it0code
The code for the first iteration. [View on GitHub]

This worked splendidly, as the swap was completely seamless. This came at a price, however, as unity doesn’t batch MeshRenderers which use shadows. Turning off shadows lead to the game looking far less nice, so some other solution was neccessary.

Staten_Kapitalet_noshadows
Much more optimized due to batching, but shadows would be a nice thing to have

Third Iteration – Combining Meshes

What if we took a slightly more manual approach to batching ourselves? The mesh has the interesting functionality of being able to be combined with other meshes, resulting in one large mesh. It does have it’s restrictions as the meshes must use the same material and the number of triangles allowed inside a single mesh is limited. Luckily the buildings and trees only use three different materials in total, so we should be able to combine an entire block into a maximum of three meshes.

Here the part where I don’t even fully understand what’s going on. In essence, you create a CombineInstance, pass the BakeMesh() mesh to it and add the transform to it. The MOST important part is to make sure that all scaling is negated on the transform matrix, or else the mesh will get bloated. The resulting combined mesh gets added to it’s own GameObject, which will be removed when the block leaves this state.

Staten_Kapitalet_codeit3
Function that combines all meshes in a block that shares the same material. At this point I realize I probably should have done some documentation… [View on GitHub]

Getting this to work was a pain. There were so many factors in play which had their own wierd effect on the result that I resorted to trying every single thing; every combination possible. The result was way worth it, though as now the game has good framerate with shadows!

Staten_Kapitalet_combMesh
The GameObject which has the combined meshes of the red houses is selected. Also, shadows.

Fourth Iteration – The next level

A design feature of the game is that the final block for each faction will remain once transitioned to; so if they’ll never change, why not combine multiple blocks of them together? At first I tried combining every single one but I soon became aware of the aforementioned restriction of mesh size. By splitting the city up in a grid we can combine the meshes of several blocks to a single mesh, allowing for an even greater reduction in drawcalls.

Staten_Kapitalet_multiblockComb
A GameObject containing a subcolumn of meshes spanning several blocks

Ending Notes

So I suppose the essence of this post boils down to three points:

  • Don’t optimize too early on, you can always do it later.
  • Find the loopholes! If something doesn’t work with X, can you temporarily make it into a Y?
  • Use the Design; assumptions buys performance.

Thanks for reading!

View the project at GitHub.
Play the Game!


Leave a Reply

You must be logged in to post a comment.

[cache: storing page]