Frustum Culling with Unity Jobs

Feb 11, 2022

Here's a fast way to find out which objects are visible to the camera. Use Unity's job system to test if each object's bounding box is inside the camera's frustum planes.

First here's the code for the job.

using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Burst;

[BurstCompile]
struct CullingJob : IJobParallelForFilter
{
    [ReadOnly] public NativeArray<float4> frustumPlanes;
    [ReadOnly] public NativeArray<float3> positions;
    [ReadOnly] public NativeArray<float3> extents;


    public bool Execute(int i) {
        return FrustumContainsBox(positions[i] - extents[i], positions[i] + extents[i]);
    }


    float DistanceToPlane( float4 plane, float3 position )
    {
        return math.dot(plane.xyz, position) + plane.w;
    }

    bool FrustumContainsBox(float3 bboxMin, float3 bboxMax) {
        float3 pos;

        for (int i = 0; i < 6; i++) {
            pos.x = frustumPlanes[i].x > 0 ? bboxMax.x : bboxMin.x;
            pos.y = frustumPlanes[i].y > 0 ? bboxMax.y : bboxMin.y;
            pos.z = frustumPlanes[i].z > 0 ? bboxMax.z : bboxMin.z;

            if (DistanceToPlane(frustumPlanes[i], pos) < 0) {
                return false;
            }
        }

        return true;
    }
}

Here is the code to setup and run the job.

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

public class OjbectCuller : MonoBehaviour {
    Plane[] frustumPlanes;
    NativeArray<float4> frustumPlanesNative;
    public Transform[] objects;
    public Camera playerCamera;


    void Awake() {
        frustumPlanesNative = new NativeArray<float4>(6, Allocator.Persistent);
    }


    void Update() {
        // put object bounds into native arrays
        NativeArray<float3> objectPositions = new NativeArray<float3>(objects.Length, Allocator.TempJob);
        NativeArray<float3> objectExtents = new NativeArray<float3>(objects.Length, Allocator.TempJob);

        for (int i = 0; i < objects.Length; i++) {
            objectPositions[i] = objects[i].bounds.center;
            objectExtents[i] = objects[i].bounds.extents;
        }

        // get camera frustum planes
        frustumPlanes = GeometryUtility.CalculateFrustumPlanes(playerCamera);

        // put planes into native array for job
        for (int i = 0; i < 6; i++) {
            frustumPlanesNative[i] = new float4(frustumPlanes[i].normal.x, frustumPlanes[i].normal.y, frustumPlanes[i].normal.z, frustumPlanes[i].distance);
        }

        // this will hold indexes of visible objects when job is complete
        NativeList<int> visibleIndexes = new NativeList<int>(Allocator.TempJob);

        FoilageCellCulling filterJob = new FoilageCellCulling()
        {        
            frustumPlanes = frustumPlanesNative,
            positions = objectPositions,
            extents = objectExtents
        };

        filterJob.ScheduleAppend(visibleIndexes, numObjects, 32).Complete();

        // do something with visible objects
        for (int i = 0; i < visibleIndexes.Length; i++) {
            // DrawObject(objectPositions[visibleIndexes[i]], objectExtents[visibleIndexes[i]]);
        }

        objectPositions.Dispose();
        objectExtents.Dispose();
    }


    void OnDestroy() {
        frustumPlanesNative.Dispose();
    }
}