yum/3ner

A toon shader for Unity's BIRP.

git clone https://git.yummers.dev/yum/3ner

yumFold: move operations into separate filec09509e

master
27.5 KiB710 linesraw
1using System;
2using System.Collections.Generic;
3using System.Reflection;
4using UnityEditor;
5using UnityEngine;
6
7public class FoldEditorWindow : EditorWindow
8{
9    [SerializeField] Material targetMaterial;
10    [SerializeField] Vector2 scrollPos;
11    [SerializeReference] List<DeformOperation> operations = new();
12    [SerializeField] List<int> expandedOps = new();
13    [SerializeField] GameObject targetObject;
14    int frameStep;
15    bool wasInAnimationMode;
16    float lastAnimTime = -1f;
17    bool isRecording;
18
19    const string FrameStepPrefKey = "Fold_FrameStep";
20
21    static Type s_animWindowType;
22    static PropertyInfo s_clipProp, s_timeProp;
23
24    static class Styles
25    {
26        public static GUIStyle card;
27        public static GUIStyle cardHeader;
28        public static GUIStyle cardBody;
29        public static GUIStyle miniButtonLeft;
30        public static GUIStyle miniButtonMid;
31        public static GUIStyle miniButtonRight;
32        public static GUIStyle footerButton;
33
34        public static GUIContent iconUp;
35        public static GUIContent iconDown;
36        public static GUIContent iconRemove;
37        public static GUIContent iconAdd;
38        public static GUIContent iconKey;
39        public static GUIContent iconRead;
40        public static GUIContent iconDeleteKey;
41        public static GUIContent iconClear;
42        public static GUIContent iconPrev;
43        public static GUIContent iconNext;
44
45        public static void Init()
46        {
47            if (card != null) return;
48
49            card = new GUIStyle(EditorStyles.helpBox);
50            card.padding = new RectOffset(1, 1, 1, 1);
51            card.margin = new RectOffset(4, 4, 4, 4);
52
53            cardHeader = new GUIStyle(EditorStyles.toolbar);
54            cardHeader.fontStyle = FontStyle.Bold;
55            cardHeader.alignment = TextAnchor.MiddleLeft;
56            cardHeader.padding = new RectOffset(5, 5, 0, 0);
57            cardHeader.fixedHeight = 24;
58
59            cardBody = new GUIStyle(EditorStyles.inspectorDefaultMargins);
60            cardBody.padding = new RectOffset(10, 10, 10, 10);
61
62            miniButtonLeft = EditorStyles.miniButtonLeft;
63            miniButtonMid = EditorStyles.miniButtonMid;
64            miniButtonRight = EditorStyles.miniButtonRight;
65
66            footerButton = new GUIStyle(EditorStyles.miniButton);
67            footerButton.fixedHeight = 24;
68            footerButton.fixedWidth = 32;
69
70            iconUp        = EditorGUIUtility.IconContent("d_scrollup@2x");
71            iconDown      = EditorGUIUtility.IconContent("d_scrolldown@2x");
72            iconRemove    = EditorGUIUtility.IconContent("TreeEditor.Trash");
73            iconAdd       = EditorGUIUtility.IconContent("Toolbar Plus");
74            iconKey       = EditorGUIUtility.IconContent("Animation.Record");
75            iconRead      = EditorGUIUtility.IconContent("d_Import");
76            iconDeleteKey = EditorGUIUtility.IconContent("d_Toolbar Minus");
77            iconClear     = EditorGUIUtility.IconContent("TreeEditor.Trash");
78            iconPrev      = EditorGUIUtility.IconContent("Animation.PrevKey");
79            iconNext      = EditorGUIUtility.IconContent("Animation.NextKey");
80
81            if (iconUp.image == null)        iconUp.text = "▲";
82            if (iconDown.image == null)      iconDown.text = "▼";
83            if (iconKey.image == null)       iconKey.text = "Rec";
84            if (iconRead.image == null)      iconRead.text = "Load";
85            if (iconDeleteKey.image == null) iconDeleteKey.text = "-";
86        }
87    }
88
89    static void CacheAnimWindowReflection()
90    {
91        if (s_animWindowType != null) return;
92        s_animWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnimationWindow");
93        const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
94        s_clipProp = s_animWindowType?.GetProperty("animationClip", flags);
95        s_timeProp = s_animWindowType?.GetProperty("time", flags);
96    }
97
98    [MenuItem("Tools/yum_food/Fold")]
99    static void ShowWindow()
100    {
101        var window = GetWindow<FoldEditorWindow>("Fold");
102        window.minSize = new Vector2(350, 400);
103    }
104
105    void OnEnable()
106    {
107        frameStep = EditorPrefs.GetInt(FrameStepPrefKey, 1);
108        EditorApplication.update += OnEditorUpdate;
109    }
110
111    void OnDisable()
112    {
113        EditorApplication.update -= OnEditorUpdate;
114        EditorPrefs.SetInt(FrameStepPrefKey, frameStep);
115        ClearPropertyBlock();
116    }
117
118    void OnEditorUpdate()
119    {
120        if (isRecording) return;
121
122        if (!AnimationMode.InAnimationMode() || targetMaterial == null)
123        {
124            lastAnimTime = -1f;
125            return;
126        }
127
128        if (!TryGetAnimationWindowState(out var clip, out float time)) return;
129
130        if (!Mathf.Approximately(time, lastAnimTime))
131        {
132            lastAnimTime = time;
133            LoadFromClipPreservingExpanded(clip, time);
134            Repaint();
135        }
136    }
137
138    void OnGUI()
139    {
140        Styles.Init();
141
142        bool inAnimMode = AnimationMode.InAnimationMode();
143        if (wasInAnimationMode && !inAnimMode)
144            ClearPropertyBlock();
145        wasInAnimationMode = inAnimMode;
146
147        EditorGUILayout.Space(5);
148        DrawHeader();
149        EditorGUILayout.Space(5);
150
151        if (targetMaterial == null)
152        {
153            EditorGUILayout.HelpBox("Select a material to build deformation pipelines", MessageType.Info);
154            return;
155        }
156
157        DrawToolbar();
158
159        EditorGUI.BeginChangeCheck();
160        scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
161        DrawOperationsList();
162        EditorGUILayout.EndScrollView();
163        if (EditorGUI.EndChangeCheck() && targetMaterial != null)
164            ApplyToMaterial();
165
166        DrawFooter();
167        EditorGUILayout.Space(5);
168    }
169
170    void DrawHeader()
171    {
172        EditorGUILayout.BeginVertical(EditorStyles.inspectorDefaultMargins);
173
174        EditorGUILayout.BeginHorizontal();
175        EditorGUILayout.LabelField("Target Material", EditorStyles.boldLabel, GUILayout.Width(100));
176        var newMat = EditorGUILayout.ObjectField(targetMaterial, typeof(Material), false) as Material;
177        if (newMat != targetMaterial)
178        {
179            targetMaterial = newMat;
180            if (targetMaterial != null)
181                LoadFromMaterial();
182        }
183        EditorGUILayout.EndHorizontal();
184
185        EditorGUILayout.BeginHorizontal();
186        EditorGUILayout.LabelField("Target Object", EditorStyles.boldLabel, GUILayout.Width(100));
187        targetObject = EditorGUILayout.ObjectField(targetObject, typeof(GameObject), true) as GameObject;
188        EditorGUILayout.EndHorizontal();
189
190        EditorGUILayout.EndVertical();
191    }
192
193    void DrawToolbar()
194    {
195        EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
196
197        bool pipelineFull = operations.Count >= 16;
198        using (new EditorGUI.DisabledScope(pipelineFull))
199        {
200            var content = pipelineFull
201                ? new GUIContent("Add Operation (Full)", "Pipeline full (16/16)")
202                : new GUIContent(" Add Operation", Styles.iconAdd.image);
203            if (GUILayout.Button(content, EditorStyles.toolbarDropDown, GUILayout.Width(130)))
204                ShowAddOperationMenu();
205        }
206
207        GUILayout.FlexibleSpace();
208        EditorGUILayout.EndHorizontal();
209    }
210
211    void DrawOperationsList()
212    {
213        if (operations.Count == 0)
214        {
215            GUILayout.BeginVertical(Styles.card);
216            GUILayout.Label("Pipeline is empty.", EditorStyles.centeredGreyMiniLabel);
217            GUILayout.EndVertical();
218            return;
219        }
220
221        for (int i = 0; i < operations.Count; i++)
222            DrawOperation(i);
223    }
224
225    void DrawOperation(int index)
226    {
227        var op = operations[index];
228        bool isExpanded = expandedOps.Contains(index);
229
230        var defaultColor = GUI.backgroundColor;
231        if (isExpanded) GUI.backgroundColor = new Color(0.9f, 0.95f, 1f);
232
233        EditorGUILayout.BeginVertical(Styles.card);
234        GUI.backgroundColor = defaultColor;
235
236        Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
237        if (Event.current.type == EventType.Repaint)
238            Styles.cardHeader.Draw(headerRect, GUIContent.none, false, false, false, false);
239
240        float btnWidth = 24, btnHeight = 18;
241        float btnY = headerRect.y + (headerRect.height - btnHeight) / 2;
242        float rightX = headerRect.xMax - 5;
243
244        Rect removeRect = new Rect(rightX - btnWidth,       btnY, btnWidth, btnHeight);
245        Rect downRect   = new Rect(removeRect.x - btnWidth, btnY, btnWidth, btnHeight);
246        Rect upRect     = new Rect(downRect.x - btnWidth,   btnY, btnWidth, btnHeight);
247
248        Rect arrowRect = new Rect(headerRect.x + 5,  headerRect.y + 4, 15, 16);
249        Rect indexRect = new Rect(headerRect.x + 20, headerRect.y + 4, 30, 16);
250        Rect labelRect = new Rect(headerRect.x + 50, headerRect.y + 4, upRect.x - (headerRect.x + 50), 16);
251        Rect clickRect = new Rect(headerRect.x,      headerRect.y, upRect.x - headerRect.x, headerRect.height);
252
253        if (GUI.Button(clickRect, GUIContent.none, GUIStyle.none))
254        {
255            if (isExpanded) expandedOps.Remove(index);
256            else expandedOps.Add(index);
257        }
258
259        GUI.Label(arrowRect, isExpanded ? "▼" : "▶", EditorStyles.label);
260        GUI.Label(indexRect, $"#{index}", EditorStyles.miniLabel);
261        GUI.Label(labelRect, op.GetDisplayName(), EditorStyles.boldLabel);
262
263        if (GUI.Button(upRect, Styles.iconUp, Styles.miniButtonLeft) && index > 0)
264        {
265            operations.RemoveAt(index);
266            operations.Insert(index - 1, op);
267            if (expandedOps.Remove(index)) expandedOps.Add(index - 1);
268        }
269        if (GUI.Button(downRect, Styles.iconDown, Styles.miniButtonMid) && index < operations.Count - 1)
270        {
271            operations.RemoveAt(index);
272            operations.Insert(index + 1, op);
273            if (expandedOps.Remove(index)) expandedOps.Add(index + 1);
274        }
275        if (GUI.Button(removeRect, Styles.iconRemove, Styles.miniButtonRight))
276        {
277            if (EditorUtility.DisplayDialog("Remove Operation", $"Remove {op.GetDisplayName()}?", "Yes", "Cancel"))
278            {
279                operations.RemoveAt(index);
280                expandedOps.Remove(index);
281                for (int i = 0; i < expandedOps.Count; i++)
282                    if (expandedOps[i] > index) expandedOps[i]--;
283            }
284        }
285
286        if (isExpanded)
287        {
288            EditorGUILayout.BeginVertical(Styles.cardBody);
289            EditorGUIUtility.labelWidth = 140;
290            op.DrawParameters();
291            EditorGUIUtility.labelWidth = 0;
292            EditorGUILayout.EndVertical();
293        }
294
295        EditorGUILayout.EndVertical();
296    }
297
298    void DrawFooter()
299    {
300        EditorGUILayout.BeginVertical(EditorStyles.inspectorDefaultMargins);
301        EditorGUILayout.Space(5);
302
303        EditorGUILayout.BeginHorizontal();
304        if (GUILayout.Button(new GUIContent(Styles.iconRead.image, "Read from Playhead"), Styles.miniButtonLeft, GUILayout.Width(35), GUILayout.Height(24)))
305            ReadFromPlayhead();
306        if (GUILayout.Button(new GUIContent(Styles.iconKey.image, "Record Keyframe"), Styles.miniButtonMid, GUILayout.Width(35), GUILayout.Height(24)))
307            ApplyToMaterial(recordKeyframes: true);
308        if (GUILayout.Button(new GUIContent(Styles.iconDeleteKey.image, "Delete Keyframe"), Styles.miniButtonMid, GUILayout.Width(35), GUILayout.Height(24)))
309            DeleteKeyframeAtCurrentTime();
310        if (GUILayout.Button(new GUIContent("Snap", "Snap to nearest keyframe"), Styles.miniButtonRight, GUILayout.Width(50), GUILayout.Height(24)))
311            SnapToNearestKeyframe();
312
313        GUILayout.FlexibleSpace();
314
315        if (GUILayout.Button(new GUIContent(Styles.iconClear.image, "Clear All"), Styles.footerButton))
316        {
317            if (EditorUtility.DisplayDialog("Clear All Operations", "Remove all operations from the pipeline?", "Clear", "Cancel"))
318            {
319                operations.Clear();
320                expandedOps.Clear();
321                if (targetMaterial != null)
322                    ApplyToMaterial();
323            }
324        }
325        EditorGUILayout.EndHorizontal();
326
327        EditorGUILayout.Space(4);
328
329        EditorGUILayout.BeginHorizontal();
330        if (GUILayout.Button(new GUIContent(Styles.iconPrev.image, "Step Back"), Styles.miniButtonLeft, GUILayout.Width(35), GUILayout.Height(24)))
331            AdvancePlayhead(-frameStep);
332        var centerField = new GUIStyle(EditorStyles.numberField) { alignment = TextAnchor.MiddleCenter, fixedHeight = 24 };
333        frameStep = EditorGUILayout.IntField(frameStep, centerField, GUILayout.Width(40));
334        if (frameStep < 1) frameStep = 1;
335        if (GUILayout.Button(new GUIContent(Styles.iconNext.image, "Step Forward"), Styles.miniButtonRight, GUILayout.Width(35), GUILayout.Height(24)))
336            AdvancePlayhead(frameStep);
337        GUILayout.FlexibleSpace();
338        EditorGUILayout.EndHorizontal();
339
340        EditorGUILayout.Space(5);
341        EditorGUILayout.LabelField($"{operations.Count} / 16 Operations", EditorStyles.centeredGreyMiniLabel);
342        EditorGUILayout.EndVertical();
343    }
344
345    void ShowAddOperationMenu()
346    {
347        var menu = new GenericMenu();
348        menu.AddItem(new GUIContent("Tube to Plane"),            false, () => AddOperation(new TubeToPlaneOp()));
349        menu.AddItem(new GUIContent("Plane to Tube"),            false, () => AddOperation(new PlaneToTubeOp()));
350        menu.AddItem(new GUIContent("Plane to Hemi-Octahedron"), false, () => AddOperation(new PlaneToHemiOctahedronOp()));
351        menu.AddItem(new GUIContent("Hemi-Octahedron to Plane"), false, () => AddOperation(new HemiOctahedronToPlaneOp()));
352        menu.AddItem(new GUIContent("Plane to Octahedron"),      false, () => AddOperation(new PlaneToOctahedronOp()));
353        menu.AddItem(new GUIContent("Octahedron to Plane"),      false, () => AddOperation(new OctahedronToPlaneOp()));
354        menu.AddSeparator("");
355        menu.AddItem(new GUIContent("Point Align"),              false, () => AddOperation(new PointAlignOp()));
356        menu.AddItem(new GUIContent("Axis Align"),               false, () => AddOperation(new AxisAlignOp()));
357        menu.AddSeparator("");
358        menu.AddItem(new GUIContent("Scale"),                    false, () => AddOperation(new ScaleOp()));
359        menu.AddItem(new GUIContent("Translate"),                false, () => AddOperation(new TranslateOp()));
360        menu.AddItem(new GUIContent("Rotate"),                   false, () => AddOperation(new RotateOp()));
361        menu.AddItem(new GUIContent("Norm Conversion"),          false, () => AddOperation(new NormConversionOp()));
362        menu.AddItem(new GUIContent("Seal"),                     false, () => AddOperation(new SealOp()));
363        menu.AddSeparator("");
364        menu.AddItem(new GUIContent("Sine Waves"),               false, () => AddOperation(new SineWavesOp()));
365        menu.AddItem(new GUIContent("FBM"),                      false, () => AddOperation(new FBMOp()));
366        menu.ShowAsContext();
367    }
368
369    void AddOperation(DeformOperation op)
370    {
371        if (operations.Count >= 16) return;
372        operations.Add(op);
373        expandedOps.Add(operations.Count - 1);
374    }
375
376    void ApplyToMaterial(bool recordKeyframes = false)
377    {
378        var builder = FoldPipelineBuilder.Create().For(targetMaterial);
379        foreach (var op in operations)
380            op.ApplyTo(builder);
381        builder.Apply();
382
383        if (AnimationMode.InAnimationMode())
384            ApplyPropertyBlock(builder);
385        if (recordKeyframes)
386            RecordAnimationKeyframes(builder);
387    }
388
389    void ApplyPropertyBlock(FoldPipelineBuilder builder)
390    {
391        if (targetObject == null) return;
392        var renderer = targetObject.GetComponent<Renderer>();
393        if (renderer == null) return;
394
395        var mpb = new MaterialPropertyBlock();
396        renderer.GetPropertyBlock(mpb);
397        mpb.SetFloat("_Vertex_Deformation_Enabled", 1f);
398        for (int i = 0; i < 16; i++)
399        {
400            var slot = builder.GetSlot(i);
401            if (slot != null) slot.ApplyToPropertyBlock(mpb, i);
402            else FoldSlot.ClearInPropertyBlock(mpb, i);
403        }
404        renderer.SetPropertyBlock(mpb);
405    }
406
407    void ClearPropertyBlock()
408    {
409        if (targetObject == null) return;
410        var renderer = targetObject.GetComponent<Renderer>();
411        if (renderer != null)
412            renderer.SetPropertyBlock(null);
413    }
414
415    #region Animation Recording
416
417    void ReadFromPlayhead()
418    {
419        if (!TryGetAnimationWindowState(out var clip, out float time)) return;
420        LoadFromClipPreservingExpanded(clip, time);
421        lastAnimTime = time;
422        ApplyToMaterial();
423        Repaint();
424    }
425
426    void LoadFromClipPreservingExpanded(AnimationClip clip, float time)
427    {
428        var savedExpanded = new List<int>(expandedOps);
429        LoadFromAnimationClip(clip, time);
430        expandedOps = savedExpanded;
431        expandedOps.RemoveAll(i => i >= operations.Count);
432    }
433
434    void RecordAnimationKeyframes(FoldPipelineBuilder builder)
435    {
436        if (!TryGetRecordingState(out var clip, out float time, out _, out string path)) return;
437
438        isRecording = true;
439        try
440        {
441            Undo.RecordObject(clip, "Create Fold Keyframe");
442            SetFloatKey(clip, path, "material._Vertex_Deformation_Enabled", 1f, time);
443
444            for (int i = 0; i < 16; i++)
445            {
446                var slot = builder.GetSlot(i);
447                var prefix = $"material._Vertex_Deformation_Slot_{i}_";
448                SetFloatKey(clip, path, prefix + "Enabled", slot != null ? 1f : 0f, time);
449                SetDiscreteKey(clip, path, prefix + "Opcode", slot?.opcode ?? 0, time);
450                SetFloatKey(clip, path, prefix + "Float_0", slot?.float0 ?? 0f, time);
451                SetFloatKey(clip, path, prefix + "Float_1", slot?.float1 ?? 0f, time);
452                SetFloatKey(clip, path, prefix + "Float_2", slot?.float2 ?? 0f, time);
453                SetFloatKey(clip, path, prefix + "Float_3", slot?.float3 ?? 0f, time);
454                SetVectorKey(clip, path, prefix + "Vector_0", slot?.vec0 ?? Vector4.zero, time);
455                SetVectorKey(clip, path, prefix + "Vector_1", slot?.vec1 ?? Vector4.zero, time);
456                SetVectorKey(clip, path, prefix + "Vector_2", slot?.vec2 ?? Vector4.zero, time);
457                SetVectorKey(clip, path, prefix + "Vector_3", slot?.vec3 ?? Vector4.zero, time);
458            }
459
460            lastAnimTime = time;
461        }
462        finally
463        {
464            isRecording = false;
465        }
466    }
467
468    void DeleteKeyframeAtCurrentTime()
469    {
470        if (!TryGetRecordingState(out var clip, out float time, out _, out string path)) return;
471
472        Undo.RecordObject(clip, "Delete Fold Keyframe");
473        foreach (var binding in AnimationUtility.GetCurveBindings(clip))
474        {
475            if (binding.type != typeof(Renderer)) continue;
476            if (binding.path != path) continue;
477            if (!binding.propertyName.StartsWith("material._Vertex_Deformation_")) continue;
478
479            var curve = AnimationUtility.GetEditorCurve(clip, binding);
480            if (curve == null) continue;
481
482            bool removed = false;
483            for (int i = curve.length - 1; i >= 0; i--)
484            {
485                if (Mathf.Approximately(curve.keys[i].time, time))
486                {
487                    curve.RemoveKey(i);
488                    removed = true;
489                }
490            }
491
492            if (removed)
493                AnimationUtility.SetEditorCurve(clip, binding, curve.length == 0 ? null : curve);
494        }
495    }
496
497    bool TryGetRecordingState(out AnimationClip clip, out float time, out Renderer renderer, out string path)
498    {
499        clip = null; time = 0f; renderer = null; path = "";
500        if (targetObject == null || !AnimationMode.InAnimationMode()) return false;
501        renderer = targetObject.GetComponent<Renderer>();
502        if (renderer == null) return false;
503        if (!TryGetAnimationWindowState(out clip, out time)) return false;
504        var animator = targetObject.GetComponentInParent<Animator>();
505        path = animator != null
506            ? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform)
507            : "";
508        return true;
509    }
510
511    static void SetFloatKey(AnimationClip clip, string path, string prop, float value, float time)
512    {
513        var binding = EditorCurveBinding.FloatCurve(path, typeof(Renderer), prop);
514        var curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve();
515        AddOrReplaceKey(curve, time, value);
516        AnimationUtility.SetEditorCurve(clip, binding, curve);
517    }
518
519    static void SetDiscreteKey(AnimationClip clip, string path, string prop, int value, float time)
520    {
521        var binding = EditorCurveBinding.DiscreteCurve(path, typeof(Renderer), prop);
522        var curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve();
523        AddOrReplaceKey(curve, time, value);
524        AnimationUtility.SetEditorCurve(clip, binding, curve);
525    }
526
527    static void SetVectorKey(AnimationClip clip, string path, string prop, Vector4 v, float time)
528    {
529        SetFloatKey(clip, path, prop + ".x", v.x, time);
530        SetFloatKey(clip, path, prop + ".y", v.y, time);
531        SetFloatKey(clip, path, prop + ".z", v.z, time);
532        SetFloatKey(clip, path, prop + ".w", v.w, time);
533    }
534
535    static void AddOrReplaceKey(AnimationCurve curve, float time, float value)
536    {
537        for (int i = curve.length - 1; i >= 0; i--)
538            if (Mathf.Approximately(curve.keys[i].time, time))
539                curve.RemoveKey(i);
540        curve.AddKey(new Keyframe(time, value));
541    }
542
543    void SeekTo(float time)
544    {
545        SetAnimationWindowTime(time);
546        lastAnimTime = time;
547        ApplyToMaterial();
548        Repaint();
549    }
550
551    void AdvancePlayhead(int frames)
552    {
553        if (!TryGetAnimationWindowState(out var clip, out float time)) return;
554        SeekTo(time + frames * (1f / clip.frameRate));
555    }
556
557    void SnapToNearestKeyframe()
558    {
559        if (!TryGetAnimationWindowState(out var clip, out float time)) return;
560
561        float bestTime = time;
562        float bestDist = float.MaxValue;
563
564        foreach (var binding in AnimationUtility.GetCurveBindings(clip))
565        {
566            if (!binding.propertyName.StartsWith("material._Vertex_Deformation_")) continue;
567            var curve = AnimationUtility.GetEditorCurve(clip, binding);
568            if (curve == null) continue;
569            foreach (var key in curve.keys)
570            {
571                float dist = Mathf.Abs(key.time - time);
572                if (dist > 0f && dist < bestDist) { bestDist = dist; bestTime = key.time; }
573            }
574        }
575
576        if (bestDist < float.MaxValue)
577            SeekTo(bestTime);
578    }
579
580    static void SetAnimationWindowTime(float time)
581    {
582        CacheAnimWindowReflection();
583        if (s_animWindowType == null || s_timeProp == null) return;
584        var windows = Resources.FindObjectsOfTypeAll(s_animWindowType);
585        if (windows.Length == 0) return;
586        s_timeProp.SetValue(windows[0], time);
587        ((EditorWindow)windows[0]).Repaint();
588    }
589
590    static bool TryGetAnimationWindowState(out AnimationClip clip, out float time)
591    {
592        clip = null;
593        time = 0f;
594        CacheAnimWindowReflection();
595        if (s_animWindowType == null || s_clipProp == null || s_timeProp == null) return false;
596        var windows = Resources.FindObjectsOfTypeAll(s_animWindowType);
597        if (windows.Length == 0) return false;
598        var window = windows[0];
599        clip = s_clipProp.GetValue(window) as AnimationClip;
600        time = (float)s_timeProp.GetValue(window);
601        return clip != null;
602    }
603
604    #endregion
605
606    void LoadFromMaterial()
607    {
608        operations.Clear();
609        expandedOps.Clear();
610
611        for (int i = 0; i < 16; i++)
612        {
613            var prefix = $"_Vertex_Deformation_Slot_{i}_";
614            if (targetMaterial.GetFloat(prefix + "Enabled") < 0.5f) break;
615
616            int opcode = targetMaterial.GetInteger(prefix + "Opcode");
617            if (opcode == 0) break;
618
619            var slot = new FoldSlot
620            {
621                opcode = opcode,
622                float0 = targetMaterial.GetFloat(prefix + "Float_0"),
623                float1 = targetMaterial.GetFloat(prefix + "Float_1"),
624                float2 = targetMaterial.GetFloat(prefix + "Float_2"),
625                float3 = targetMaterial.GetFloat(prefix + "Float_3"),
626                vec0   = targetMaterial.GetVector(prefix + "Vector_0"),
627                vec1   = targetMaterial.GetVector(prefix + "Vector_1"),
628                vec2   = targetMaterial.GetVector(prefix + "Vector_2"),
629                vec3   = targetMaterial.GetVector(prefix + "Vector_3"),
630            };
631
632            var op = DeformOperation.Create(slot);
633            if (op == null) break;
634            operations.Add(op);
635        }
636    }
637
638    void LoadFromAnimationClip(AnimationClip clip, float time)
639    {
640        if (targetObject == null) return;
641        var renderer = targetObject.GetComponent<Renderer>();
642        if (renderer == null) return;
643
644        var animator = targetObject.GetComponentInParent<Animator>();
645        string path = animator != null
646            ? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform)
647            : "";
648
649        operations.Clear();
650
651        for (int i = 0; i < 16; i++)
652        {
653            var prefix = $"material._Vertex_Deformation_Slot_{i}_";
654
655            float enabled = SampleFloatCurve(clip, path, prefix + "Enabled", time);
656            if (enabled < 0.5f) break;
657
658            int opcode = SampleDiscreteCurve(clip, path, prefix + "Opcode", time);
659            if (opcode == 0) break;
660
661            var slot = new FoldSlot
662            {
663                opcode = opcode,
664                float0 = SampleFloatCurve(clip, path, prefix + "Float_0", time),
665                float1 = SampleFloatCurve(clip, path, prefix + "Float_1", time),
666                float2 = SampleFloatCurve(clip, path, prefix + "Float_2", time),
667                float3 = SampleFloatCurve(clip, path, prefix + "Float_3", time),
668                vec0   = SampleVectorCurve(clip, path, prefix + "Vector_0", time),
669                vec1   = SampleVectorCurve(clip, path, prefix + "Vector_1", time),
670                vec2   = SampleVectorCurve(clip, path, prefix + "Vector_2", time),
671                vec3   = SampleVectorCurve(clip, path, prefix + "Vector_3", time),
672            };
673
674            var op = DeformOperation.Create(slot);
675            if (op == null) break;
676            operations.Add(op);
677        }
678    }
679
680    static float SampleFloatCurve(AnimationClip clip, string path, string prop, float time)
681    {
682        var binding = EditorCurveBinding.FloatCurve(path, typeof(Renderer), prop);
683        var curve = AnimationUtility.GetEditorCurve(clip, binding);
684        return curve?.Evaluate(time) ?? 0f;
685    }
686
687    static int SampleDiscreteCurve(AnimationClip clip, string path, string prop, float time)
688    {
689        var binding = EditorCurveBinding.DiscreteCurve(path, typeof(Renderer), prop);
690        var curve = AnimationUtility.GetEditorCurve(clip, binding);
691        if (curve == null || curve.length == 0) return 0;
692
693        int value = Mathf.RoundToInt(curve.keys[0].value);
694        foreach (var key in curve.keys)
695        {
696            if (key.time > time) break;
697            value = Mathf.RoundToInt(key.value);
698        }
699        return value;
700    }
701
702    static Vector4 SampleVectorCurve(AnimationClip clip, string path, string prop, float time) =>
703        new Vector4(
704            SampleFloatCurve(clip, path, prop + ".x", time),
705            SampleFloatCurve(clip, path, prop + ".y", time),
706            SampleFloatCurve(clip, path, prop + ".z", time),
707            SampleFloatCurve(clip, path, prop + ".w", time));
708
709}
710