Skip to content

Instantly share code, notes, and snippets.

@totallyRonja
Last active December 25, 2022 07:53
Show Gist options
  • Save totallyRonja/a7847b7e614a990cf43e1c15adc714a9 to your computer and use it in GitHub Desktop.
Save totallyRonja/a7847b7e614a990cf43e1c15adc714a9 to your computer and use it in GitHub Desktop.
This script allows you to display movement of particle systems without them moving for tweaking them in the editor. This script is CC0, but I'd be happy if you credit me as Ronja(https://twitter.com/totallyRonja) and maybe give me some money for what I do (https://www.patreon.com/RonjaTutorials) (https://ko-fi.com/ronjatutorials)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
// Tagging a class with the EditorTool attribute and no target type registers a global tool. Global tools are valid for any selection, and are accessible through the top left toolbar in the editor.
[EditorTool("Moving Particle Context")]
class MovingParticleContext : EditorTool
{
// Serialize this value to set a default value in the Inspector.
[SerializeField] private Texture2D ToolIcon;
[SerializeField] private List<ParticleSystem> Systems;
[SerializeField] private Vector3 Direction = Vector3.forward * 3;
private bool hasFloor = false;
private float floorHeight = 5;
private float floorSize = 5;
private float floorDistance = 5;
private float lastEditorTime = -1;
private bool showHandles = true;
private Dictionary<ParticleSystem, Transform> contextObjects = new Dictionary<ParticleSystem, Transform>();
GUIContent IconContent;
public void OnEnable() {
IconContent = new GUIContent()
{
image = ToolIcon,
text = "Moving Particle Context",
tooltip = "Creates a Moving Context for Particle Systems"
};
}
public override GUIContent toolbarIcon => IconContent;
public override void OnActivated() {
foreach (var system in Systems) {
var contextObject = new GameObject("Particle Context Object").transform;
contextObject.position = Vector3.zero;
contextObject.gameObject.hideFlags = HideFlags.HideAndDontSave;
contextObjects[system] = contextObject;
ParticleSystem.MainModule systemMain = system.main;
systemMain.customSimulationSpace = contextObject;
systemMain.simulationSpace = ParticleSystemSimulationSpace.Custom;
}
}
public override void OnWillBeDeactivated() {
foreach (var system in Systems) {
ParticleSystem.MainModule systemMain = system.main;
//could also be local in theory, but then you wouldnt use this tool
systemMain.simulationSpace = ParticleSystemSimulationSpace.World;
systemMain.customSimulationSpace = null;
//kill context
DestroyImmediate(contextObjects[system].gameObject);
//try to wake up system
var pos = system.transform.position;
system.transform.Translate(Vector3.one);
system.transform.position = pos;
}
contextObjects.Clear();
}
// This is called for each window that your tool is active in. Put the functionality of your tool here.
public override void OnToolGUI(EditorWindow window) {
if (!(window is SceneView sceneView)) return;
sceneView.sceneViewState.alwaysRefresh = true;
sceneView.sceneViewState.fxEnabled = true;
float deltaTime = lastEditorTime > 0 ? (float)EditorApplication.timeSinceStartup - lastEditorTime : 0f;
lastEditorTime = (float) EditorApplication.timeSinceStartup;
//decode direction
float length = Direction.magnitude;
Quaternion rotation = Quaternion.identity;
if (length > 0.01f)
rotation = Quaternion.LookRotation(Direction);
Vector3 position = Tools.handlePosition;
//direction text fields
Handles.BeginGUI();
GUI.backgroundColor = new Color(0, 0, 0, 2f);
using (new GUILayout.HorizontalScope())
{
using (new GUILayout.VerticalScope(EditorStyles.helpBox)) {
showHandles = EditorGUILayout.Toggle("Show Handles", showHandles);
using (var change = new EditorGUI.ChangeCheckScope()) {
Direction = EditorGUILayout.Vector3Field("Movement", Direction);
}
using (var change = new EditorGUI.ChangeCheckScope()) {
length = EditorGUILayout.FloatField("Speed", length);
if (change.changed)
Direction = Direction.normalized * length;
}
if (GUILayout.Button("On Plane")) {
Direction = new Vector3(Direction.x, 0, Direction.y);
Direction = Direction.normalized * length;
}
GUILayout.Label("Affected Systems:");
//fuck this
using (new GUILayout.VerticalScope()) {
var ToRemove = new List<int>();
for (var i = 0; i < Systems.Count; i++) {
using (new GUILayout.HorizontalScope(GUIStyle.none)) {
using var change = new EditorGUI.ChangeCheckScope();
var newObject = EditorGUILayout.ObjectField(Systems[i], typeof(ParticleSystem), true) as ParticleSystem;
if (change.changed) {
if (Systems[i] != null) {
var systemMain = Systems[i].main;
systemMain.simulationSpace = ParticleSystemSimulationSpace.World;
systemMain.customSimulationSpace = null;
}
Systems[i] = newObject;
}
if (GUILayout.Button("x")) {
ToRemove.Add(i);
}
}
}
for (int i = ToRemove.Count - 1; i >= 0; i--) {
if (Systems[ToRemove[i]] != null) {
var system = Systems[ToRemove[i]];
var systemMain = system.main;
systemMain.simulationSpace = ParticleSystemSimulationSpace.World;
systemMain.customSimulationSpace = null;
//kill context
DestroyImmediate(contextObjects[system].gameObject);
//try to wake up system
var pos = system.transform.position;
system.transform.Translate(Vector3.one);
system.transform.position = pos;
contextObjects.Remove(system);
}
Systems.RemoveAt(ToRemove[i]);
}
if (GUILayout.Button("add")) {
Systems.Add(null);
}
GUILayout.Space(16);
hasFloor = EditorGUILayout.Toggle("Show Reference Floor", hasFloor);
if (hasFloor) {
floorHeight = EditorGUILayout.FloatField("Floor Height", floorHeight);
floorSize = EditorGUILayout.FloatField("Floor Size", floorSize);
floorDistance = EditorGUILayout.FloatField("Floor Bar Distance", floorDistance);
floorDistance = Mathf.Max(floorDistance, 0.1f);
}
}
}
GUILayout.FlexibleSpace();
}
Handles.EndGUI();
//direction handles
if (showHandles) {
using (var changeCheck = new EditorGUI.ChangeCheckScope()) {
using var _ = new Handles.DrawingScope(new Color(1f, 0.6f, 0.06f));
switch (Tools.pivotRotation) {
case PivotRotation.Local:
rotation = Handles.RotationHandle(rotation, position);
break;
case PivotRotation.Global:
rotation = GlobalRotationHandle.RotationHandle(rotation, position);
break;
default: throw new ArgumentOutOfRangeException();
}
var handleScale = HandleUtility.GetHandleSize(position) / 5;
var handleResult = Handles.Slider(position+Direction*handleScale, Direction, handleScale, Handles.SphereHandleCap, 0);
Handles.DrawLine(position, position + Direction * handleScale, 5);
length = Mathf.Abs(Vector3.Dot((handleResult-position)/handleScale, Direction.normalized));
if (changeCheck.changed) {
Direction = rotation * Vector3.forward;
Direction *= length;
}
}
}
//show ground
if (hasFloor) {
var inDirection = Direction.normalized;
var orthogonal = Vector3.Cross(inDirection, Vector3.up);
orthogonal = orthogonal.magnitude > 0.01 ? orthogonal.normalized : Vector3.right;
var normal = Vector3.Cross(inDirection, orthogonal);
var DistanceBetween = Mathf.Max(floorDistance, 0.1f);
Handles.color = Color.cyan;
for (float i = -floorSize - ((Time.time * length) % DistanceBetween); i < floorSize; i += DistanceBetween) {
Handles.DrawLine(
inDirection * i + orthogonal * floorSize + normal * floorHeight,
inDirection*i - orthogonal * floorSize+ normal * floorHeight);
}
}
//move objects
foreach (var system in Systems.Where(s => s != null && s.isPlaying)) {
if (!contextObjects.TryGetValue(system, out var contextObject)) {
contextObject = new GameObject("Particle Context Object").transform;
contextObject.position = Vector3.zero;
contextObject.gameObject.hideFlags = HideFlags.HideAndDontSave;
contextObjects[system] = contextObject;
ParticleSystem.MainModule systemMain = system.main;
systemMain.customSimulationSpace = contextObject;
systemMain.simulationSpace = ParticleSystemSimulationSpace.Custom;
}
contextObject.Translate(Direction * -deltaTime);
}
}
}
//everything from here is stuff copied from unity source because it wasnt flexible enough and tons of stuff is internal
public static class GlobalRotationHandle {
public static Quaternion RotationHandle(Quaternion rotation, Vector3 position) => DoRotationHandle(rotation, position);
public static Quaternion DoRotationHandle(Quaternion rotation, Vector3 position) =>
DoRotationHandle(RotationHandleIds.@default, rotation, position, RotationHandleParam.Default);
internal static Quaternion DoRotationHandle(
RotationHandleIds ids,
Quaternion rotation,
Vector3 position,
RotationHandleParam param) {
UnityEngine.Event current = UnityEngine.Event.current;
Vector3 vector3 = Handles.inverseMatrix.MultiplyVector(
(UnityEngine.Object) Camera.current != (UnityEngine.Object) null
? Camera.current.transform.forward
: Vector3.forward);
float handleSize = HandleUtility.GetHandleSize(position);
Color color = Handles.color;
bool flag1 = !GUI.enabled;
bool flag2 = ids.Has(GUIUtility.hotControl);
if (!flag1 /*&& param.ShouldShow(Handles.RotationHandleParam.Handle.XYZ)*/ &&
(ids.xyz == GUIUtility.hotControl || !flag2)) {
Handles.color = new Color(0.0f, 0.0f, 0.0f, 0.3f);
rotation = Handles.FreeRotateHandle(ids.xyz, rotation, position, handleSize * param.xyzSize/*, param.displayXYZCircle*/);
}
for (int index = 0; index < 3; ++index) {
if (param.ShouldShow(index)) {
Color colorByAxis = GetColorByAxis(index);
Handles.color = colorByAxis;//flag1 ? Color.Lerp(colorByAxis, Handles.staticColor, Handles.staticBlend) : colorByAxis;
Handles.color = ToActiveColorSpace(Handles.color);
Vector3 axisVector = GetAxisVector(index);
float size = handleSize * param.axisSize[index];
rotation = Handles.Disc(ids[index], rotation, position, axisVector, size, true, EditorSnapSettings.rotate);
}
}
if (flag2 && current.type == UnityEngine.EventType.Repaint) {
Handles.color = ToActiveColorSpace(s_DisabledHandleColor);
Handles.DrawWireDisc(position, vector3, handleSize * param.axisSize[0], Handles.lineThickness);
}
if (!flag1 && param.ShouldShow(RotationHandleParam.Handle.CameraAxis) &&
(ids.cameraAxis == GUIUtility.hotControl || !flag2)) {
Handles.color = ToActiveColorSpace(Handles.centerColor);
rotation = Handles.Disc(ids.cameraAxis, rotation, position, vector3,
handleSize * param.cameraAxisSize, false, 0.0f);
}
Handles.color = color;
return rotation;
}
internal static Color ToActiveColorSpace(Color color) => QualitySettings.activeColorSpace == ColorSpace.Linear ? color.linear : color;
private static Vector3 GetAxisVector(int axis) => s_AxisVector[axis];
private static readonly Color k_RotationPieColor = new Color(0.9647059f, 0.9490196f, 0.1960784f, 0.89f);
internal static Color s_DisabledHandleColor = new Color(0.5f, 0.5f, 0.5f, 0.5f);
internal static PrefColor s_XAxisColor = new PrefColor("Scene/X Axis", 0.8588235f, 0.2431373f, 0.1137255f, 0.93f);
internal static PrefColor s_YAxisColor = new PrefColor("Scene/Y Axis", 0.6039216f, 0.9529412f, 0.282353f, 0.93f);
internal static PrefColor s_ZAxisColor = new PrefColor("Scene/Z Axis", 0.227451f, 0.4784314f, 0.972549f, 0.93f);
private static PrefColor[] s_AxisColor = new PrefColor[3] {
s_XAxisColor,
s_YAxisColor,
s_ZAxisColor
};
private static Vector3[] s_AxisVector = new Vector3[3]
{
Vector3.right,
Vector3.up,
Vector3.forward
};
internal static Color GetColorByAxis(int axis) => (Color) s_AxisColor[axis];
internal static int s_xRotateHandleHash = "xRotateHandleHash".GetHashCode();
internal struct RotationHandleIds {
public readonly int x;
public readonly int y;
public readonly int z;
public readonly int cameraAxis;
public readonly int xyz;
internal static int s_xRotateHandleHash = "xRotateHandleHash".GetHashCode();
internal static int s_yRotateHandleHash = "yRotateHandleHash".GetHashCode();
internal static int s_zRotateHandleHash = "zRotateHandleHash".GetHashCode();
internal static int s_cameraAxisRotateHandleHash = "cameraAxisRotateHandleHash".GetHashCode();
internal static int s_xyzRotateHandleHash = "xyzRotateHandleHash".GetHashCode();
public static RotationHandleIds @default => new RotationHandleIds(
GUIUtility.GetControlID(s_xRotateHandleHash, FocusType.Passive),
GUIUtility.GetControlID(s_yRotateHandleHash, FocusType.Passive),
GUIUtility.GetControlID(s_zRotateHandleHash, FocusType.Passive),
GUIUtility.GetControlID(s_cameraAxisRotateHandleHash, FocusType.Passive),
GUIUtility.GetControlID(s_xyzRotateHandleHash, FocusType.Passive));
public int this[int index] {
get {
switch (index) {
case 0:
return this.x;
case 1:
return this.y;
case 2:
return this.z;
case 3:
return this.cameraAxis;
case 4:
return this.xyz;
default:
return -1;
}
}
}
public bool Has(int id) => this.x == id || this.y == id || (this.z == id || this.cameraAxis == id) || this.xyz == id;
public RotationHandleIds(int x, int y, int z, int cameraAxis, int xyz)
{
this.x = x;
this.y = y;
this.z = z;
this.cameraAxis = cameraAxis;
this.xyz = xyz;
}
}
internal struct RotationHandleParam {
private static RotationHandleParam s_Default =
new RotationHandleParam(RotationHandleParam.Handle.All, Vector3.one, 1f, 1.1f, true, true);
public readonly Vector3 axisSize;
public readonly float cameraAxisSize;
public readonly float xyzSize;
public readonly RotationHandleParam.Handle handles;
public readonly bool enableRayDrag;
public readonly bool displayXYZCircle;
public static RotationHandleParam Default {
get => RotationHandleParam.s_Default;
set => RotationHandleParam.s_Default = value;
}
public bool ShouldShow(int axis) => (uint) (this.handles & (RotationHandleParam.Handle) (1 << axis)) > 0U;
public bool ShouldShow(RotationHandleParam.Handle handle) => (uint) (this.handles & handle) > 0U;
public RotationHandleParam(
RotationHandleParam.Handle handles,
Vector3 axisSize,
float xyzSize,
float cameraAxisSize,
bool enableRayDrag,
bool displayXYZCircle) {
this.axisSize = axisSize;
this.xyzSize = xyzSize;
this.handles = handles;
this.cameraAxisSize = cameraAxisSize;
this.enableRayDrag = enableRayDrag;
this.displayXYZCircle = displayXYZCircle;
}
[System.Flags]
public enum Handle {
None = 0,
X = 1,
Y = 2,
Z = 4,
CameraAxis = 8,
XYZ = 16, // 0x00000010
All = -1, // 0xFFFFFFFF
}
}
internal class PrefColor /*: IPrefType*/ {
private string m_Name;
private Color m_Color;
private Color m_DefaultColor;
private bool m_SeparateColors;
private Color m_OptionalDarkColor;
private Color m_OptionalDarkDefaultColor;
private bool m_Loaded;
public PrefColor() => this.m_Loaded = true;
public PrefColor(
string name,
float defaultRed,
float defaultGreen,
float defaultBlue,
float defaultAlpha) {
this.m_Name = name;
this.m_Color = this.m_DefaultColor = new Color(defaultRed, defaultGreen, defaultBlue, defaultAlpha);
this.m_SeparateColors = false;
this.m_OptionalDarkColor = this.m_OptionalDarkDefaultColor = Color.clear;
//PrefSettings.Add((IPrefType) this);
this.m_Loaded = false;
}
public PrefColor(
string name,
float defaultRed,
float defaultGreen,
float defaultBlue,
float defaultAlpha,
float defaultRed2,
float defaultGreen2,
float defaultBlue2,
float defaultAlpha2) {
this.m_Name = name;
this.m_Color = this.m_DefaultColor = new Color(defaultRed, defaultGreen, defaultBlue, defaultAlpha);
this.m_SeparateColors = true;
this.m_OptionalDarkColor = this.m_OptionalDarkDefaultColor =
new Color(defaultRed2, defaultGreen2, defaultBlue2, defaultAlpha2);
//PrefSettings.Add((IPrefType) this);
this.m_Loaded = false;
}
public void Load() {
if (this.m_Loaded)
return;
this.m_Loaded = true;
//UnityEditor.PrefColor prefColor = PrefSettings.Get<UnityEditor.PrefColor>(this.m_Name, this);
//this.m_Name = prefColor.m_Name;
//this.m_Color = prefColor.m_Color;
//this.m_SeparateColors = prefColor.m_SeparateColors;
//this.m_OptionalDarkColor = prefColor.m_OptionalDarkColor;
}
public Color Color {
get {
this.Load();
return this.m_SeparateColors && EditorGUIUtility.isProSkin ? this.m_OptionalDarkColor : this.m_Color;
}
set {
this.Load();
if (this.m_SeparateColors && EditorGUIUtility.isProSkin)
this.m_OptionalDarkColor = value;
else
this.m_Color = value;
}
}
public string Name {
get {
this.Load();
return this.m_Name;
}
}
public static implicit operator Color(PrefColor pcolor) => pcolor.Color;
public string ToUniqueString() {
this.Load();
return this.m_SeparateColors
? string.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8}", (object) this.m_Name, (object) this.m_Color.r,
(object) this.m_Color.g, (object) this.m_Color.b, (object) this.m_Color.a,
(object) this.m_OptionalDarkColor.r, (object) this.m_OptionalDarkColor.g,
(object) this.m_OptionalDarkColor.b, (object) this.m_OptionalDarkColor.a)
: string.Format("{0};{1};{2};{3};{4}", (object) this.m_Name, (object) this.m_Color.r,
(object) this.m_Color.g, (object) this.m_Color.b, (object) this.m_Color.a);
}
public void FromUniqueString(string s) {
this.Load();
string[] strArray = s.Split(';');
if (strArray.Length != 5 && strArray.Length != 9) {
UnityEngine.Debug.LogError((object) "Parsing PrefColor failed");
} else {
this.m_Name = strArray[0];
strArray[1] = strArray[1].Replace(',', '.');
strArray[2] = strArray[2].Replace(',', '.');
strArray[3] = strArray[3].Replace(',', '.');
strArray[4] = strArray[4].Replace(',', '.');
float result1;
float result2;
float result3;
float result4;
if (float.TryParse(strArray[1], NumberStyles.Float,
(IFormatProvider) CultureInfo.InvariantCulture.NumberFormat, out result1) &
float.TryParse(strArray[2], NumberStyles.Float,
(IFormatProvider) CultureInfo.InvariantCulture.NumberFormat,
out result2) &
float.TryParse(strArray[3], NumberStyles.Float,
(IFormatProvider) CultureInfo.InvariantCulture.NumberFormat,
out result3) & float.TryParse(strArray[4], NumberStyles.Float,
(IFormatProvider) CultureInfo.InvariantCulture.NumberFormat, out result4))
this.m_Color = new Color(result1, result2, result3, result4);
else
UnityEngine.Debug.LogError((object) "Parsing PrefColor failed");
if (strArray.Length == 9) {
this.m_SeparateColors = true;
strArray[5] = strArray[5].Replace(',', '.');
strArray[6] = strArray[6].Replace(',', '.');
strArray[7] = strArray[7].Replace(',', '.');
strArray[8] = strArray[8].Replace(',', '.');
if (float.TryParse(strArray[5], NumberStyles.Float,
(IFormatProvider) CultureInfo.InvariantCulture.NumberFormat, out result1) &
float.TryParse(strArray[6], NumberStyles.Float,
(IFormatProvider) CultureInfo.InvariantCulture.NumberFormat,
out result2) &
float.TryParse(strArray[7], NumberStyles.Float,
(IFormatProvider) CultureInfo.InvariantCulture.NumberFormat,
out result3) & float.TryParse(strArray[8], NumberStyles.Float,
(IFormatProvider) CultureInfo.InvariantCulture.NumberFormat, out result4))
this.m_OptionalDarkColor = new Color(result1, result2, result3, result4);
else
UnityEngine.Debug.LogError((object) "Parsing PrefColor failed");
} else {
this.m_SeparateColors = false;
this.m_OptionalDarkColor = Color.clear;
}
}
}
internal void ResetToDefault() {
this.Load();
this.m_Color = this.m_DefaultColor;
this.m_OptionalDarkColor = this.m_OptionalDarkDefaultColor;
}
}
}
@totallyRonja
Copy link
Author

I'm not that experienced in VFX art and I'm not sure it isnt wonky at some corners, so feel free to tell me about your experiences.
But also I promise no support~

@totallyRonja
Copy link
Author

Oh and I used this as the editor icon:
comet
and you can apply it like that when you select the script in the editor:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment