Skip to content

Instantly share code, notes, and snippets.

@vvrvvd
Last active February 8, 2024 16:46
Show Gist options
  • Save vvrvvd/380aade2449ece82c0bcf5a1e73492ed to your computer and use it in GitHub Desktop.
Save vvrvvd/380aade2449ece82c0bcf5a1e73492ed to your computer and use it in GitHub Desktop.
Custom Particle System culling for Unity.
using System;
using UnityEngine;
public class ParticleSystemCulling : MonoBehaviour
{
[Serializable]
public struct SerializableBoundingSphere
{
public float radius;
public Vector3 localPosition;
public SerializableBoundingSphere(Vector3 position, float radius)
{
this.radius = radius;
this.localPosition = position;
}
}
#region Editor Fields
[SerializeField]
private bool isStatic = false;
public SerializableBoundingSphere[] cullingSpheres = default;
#endregion
#region Private Fields
private int visibleSpheresCounter = 0;
private bool isVisible = false;
private bool isInitialized = false;
private CullingGroup cullingGroup;
private int[] dynamicBoundsArray;
private Renderer[] particleRenderers;
private ParticleSystem[] particleSystems;
private BoundingSphere[] boundingSpheres;
#endregion
#region Unity Callbacks
private void OnValidate()
{
if (cullingSpheres != null)
{
return;
}
/// Add the first culling sphere when component is being created
cullingSpheres = new SerializableBoundingSphere[1];
cullingSpheres[0] = new SerializableBoundingSphere(Vector3.zero, 10);
}
private void Awake()
{
particleSystems = GetComponentsInChildren<ParticleSystem>();
particleRenderers = transform.GetComponentsInChildren<Renderer>();
}
private void OnEnable()
{
if (cullingGroup == null)
{
InitializeCullingGroup();
}
cullingGroup.enabled = true;
}
private void OnDisable()
{
if (cullingGroup != null)
{
cullingGroup.enabled = false;
}
SetParticles(true);
SetRenderers(true);
}
private void OnDestroy()
{
if (cullingGroup != null)
{
cullingGroup.Dispose();
}
}
private void LateUpdate()
{
if (isStatic)
{
return;
}
UpdateCullingSpheresPosition();
}
private void OnDrawGizmos()
{
if (!enabled)
{
return;
}
var col = Color.yellow;
if (cullingGroup != null && !isVisible)
{
col = Color.gray;
}
Gizmos.color = col;
if (boundingSpheres != null)
{
DrawBoundingSpheres();
} else
{
DrawCullingSpheres();
}
}
#endregion
#region Private Methods
private void InitializeCullingGroup()
{
cullingGroup = new CullingGroup();
cullingGroup.targetCamera = Camera.main;
boundingSpheres = new BoundingSphere[cullingSpheres.Length];
dynamicBoundsArray = new int[cullingSpheres.Length];
for (var i = 0; i < cullingSpheres.Length; i++)
{
var customSphere = cullingSpheres[i];
boundingSpheres[i] = new BoundingSphere(transform.TransformPoint(customSphere.localPosition), customSphere.radius);
}
cullingGroup.SetBoundingSpheres(boundingSpheres);
cullingGroup.SetBoundingSphereCount(boundingSpheres.Length);
cullingGroup.onStateChanged += OnCullingGroupStateChanged;
// We need to start in a culled state as onStateChanged events will be invoked in this frame
SetVisible(false);
visibleSpheresCounter = 0;
isInitialized = true;
}
private void OnCullingGroupStateChanged(CullingGroupEvent sphereInfo)
{
OnCullingSphereVisiblityChanged(sphereInfo.isVisible);
}
private void OnCullingSphereVisiblityChanged(bool isVisible)
{
if (isVisible)
{
visibleSpheresCounter++;
} else
{
visibleSpheresCounter--;
}
SetVisible(visibleSpheresCounter != 0);
}
private void SetVisible(bool state)
{
if (isInitialized && isVisible == state)
{
return;
}
SetParticles(state);
SetRenderers(state);
isVisible = state;
}
private void SetParticles(bool visible)
{
for (var i = 0; i < particleSystems.Length; i++)
{
var target = particleSystems[i];
if (visible)
{
target.Play(true);
} else
{
target.Pause(true);
}
}
}
private void SetRenderers(bool visible)
{
foreach (var particleRenderer in particleRenderers)
{
particleRenderer.enabled = visible;
}
}
private void UpdateCullingSpheresPosition()
{
for (var i = 0; i < cullingSpheres.Length; i++)
{
var customSphere = cullingSpheres[i];
boundingSpheres[i].position = transform.TransformPoint(customSphere.localPosition);
}
cullingGroup.SetBoundingSpheres(boundingSpheres);
var visibleSpheres = cullingGroup.QueryIndices(true, dynamicBoundsArray, 0);
var isVisible = visibleSpheres > 0;
SetVisible(isVisible);
}
private void DrawBoundingSpheres()
{
for (var i = 0; i < boundingSpheres.Length; i++)
{
var sphere = boundingSpheres[i];
Gizmos.DrawWireSphere(sphere.position, sphere.radius);
}
}
private void DrawCullingSpheres()
{
for (var i = 0; i < cullingSpheres.Length; i++)
{
var sphere = cullingSpheres[i];
Gizmos.DrawWireSphere(transform.TransformPoint(sphere.localPosition), sphere.radius);
}
}
#endregion
}
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
[CustomEditor(typeof(ParticleSystemCulling))]
public class ParticleSystemCulling_Editor : Editor
{
private Transform objectTransform;
private Quaternion objectRotation;
private ParticleSystemCulling cullingComponent;
private Tool savedTool;
private bool wasToolSaved = false;
private void OnEnable()
{
wasToolSaved = false;
}
private void OnSceneGUI()
{
cullingComponent = target as ParticleSystemCulling;
objectTransform = cullingComponent.transform;
objectRotation = Tools.pivotRotation == PivotRotation.Local ? objectTransform.rotation : Quaternion.identity;
var hideTools = false;
var cullingSpheres = cullingComponent.cullingSpheres;
for (var i=0; i< cullingSpheres.Length; i++)
{
var sphere = cullingSpheres[i];
var spherePosition = objectTransform.TransformPoint(sphere.localPosition);
if (Tools.current != savedTool && sphere.localPosition == Vector3.zero)
{
hideTools = true;
}
switch (savedTool)
{
case Tool.Scale:
DrawScaleTools(i, spherePosition, sphere.radius);
break;
case Tool.Move:
default:
DrawMoveTools(i, spherePosition);
break;
}
}
if(hideTools)
{
SaveTool();
}
else
{
RestoreTool();
}
}
private void SaveTool()
{
if(wasToolSaved && Tools.current == Tool.None)
{
return;
}
wasToolSaved = true;
savedTool = Tools.current;
Tools.current = Tool.None;
}
private void RestoreTool()
{
if(!wasToolSaved)
{
return;
}
Tools.current = savedTool;
}
private void DrawMoveTools(int sphereIndex, Vector3 spherePosition)
{
EditorGUI.BeginChangeCheck();
spherePosition = Handles.DoPositionHandle(spherePosition, objectRotation);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(cullingComponent, "Move Culling Sphere");
EditorUtility.SetDirty(cullingComponent);
cullingComponent.cullingSpheres[sphereIndex].localPosition = objectTransform.InverseTransformPoint(spherePosition);
}
}
private void DrawScaleTools(int i, Vector3 spherePosition, float radius)
{
var handleSize = HandleUtility.GetHandleSize(spherePosition);
EditorGUI.BeginChangeCheck();
var newScale = Handles.DoScaleHandle(Vector3.one * radius, spherePosition, objectRotation, handleSize);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(cullingComponent, "Scale Culling Sphere");
EditorUtility.SetDirty(cullingComponent);
var newScaleValue = newScale.x;
var newScaleDiff = Mathf.Abs(radius - newScale.x);
if (newScaleDiff < Mathf.Abs(radius - newScale.y))
{
newScaleValue = newScale.y;
newScaleDiff = Mathf.Abs(radius - newScale.y);
} else if (newScaleDiff < Mathf.Abs(radius - newScale.z))
{
newScaleValue = newScale.z;
newScaleDiff = Mathf.Abs(radius - newScale.z);
}
cullingComponent.cullingSpheres[i].radius = newScaleValue;
}
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment