Search Unity

Improvised run-time occlusion culling in any version of Unity via frustum culling

Discussion in 'Made With Unity' started by DesiQ, Sep 8, 2012.

  1. DesiQ

    DesiQ

    Joined:
    Jul 24, 2012
    Posts:
    3
    Full write-up with benchmarks and up-to-date code is here, some details are below.

    I wrote a script to approximate run-time occlusion culling in any version of Unity, as neither Unity Free nor Pro have it (Unity Pro requires precomputation, and therefore can't be used for procedurally-generated scenes).

    The script gets attached to the main camera. It works by firing fields of rays into the viewport, finding the maximum collision distance among them, and moving the view frustum's far plane just beyond that maximum distance in order to cut out anything behind it. You should tweak it for your game by adjusting where the rays are fired (the viewport coordinates are normalised 0–1). A 6-DOF game like Descent will require rays all over the screen, for example, while a first-person game with mouselook will have most of the rays concentrated around the centre of the screen (this is the way the script is set up now).



    In my test scene the script delivered a >80% reduction in draw calls/tris/verts and >85% faster renderer loop, at the cost of a 12% reduction of frame rate (from 75 to 66 FPS). While this is great for scenes that are being bogged down by draw calls, I'm interested in any feedback that can reduce the script's overhead to make it suitable for general use.

    You can read the full article at my site for performance benchmarks and up-to-date code. I include the code below to have syntax highlighting for your convenience, but I may forget to update it.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class ImprovisedOcclusionCulling : MonoBehaviour {
    6.  
    7.     public bool castPositiveMidfieldRays = true;
    8.     public bool castNegativeMidfieldRays = true;
    9.     public bool makeRaysVisible = false;
    10.  
    11.     public int defaultFarPlane = 100;
    12.     public int minDistance = 20;
    13.     public int maxDistance = 200;
    14.     public int farPlaneBuffer = 10;
    15.     public int rateOfChange = 16;
    16.  
    17.     // Dense crosshair field. These arrays describe a square grid centered around the crosshair, spaced 0.01 units apart. I pre-calculate it to make things easy.
    18.     private float[] crosshairFieldX = new float[] {0, -0.02f, 0.02f, 0, 0, -0.01f, -0.01f, 0.01f, 0.01f, -0.02f, -0.02f, 0.02f, 0.02f};
    19.     private float[] crosshairFieldY = new float[] {0, 0, 0, 0.02f, -0.02f, -0.01f, 0.01f, -0.01f, 0.01f, 0.02f, -0.02f, 0.02f, -0.02f};
    20.     private int crosshairFieldLength;
    21.  
    22.     // Midfield. These arrays describe a semicircle with equation y=sqrt(0.0125-x^2). The actual calculation is done in Start().
    23.     private float[] midFieldX = new float[] {-0.11f, -0.10f, -0.09f, -0.08f, -0.07f, -0.06f, -0.05f, -0.04f, -0.03f, -0.02f, -0.01f, 0.00f, 0.01f, 0.02f, 0.03f, 0.04f, 0.05f, 0.06f, 0.07f, 0.08f, 0.09f, 0.10f, 0.11f};
    24.     private float[] midFieldY;
    25.     private int midFieldLength;
    26.  
    27.     void Start ()
    28.     {
    29.         camera.farClipPlane = defaultFarPlane;
    30.  
    31.         crosshairFieldLength = crosshairFieldX.Length;
    32.         midFieldLength = midFieldX.Length;
    33.  
    34.         midFieldY = new float[midFieldLength];
    35.         for (int i = 0; i < midFieldLength; i++)
    36.         {
    37.             midFieldY[i] = Mathf.Sqrt (0.0125f - (midFieldX[i]*midFieldX[i]));
    38.         }
    39.  
    40.         StartCoroutine (AdjustFarPlane());
    41.     }
    42.  
    43.     IEnumerator AdjustFarPlane ()
    44.     {
    45.         while (true)
    46.         {
    47.             // Rays are fired in circles and semicircles around the centre of the viewport.
    48.             int distance = minDistance;
    49.  
    50.             for (int i = 0; i < crosshairFieldLength; i++)
    51.             {
    52.                 int tempDist = CastOcclusionRay (crosshairFieldX[i], crosshairFieldY[i]);
    53.                 if (tempDist > distance) distance = tempDist;
    54.             }
    55.  
    56.             yield return 0;
    57.  
    58.             if (castPositiveMidfieldRays == true)
    59.             {
    60.                 for (int i = 0; i < midFieldLength; i++)
    61.                 {
    62.                     int tempDist = CastOcclusionRay (midFieldX[i], midFieldY[i]);
    63.                     if (tempDist > distance) distance = tempDist;
    64.                 }
    65.  
    66.                 yield return 0;
    67.             }
    68.  
    69.             if (castNegativeMidfieldRays == true)
    70.             {
    71.                 for (int i = 0; i < midFieldLength; i++)
    72.                 {
    73.                     int tempDist = CastOcclusionRay (midFieldX[i], -midFieldY[i]);
    74.                     if (tempDist > distance) distance = tempDist;
    75.                 }
    76.  
    77.                 yield return 0;
    78.             }
    79.  
    80.             distance += farPlaneBuffer;
    81.  
    82.             if (camera.farClipPlane > distance)
    83.             {
    84.                 camera.farClipPlane -= rateOfChange;
    85.  
    86.                 if (camera.farClipPlane < distance)
    87.                 {
    88.                     camera.farClipPlane = distance;
    89.                 }
    90.             }
    91.             else if (camera.farClipPlane < distance)
    92.             {
    93.                 camera.farClipPlane += rateOfChange;
    94.  
    95.                 if (camera.farClipPlane > distance)
    96.                 {
    97.                     camera.farClipPlane = distance;
    98.                 }
    99.             }
    100.         }
    101.     }
    102.  
    103.     int CastOcclusionRay (float graphX, float graphY)
    104.     {
    105.         int distance = 0;
    106.  
    107.         Ray ray = camera.ViewportPointToRay (new Vector3 (0.5f + graphX, 0.5f + graphY, 0));
    108.  
    109.         if (makeRaysVisible == true) Debug.DrawRay (ray.origin, ray.direction*10, Color.red);
    110.  
    111.         RaycastHit hit;
    112.         if (Physics.Raycast (ray, out hit))
    113.         {
    114.             distance = (int) hit.distance;
    115.         }
    116.         else
    117.         {
    118.             // No collisions, therefore infinite distance.
    119.             distance = (int) maxDistance;
    120.         }
    121.  
    122.         return distance;
    123.     }
    124. }
    125.  
     
    Last edited: Sep 8, 2012
  2. animationanalyst

    animationanalyst

    Joined:
    Oct 13, 2012
    Posts:
    5
    Could you elaborate about the raygrid value of x y, how you calculated the values?
     
  3. imaginaryhuman

    imaginaryhuman

    Joined:
    Mar 21, 2010
    Posts:
    5,834
    If for example there is a window through which a far distant surface is found, that would push the far plane way back for the entire scene a be a big performance hit... so ideally this solutions works with enclosed rooms that aren't too big... I was thinking maybe you can split your scene into some kind of screen-space grid so that each cell can have its own far-plane, which would help there... but I think to do that you'd have to have multiple cameras and that would mean multiplying the draw calls.