Looking back at the previous set of posts, I think they started getting formulaic and even boring. I am new to this blogging thing and I will try to do better. The good news is that I have achieved a breakthrough in the performance of the island scene. Some of the things I have been trying in this series of posts have been helpful, but finally there were also some code changes that were very specific to the Sharkferno game. As many people have said, there is not a single recipe for improving performance. Every situation is different, and that is certainly the case here. In this post I will draw some conclusions to the performance techniques described in the blog and rank how helpful they have been, and also explain the Sharkferno-specific code changes that I think are the final breakthrough I have been looking for.
Adapting Ready-Made Environments for VR
To a large degree, this is what this series of blog posts has been about. Each scene in my Sharkferno game starts with an environment I acquired in the Unity Asset Store. In retrospect, I could have made better choices in terms of environments that already had good performance instead of choosing environments that appealed to me based only on their appearance. So the issue is what techniques are effective at optimizing the performance of ready-made Unity environments. A key parameter has been the number of draw calls. We have looked at reducing the number of objects, introducing occlusion culling and layer-based culling distances, and baking lighting. Since the last post, I have further experimented with quality settings, using different shaders (specifically, mobile shaders), disabling still more objects, different sized lightmaps, removing transparent objects, and removing fog, and using the Progressive rather than the Enlighten lightmapper. I also tried Unity’s Lightweight Render Pipeline, but found that an environment not originally developed for this requires quite significant rework to adapt to LRP, so abandoned that.
The table below shows how the number of draw calls was impacted by each of these changes:
Performance Change | Number of Draw Calls | Percentage Change | Experiment Number |
---|---|---|---|
initial | 5619 | 0 | |
disable unnecessary objects | 4353 | 22.53 | 1 |
frustum culling and culling by layer | 2319 | 46.73 | 2 |
occlusion culling | 1690 | 27.12 | 3 |
bake lighting | 754 | 55.38 | 4 |
reduce quality settings | 734 | 2.65 | 5 |
mobile shaders | 766 | -4.36 | 6 |
disable tiny objects | 768 | -0.26 | 7 |
bake lighting with larger lightmap | 790 | -2.86 | 8 |
bake lighting with medium lightmap | 760 | 3.8 | 9 |
disable transparent objects | 760 | 0 | 10 |
disable fog | 738 | 2.89 | 11 |
Progressive rather than Enlighen lightmapper | 1526 | -106.78 | 12 |
You can sort this list yourself by clicking on “Percentage Change.” Click twice to sort in decreasing order. Although every situation is different, the biggest impact for the island scene are these:
- bake lighting
- frustum culling and culling by layer
- occlusion culling
- disable unnecessary objects
It does seem very reasonable to do these things for any ready-made Unity environment to improve performance. A few other factors, although having only a nominal effect in this example, should also be considered:
- adjust quality settings
- switch to more efficient shaders
I think fog is in an interesting trade-off. Some of Unity’s guidelines for optimizing performance recommend not using fog. On the other hand, I could imagine that sometimes fog would allow culling objects that are far enough that fog may cover them. It is hard to say whether fog would hurt or actually help performance in this situation without trying it (which I have not done).
So how close did these changes come to Oculus’ guidelines, which I listed in the very first blog post? CPU time, GPU time, and numbers of triangles and vertices are on target or better. Number of draw calls are still high, with the guideline being less than 100 per frame. We did manage to reduce from 5619 to about 760, an 86% reduction. Is that enough? These numbers are for the island environment itself. The dynamic game objects and game logic for Sharkferno add more demand on compute power. The best way to test at this point is through play testing. How does the whole thing look with these performance improvements? Is the VR experience smooth and pleasant or is there still judder?
But the recipe isn’t enough
So, after these performance improvements, especially light baking, different types of culling, and eliminating some unnecessary objects, I was quite disappointed to discover that there is still judder in the game, especially when controlling a character and having that character move quickly in a sideways direction. Not only disappointed, I was also puzzled, because the Oculus Performance HUD indicated that performance was about 90 frames per second.
I racked my brains about what other factor besides rendering performance could be causing the judder. I thought of two possible culprits:
- The script that I use for human character movement is taken from my own NPC Populator. There are some tricks to coordinating Unity navigation and animation. Perhaps something here was causing judder.
- The script for camera following is taken from my own Flying Drone Toolkit. Maybe a problem with this?
I first checked out the navigation / animation coordination. Unity’s navigation and animation each move a game object. When they are moved together, a technique is needed to keep the position from navigation and animation in sync. I reviewed this code carefully, but could not find any flaw with it.
The camera following script turned out to be the issue. There were a couple of different problems. Although these were somewhat subtle, I do kick myself for having missed these issues at first. (And note to users of the Flying Drone Toolkit: The issues I discovered are specific the way I am using the script in Sharkferno. The Flying Drone Toolkit itself is fine.)
My first experiment that demonstrated that judder was the result of the camera following script was simply to disable that script. The camera would no longer follow the character, and there was also no judder!
After some more experimentation, I ended up making two main changes to my camera follow script:
- Initially there was a Vector3.Lerp() to adjust position, followed by a Transform.LookAt() to adjust rotation. Although this works fine outside of VR, perhaps the higher VR framerate caused the LookAt() function to be too jarring. I rewrote the code the instead adjust rotation using Quaternion.RotateTowards().
- The camera follow script was always driven by FixedUpdate(). This is correct for the Flying Drone Toolkit, in which drone movement is physics-based. However, this is not correct when using the same script for following human characters. These are driven by Mecanim animation, not physics. And should be driven by LateUpdate() rather than FixedUpdate(). So my other change to the camera follow script was to use FixedUpdate() when following a drone (physics-based) and to use LateUpdate() when following a human character (not physics-based).
The above changes helped, especially for human characters, the judder was gone. However, there was still judder when following the drone, although this had not been the case when using the Flying Drone Toolkit outside of VR. It then occurred to me that judder would result if the Unity engine’s timestep is not running at 90fps (0.011 sec). In fact it was running at the default value of 0.02 (50 fps). This means that many frames actually had two consecutive images captured by the camera, causing the judder. I changed the game’s Fixed Timestep to 0.011, and the appearance of judder essentially vanished! Running Sharkferno with the Oculus Performance HUD showed a pretty consistent 90 fps, with occasional dips usually just during scene transitions and very occasionally when new objects enter the view.
The Other Sharkferno Scenes
At this point I congratulated myself that I had a recipe that I hoped would work to fix the performance for any Unity environment, including the other two Sharkferno scenes: the Polygon Town and Dark City. Remove unnecessary objects, bake the lighting, and set up culling, right? Unfortunately, I tried a number of permutations of this for Polygon Town but could never get it to hit a steady 90 fps. Dark City was even worse. The environment was already set up with proper configuration of baked lighting. However, I tried several times to run the bake, but it would never complete, always getting “stuck” for hours at the same state in the light baking workflow, with the CPU pegged at 100%. A lesson learned here: Although it is good to test on a machine having minimum VR specs, some development tasks (especially light baking) require a machine with even higher specs. Thus, in many situations a development project may require multiple computers of different spec levels. This can be a problem for a one-person development project, unless that person owns or has access to a variety of computers.
Bringing Closure to Sharkferno
So I now have a version of Sharkferno in which the tutorial and the island environment run smoothly, but the other environments do not. I have decided to disable the other environments, reduce the price of Sharkferno to a mere $1.99, and resubmit to the Oculus store.
Is $1.99 too expensive for a game having a single level plus tutorial? I know I am biased but this seems reasonable to me. There is fun in repeated gameplay. If Sharkferno were to be published on the Asset Store, I would periodically add more levels. Based on the lessons learned in this blog series, I would check each new level for performance before building it into a Sharkferno level.
So here, finally, is a glimpse of Sharkferno gameplay, running smoothly, without judder:
What’s Next
I will submit this version of Sharkferno to the Oculus Store. It runs smoothly and passes the pre-validation tests. Wish me luck on that. While that is pending, I have a couple of other topics for which I will write blog entries in the near future. These will be different from the last series on performance considerations.