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();
}
}