Created
September 6, 2025 18:06
-
-
Save adammyhre/ff8253ea87d06c08d5ddfed337ba76dd to your computer and use it in GitHub Desktop.
CullingGroup API for Unity C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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