Regardless of your target platform, having a good way to control which parts of your level are active at any particular time is tremendously important to achieve the best possible performance.
Tell Me What You See
If you read about games and game engines, you've probably come across the term visibility culling. This is the process of only rendering those parts of the game world which are currently visible to the player. Unity has two built-in systems to do this, frustum culling and occlusion culling. Frustum culling simply means to cull out any object that is not within the camera's field of vision, this is on by default, and is really the bare minimum when it comes to visibility culling in 3D games. Occlusion culling, supplements the frustum culling by allowing you to precompute additional visibility data based on the static objects in your scene, allowing the engine to figure out which objects are hidden behind other ones in real time while playing the game, thus culling even more objects from the render list.
More Than Meets the Eye
Frustum culling can not be turned off in Unity, nor would I want to. Occlusion culling, on the other hand, requires some set up, has certain performance and memory overheads and turned out not to be a win for Suzy Cube for a couple of reasons.
First, robust occlusion culling, like that which is found in Unity, is really at its best when the game allows the player to roam the level freely and look around in, basically, any direction. In Suzy Cube, the player can move around freely, but the camera is strictly controlled throughout the level. This mean that, as a designer, I can more easily predict exactly what needs to be visible and when, making a more freeform system, like the one built into Unity, kind of overkill.
The second reason is that there is more to performance than rendering, especially when running on low spec hardware like mobile devices. The culling system I wrote is extremely simple, yes, but it also has the advantage that it controls not only an objet's visibility but whether the object is active or not. This means that objects that are being culled no longer take up any CPU cycles either. This is great for things like enemies which might include complex behaviour scripts and animations which would tank the framerate if they were all running at once.
Where Are You Now?
The culling system in Suzy Cube is extremely simple. At it's core, it's simply a matter of checking whether or not Suzy is inside a particular culling zone and turning on or off the contents of that zone accordingly.
As you can see in the screenshot from the editor at the top of the post, I break the levels up into sections, and the objects that make up each section are all parented under a single object known as a culling zone.
Hierarchy view of a culling zone and trigger
Culling zones are, in turn, parented under their own culling trigger. While the game is running, the culling trigger's job is simply to enable or disable the culling zone based on whether or not the player is within the trigger's bounds, seen as the yellow box in the top screenshot. Since all the objects that make up the section of the level are all children of the culling zone, when you exit the bounds of the trigger, they will all get disabled along with their parent.
In my first implementation of this system, I manually defined the bounds of the culling trigger. Since then, however, I've extended the culling trigger's functionality in order to be able to automatically build the bounds of the trigger in edit mode along with controls for offsetting and resizing the box. The initial bounds are automatically build by iterating through each object in the culling zone and readjusting the bounds of the trigger to encompass the object. By the end of this process, I end up with an axis aligned box that perfectly encompasses all objects to which I can then add an offset and or extra padding.
These culling zones are also very easy to set up and use. For those familiar with Unity, I've created a prefab which is simply a culling zone trigger with a parented culling zone. The trigger comes set up with reasonable padding values so setting one up is as simple as dragging the prefab into the scene and then populating the level section by simply adding objects as children under the culling zone. With the default values, it just works, as is. Once in place, I can then start adjusting individual culling trigger offsets and padding to get rid of visible popping or to help further optimize performance.
What Difference Does it Make?
The short answer is that this culling system more than doubled my framerate during early tests.
View from the editor without culling
View from the editor with culling
Here, we see the start of Level 1-1 from the editor view. Without culling, the whole level is active, being rendered and taking up CPU cycles. With the culling turned on, only the start section and the first platform section are active. This makes a huge difference, especially when running on a mobile device.
Old Dog, New Trick
Ever since I initially coded the system, I realized that culling zones could work perfectly well as children of other culling zones. In other words, I could nest smaller culling zones within larger ones. Mind you, this was not how I intended to use the system but I saw no reason for it not to work.
Recently, while working on Level 4-3, I put the hypothesis to the test and used nested culling zones for the first time.
Nested culling zones set up in Level 4-3
The level is set up to seem like one long continuous tower. It is, in fact, three smaller towers, side by side, but that's beside the point. Each tower is made up of several vertical sections, each with its own culling zone. In turn, each one of these vertical sections contains four nested culling zones, one for each face of the square tower. The large vertical culling zones ensure that only the relevant part of the tower is currently visible and active, the face specific culling zones ensure that the game isn't wasting time rendering and computing parts of the level which lie on the opposite side of the tower.
Taking the idea further, parenting culling zones in this way also allows me to be even more aggressive with optimizing level performance. All objects in a zone don't need to be active just because the zone is in view. Take, for example, this view from Level 1-1:
Level visible in the distance
Even though the later part of the level is visible in the distance, it doesn't mean that, say, the three enemies in the depression to the top right need to be active. Enemies are much more expensive on the CPU than static geometry, culling them until the player gets closer could increase the frame rate by as much as a couple of frames per second.
Add More By Taking Away
By having a system that allows me to intelligently control which parts of the level are active and visible at any particular time, I'm able to regain precious GPU and CPU cycles that I can spend on what really matters, what's up close and interacting with the player.
Small decorative objects like grass tufts or small rocks may not even read at a distance. If culling them out means I can put more up close to the camera, that's a big win for the look of the level.
And this principal is true regardless of your target platform. If your game already runs great on a beefy PC, for instance, imagine how lush it could look with some intelligent culling. Those resources you're no longer wasting off screen or in the distance could, instead be used to gussy up the foreground where the player is actually paying attention.
All this being said, the system I wrote works for Suzy Cube because of how the game plays and how the camera is controlled. A system like this won't be of much use to you if you are working on a flight simulator or a first person shooter, for instance. It just goes to show, that sometimes, the best solution for your game is to come up with a solution for your game.
What have your experiences with culling in 3D games been? Share your thoughts in the comments.