Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created September 6, 2025 18:06
Show Gist options
  • Save adammyhre/ff8253ea87d06c08d5ddfed337ba76dd to your computer and use it in GitHub Desktop.
Save adammyhre/ff8253ea87d06c08d5ddfed337ba76dd to your computer and use it in GitHub Desktop.
CullingGroup API for Unity C#
using UnityEngine;
using System.Collections.Generic;
using UnityUtils; // https://github.com/adammyhre/Unity-Utils
public class CullingManager : Singleton<CullingManager> {
#region Fields
public Camera cullingCamera;
public float maxCullingDistance = 100f;
public LayerMask cullableLayers;
public List<string> cullableTags = new List<string>();
public float updateInterval = 0.1f;
CullingGroup group;
BoundingSphere[] spheres = new BoundingSphere[64];
List<CullingTarget> owners = new List<CullingTarget>(64);
Dictionary<CullingTarget, int> map = new Dictionary<CullingTarget, int>(64);
int count;
float tPos;
int[] tmp = new int[256];
HashSet<string> tagSet;
#endregion
new void Awake() {
base.Awake();
if (!cullingCamera) cullingCamera = Camera.main;
group = new CullingGroup();
group.onStateChanged = OnStateChanged;
group.targetCamera = cullingCamera;
group.SetBoundingSpheres(spheres);
group.SetBoundingSphereCount(0);
group.SetDistanceReferencePoint(cullingCamera.transform);
group.SetBoundingDistances(new float[] { maxCullingDistance }); // single band
// group.SetBoundingDistances(new float[]{ 10f, 25f, 60f }); // example of multiple bands, increasing distances
tagSet = new HashSet<string>(cullableTags);
}
void Update() {
tPos += Time.deltaTime;
if (tPos >= updateInterval) {
for (int i = 0; i < count; i++) {
var o = owners[i];
if (!o) continue;
var s = spheres[i];
s.position = o.transform.position;
s.radius = o.boundarySphereRadius;
spheres[i] = s;
}
tPos = 0f;
}
}
public void Register(CullingTarget t) {
if (!t) return;
if (count == spheres.Length) {
System.Array.Resize(ref spheres, count * 2);
group.SetBoundingSpheres(spheres);
}
owners.Add(t);
map[t] = count;
spheres[count] = new BoundingSphere(t.transform.position, t.boundarySphereRadius);
count++;
group.SetBoundingSphereCount(count);
}
public void Deregister(CullingTarget t) {
if (group == null || !t || !map.TryGetValue(t, out int i)) return;
group.EraseSwapBack(i);
CullingGroup.EraseSwapBack(i, spheres, ref count);
var last = owners.Count - 1;
var moved = owners[last];
owners[i] = moved;
owners.RemoveAt(last);
if (moved) map[moved] = i;
map.Remove(t);
group.SetBoundingSphereCount(count);
}
void OnStateChanged(CullingGroupEvent e) {
var cullingTarget = owners[e.index];
if (!cullingTarget) return;
if (!IsCullable(cullingTarget.gameObject)) {
cullingTarget.ToggleOn();
return;
}
bool inRange = e.currentDistance == 0;
if (e.isVisible && inRange) cullingTarget.ToggleOn();
else cullingTarget.ToggleOff();
}
bool IsCullable(GameObject obj) {
return ((1 << obj.layer) & cullableLayers) != 0 && tagSet.Contains(obj.tag); // layer and tag check
}
bool IsWithinDistance(Vector3 p) {
return Vector3.Distance(cullingCamera.transform.position, p) <= maxCullingDistance;
}
int GetBandTargets(int band, List<CullingTarget> outList, bool? visible = null) {
if (tmp.Length < count) tmp = new int[count];
int n = visible.HasValue
? group.QueryIndices(visible.Value, band, tmp, 0)
: group.QueryIndices(band, tmp, 0);
outList.Clear();
for (int i = 0; i < n; i++) outList.Add(owners[tmp[i]]);
return n;
}
public (int visible, int culled) Snapshot() {
if (tmp.Length < count) tmp = new int[count];
int vis = group.QueryIndices(true, tmp, 0);
return (vis, count - vis);
}
void OnDisable() {
if (group != null) {
group.onStateChanged = null;
group.Dispose();
group = null;
}
}
}
using UnityEngine;
using UnityEngine.Events;
using ImprovedTimers; // https://github.com/adammyhre/Unity-Improved-Timers
public enum CullingBehavior { None, ToggleScripts, FadeInOut }
public class CullingTarget : MonoBehaviour {
#region Fields
public UnityEvent onCulled, onVisible;
public float boundarySphereRadius = 1f;
public Renderer objectRenderer;
public float fadeDuration = 2f;
public CullingBehavior cullingMode = CullingBehavior.FadeInOut;
public bool isPriorityObject;
MaterialPropertyBlock mpb;
MonoBehaviour[] scripts;
static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
static readonly int ColorId = Shader.PropertyToID("_Color");
CountdownTimer fadeTimer;
float startAlpha;
float currentAlpha;
float targetAlpha;
#endregion
void Awake() {
objectRenderer = gameObject.GetComponentInChildren<Renderer>();
scripts = GetComponents<MonoBehaviour>();
for (int i = 0; i < scripts.Length; i++) {
if (scripts[i] == this) scripts[i] = null;
}
fadeTimer = new CountdownTimer(fadeDuration);
}
void OnEnable() {
currentAlpha = GetAlpha();
if (isPriorityObject) onVisible?.Invoke();
else CullingManager.Instance.Register(this);
}
void OnDisable() {
if (!isPriorityObject) CullingManager.Instance.Deregister(this);
}
void Update() {
if (fadeTimer.IsRunning) {
float t = 1f - Mathf.Clamp01(fadeTimer.Progress);
float a = Mathf.Lerp(startAlpha, targetAlpha, t);
SetAlpha(a);
}
}
void BeginFadeTo(float target, bool deactivate) {
if (!objectRenderer) return;
mpb ??= new MaterialPropertyBlock();
startAlpha = currentAlpha;
targetAlpha = Mathf.Clamp01(target);
if (deactivate && targetAlpha <= 0f) {
fadeTimer.OnTimerStop = () => { if (objectRenderer) objectRenderer.enabled = false; };
}
else {
fadeTimer.OnTimerStop = () => { };
}
fadeTimer.Reset(fadeDuration);
fadeTimer.Start();
}
void EnableScripts(bool v) {
for (int i = 0; i < scripts.Length; i++) {
var s = scripts[i];
if (s == null) continue;
s.enabled = v;
}
}
public void ToggleOn() {
if (fadeTimer.IsRunning) fadeTimer.Stop();
if (isPriorityObject) {
onVisible?.Invoke();
return;
}
switch (cullingMode) {
case CullingBehavior.FadeInOut:
if (objectRenderer && !objectRenderer.enabled) objectRenderer.enabled = true;
BeginFadeTo(1f, deactivate: false);
break;
case CullingBehavior.ToggleScripts:
EnableScripts(true);
break;
}
}
public void ToggleOff() {
if (fadeTimer.IsRunning) fadeTimer.Stop();
if (isPriorityObject) return;
switch (cullingMode) {
case CullingBehavior.FadeInOut:
BeginFadeTo(0f, deactivate: true);
break;
case CullingBehavior.ToggleScripts:
EnableScripts(false);
break;
}
onCulled?.Invoke();
}
float GetAlpha() {
if (!objectRenderer) return 1f;
var m = objectRenderer.sharedMaterial;
if (!m) return 1f;
if (m.HasProperty(BaseColorId)) return m.GetColor(BaseColorId).a;
if (m.HasProperty(ColorId)) return m.GetColor(ColorId).a;
return 1f;
}
void SetAlpha(float a) {
if (!objectRenderer) return;
var m = objectRenderer.sharedMaterial;
if (!m) return;
currentAlpha = Mathf.Clamp01(a);
mpb ??= new MaterialPropertyBlock();
objectRenderer.GetPropertyBlock(mpb);
if (m.HasProperty(BaseColorId)) {
var c = m.GetColor(BaseColorId); c.a = currentAlpha;
mpb.SetColor(BaseColorId, c);
}
else if (m.HasProperty(ColorId)) {
var c = m.GetColor(ColorId); c.a = currentAlpha;
mpb.SetColor(ColorId, c);
}
objectRenderer.SetPropertyBlock(mpb);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment