Oculus Performance in Unity Part 5: Culling

In the last post, I achieved slight improvements in numbers of draw calls, triangles, and vertices, by turning off some of the objects (trees and plants) in the island environment. The triangles and vertices are high but acceptable, but the number of draw calls is still way to high. The reduction was from an average of 5619 to 4353, but the goal is 50-100!

In this post I will try more formal culling techniques to reduce the number of objects that are displayed. In particular, I will add:

  • Frustum culling
  • Occlusion culling

Frustum Culling

A perspective camera renders within a frustum, which is a 3 dimensional shape like a pyramid with the top cut off parallel to the base. This is explained in more detail in the Unity Manual. The Unity engine automatically omits from rendering any objects that are not within the view frustum which helps performance automatically. However, there is a trick available to developers to make frustum culling help performance even more.

The shape of the view frustrum is defined by camera properties: fieldOfView, nearClipPlane, and farClipPlane. Here is the trick: For small objects, the farClipPlane could be closer. In other words, suppose your farClipPlane permits you to see a mile in front of you. Do you need to see small pebbles that are that far away or only about 10 feet in front of you?

The trick is to use another camera property: layerCullDistances. I define new layers called SmallEnvObject, MediumEnvObject, and LargeEnvObject.

The camera’s layerCullDistances cannot be set in the Inspector. A small script is required such as this, added to the camera object:

using UnityEngine;

public class FrustumCullScript : MonoBehaviour {

   public float[] distances;

   void Start () {
        Camera myCamera = GetComponent<Camera>();
        myCamera.layerCullDistances = distances;
        myCamera.layerCullSpherical = true;
    }
   
}

The script above enables a distances array to be edited in the Inspector. The index of each distance element corresponds to the layer number. So referring to the screen shot above, array element 9 is the distance limit for SmallEnvObject’s, while array element 10 is the distance limit for MediumEnvObject’s. Any array element whose value is left as zero defaults to the camera’s farClipPlane. I will just leave LargeEnvObject’s set to that default.

Note that camera.layerCullSpherical is set to true. This uses spherical distance rather than a clip plane, which has the benefit of keeping the same objects visible if the viewpoint is rotated. This may often happen in VR.

So to add all this to the island test scene:

  • I define the Small…, Medium…, and LargeEnvObject layers as layer numbers 9, 10, 11 as shown above.
  • I create the FrustumCullScript shown above and add it to the camera object. (The camera component is in CenterEyeAnchor under the OVRCameraRig. This is what I added the FrustumCullScript to.)
  • In the Inspector, I created an array of 32 elements in the FrustumCullScript.
  • I set values for layers 9 (small objects) and 10 (medium) objects. These are the distances beyond which small and medium objects will be culled. I set these to 20 units and 40 units respectively. How did I arrive at these values? I did so empirically by adding a cube object to the scene and moving it while estimating at what distance would it be reasonable to omit small objects. This seemed to be about 20. I doubled that to 40 for medium objects.
  • I browsed the various island objects in the Hierarchy and set these as SmallEnvObjects:
    • Shells
    • Pebbles
  • Likewise, I set these as MediumEnvObjects:
    • BeachRocksSmall
    • BeachRocksPointy
    • BeachRocksFlat

The search area in the Hierarchy can be used to quickly find all the objects of a given type, and change the layer for all of them at once.

Of course, how to assign which objects to which sizes and getting the correct distance thresholds requires some experimentation. This is intended as a starting point. If frustum culling does help with performance, these choices can be refined through testing.

I take performance metrics following the recipe developed in the previous posts. The result of frustum culling is to approximately reduce the numbers of draw calls, triangles, and vertices by half! However, CPU and GPU times are not significantly changed.

Occlusion Culling

Occlusion culling is the technique of not rendering objects that are not visible because they are occluded or covered by other objects. This seems obvious and one may assume that it is done by default, but this is not the case. The Unity Manual has a very clear description of occlusion culling and the steps required to implement it.

The first step is to mark objects as being Occluder Static (can obscure other objects) and/or Occludee Static (can be obscured by other objects).

In the island scene, most environmental objects are already set as Occluder and Occludee Static, include the cliffs, rocks, props, terrain. Really small rocks, like pebbles don’t need to be Occluder Static, which may make the baking process more efficient, but as a first attempt, I just take these default settings and bake. I run the performance test and have gotten a good drop in draw calls.

Culling Results

Frames per SecondCPU timeGPU timedraw calls / frametriangles / framevertices / framedropped frames
Goal90 consistentlyat most 11 msat most 11 ms50 - 1001-2M1-2Mrare, maybe a few at scene transitions
Baseline90 but with downward spikes11.11 ms7.48 ms56191.6M1.8Msome during spikes
Fewer Objects90 but with downward spikes11.12 ms7.89 ms43531.1M1.2Msome during spikes
Frustum Culling90 with very occasional downward spikes11.10 ms8.61 ms2319645K776Ksome during spikes
Occlusion Culling90 with very occasional downward spikes11.11 ms8.02 ms1690459K532Ksome during spikes

The combination for frustum culling, including the trick to prune small objects at a distance, along with occlusion culling has reduced the number of draw calls by over 60%. However, there is still a long way to go to reduce the number all the way down to 50-100.

2 thoughts on “Oculus Performance in Unity Part 5: Culling”

  1. The new Anim Budgeter tool enables you to set a fixed budget per platform (ms of work to perform on the gamethread), and it works out if it can do it all, or if it needs to cut down on the work done. IT works by measuring the total cost of animation updates, and calculating the cost of a single work unit. If work needs to be cut down to meet the budget, it does it based on significance and targets several areas: stop ticking and use Master Pose Component, update a lower rate, interpolate (or not) between updates, etc. The goal is to dynamically adjust load to fit within fixed (gamethread) budget. Added a new Animation Sharing plugin that reduces the overall amount of animation work required for a crowd of actors. It is based upon the Master-Pose Component system, while adding blending and additive Animation States. The Animation states are buckets for which animation instances are evaluated. The resulting poses are then transferred to all child components part of the bucket. See the diagram below for a breakdown of the system.

    1. This tool sounds interesting. Could you please provide a link? Is it for Unity? (I am able to find a tool something like this for Unreal but not Unity.)

Comments are closed.