summaryrefslogtreecommitdiffstats
path: root/Scripts
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2026-02-17 20:08:59 -0800
committeryum <yum.food.vr@gmail.com>2026-02-17 20:08:59 -0800
commit75c5679714c339fced0a3769fa768f8cb98806d9 (patch)
tree1327c8c673f4032bbbd20ecc5fd9716199941bf2 /Scripts
parentec6c0dc6ed672129e5a773b2a3158443951ed101 (diff)
Fold: animation workflow bugfixes
Diffstat (limited to 'Scripts')
-rwxr-xr-xScripts/Fold/Editor/FoldEditorWindow.cs282
1 files changed, 226 insertions, 56 deletions
diff --git a/Scripts/Fold/Editor/FoldEditorWindow.cs b/Scripts/Fold/Editor/FoldEditorWindow.cs
index 31d47fd..48bfe2b 100755
--- a/Scripts/Fold/Editor/FoldEditorWindow.cs
+++ b/Scripts/Fold/Editor/FoldEditorWindow.cs
@@ -14,9 +14,23 @@ public class FoldEditorWindow : EditorWindow
[SerializeField] GameObject targetObject;
int frameStep;
bool wasInAnimationMode;
+ float lastAnimTime = -1f;
+ bool isRecording;
const string FrameStepPrefKey = "Fold_FrameStep";
+ static Type s_animWindowType;
+ static PropertyInfo s_clipProp, s_timeProp;
+
+ static void CacheAnimWindowReflection()
+ {
+ if (s_animWindowType != null) return;
+ s_animWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnimationWindow");
+ const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+ s_clipProp = s_animWindowType?.GetProperty("animationClip", flags);
+ s_timeProp = s_animWindowType?.GetProperty("time", flags);
+ }
+
[MenuItem("Tools/yum_food/Fold")]
static void ShowWindow()
{
@@ -24,14 +38,43 @@ public class FoldEditorWindow : EditorWindow
window.minSize = new Vector2(400, 300);
}
- void OnEnable() => frameStep = EditorPrefs.GetInt(FrameStepPrefKey, 1);
+ void OnEnable()
+ {
+ frameStep = EditorPrefs.GetInt(FrameStepPrefKey, 1);
+ EditorApplication.update += OnEditorUpdate;
+ }
void OnDisable()
{
+ EditorApplication.update -= OnEditorUpdate;
EditorPrefs.SetInt(FrameStepPrefKey, frameStep);
ClearPropertyBlock();
}
+ void OnEditorUpdate()
+ {
+ if (isRecording) return;
+
+ if (!AnimationMode.InAnimationMode() || targetMaterial == null)
+ {
+ lastAnimTime = -1f;
+ return;
+ }
+
+ if (!TryGetAnimationWindowState(out var clip, out float time))
+ return;
+
+ if (!Mathf.Approximately(time, lastAnimTime))
+ {
+ lastAnimTime = time;
+ var savedExpanded = new List<int>(expandedOps);
+ LoadFromAnimationClip(clip, time);
+ expandedOps = savedExpanded;
+ expandedOps.RemoveAll(i => i >= operations.Count);
+ Repaint();
+ }
+ }
+
void OnGUI()
{
bool inAnimMode = AnimationMode.InAnimationMode();
@@ -219,6 +262,8 @@ public class FoldEditorWindow : EditorWindow
{
operations.Clear();
expandedOps.Clear();
+ if (targetMaterial != null)
+ ApplyToMaterial();
}
}
@@ -227,12 +272,14 @@ public class FoldEditorWindow : EditorWindow
EditorGUILayout.Space(3);
EditorGUILayout.BeginHorizontal();
- if (GUILayout.Button("<", GUILayout.Width(30), GUILayout.Height(20)))
+ if (GUILayout.Button(new GUIContent("<", "Retreat playhead"), GUILayout.Width(30), GUILayout.Height(20)))
AdvancePlayhead(-frameStep);
frameStep = EditorGUILayout.IntField(frameStep, GUILayout.Width(40));
if (frameStep < 1) frameStep = 1;
- if (GUILayout.Button(">", GUILayout.Width(30), GUILayout.Height(20)))
+ if (GUILayout.Button(new GUIContent(">", "Advance playhead"), GUILayout.Width(30), GUILayout.Height(20)))
AdvancePlayhead(frameStep);
+ if (GUILayout.Button(new GUIContent("x", "Snap to nearest keyframe"), GUILayout.Width(30), GUILayout.Height(20)))
+ SnapToNearestKeyframe();
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField($"Operations: {operations.Count}/16", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
@@ -345,23 +392,39 @@ public class FoldEditorWindow : EditorWindow
? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform)
: "";
- SetFloatKey(clip, path, "material._Vertex_Deformation_Enabled", 1f, time);
-
- for (int i = 0; i < 16; i++)
+ // Guard against OnEditorUpdate reloading a partially-written clip.
+ isRecording = true;
+ try
{
- var slot = builder.GetSlot(i);
- var prefix = $"material._Vertex_Deformation_Slot_{i}_";
+ Undo.RecordObject(clip, "Create Fold Keyframe");
+
+ SetFloatKey(clip, path, "material._Vertex_Deformation_Enabled", 1f, time);
+
+ for (int i = 0; i < 16; i++)
+ {
+ var slot = builder.GetSlot(i);
+ var prefix = $"material._Vertex_Deformation_Slot_{i}_";
+
+ SetFloatKey(clip, path, prefix + "Enabled", slot != null ? 1f : 0f, time);
+ SetDiscreteKey(clip, path, prefix + "Opcode", slot?.opcode ?? 0, time);
+ SetFloatKey(clip, path, prefix + "Float_0", slot?.float0 ?? 0f, time);
+ SetFloatKey(clip, path, prefix + "Float_1", slot?.float1 ?? 0f, time);
+ SetFloatKey(clip, path, prefix + "Float_2", slot?.float2 ?? 0f, time);
+ SetFloatKey(clip, path, prefix + "Float_3", slot?.float3 ?? 0f, time);
+ SetVectorKey(clip, path, prefix + "Vector_0", slot?.vec0 ?? Vector4.zero, time);
+ SetVectorKey(clip, path, prefix + "Vector_1", slot?.vec1 ?? Vector4.zero, time);
+ SetVectorKey(clip, path, prefix + "Vector_2", slot?.vec2 ?? Vector4.zero, time);
+ SetVectorKey(clip, path, prefix + "Vector_3", slot?.vec3 ?? Vector4.zero, time);
+ }
- SetFloatKey(clip, path, prefix + "Enabled", slot != null ? 1f : 0f, time);
- SetDiscreteKey(clip, path, prefix + "Opcode", slot?.opcode ?? 0, time);
- SetFloatKey(clip, path, prefix + "Float_0", slot?.float0 ?? 0f, time);
- SetFloatKey(clip, path, prefix + "Float_1", slot?.float1 ?? 0f, time);
- SetFloatKey(clip, path, prefix + "Float_2", slot?.float2 ?? 0f, time);
- SetFloatKey(clip, path, prefix + "Float_3", slot?.float3 ?? 0f, time);
- SetVectorKey(clip, path, prefix + "Vector_0", slot?.vec0 ?? Vector4.zero, time);
- SetVectorKey(clip, path, prefix + "Vector_1", slot?.vec1 ?? Vector4.zero, time);
- SetVectorKey(clip, path, prefix + "Vector_2", slot?.vec2 ?? Vector4.zero, time);
- SetVectorKey(clip, path, prefix + "Vector_3", slot?.vec3 ?? Vector4.zero, time);
+ // Pin the time so OnEditorUpdate doesn't treat the clip modification
+ // as a time change and reload (which would overwrite the UI state we
+ // just recorded from).
+ lastAnimTime = time;
+ }
+ finally
+ {
+ isRecording = false;
}
}
@@ -374,7 +437,11 @@ public class FoldEditorWindow : EditorWindow
}
var renderer = targetObject.GetComponent<Renderer>();
- if (renderer == null) return;
+ if (renderer == null)
+ {
+ Debug.LogWarning("Fold: Target object has no Renderer component.");
+ return;
+ }
if (!TryGetAnimationWindowState(out var clip, out float time))
return;
@@ -453,19 +520,51 @@ public class FoldEditorWindow : EditorWindow
return;
float frameDuration = 1f / clip.frameRate;
SetAnimationWindowTime(time + frames * frameDuration);
+ Repaint();
+ }
+
+ void SnapToNearestKeyframe()
+ {
+ if (!TryGetAnimationWindowState(out var clip, out float time))
+ return;
+
+ float bestTime = time;
+ float bestDist = float.MaxValue;
+
+ foreach (var binding in AnimationUtility.GetCurveBindings(clip))
+ {
+ if (!binding.propertyName.StartsWith("material._Vertex_Deformation_")) continue;
+
+ var curve = AnimationUtility.GetEditorCurve(clip, binding);
+ if (curve == null) continue;
+
+ for (int i = 0; i < curve.length; i++)
+ {
+ float dist = Mathf.Abs(curve.keys[i].time - time);
+ if (dist > 0f && dist < bestDist)
+ {
+ bestDist = dist;
+ bestTime = curve.keys[i].time;
+ }
+ }
+ }
+
+ if (bestDist < float.MaxValue)
+ {
+ SetAnimationWindowTime(bestTime);
+ Repaint();
+ }
}
static void SetAnimationWindowTime(float time)
{
- var windowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnimationWindow");
- if (windowType == null) return;
+ CacheAnimWindowReflection();
+ if (s_animWindowType == null || s_timeProp == null) return;
- var windows = Resources.FindObjectsOfTypeAll(windowType);
+ var windows = Resources.FindObjectsOfTypeAll(s_animWindowType);
if (windows.Length == 0) return;
- var timeProp = windowType.GetProperty("time",
- BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- timeProp?.SetValue(windows[0], time);
+ s_timeProp.SetValue(windows[0], time);
}
static bool TryGetAnimationWindowState(out AnimationClip clip, out float time)
@@ -473,21 +572,15 @@ public class FoldEditorWindow : EditorWindow
clip = null;
time = 0f;
- var windowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnimationWindow");
- if (windowType == null) return false;
+ CacheAnimWindowReflection();
+ if (s_animWindowType == null || s_clipProp == null || s_timeProp == null) return false;
- var windows = Resources.FindObjectsOfTypeAll(windowType);
+ var windows = Resources.FindObjectsOfTypeAll(s_animWindowType);
if (windows.Length == 0) return false;
var window = windows[0];
- const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
-
- var clipProp = windowType.GetProperty("animationClip", flags);
- var timeProp = windowType.GetProperty("time", flags);
- if (clipProp == null || timeProp == null) return false;
-
- clip = clipProp.GetValue(window) as AnimationClip;
- time = (float)timeProp.GetValue(window);
+ clip = s_clipProp.GetValue(window) as AnimationClip;
+ time = (float)s_timeProp.GetValue(window);
return clip != null;
}
@@ -521,32 +614,109 @@ public class FoldEditorWindow : EditorWindow
vec3 = targetMaterial.GetVector(prefix + "Vector_3"),
};
- DeformOperation op = opcode switch
+ var op = CreateOperationFromSlot(slot);
+ if (op == null) break;
+ operations.Add(op);
+ }
+ }
+
+ void LoadFromAnimationClip(AnimationClip clip, float time)
+ {
+ if (targetObject == null) return;
+ var renderer = targetObject.GetComponent<Renderer>();
+ if (renderer == null) return;
+
+ var animator = targetObject.GetComponentInParent<Animator>();
+ string path = animator != null
+ ? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform)
+ : "";
+
+ operations.Clear();
+
+ for (int i = 0; i < 16; i++)
+ {
+ var prefix = $"material._Vertex_Deformation_Slot_{i}_";
+
+ float enabled = SampleFloatCurve(clip, path, prefix + "Enabled", time);
+ if (enabled < 0.5f) break;
+
+ int opcode = SampleDiscreteCurve(clip, path, prefix + "Opcode", time);
+ if (opcode == FoldPipelineBuilder.Opcodes.None) break;
+
+ var slot = new FoldSlot
{
- FoldPipelineBuilder.Opcodes.TubeToPlane => new TubeToPlaneOp(slot),
- FoldPipelineBuilder.Opcodes.PlaneToTube => new PlaneToTubeOp(slot),
- FoldPipelineBuilder.Opcodes.PlaneToHemiOctahedron => new PlaneToHemiOctahedronOp(slot),
- FoldPipelineBuilder.Opcodes.HemiOctahedronToPlane => new HemiOctahedronToPlaneOp(slot),
- FoldPipelineBuilder.Opcodes.Scale => new ScaleOp(slot),
- FoldPipelineBuilder.Opcodes.Translate => new TranslateOp(slot),
- FoldPipelineBuilder.Opcodes.PlaneToOctahedron => new PlaneToOctahedronOp(slot),
- FoldPipelineBuilder.Opcodes.OctahedronToPlane => new OctahedronToPlaneOp(slot),
- FoldPipelineBuilder.Opcodes.Rotate => new RotateOp(slot),
- FoldPipelineBuilder.Opcodes.PointAlign => new PointAlignOp(slot),
- FoldPipelineBuilder.Opcodes.AxisAlign => new AxisAlignOp(slot),
- FoldPipelineBuilder.Opcodes.NormConversion => new NormConversionOp(slot),
- FoldPipelineBuilder.Opcodes.Seal => new SealOp(slot),
- FoldPipelineBuilder.Opcodes.SineWaves => new SineWavesOp(),
- FoldPipelineBuilder.Opcodes.FBM => new FBMOp(),
- _ => null
+ opcode = opcode,
+ float0 = SampleFloatCurve(clip, path, prefix + "Float_0", time),
+ float1 = SampleFloatCurve(clip, path, prefix + "Float_1", time),
+ float2 = SampleFloatCurve(clip, path, prefix + "Float_2", time),
+ float3 = SampleFloatCurve(clip, path, prefix + "Float_3", time),
+ vec0 = SampleVectorCurve(clip, path, prefix + "Vector_0", time),
+ vec1 = SampleVectorCurve(clip, path, prefix + "Vector_1", time),
+ vec2 = SampleVectorCurve(clip, path, prefix + "Vector_2", time),
+ vec3 = SampleVectorCurve(clip, path, prefix + "Vector_3", time),
};
- if (op == null)
- break;
-
+ var op = CreateOperationFromSlot(slot);
+ if (op == null) break;
operations.Add(op);
}
}
+
+ static float SampleFloatCurve(AnimationClip clip, string path, string prop, float time)
+ {
+ var binding = EditorCurveBinding.FloatCurve(path, typeof(Renderer), prop);
+ var curve = AnimationUtility.GetEditorCurve(clip, binding);
+ return curve?.Evaluate(time) ?? 0f;
+ }
+
+ static int SampleDiscreteCurve(AnimationClip clip, string path, string prop, float time)
+ {
+ var binding = EditorCurveBinding.DiscreteCurve(path, typeof(Renderer), prop);
+ var curve = AnimationUtility.GetEditorCurve(clip, binding);
+ if (curve == null || curve.length == 0) return 0;
+
+ // Step behavior: use the most recent keyframe at or before time.
+ var keys = curve.keys;
+ int value = Mathf.RoundToInt(keys[0].value);
+ for (int i = 0; i < keys.Length; i++)
+ {
+ if (keys[i].time > time) break;
+ value = Mathf.RoundToInt(keys[i].value);
+ }
+ return value;
+ }
+
+ static Vector4 SampleVectorCurve(AnimationClip clip, string path, string prop, float time)
+ {
+ return new Vector4(
+ SampleFloatCurve(clip, path, prop + ".x", time),
+ SampleFloatCurve(clip, path, prop + ".y", time),
+ SampleFloatCurve(clip, path, prop + ".z", time),
+ SampleFloatCurve(clip, path, prop + ".w", time));
+ }
+
+ static DeformOperation CreateOperationFromSlot(FoldSlot slot)
+ {
+ return slot.opcode switch
+ {
+ FoldPipelineBuilder.Opcodes.TubeToPlane => new TubeToPlaneOp(slot),
+ FoldPipelineBuilder.Opcodes.PlaneToTube => new PlaneToTubeOp(slot),
+ FoldPipelineBuilder.Opcodes.PlaneToHemiOctahedron => new PlaneToHemiOctahedronOp(slot),
+ FoldPipelineBuilder.Opcodes.HemiOctahedronToPlane => new HemiOctahedronToPlaneOp(slot),
+ FoldPipelineBuilder.Opcodes.Scale => new ScaleOp(slot),
+ FoldPipelineBuilder.Opcodes.Translate => new TranslateOp(slot),
+ FoldPipelineBuilder.Opcodes.PlaneToOctahedron => new PlaneToOctahedronOp(slot),
+ FoldPipelineBuilder.Opcodes.OctahedronToPlane => new OctahedronToPlaneOp(slot),
+ FoldPipelineBuilder.Opcodes.Rotate => new RotateOp(slot),
+ FoldPipelineBuilder.Opcodes.PointAlign => new PointAlignOp(slot),
+ FoldPipelineBuilder.Opcodes.AxisAlign => new AxisAlignOp(slot),
+ FoldPipelineBuilder.Opcodes.NormConversion => new NormConversionOp(slot),
+ FoldPipelineBuilder.Opcodes.Seal => new SealOp(slot),
+ FoldPipelineBuilder.Opcodes.SineWaves => new SineWavesOp(),
+ FoldPipelineBuilder.Opcodes.FBM => new FBMOp(),
+ _ => null
+ };
+ }
}
// Base class for deformation operations