GPU Occlusion Culling and additive scenes issue.

Hello,

I am integrating a GPU Based Occlusion Culling system in my unity project that was created by Przemyslaw Zaworski (GitHub - przemyslawzaworski/Unity-GPU-Based-Occlusion-Culling)

Overall it’s been working better than unity’s occlusion culling so that’s why i wanted to use it, however i’ve ran into an issue when loading additive scenes and using the script in multiple scenes at once.

The issue is the following:

1 - Upon starting the game, scenes called “GameScene” and “TunnelArea” are loaded.

  • GameScene contains the player with the Main Camera, canvases, etc…
  • TunnelArea contains the occlusion culling script, inside the scene are the props that will be culled.

2- After reaching a trigger, i additively load more scenes, as the map is quite big and i had to split it into areas.

3- When the new area is loaded, the occlusion script from the “TunnelArea” stops working (all the meshrenderers in the target list get disabled). But the script from the new loaded area works fine, and the culling for the target objects in that scene are culled correctly.

I know my explanation might not be the best i am probably setting it up wrong but i’ve been trying to implement this without luck. So any help is appreciated.

Here’s a gif of the problem:

This is the script, it’s slightly modified from the github repo.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Rendering;
using Unity.Collections;
public class HardwareOcclusion : MonoBehaviour
{
    public GameObject[] Targets;
    public Shader HardwareOcclusionShader;
    public ComputeShader IntersectionShader;
    public bool Intersection = true;
    //public bool Dynamic = false;
    public uint Delay = 1;
    public bool Debug = false;

    private Material _Material;
    private ComputeBuffer _Reader;
    private ComputeBuffer _Writer;
    private Vector4[] _Elements;
    private Vector4[] _Cache;
    private List<List<MeshRenderer>> _MeshRenderers;
    private List<Vector4> _Vertices;

    private ComputeBuffer _AABB;
    private ComputeBuffer _Intersection;
    private Cuboid[] _Cuboids;
    private int[] _Reset;
    private int _CellIndex = -1;
    private Coroutine _Coroutine;
    private Camera mainCamera;

    struct Cuboid
    {
        public Vector3 Center;
        public Vector3 Scale;
    }

    void Start()
    {
        mainCamera = Camera.main;
        if (Targets.Length == 0) return;
        Init();
    }

    Vector3 GetCenterFromCubeVertices (Vector4[] verts)
    {
        Vector3 total = Vector3.zero;
        int length = verts.Length;
        for (int i = 0; i < length; i++)
        {
            total += new Vector3(verts[i].x, verts[i].y, verts[i].z);
        }
        return total / length;
    }

    Vector3 GetScaleFromCubeVertices (Vector4[] verts)
    {
        Vector3 min = Vector3.positiveInfinity;
        Vector3 max = Vector3.negativeInfinity;
        for (int i = 0; i < verts.Length; i++)
        {
            Vector3 point = new Vector3(verts[i].x, verts[i].y, verts[i].z);
            min = Vector3.Min(min, point);
            max = Vector3.Max(max, point);
        }
        return (max - min) * 0.5f;
    }

    Vector4[] GenerateCell(GameObject parent, int index)
    {
        BoxCollider bc = parent.AddComponent<BoxCollider>();
        bc.isTrigger = true; // Set collider as trigger
        Bounds bounds = new Bounds(Vector3.zero, Vector3.zero);
        bool hasBounds = false;
        MeshRenderer[] renderers = parent.GetComponentsInChildren<MeshRenderer>();
        for (int i = 0; i < renderers.Length; i++)
        {
            if (hasBounds)
            {
                bounds.Encapsulate(renderers[i].bounds);
            }
            else
            {
                bounds = renderers[i].bounds;
                hasBounds = true;
            }
        }
        if (hasBounds)
        {
            bc.center = bounds.center - parent.transform.position;
            bc.size = bounds.size;
        }
        else
        {
            bc.size = bc.center = Vector3.zero;
            bc.size = Vector3.zero;
        }
        bc.size = Vector3.Scale(bc.size, new Vector3(1.01f, 1.01f, 1.01f));
        GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        cube.transform.position = parent.transform.position + bc.center;
        cube.transform.localScale = bc.size;
        Mesh mesh = cube.GetComponent<MeshFilter>().sharedMesh;
        Vector4[] vertices = new Vector4[mesh.triangles.Length];
        for (int i = 0; i < vertices.Length; i++)
        {
            Vector3 p = cube.transform.TransformPoint(mesh.vertices[mesh.triangles[i]]);
            vertices[i] = new Vector4(p.x, p.y, p.z, index);
        }
        Destroy(bc);
        Destroy(cube);
        return vertices;
    }

    // void GenerateMap()
    // {
    //     _Vertices.Clear();
    //     _Vertices.TrimExcess();
    //     for (int i = 0; i < Targets.Length; i++)
    //     {
    //         Vector4[] aabb = GenerateCell(Targets[i], i);
    //         _Cuboids[i].Center = GetCenterFromCubeVertices(aabb);
    //         _Cuboids[i].Scale = GetScaleFromCubeVertices(aabb);
    //         _Vertices.AddRange(aabb);
    //     }
    //     _Reader.SetData(_Vertices.ToArray());
    // }

    bool ArrayState (Vector4[] a, Vector4[] b)
    {
        for (int i = 0; i < a.Length; i++)
        {
            bool x = Vector4.Dot(a[i], a[i]) > 0.0f;
            bool y = Vector4.Dot(b[i], b[i]) > 0.0f;
            if (x != y) return false;
        }
        return true;
    }

    void ArrayCopy (Vector4[] source, Vector4[] destination)
    {
        for (int i = 0; i < source.Length; i++) destination[i] = source[i];
    }

    void Init()
    {
        if (_Material == null)
            _Material = new Material(HardwareOcclusionShader);

        _MeshRenderers = new List<List<MeshRenderer>>();

        int stride = System.Runtime.InteropServices.Marshal.SizeOf(typeof(Cuboid));

        _Writer = new ComputeBuffer(Targets.Length, 16, ComputeBufferType.Default);
        _Elements = new Vector4[Targets.Length];
        _Cache = new Vector4[Targets.Length];
        _Cuboids = new Cuboid[Targets.Length];

        if (_Cache.Length > 0)
            _Cache[0] = Vector4.one;

        _Vertices = new List<Vector4>();

        Graphics.ClearRandomWriteTargets();
        Graphics.SetRandomWriteTarget(1, _Writer, false);

        for (int i = 0; i < Targets.Length; i++)
        {
            _MeshRenderers.Add(Targets[i].GetComponentsInChildren<MeshRenderer>().ToList());
            Vector4[] aabb = GenerateCell(Targets[i], i);
            _Cuboids[i].Center = GetCenterFromCubeVertices(aabb);
            _Cuboids[i].Scale = GetScaleFromCubeVertices(aabb);
            _Vertices.AddRange(aabb);
        }

        _Reader = new ComputeBuffer(_Vertices.Count, 16, ComputeBufferType.Default);
        _Reader.SetData(_Vertices.ToArray());

        _Material.SetBuffer("_Reader", _Reader);
        _Material.SetBuffer("_Writer", _Writer);
        _Material.SetInt("_Debug", System.Convert.ToInt32(Debug));

        // Adjusted the stride here as well
        _AABB = new ComputeBuffer(_Cuboids.Length, stride, ComputeBufferType.Default);

        _Intersection = new ComputeBuffer(1, sizeof(int), ComputeBufferType.Default);

        IntersectionShader.SetBuffer(0, "_AABB", _AABB);
        IntersectionShader.SetBuffer(0, "_Intersection", _Intersection);

        //// Adjusted the size of _Cuboids to match the stride

        _AABB.SetData(_Cuboids, 0, 0, _Cuboids.Length);

        //// Create an array of int to hold the reset value
        _Reset = new int[1] { -1 };

        // Check if the Intersection coroutine should be started
        //_Coroutine = Intersection ? StartCoroutine(UpdateAsync()) : null;
        StartCoroutine(UpdateAsync());
    }

    void Update()
    {
        if (Targets.Length == 0) return;
        //if (Dynamic) GenerateMap();
        if (Time.frameCount % Delay != 0) return;
        _Writer.GetData(_Elements);
        bool state = ArrayState(_Elements, _Cache);
        if (!state)
        {
            for (int i = 0; i < _MeshRenderers.Count; i++)
            {
                for (int j = 0; j < _MeshRenderers[i].Count; j++)
                {
                    if (i == _CellIndex)
                        _MeshRenderers[i][j].enabled = true;
                    else
                        _MeshRenderers[i][j].enabled = (Vector4.Dot(_Elements[i], _Elements[i]) > 0.0f);
                }
            }
            ArrayCopy(_Elements, _Cache);
        }
        System.Array.Clear(_Elements, 0, _Elements.Length);
        _Writer.SetData(_Elements);
    }

    IEnumerator UpdateAsync()
    {
        yield return new WaitUntil(() => mainCamera != null);

        while (true)
        {
            Vector3 position = mainCamera.transform.position;
            IntersectionShader.SetVector("_Point", new Vector4(position.x, position.y, position.z, 0.0f));
            _Intersection.SetData(_Reset);
            int threadGroupsX = (int)Mathf.Ceil(_Cuboids.Length / 8.0f);
            IntersectionShader.Dispatch(0, threadGroupsX, 1, 1);
            AsyncGPUReadbackRequest request = AsyncGPUReadback.Request(_Intersection);
            yield return new WaitUntil(() => request.done);
            _CellIndex = request.GetData<int>()[0];
        }
    }

    void OnRenderObject()
    {
        if (_Vertices == null) return;
        _Material.SetPass(0);
        Graphics.DrawProceduralNow(MeshTopology.Triangles, _Vertices.Count, 1);
    }

    void OnDisable()
    {
        if (Targets.Length == 0) return;
        if (_Coroutine != null) StopCoroutine(_Coroutine);
        _Reader.Release();
        _Writer.Release();
        _AABB.Release();
        _Intersection.Release();
    }
}

EDIT: Currently trying another approach, using the script in the main scene then filling the array with objects from the loaded scenes.

Any help in making these scripts better is greatly appreciated. As of now its most likely leaking gpu memory, since it shows multiple garbage collector computebuffer warnings in the inspector.

Ok it might have a problem :hushed::face_with_spiral_eyes::smile:

I’ve ditched the previous system, ended up using this instead.

Edited the ‘CullingTargetRenderers’ script to disable the renderers with “forceRenderingOff” instead of disabling the mesh renderer and it’s working great :slight_smile:

1 Like