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 GUIStyle footerTextButton; 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; footerTextButton = new GUIStyle(EditorStyles.miniButton); footerTextButton.fixedHeight = 24; 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"); // Fallbacks if icons are missing 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; var savedExpanded = new List(expandedOps); LoadFromAnimationClip(clip, time); expandedOps = savedExpanded; expandedOps.RemoveAll(i => i >= operations.Count); 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); // Tint background if expanded var defaultColor = GUI.backgroundColor; if (isExpanded) GUI.backgroundColor = new Color(0.9f, 0.95f, 1f); // Light blue tint EditorGUILayout.BeginVertical(Styles.card); GUI.backgroundColor = defaultColor; // Reset for content // Header Rect Rect headerRect = EditorGUILayout.GetControlRect(false, 24); // Background for header if (Event.current.type == EventType.Repaint) { Styles.cardHeader.Draw(headerRect, GUIContent.none, false, false, false, false); } // Calculate rects for controls inside the header float btnWidth = 24; float btnHeight = 18; float btnSpacing = 0; float rightPadding = 5; float rightX = headerRect.xMax - rightPadding; float btnY = headerRect.y + (headerRect.height - btnHeight) / 2; Rect removeRect = new Rect(rightX - btnWidth, btnY, btnWidth, btnHeight); Rect downRect = new Rect(removeRect.x - btnWidth - btnSpacing, btnY, btnWidth, btnHeight); Rect upRect = new Rect(downRect.x - btnWidth - btnSpacing, btnY, btnWidth, btnHeight); // Labels 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); // Foldout Click Area (covers everything except buttons) 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); } // Draw Labels GUI.Label(arrowRect, isExpanded ? "▼" : "▶", EditorStyles.label); GUI.Label(indexRect, $"#{index}", EditorStyles.miniLabel); GUI.Label(labelRect, op.GetDisplayName(), EditorStyles.boldLabel); // Draw Buttons if (GUI.Button(upRect, Styles.iconUp, Styles.miniButtonLeft)) { if (index > 0) { operations.RemoveAt(index); operations.Insert(index - 1, op); if (expandedOps.Contains(index)) { expandedOps.Remove(index); expandedOps.Add(index - 1); } } } if (GUI.Button(downRect, Styles.iconDown, Styles.miniButtonMid)) { if (index < operations.Count - 1) { operations.RemoveAt(index); operations.Insert(index + 1, op); if (expandedOps.Contains(index)) { 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]--; } } // Parameters Body 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); // Row 1: Keyframe & Animation Tools 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); // Row 2: Timeline Tools 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); centerField.alignment = TextAnchor.MiddleCenter; centerField.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(); // Row 3: Info 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); var prefix = $"_Vertex_Deformation_Slot_{i}_"; mpb.SetFloat(prefix + "Enabled", slot != null ? 1f : 0f); mpb.SetInteger(prefix + "Opcode", slot?.opcode ?? 0); mpb.SetFloat(prefix + "Float_0", slot?.float0 ?? 0f); mpb.SetFloat(prefix + "Float_1", slot?.float1 ?? 0f); mpb.SetFloat(prefix + "Float_2", slot?.float2 ?? 0f); mpb.SetFloat(prefix + "Float_3", slot?.float3 ?? 0f); mpb.SetVector(prefix + "Vector_0", slot?.vec0 ?? Vector4.zero); mpb.SetVector(prefix + "Vector_1", slot?.vec1 ?? Vector4.zero); mpb.SetVector(prefix + "Vector_2", slot?.vec2 ?? Vector4.zero); mpb.SetVector(prefix + "Vector_3", slot?.vec3 ?? Vector4.zero); } 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; var savedExpanded = new List(expandedOps); LoadFromAnimationClip(clip, time); expandedOps = savedExpanded; expandedOps.RemoveAll(i => i >= operations.Count); lastAnimTime = time; ApplyToMaterial(); Repaint(); } void RecordAnimationKeyframes(FoldPipelineBuilder builder) { if (targetObject == null || !AnimationMode.InAnimationMode()) return; var renderer = targetObject.GetComponent(); if (renderer == null) return; if (!TryGetAnimationWindowState(out var clip, out float time)) return; var animator = targetObject.GetComponentInParent(); string path = animator != null ? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform) : ""; // Guard against OnEditorUpdate reloading a partially-written clip. 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); } // 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; } } void DeleteKeyframeAtCurrentTime() { if (targetObject == null || !AnimationMode.InAnimationMode()) { Debug.LogWarning("Fold: No target object set or Animation window is not recording."); return; } var renderer = targetObject.GetComponent(); if (renderer == null) { Debug.LogWarning("Fold: Target object has no Renderer component."); return; } if (!TryGetAnimationWindowState(out var clip, out float time)) return; var animator = targetObject.GetComponentInParent(); string path = animator != null ? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform) : ""; 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) { if (curve.length == 0) AnimationUtility.SetEditorCurve(clip, binding, null); else AnimationUtility.SetEditorCurve(clip, binding, curve); } } } 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 AdvancePlayhead(int frames) { if (!TryGetAnimationWindowState(out var clip, out float time)) return; float frameDuration = 1f / clip.frameRate; float newTime = time + frames * frameDuration; SetAnimationWindowTime(newTime); lastAnimTime = newTime; // Suppress OnEditorUpdate from reloading animation data. ApplyToMaterial(); // Re-apply current editor state so the scene stays in sync. 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); lastAnimTime = bestTime; // Suppress OnEditorUpdate from reloading animation data. ApplyToMaterial(); // Re-apply current editor state so the scene stays in sync. Repaint(); } } 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 == FoldPipelineBuilder.Opcodes.None) 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 = CreateOperationFromSlot(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 == FoldPipelineBuilder.Opcodes.None) 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 = 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 [System.Serializable] public abstract class DeformOperation { public abstract string GetDisplayName(); public abstract void DrawParameters(); public abstract void ApplyTo(FoldPipelineBuilder builder); } [System.Serializable] public class TubeToPlaneOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 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() { 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); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.TubeToPlane(p, r, s, t); } [System.Serializable] public class PlaneToTubeOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 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() { 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); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.PlaneToTube(p, r, s, t); } [System.Serializable] public class PlaneToHemiOctahedronOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 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() { 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); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.PlaneToHemiOctahedron(p, r, s, t); } [System.Serializable] public class ScaleOp : DeformOperation { 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 builder) => builder.Scale(k, t); } [System.Serializable] public class TranslateOp : DeformOperation { 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 builder) => builder.Translate(offset, t); } [System.Serializable] public class RotateOp : DeformOperation { public Vector3 center = Vector3.zero; public Vector3 axis = Vector3.up; public float angleDeg = 90f; public float 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 builder) => builder.Rotate(center, axis, angleDeg * Mathf.Deg2Rad, t); } [System.Serializable] public class HemiOctahedronToPlaneOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 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() { 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); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.HemiOctahedronToPlane(p, r, s, t); } [System.Serializable] public class PlaneToOctahedronOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 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() { 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); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.PlaneToOctahedron(p, r, s, t); } [System.Serializable] public class OctahedronToPlaneOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 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() { 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); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.OctahedronToPlane(p, r, s, t); } [System.Serializable] public class PointAlignOp : DeformOperation { public Vector3 po = Vector3.zero; public Vector3 pp = Vector3.up; public Vector3 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() { 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); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.PointAlign(po, pp, r, t); } [System.Serializable] public class AxisAlignOp : DeformOperation { public Vector3 po = Vector3.zero; public Vector3 pp = Vector3.up; public Vector3 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() { 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); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.AxisAlign(po, pp, r, t); } [System.Serializable] public class NormConversionOp : DeformOperation { public float inputK = 2f; public float outputK = 1f; public float 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) { if (float.IsPositiveInfinity(k)) return "∞"; return 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 builder) => builder.NormConversion(inputK, outputK, t); } [System.Serializable] public class SealOp : DeformOperation { public float A = 0.1f; public float k = 2f; public float st = 0.8f; public float 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 builder) => builder.Seal(A, k, st, t); } [System.Serializable] public class SineWavesOp : DeformOperation { public override string GetDisplayName() => "Sine Waves"; public override void DrawParameters() { EditorGUILayout.HelpBox("Sine waves use shader-side parameters", MessageType.Info); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.SineWaves(); } [System.Serializable] public class FBMOp : DeformOperation { public override string GetDisplayName() => "FBM (Fractal Brownian Motion)"; public override void DrawParameters() { EditorGUILayout.HelpBox("FBM uses shader-side parameters", MessageType.Info); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.FBM(); }