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)); } // ─── Operation base class ───────────────────────────────────────────────────── [System.Serializable] public abstract class DeformOperation { public abstract string GetDisplayName(); public virtual void DrawParameters() { } public abstract void ApplyTo(FoldPipelineBuilder builder); public static DeformOperation Create(FoldSlot slot) => slot.opcode switch { TubeToPlaneOp.Opcode => new TubeToPlaneOp(slot), PlaneToTubeOp.Opcode => new PlaneToTubeOp(slot), PlaneToHemiOctahedronOp.Opcode => new PlaneToHemiOctahedronOp(slot), HemiOctahedronToPlaneOp.Opcode => new HemiOctahedronToPlaneOp(slot), ScaleOp.Opcode => new ScaleOp(slot), TranslateOp.Opcode => new TranslateOp(slot), PlaneToOctahedronOp.Opcode => new PlaneToOctahedronOp(slot), OctahedronToPlaneOp.Opcode => new OctahedronToPlaneOp(slot), RotateOp.Opcode => new RotateOp(slot), PointAlignOp.Opcode => new PointAlignOp(slot), AxisAlignOp.Opcode => new AxisAlignOp(slot), NormConversionOp.Opcode => new NormConversionOp(slot), SealOp.Opcode => new SealOp(slot), SineWavesOp.Opcode => new SineWavesOp(), FBMOp.Opcode => new FBMOp(), _ => null }; protected static void DrawPrsParams(ref Vector3 p, ref Vector3 r, ref Vector3 s, ref float t) { p = EditorGUILayout.Vector3Field("Origin (p)", p); r = EditorGUILayout.Vector3Field("R Axis", r); s = EditorGUILayout.Vector3Field("S Axis", s); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } protected static void DrawAlignParams(ref Vector3 po, ref Vector3 pp, ref Vector3 r, ref float t) { po = EditorGUILayout.Vector3Field("Origin Point (po)", po); pp = EditorGUILayout.Vector3Field("Target Point (pp)", pp); r = EditorGUILayout.Vector3Field("Rotation Axis (r)", r); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } } // ─── Concrete operations ────────────────────────────────────────────────────── [System.Serializable] public class TubeToPlaneOp : DeformOperation { public const int Opcode = 1; public Vector3 p = Vector3.zero, r = Vector3.right, s = Vector3.forward; public float t = 1f; public TubeToPlaneOp() { } public TubeToPlaneOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Tube to Plane"; public override void DrawParameters() => DrawPrsParams(ref p, ref r, ref s, ref t); public override void ApplyTo(FoldPipelineBuilder b) => b.TubeToPlane(p, r, s, t); } [System.Serializable] public class PlaneToTubeOp : DeformOperation { public const int Opcode = 2; public Vector3 p = Vector3.zero, r = Vector3.right, s = Vector3.forward; public float t = 1f; public PlaneToTubeOp() { } public PlaneToTubeOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Plane to Tube"; public override void DrawParameters() => DrawPrsParams(ref p, ref r, ref s, ref t); public override void ApplyTo(FoldPipelineBuilder b) => b.PlaneToTube(p, r, s, t); } [System.Serializable] public class PlaneToHemiOctahedronOp : DeformOperation { public const int Opcode = 9; public Vector3 p = Vector3.zero, r = Vector3.right, s = Vector3.forward; public float t = 1f; public PlaneToHemiOctahedronOp() { } public PlaneToHemiOctahedronOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Plane to Hemi-Octahedron"; public override void DrawParameters() => DrawPrsParams(ref p, ref r, ref s, ref t); public override void ApplyTo(FoldPipelineBuilder b) => b.PlaneToHemiOctahedron(p, r, s, t); } [System.Serializable] public class HemiOctahedronToPlaneOp : DeformOperation { public const int Opcode = 10; public Vector3 p = Vector3.zero, r = Vector3.right, s = Vector3.forward; public float t = 1f; public HemiOctahedronToPlaneOp() { } public HemiOctahedronToPlaneOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Hemi-Octahedron to Plane"; public override void DrawParameters() => DrawPrsParams(ref p, ref r, ref s, ref t); public override void ApplyTo(FoldPipelineBuilder b) => b.HemiOctahedronToPlane(p, r, s, t); } [System.Serializable] public class PlaneToOctahedronOp : DeformOperation { public const int Opcode = 13; public Vector3 p = Vector3.zero, r = Vector3.right, s = Vector3.forward; public float t = 1f; public PlaneToOctahedronOp() { } public PlaneToOctahedronOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Plane to Octahedron"; public override void DrawParameters() => DrawPrsParams(ref p, ref r, ref s, ref t); public override void ApplyTo(FoldPipelineBuilder b) => b.PlaneToOctahedron(p, r, s, t); } [System.Serializable] public class OctahedronToPlaneOp : DeformOperation { public const int Opcode = 14; public Vector3 p = Vector3.zero, r = Vector3.right, s = Vector3.forward; public float t = 1f; public OctahedronToPlaneOp() { } public OctahedronToPlaneOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Octahedron to Plane"; public override void DrawParameters() => DrawPrsParams(ref p, ref r, ref s, ref t); public override void ApplyTo(FoldPipelineBuilder b) => b.OctahedronToPlane(p, r, s, t); } [System.Serializable] public class PointAlignOp : DeformOperation { public const int Opcode = 3; public Vector3 po = Vector3.zero, pp = Vector3.up, r = Vector3.right; public float t = 1f; public PointAlignOp() { } public PointAlignOp(FoldSlot slot) { po = slot.vec0; pp = slot.vec1; r = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Point Align"; public override void DrawParameters() => DrawAlignParams(ref po, ref pp, ref r, ref t); public override void ApplyTo(FoldPipelineBuilder b) => b.PointAlign(po, pp, r, t); } [System.Serializable] public class AxisAlignOp : DeformOperation { public const int Opcode = 4; public Vector3 po = Vector3.zero, pp = Vector3.up, r = Vector3.right; public float t = 1f; public AxisAlignOp() { } public AxisAlignOp(FoldSlot slot) { po = slot.vec0; pp = slot.vec1; r = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Axis Align"; public override void DrawParameters() => DrawAlignParams(ref po, ref pp, ref r, ref t); public override void ApplyTo(FoldPipelineBuilder b) => b.AxisAlign(po, pp, r, t); } [System.Serializable] public class ScaleOp : DeformOperation { public const int Opcode = 11; public Vector3 k = Vector3.one; public float t = 1f; public ScaleOp() { } public ScaleOp(FoldSlot slot) { k = slot.vec0; t = slot.float0; } public override string GetDisplayName() => "Scale"; public override void DrawParameters() { k = EditorGUILayout.Vector3Field("Scale", k); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder b) => b.Scale(k, t); } [System.Serializable] public class TranslateOp : DeformOperation { public const int Opcode = 12; public Vector3 offset = Vector3.zero; public float t = 1f; public TranslateOp() { } public TranslateOp(FoldSlot slot) { offset = slot.vec0; t = slot.float0; } public override string GetDisplayName() => "Translate"; public override void DrawParameters() { offset = EditorGUILayout.Vector3Field("Offset", offset); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder b) => b.Translate(offset, t); } [System.Serializable] public class RotateOp : DeformOperation { public const int Opcode = 15; public Vector3 center = Vector3.zero, axis = Vector3.up; public float angleDeg = 90f, t = 1f; public RotateOp() { } public RotateOp(FoldSlot slot) { center = slot.vec0; axis = slot.vec1; angleDeg = slot.float0 * Mathf.Rad2Deg; t = slot.float1; } public override string GetDisplayName() => "Rotate"; public override void DrawParameters() { center = EditorGUILayout.Vector3Field("Center", center); axis = EditorGUILayout.Vector3Field("Axis", axis); angleDeg = EditorGUILayout.Slider("Angle", angleDeg, 0f, 360f); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder b) => b.Rotate(center, axis, angleDeg * Mathf.Deg2Rad, t); } [System.Serializable] public class NormConversionOp : DeformOperation { public const int Opcode = 5; public float inputK = 2f, outputK = 1f, t = 1f; public NormConversionOp() { } public NormConversionOp(FoldSlot slot) { inputK = slot.float0; outputK = slot.float1; t = slot.float2; } public override string GetDisplayName() => $"Norm Conversion (L{FormatNorm(inputK)}→L{FormatNorm(outputK)})"; string FormatNorm(float k) => float.IsPositiveInfinity(k) ? "∞" : k.ToString("F1"); public override void DrawParameters() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Input Norm (k)", GUILayout.Width(120)); inputK = EditorGUILayout.FloatField(inputK); if (GUILayout.Button("L∞", GUILayout.Width(30))) inputK = float.PositiveInfinity; EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Output Norm (k)", GUILayout.Width(120)); outputK = EditorGUILayout.FloatField(outputK); if (GUILayout.Button("L∞", GUILayout.Width(30))) outputK = float.PositiveInfinity; EditorGUILayout.EndHorizontal(); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); EditorGUILayout.HelpBox("Common: L1=diamond, L2=sphere, L∞=cube", MessageType.Info); } public override void ApplyTo(FoldPipelineBuilder b) => b.NormConversion(inputK, outputK, t); } [System.Serializable] public class SealOp : DeformOperation { public const int Opcode = 6; public float A = 0.1f, k = 2f, st = 0.8f, t = 1f; public SealOp() { } public SealOp(FoldSlot slot) { A = slot.float0; k = slot.float1; st = slot.float2; t = slot.float3; } public override string GetDisplayName() => "Seal"; public override void DrawParameters() { A = EditorGUILayout.FloatField("Amplitude (A)", A); k = EditorGUILayout.FloatField("Smoothness (k)", k); st = EditorGUILayout.FloatField("Scale (st)", st); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder b) => b.Seal(A, k, st, t); } [System.Serializable] public class SineWavesOp : DeformOperation { public const int Opcode = 7; public override string GetDisplayName() => "Sine Waves"; public override void DrawParameters() => EditorGUILayout.HelpBox("Uses shader-side parameters", MessageType.Info); public override void ApplyTo(FoldPipelineBuilder b) => b.SineWaves(); } [System.Serializable] public class FBMOp : DeformOperation { public const int Opcode = 8; public override string GetDisplayName() => "FBM (Fractal Brownian Motion)"; public override void DrawParameters() => EditorGUILayout.HelpBox("Uses shader-side parameters", MessageType.Info); public override void ApplyTo(FoldPipelineBuilder b) => b.FBM(); }