diff options
| author | yum <yum.food.vr@gmail.com> | 2026-02-17 20:08:59 -0800 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2026-02-17 20:08:59 -0800 |
| commit | 75c5679714c339fced0a3769fa768f8cb98806d9 (patch) | |
| tree | 1327c8c673f4032bbbd20ecc5fd9716199941bf2 /Scripts/Fold/Editor | |
| parent | ec6c0dc6ed672129e5a773b2a3158443951ed101 (diff) | |
Fold: animation workflow bugfixes
Diffstat (limited to 'Scripts/Fold/Editor')
| -rwxr-xr-x | Scripts/Fold/Editor/FoldEditorWindow.cs | 282 |
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 |
