This article was originally posted in a different version of the blog in the spring of 2021.
Disabling game objects to see where the main culprits are
Unity version used: 2021.2.2f1 with HDRP
I started out by disabling groups of game objects or components, and marking down the frame rate impacts that each change has. To help with this task, I made scripts that disable all the AI scripts, AI animators, AI character models, and character prefabs. Here are some findings:
- disabling a group of 20 barrels and 10 crates with dynamic rigidbodies increased frame rate by 40%
- disabling shadow casting lights that were not needed, a spotlight and 5 point lights with quite small radius, also had an effect of a couple of frames per second
- unsurprisingly, the massive amount of game objects in the hull of ”Seashell”, a rocking ship that consists entirely of modular pieces and props seemed to have a big impact on framerate due to the huge number of draw calls and maybe the complexity of the hierarchy
- Disabling the skinned mesh renderers of 19 characters in the scene, despite having 25k tris each, had surprisingly little impact, but disabling the whole character model objects with their animators and rig hiearchy affected more
My notes from pretty early in the process. The lines written in black are permanent changes that the grey lines below them can be compared to. The numbers refer to frame rates.
Timestep and physics objects
While looking at the profiler, I noticed there were the repetitive bars of FixedUpdate that seemed to form a big chunk of the time that GPU has to wait for commands from CPU. I had set the Fixed Timestep to be as little as 0.005 in an attempt to fix some occasional accidents of the rowboat shooting up in the air in build. By raising it to 0.02, I was able to largely remove that bottleneck from the process of rendering a frame and raise the frame rate by about 30%.
Another change that eased the burden of physics was changing some complex mesh colliders on rigidbodies to simpler colliders. The pushable crates were already OK as they had box colliders. For the barrels, I made low-poly mesh colliders with 40-80 tris.
In optimizing colliders, it might be handy to keep in mind that a rigidbody combines the colliders in it children into a compound collider that has all the layers of the colliders, as long as the child colliders don’t have their own rigidbodies. This way you can build clusters of weird shapes even for the collider of a dynamic rigidbody, without it having to be convex like a mesh collider would. In the case of these tools, I put two box colliders directly in the rigidbody object though, since they’re so evenly shaped.
The change of replacing the more complex mesh colliders in the 20 barrels gained me 5-10% more frames per second. For ragdolls on the characters, I already had a ”rigor mortis” script attached that changed them to kinematic 5 seconds after death.
Lights
Shadow casters have a huge impact on performance, and for this reason I’ve been keeping a minimal amount of them enabled at any one time. I have a ”Room” script that lists the light sources within that room and only enables them when player is inside. This also goes for the directional light when player is outside. However, I noticed I still had some light sources in game objects that increased the framerate by 5-10%. Rearranging them so that they get hidden with the room scripts, and adding a script for one of them that enables the game object when player gets close, helped a bit.
Animators
Even though the sets of movements needed for player and AI characters vary to some extent, I’ve insisted on using the same animator for all characters so that I don’t have to make changes in several controllers. This, however, means that the first two scenes in the game have 20-30 characters with a controller of 801 animation clips (some of my blend trees have blown up a bit, one of them consistings of 282 clips of static poses).
I suppose it’s no wonder that disabling the game objects of the 17 enemy character model parents with the Animator component attached resulted in a pretty considerable frame rate increase of 20-30%.
To address the problem, I scripted two ways to reduce the number of enabled animators. One of them is a kind of frustrum culling that disables the whole character model parent when the character is not in view. The other one is disabling all AI characters in different rooms than player is in (except if they’re opening a door to player’s room). However, disabling the animator means that it doesn’t get updated. To keep track of the state the animator should be in, I made a set of special functions for updating the values in it. In addition to calling animator.SetFloat, .SetBool, .SetLayerWeight or .Play, it also adds a custom variable to a list. This way, I can check that list for changes that need to be made to the animator when it gets re-enabled. There’s bound to be some animation transitions that don’t get made fast enough and things might look weird in some situations, but so far nothing horrific has turned up.
Combining Meshes
When all the less laborous changes were done, the rest of it has been about balancing between combining meshes (reducing the number of batches) and using LOD groups (reducing the number of vertices). On the tests I’ve done in my two scenes with two ships, in one scene being far apart and in the other next to each other, LODs didn’t seem to have much of an effect. I suppose it’s optimal for something very repetitive that can be static batched (which apparently works with LODs without a problem) like terrain detail meshes and trees. However, unlike polycount, reducing the number of batches had a clear effect on frame rate, at least to a point.
With my scenes consisting of 2 rocking ships, Unity’s static batching wasn’t an option. I got Mesh Baker plugin to do the heavy lifting in combining meshes. After fumbling a bit with hidden game objects in the hierarchy that became visible, I got everything arranged so that I could bake the non-skinned objects in the hull of the modular ship in one baked model, and those on the masts and sails in another. The tool seemed flexible and pretty ideal for my use case. All in all, I baked 904 mesh renderers with 43 materials to 2 models, which reduced the number of batches from 10 800 to 6300 and improved frame rate by 2-3 frames (about 10%). Mesh Baker also has the possibity for baking textures into atlases, but that would’ve required setting up prefabs in a different way than I had them to make the workflow convenient, so I decided not to get deeper into it.
I also made a foray into Mesh Baker Lod, an add-on to Mesh Baker, which divides the scene into a 3d grid, and bakes all the LOD versions in a grid cell so that one cell has a cluster of models for lod 0, lod 1, etc. However, as I should’ve come to think of beforehand, the 3d grid is always in world space and can’t be relative to a moving object, so it didn’t work with the rocking ships.
LODs
Disabling all the skinned mesh renderers on AI characters in scene with a script seemed to have some impact in some situations but less than I expected. However, with the tri count of the characters ranging from 25k to 100k tris, situations may come up where the number of vertices would form a bottleneck.
To reduce the likelyhood of that happening, I made LODs for some of the characters, with priority on those with a high polycount or ones that are numerous (enemies). To get the fade of Unity’s LOD Group working between the LOD versions, one can add a noise texture in the diffuse alpha and turn on Alpha Clipping in the material (at least in HDRP).
I also combined some clusters of non-skinned mesh renderers using some same materials, by exporting them from Unity with FBX Exporter (all LOD versions at once) and combining all the models of each LOD level into one cluster in Blender. The change is 1000 batches out of 7700 in that scene. It doesn’t seem to have any noticable effect on frame rate at the moment.
Below are some screenshots from different places, with the Statistics window visible so you can see the changes in Tris and Batches at the beginning and end of the whole optimization process.
On the top screenshot, it’s weird how the tri count while standing on the deck on night time has almost doubled. I rechecked it and it’s not a mistake. Perhaps it’s caused by the fact that there’s a huge single object that includes a lot of geometry from out of frustrum as well, which would’ve been culled out without combining them as a single mesh.
Dispite the setting of rocking ships being a bit nightmarish from optimization standpoint, I’ve about doubled the frame rate so I think I can be pretty happy with that. Back to making new content I guess. Thanks for reading!