using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace Yum.Fold { // Translate authored keys directly into renderer material curves; no temporal sampling. public static class FoldBaker { public static AnimationClip BakeBesideSource(AnimationClip source, Transform root, float speed) { string sourcePath = AssetDatabase.GetAssetPath(source); if (string.IsNullOrEmpty(sourcePath) || !sourcePath.StartsWith("Assets/", StringComparison.Ordinal)) throw new InvalidOperationException("Save the source animation in Assets before baking."); string path = System.IO.Path.ChangeExtension(sourcePath, null) + "_Baked.anim"; var existing = AssetDatabase.LoadMainAssetAtPath(path); if (!existing) return Bake(source, root, path, speed); if (!(existing is AnimationClip destination)) throw new InvalidOperationException("The baked output path is occupied by a different asset type: " + path); // Finish conversion before touching the previous bake; preserve its GUID for Animator references. string temporary = AssetDatabase.GenerateUniqueAssetPath(System.IO.Path.ChangeExtension(sourcePath, null) + "_BakeTemp.anim"); var result = Bake(source, root, temporary, speed); try { var oldMaterials = AssetDatabase.LoadAllAssetsAtPath(path).OfType().ToArray(); EditorUtility.CopySerialized(result, destination); destination.name = System.IO.Path.GetFileNameWithoutExtension(path); foreach (var material in oldMaterials) UnityEngine.Object.DestroyImmediate(material, true); EditorUtility.SetDirty(destination); AssetDatabase.SaveAssets(); return destination; } finally { AssetDatabase.DeleteAsset(temporary); } } public static AnimationClip Bake(AnimationClip source, Transform root, string path, float speed = 1) { if (!source || float.IsNaN(speed) || speed < 0 || speed > 10) throw new ArgumentException("Choose a clip and a Bake speed from 0 to 10."); var timed = UnityEngine.Object.Instantiate(source); try { float duration = speed > 0 ? source.length / speed : Mathf.Max(source.length, 1f / 30); foreach (var binding in AnimationUtility.GetCurveBindings(source)) { var curve = AnimationUtility.GetEditorCurve(source, binding); if (speed == 0) { float value = binding.isDiscreteCurve ? curve.keys[0].value : curve.Evaluate(0); curve = AnimationCurve.Constant(0, duration, value); } else { var keys = curve.keys; for (int i = 0; i < keys.Length; i++) { keys[i].time /= speed; keys[i].inTangent *= speed; keys[i].outTangent *= speed; } curve.keys = keys; } AnimationUtility.SetEditorCurve(timed, binding, curve); } foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(source)) { var keys = AnimationUtility.GetObjectReferenceCurve(source, binding); if (speed == 0 && keys.Length > 0) keys = new[] { new ObjectReferenceKeyframe { time = 0, value = keys[0].value }, new ObjectReferenceKeyframe { time = duration, value = keys[0].value } }; else for (int i = 0; i < keys.Length; i++) keys[i].time /= speed; AnimationUtility.SetObjectReferenceCurve(timed, binding, keys); } var events = speed == 0 ? new AnimationEvent[0] : AnimationUtility.GetAnimationEvents(source); foreach (var entry in events) entry.time /= speed; AnimationUtility.SetAnimationEvents(timed, events); var settings = AnimationUtility.GetAnimationClipSettings(source); settings.startTime = speed > 0 ? settings.startTime / speed : 0; settings.stopTime = duration; AnimationUtility.SetAnimationClipSettings(timed, settings); return BakeCurves(timed, root, path); } finally { UnityEngine.Object.DestroyImmediate(timed); } } static AnimationClip BakeCurves(AnimationClip source, Transform root, string path) { if (!root) throw new ArgumentException("Choose the animation root."); if (!path.StartsWith("Assets/", StringComparison.Ordinal) || !path.EndsWith(".anim", StringComparison.OrdinalIgnoreCase) || AssetDatabase.LoadMainAssetAtPath(path)) throw new ArgumentException("Choose a new .anim asset path inside Assets."); var bindings = AnimationUtility.GetCurveBindings(source); var groups = bindings.Where(b => b.type == typeof(FoldMaterialAnimator)).GroupBy(b => b.path).ToArray(); if (groups.Length == 0) throw new InvalidOperationException("This clip has no Fold animation tracks to bake."); var baked = UnityEngine.Object.Instantiate(source); baked.name = System.IO.Path.GetFileNameWithoutExtension(path); bool created = false; try { var destinations = new HashSet(); foreach (var group in groups) { var transform = string.IsNullOrEmpty(group.Key) ? root : root.Find(group.Key); var driver = transform ? transform.GetComponent() : null; if (!driver || !driver.targetRenderer || !driver.material) throw new InvalidOperationException("Missing Fold driver, renderer or material at " + group.Key); var renderer = driver.targetRenderer; if (renderer.transform != root && !renderer.transform.IsChildOf(root)) throw new InvalidOperationException("The Fold renderer must be under Animation root."); var materials = renderer.sharedMaterials; if (materials.Length != 1 || driver.materialIndex != 0) throw new InvalidOperationException("Direct material animation requires one material per renderer. Split the material slots onto separate renderers before baking: " + renderer.name); if (materials[0] != driver.material) throw new InvalidOperationException("The Fold material slot has changed. Reinitialize the stack before baking."); if (!driver.material.HasProperty(FoldOperator.Prefix(0) + "AnimatedOpcode")) throw new InvalidOperationException("Update to the Fold shader with AnimatedOpcode properties before baking."); string rendererPath = AnimationUtility.CalculateTransformPath(renderer.transform, root); if (AnimationUtility.GetObjectReferenceCurveBindings(source).Any(b => b.path == rendererPath && b.propertyName.StartsWith("m_Materials", StringComparison.Ordinal))) throw new InvalidOperationException("Remove conflicting material-swap tracks before baking: " + renderer.name); foreach (var binding in group) { var match = System.Text.RegularExpressions.Regex.Match(binding.propertyName, @"^slot(\d+)_(enabled|opcode|float[0-3]|vector[0-3]\.[xyzw])$"); if (!match.Success || !int.TryParse(match.Groups[1].Value, out int slot) || slot >= 16) throw new InvalidOperationException("Unknown Fold track: " + binding.propertyName); string field = match.Groups[2].Value; string property = field == "enabled" ? "Enabled" : field == "opcode" ? "AnimatedOpcode" : field.StartsWith("float", StringComparison.Ordinal) ? "Float_" + field[5] : "Vector_" + field.Substring(6); property = "material." + FoldOperator.Prefix(slot) + property; if (!destinations.Add(rendererPath + ":" + property) || bindings.Any(b => b.path == rendererPath && b.propertyName == property)) throw new InvalidOperationException("Conflicting material curve: " + property); var curve = AnimationUtility.GetEditorCurve(source, binding); if (field == "opcode") { var keys = curve.keys; for (int i = 0; i < keys.Length; i++) { keys[i].value = BitConverter.ToInt32(BitConverter.GetBytes(keys[i].value), 0); keys[i].inTangent = keys[i].outTangent = float.PositiveInfinity; } curve.keys = keys; for (int i = 0; i < keys.Length; i++) { AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Constant); AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Constant); } } AnimationUtility.SetEditorCurve(baked, binding, null); AnimationUtility.SetEditorCurve(baked, EditorCurveBinding.FloatCurve(rendererPath, renderer.GetType(), property), curve); } } AssetDatabase.CreateAsset(baked, path); created = true; AssetDatabase.SaveAssets(); return baked; } catch { if (created) AssetDatabase.DeleteAsset(path); else UnityEngine.Object.DestroyImmediate(baked); throw; } } } }