One question you’ll face when developing for a still emerging market is whether you should spend your precious time making sure you hit the required 60 frames per seconds consistently on all available devices. Is it more important to not miss out on any potential player out there, or take the quick road to release and limit your game to high end phones and blocking out a portion of the already small VR user base as a result?
Having created something unique - at least we like to think so - we decided to leave no phone and no potential gamer behind. Strapping on the coding gloves and ordering a pallet of dodgy energy drinks, we set out to make our game run smoothly on every Oculus Gear compatible phone, all the way back to the ancient Note 4.
Oculus requires game play of a smooth 60 frames per second and knowing the restrictions of mobiles, we had already limited ourselves to one dynamic light; the so important flashlight that you carry as you break into the police station in hopes of finding evidence that will free you of the murder trial. All other lights were baked and neatly packaged into light maps to make the mobiles happy.
We had also made sure all our 3D models are low poly and that our levels are not too crowded while still keeping enough props and details to make them realistic for the experience. It is a police station after all, so expect some donuts and empty coffee cups in this murder mystery game.
Loading up the game on the old Galaxy Note for the first time, we were carefully optimistic, all until we reached the more intense locations of our levels and saw the frame rates plummet to around 30 frames per second. With that low frame rate, we knew we that we missed the 16.7 microsecond deadline for the GPU to complete the next frame most of the time.
Meshes, Textures And Shaders
We now turned to the built-in stats monitor in the Unity editor that tells you the number of polygons and number of draw calls needed to render the current view in your scene among other things. After replaying the intense locations in the editor, we could see that we had a problem with the number of draw calls, which was getting close to 200 in the worst cases.
Draw calls are essentially preparations that the CPU needs to do to load the GPU with one or more meshes, one material and a shader for rendering. Having too many draw calls increases the risk that the GPU will not render the entire view in time for the next frame to be shown on screen. The key here is that the GPU can be loaded with multiple meshes at the same time, as long as they share the same texture and shader settings.
Since we wanted to keep the individual materials on our objects in the scene, we decided to create a texture atlas, which is basically a large texture containing the original textures tiled. Each model will then receive an offset into the atlas where its texture is stored.
Apart from creating a texture atlas, we also reworked our level design to avoid choke points with too many models in view at the same time. For example, extending short corridors going out at an angle from the larger rooms to fully utilize the occlusion culling when moving in and out of rooms.
By default, the Unity engine won’t render objects that are located outside the camera's field of view, but it will render objects that are in front of the camera but still fully obscured by another object. Occlusion culling is a way to divide the scene into smaller cells, mark those objects that are non-moveable and thus will hide or be hidden by other objects. And finally, bake an occlusion dataset that is used by the engine while rendering the current view. This is a very important tool for keeping your draw calls and number of polygons down.
After creating texture atlases and reworking the level to improve occlusion culling, we managed to get the average number of draw calls to around 50 and never exceeding 100, even in the most intense views, even with our dynamic flashlight enabled. With this, we were able to reach 60 frames per second even on the humble old Note 4.