Last active
February 8, 2024 16:46
-
-
Save vvrvvd/380aade2449ece82c0bcf5a1e73492ed to your computer and use it in GitHub Desktop.
Custom Particle System culling for Unity.
This file contains 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 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 | |
} |
This file contains 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; | |
#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