using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; public class FoldEditorWindow : EditorWindow { [SerializeField] Material targetMaterial; [SerializeField] Vector2 scrollPos; [SerializeReference] List operations = new(); [SerializeField] List expandedOps = new(); [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 class Styles { public static GUIStyle card; public static GUIStyle cardHeader; public static GUIStyle cardBody; public static GUIStyle miniButtonLeft; public static GUIStyle miniButtonMid; public static GUIStyle miniButtonRight; public static GUIStyle footerButton; public static GUIContent iconUp; public static GUIContent iconDown; public static GUIContent iconRemove; public static GUIContent iconAdd; public static GUIContent iconKey; public static GUIContent iconRead; public static GUIContent iconDeleteKey; public static GUIContent iconClear; public static GUIContent iconPrev; public static GUIContent iconNext; public static void Init() { if (card != null) return; card = new GUIStyle(EditorStyles.helpBox); card.padding = new RectOffset(1, 1, 1, 1); card.margin = new RectOffset(4, 4, 4, 4); cardHeader = new GUIStyle(EditorStyles.toolbar); cardHeader.fontStyle = FontStyle.Bold; cardHeader.alignment = TextAnchor.MiddleLeft; cardHeader.padding = new RectOffset(5, 5, 0, 0); cardHeader.fixedHeight = 24; cardBody = new GUIStyle(EditorStyles.inspectorDefaultMargins); cardBody.padding = new RectOffset(10, 10, 10, 10); miniButtonLeft = EditorStyles.miniButtonLeft; miniButtonMid = EditorStyles.miniButtonMid; miniButtonRight = EditorStyles.miniButtonRight; footerButton = new GUIStyle(EditorStyles.miniButton); footerButton.fixedHeight = 24; footerButton.fixedWidth = 32; iconUp = EditorGUIUtility.IconContent("d_scrollup@2x"); iconDown = EditorGUIUtility.IconContent("d_scrolldown@2x"); iconRemove = EditorGUIUtility.IconContent("TreeEditor.Trash"); iconAdd = EditorGUIUtility.IconContent("Toolbar Plus"); iconKey = EditorGUIUtility.IconContent("Animation.Record"); iconRead = EditorGUIUtility.IconContent("d_Import"); iconDeleteKey = EditorGUIUtility.IconContent("d_Toolbar Minus"); iconClear = EditorGUIUtility.IconContent("TreeEditor.Trash"); iconPrev = EditorGUIUtility.IconContent("Animation.PrevKey"); iconNext = EditorGUIUtility.IconContent("Animation.NextKey"); if (iconUp.image == null) iconUp.text = "▲"; if (iconDown.image == null) iconDown.text = "▼"; if (iconKey.image == null) iconKey.text = "Rec"; if (iconRead.image == null) iconRead.text = "Load"; if (iconDeleteKey.image == null) iconDeleteKey.text = "-"; } } 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() { var window = GetWindow("Fold"); window.minSize = new Vector2(350, 400); } 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; LoadFromClipPreservingExpanded(clip, time); Repaint(); } } void OnGUI() { Styles.Init(); bool inAnimMode = AnimationMode.InAnimationMode(); if (wasInAnimationMode && !inAnimMode) ClearPropertyBlock(); wasInAnimationMode = inAnimMode; EditorGUILayout.Space(5); DrawHeader(); EditorGUILayout.Space(5); if (targetMaterial == null) { EditorGUILayout.HelpBox("Select a material to build deformation pipelines", MessageType.Info); return; } DrawToolbar(); EditorGUI.BeginChangeCheck(); scrollPos = EditorGUILayout.BeginScrollView(scrollPos); DrawOperationsList(); EditorGUILayout.EndScrollView(); if (EditorGUI.EndChangeCheck() && targetMaterial != null) ApplyToMaterial(); DrawFooter(); EditorGUILayout.Space(5); } void DrawHeader() { EditorGUILayout.BeginVertical(EditorStyles.inspectorDefaultMargins); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Target Material", EditorStyles.boldLabel, GUILayout.Width(100)); var newMat = EditorGUILayout.ObjectField(targetMaterial, typeof(Material), false) as Material; if (newMat != targetMaterial) { targetMaterial = newMat; if (targetMaterial != null) LoadFromMaterial(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Target Object", EditorStyles.boldLabel, GUILayout.Width(100)); targetObject = EditorGUILayout.ObjectField(targetObject, typeof(GameObject), true) as GameObject; EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); bool pipelineFull = operations.Count >= 16; using (new EditorGUI.DisabledScope(pipelineFull)) { var content = pipelineFull ? new GUIContent("Add Operation (Full)", "Pipeline full (16/16)") : new GUIContent(" Add Operation", Styles.iconAdd.image); if (GUILayout.Button(content, EditorStyles.toolbarDropDown, GUILayout.Width(130))) ShowAddOperationMenu(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } void DrawOperationsList() { if (operations.Count == 0) { GUILayout.BeginVertical(Styles.card); GUILayout.Label("Pipeline is empty.", EditorStyles.centeredGreyMiniLabel); GUILayout.EndVertical(); return; } for (int i = 0; i < operations.Count; i++) DrawOperation(i); } void DrawOperation(int index) { var op = operations[index]; bool isExpanded = expandedOps.Contains(index); var defaultColor = GUI.backgroundColor; if (isExpanded) GUI.backgroundColor = new Color(0.9f, 0.95f, 1f); EditorGUILayout.BeginVertical(Styles.card); GUI.backgroundColor = defaultColor; Rect headerRect = EditorGUILayout.GetControlRect(false, 24); if (Event.current.type == EventType.Repaint) Styles.cardHeader.Draw(headerRect, GUIContent.none, false, false, false, false); float btnWidth = 24, btnHeight = 18; float btnY = headerRect.y + (headerRect.height - btnHeight) / 2; float rightX = headerRect.xMax - 5; Rect removeRect = new Rect(rightX - btnWidth, btnY, btnWidth, btnHeight); Rect downRect = new Rect(removeRect.x - btnWidth, btnY, btnWidth, btnHeight); Rect upRect = new Rect(downRect.x - btnWidth, btnY, btnWidth, btnHeight); Rect arrowRect = new Rect(headerRect.x + 5, headerRect.y + 4, 15, 16); Rect indexRect = new Rect(headerRect.x + 20, headerRect.y + 4, 30, 16); Rect labelRect = new Rect(headerRect.x + 50, headerRect.y + 4, upRect.x - (headerRect.x + 50), 16); Rect clickRect = new Rect(headerRect.x, headerRect.y, upRect.x - headerRect.x, headerRect.height); if (GUI.Button(clickRect, GUIContent.none, GUIStyle.none)) { if (isExpanded) expandedOps.Remove(index); else expandedOps.Add(index); } GUI.Label(arrowRect, isExpanded ? "▼" : "▶", EditorStyles.label); GUI.Label(indexRect, $"#{index}", EditorStyles.miniLabel); GUI.Label(labelRect, op.GetDisplayName(), EditorStyles.boldLabel); if (GUI.Button(upRect, Styles.iconUp, Styles.miniButtonLeft) && index > 0) { operations.RemoveAt(index); operations.Insert(index - 1, op); if (expandedOps.Remove(index)) expandedOps.Add(index - 1); } if (GUI.Button(downRect, Styles.iconDown, Styles.miniButtonMid) && index < operations.Count - 1) { operations.RemoveAt(index); operations.Insert(index + 1, op); if (expandedOps.Remove(index)) expandedOps.Add(index + 1); } if (GUI.Button(removeRect, Styles.iconRemove, Styles.miniButtonRight)) { if (EditorUtility.DisplayDialog("Remove Operation", $"Remove {op.GetDisplayName()}?", "Yes", "Cancel")) { operations.RemoveAt(index); expandedOps.Remove(index); for (int i = 0; i < expandedOps.Count; i++) if (expandedOps[i] > index) expandedOps[i]--; } } if (isExpanded) { EditorGUILayout.BeginVertical(Styles.cardBody); EditorGUIUtility.labelWidth = 140; op.DrawParameters(); EditorGUIUtility.labelWidth = 0; EditorGUILayout.EndVertical(); } EditorGUILayout.EndVertical(); } void DrawFooter() { EditorGUILayout.BeginVertical(EditorStyles.inspectorDefaultMargins); EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(new GUIContent(Styles.iconRead.image, "Read from Playhead"), Styles.miniButtonLeft, GUILayout.Width(35), GUILayout.Height(24))) ReadFromPlayhead(); if (GUILayout.Button(new GUIContent(Styles.iconKey.image, "Record Keyframe"), Styles.miniButtonMid, GUILayout.Width(35), GUILayout.Height(24))) ApplyToMaterial(recordKeyframes: true); if (GUILayout.Button(new GUIContent(Styles.iconDeleteKey.image, "Delete Keyframe"), Styles.miniButtonMid, GUILayout.Width(35), GUILayout.Height(24))) DeleteKeyframeAtCurrentTime(); if (GUILayout.Button(new GUIContent("Snap", "Snap to nearest keyframe"), Styles.miniButtonRight, GUILayout.Width(50), GUILayout.Height(24))) SnapToNearestKeyframe(); GUILayout.FlexibleSpace(); if (GUILayout.Button(new GUIContent(Styles.iconClear.image, "Clear All"), Styles.footerButton)) { if (EditorUtility.DisplayDialog("Clear All Operations", "Remove all operations from the pipeline?", "Clear", "Cancel")) { operations.Clear(); expandedOps.Clear(); if (targetMaterial != null) ApplyToMaterial(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(new GUIContent(Styles.iconPrev.image, "Step Back"), Styles.miniButtonLeft, GUILayout.Width(35), GUILayout.Height(24))) AdvancePlayhead(-frameStep); var centerField = new GUIStyle(EditorStyles.numberField) { alignment = TextAnchor.MiddleCenter, fixedHeight = 24 }; frameStep = EditorGUILayout.IntField(frameStep, centerField, GUILayout.Width(40)); if (frameStep < 1) frameStep = 1; if (GUILayout.Button(new GUIContent(Styles.iconNext.image, "Step Forward"), Styles.miniButtonRight, GUILayout.Width(35), GUILayout.Height(24))) AdvancePlayhead(frameStep); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); EditorGUILayout.LabelField($"{operations.Count} / 16 Operations", EditorStyles.centeredGreyMiniLabel); EditorGUILayout.EndVertical(); } void ShowAddOperationMenu() { var menu = new GenericMenu(); menu.AddItem(new GUIContent("Tube to Plane"), false, () => AddOperation(new TubeToPlaneOp())); menu.AddItem(new GUIContent("Plane to Tube"), false, () => AddOperation(new PlaneToTubeOp())); menu.AddItem(new GUIContent("Plane to Hemi-Octahedron"), false, () => AddOperation(new PlaneToHemiOctahedronOp())); menu.AddItem(new GUIContent("Hemi-Octahedron to Plane"), false, () => AddOperation(new HemiOctahedronToPlaneOp())); menu.AddItem(new GUIContent("Plane to Octahedron"), false, () => AddOperation(new PlaneToOctahedronOp())); menu.AddItem(new GUIContent("Octahedron to Plane"), false, () => AddOperation(new OctahedronToPlaneOp())); menu.AddSeparator(""); menu.AddItem(new GUIContent("Point Align"), false, () => AddOperation(new PointAlignOp())); menu.AddItem(new GUIContent("Axis Align"), false, () => AddOperation(new AxisAlignOp())); menu.AddSeparator(""); menu.AddItem(new GUIContent("Scale"), false, () => AddOperation(new ScaleOp())); menu.AddItem(new GUIContent("Translate"), false, () => AddOperation(new TranslateOp())); menu.AddItem(new GUIContent("Rotate"), false, () => AddOperation(new RotateOp())); menu.AddItem(new GUIContent("Norm Conversion"), false, () => AddOperation(new NormConversionOp())); menu.AddItem(new GUIContent("Seal"), false, () => AddOperation(new SealOp())); menu.AddSeparator(""); menu.AddItem(new GUIContent("Sine Waves"), false, () => AddOperation(new SineWavesOp())); menu.AddItem(new GUIContent("FBM"), false, () => AddOperation(new FBMOp())); menu.ShowAsContext(); } void AddOperation(DeformOperation op) { if (operations.Count >= 16) return; operations.Add(op); expandedOps.Add(operations.Count - 1); } void ApplyToMaterial(bool recordKeyframes = false) { var builder = FoldPipelineBuilder.Create().For(targetMaterial); foreach (var op in operations) op.ApplyTo(builder); builder.Apply(); if (AnimationMode.InAnimationMode()) ApplyPropertyBlock(builder); if (recordKeyframes) RecordAnimationKeyframes(builder); } void ApplyPropertyBlock(FoldPipelineBuilder builder) { if (targetObject == null) return; var renderer = targetObject.GetComponent(); if (renderer == null) return; var mpb = new MaterialPropertyBlock(); renderer.GetPropertyBlock(mpb); mpb.SetFloat("_Vertex_Deformation_Enabled", 1f); for (int i = 0; i < 16; i++) { var slot = builder.GetSlot(i); if (slot != null) slot.ApplyToPropertyBlock(mpb, i); else FoldSlot.ClearInPropertyBlock(mpb, i); } renderer.SetPropertyBlock(mpb); } void ClearPropertyBlock() { if (targetObject == null) return; var renderer = targetObject.GetComponent(); if (renderer != null) renderer.SetPropertyBlock(null); } #region Animation Recording void ReadFromPlayhead() { if (!TryGetAnimationWindowState(out var clip, out float time)) return; LoadFromClipPreservingExpanded(clip, time); lastAnimTime = time; ApplyToMaterial(); Repaint(); } void LoadFromClipPreservingExpanded(AnimationClip clip, float time) { var savedExpanded = new List(expandedOps); LoadFromAnimationClip(clip, time); expandedOps = savedExpanded; expandedOps.RemoveAll(i => i >= operations.Count); } void RecordAnimationKeyframes(FoldPipelineBuilder builder) { if (!TryGetRecordingState(out var clip, out float time, out _, out string path)) return; isRecording = true; try { 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); } lastAnimTime = time; } finally { isRecording = false; } } void DeleteKeyframeAtCurrentTime() { if (!TryGetRecordingState(out var clip, out float time, out _, out string path)) return; Undo.RecordObject(clip, "Delete Fold Keyframe"); foreach (var binding in AnimationUtility.GetCurveBindings(clip)) { if (binding.type != typeof(Renderer)) continue; if (binding.path != path) continue; if (!binding.propertyName.StartsWith("material._Vertex_Deformation_")) continue; var curve = AnimationUtility.GetEditorCurve(clip, binding); if (curve == null) continue; bool removed = false; for (int i = curve.length - 1; i >= 0; i--) { if (Mathf.Approximately(curve.keys[i].time, time)) { curve.RemoveKey(i); removed = true; } } if (removed) AnimationUtility.SetEditorCurve(clip, binding, curve.length == 0 ? null : curve); } } bool TryGetRecordingState(out AnimationClip clip, out float time, out Renderer renderer, out string path) { clip = null; time = 0f; renderer = null; path = ""; if (targetObject == null || !AnimationMode.InAnimationMode()) return false; renderer = targetObject.GetComponent(); if (renderer == null) return false; if (!TryGetAnimationWindowState(out clip, out time)) return false; var animator = targetObject.GetComponentInParent(); path = animator != null ? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform) : ""; return true; } static void SetFloatKey(AnimationClip clip, string path, string prop, float value, float time) { var binding = EditorCurveBinding.FloatCurve(path, typeof(Renderer), prop); var curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve(); AddOrReplaceKey(curve, time, value); AnimationUtility.SetEditorCurve(clip, binding, curve); } static void SetDiscreteKey(AnimationClip clip, string path, string prop, int value, float time) { var binding = EditorCurveBinding.DiscreteCurve(path, typeof(Renderer), prop); var curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve(); AddOrReplaceKey(curve, time, value); AnimationUtility.SetEditorCurve(clip, binding, curve); } static void SetVectorKey(AnimationClip clip, string path, string prop, Vector4 v, float time) { SetFloatKey(clip, path, prop + ".x", v.x, time); SetFloatKey(clip, path, prop + ".y", v.y, time); SetFloatKey(clip, path, prop + ".z", v.z, time); SetFloatKey(clip, path, prop + ".w", v.w, time); } static void AddOrReplaceKey(AnimationCurve curve, float time, float value) { for (int i = curve.length - 1; i >= 0; i--) if (Mathf.Approximately(curve.keys[i].time, time)) curve.RemoveKey(i); curve.AddKey(new Keyframe(time, value)); } void SeekTo(float time) { SetAnimationWindowTime(time); lastAnimTime = time; ApplyToMaterial(); Repaint(); } void AdvancePlayhead(int frames) { if (!TryGetAnimationWindowState(out var clip, out float time)) return; SeekTo(time + frames * (1f / clip.frameRate)); } 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; foreach (var key in curve.keys) { float dist = Mathf.Abs(key.time - time); if (dist > 0f && dist < bestDist) { bestDist = dist; bestTime = key.time; } } } if (bestDist < float.MaxValue) SeekTo(bestTime); } static void SetAnimationWindowTime(float time) { CacheAnimWindowReflection(); if (s_animWindowType == null || s_timeProp == null) return; var windows = Resources.FindObjectsOfTypeAll(s_animWindowType); if (windows.Length == 0) return; s_timeProp.SetValue(windows[0], time); ((EditorWindow)windows[0]).Repaint(); } static bool TryGetAnimationWindowState(out AnimationClip clip, out float time) { clip = null; time = 0f; CacheAnimWindowReflection(); if (s_animWindowType == null || s_clipProp == null || s_timeProp == null) return false; var windows = Resources.FindObjectsOfTypeAll(s_animWindowType); if (windows.Length == 0) return false; var window = windows[0]; clip = s_clipProp.GetValue(window) as AnimationClip; time = (float)s_timeProp.GetValue(window); return clip != null; } #endregion void LoadFromMaterial() { operations.Clear(); expandedOps.Clear(); for (int i = 0; i < 16; i++) { var prefix = $"_Vertex_Deformation_Slot_{i}_"; if (targetMaterial.GetFloat(prefix + "Enabled") < 0.5f) break; int opcode = targetMaterial.GetInteger(prefix + "Opcode"); if (opcode == 0) break; var slot = new FoldSlot { opcode = opcode, float0 = targetMaterial.GetFloat(prefix + "Float_0"), float1 = targetMaterial.GetFloat(prefix + "Float_1"), float2 = targetMaterial.GetFloat(prefix + "Float_2"), float3 = targetMaterial.GetFloat(prefix + "Float_3"), vec0 = targetMaterial.GetVector(prefix + "Vector_0"), vec1 = targetMaterial.GetVector(prefix + "Vector_1"), vec2 = targetMaterial.GetVector(prefix + "Vector_2"), vec3 = targetMaterial.GetVector(prefix + "Vector_3"), }; var op = DeformOperation.Create(slot); if (op == null) break; operations.Add(op); } } void LoadFromAnimationClip(AnimationClip clip, float time) { if (targetObject == null) return; var renderer = targetObject.GetComponent(); if (renderer == null) return; var animator = targetObject.GetComponentInParent(); 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 == 0) break; var slot = new FoldSlot { 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), }; var op = DeformOperation.Create(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; int value = Mathf.RoundToInt(curve.keys[0].value); foreach (var key in curve.keys) { if (key.time > time) break; value = Mathf.RoundToInt(key.value); } return value; } static Vector4 SampleVectorCurve(AnimationClip clip, string path, string prop, float time) => 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)); }