/* * MIT License * * Copyright (c) 2022 bd_ * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEngine; using VRC.SDK3.Avatars.Components; using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.core.editor { internal class AnimatorCombiner { private readonly AnimatorController _combined; private List _layers = new List(); private Dictionary _parameters = new Dictionary(); private Dictionary, Motion> _motions = new Dictionary, Motion>(); private Dictionary, AnimatorStateMachine> _stateMachines = new Dictionary, AnimatorStateMachine>(); private int controllerBaseLayer = 0; public AnimatorCombiner() { _combined = Util.CreateAnimator(); } public AnimatorController Finish() { _combined.parameters = _parameters.Values.ToArray(); _combined.layers = _layers.ToArray(); return _combined; } public void AddController(string basePath, AnimatorController controller, bool? writeDefaults) { controllerBaseLayer = _layers.Count; foreach (var param in controller.parameters) { if (_parameters.TryGetValue(param.name, out var acp)) { if (acp.type != param.type) { throw new Exception( $"Parameter {param.name} has different types in {basePath} and {controller.name}"); } continue; } _parameters.Add(param.name, param); } bool first = true; foreach (var layer in controller.layers) { insertLayer(basePath, layer, first, writeDefaults); first = false; } } private void insertLayer(string basePath, AnimatorControllerLayer layer, bool first, bool? writeDefaults) { var newLayer = new AnimatorControllerLayer() { name = layer.name, avatarMask = layer.avatarMask, // TODO map transforms blendingMode = layer.blendingMode, defaultWeight = first ? 1 : layer.defaultWeight, syncedLayerIndex = layer.syncedLayerIndex, // TODO syncedLayerAffectsTiming = layer.syncedLayerAffectsTiming, // TODO iKPass = layer.iKPass, stateMachine = mapStateMachine(basePath, layer.stateMachine), }; UpdateWriteDefaults(newLayer.stateMachine, writeDefaults); _layers.Add(newLayer); } private void UpdateWriteDefaults(AnimatorStateMachine stateMachine, bool? writeDefaults) { if (!writeDefaults.HasValue) return; var queue = new Queue(); queue.Enqueue(stateMachine); while (queue.Count > 0) { var sm = queue.Dequeue(); foreach (var state in sm.states) { state.state.writeDefaultValues = writeDefaults.Value; } foreach (var child in sm.stateMachines) { queue.Enqueue(child.stateMachine); } } } private AnimatorStateMachine mapStateMachine(string basePath, AnimatorStateMachine layerStateMachine) { var cacheKey = new KeyValuePair(basePath, layerStateMachine); if (_stateMachines.TryGetValue(cacheKey, out var asm)) { return asm; } asm = deepClone(layerStateMachine, (obj) => customClone(obj, basePath)); foreach (var state in asm.states) { foreach (var behavior in state.state.behaviours) { switch (behavior) { case VRCAnimatorLayerControl layerControl: { // TODO - need to figure out how to handle cross-layer references. For now this will handle // intra-animator cases. layerControl.layer += controllerBaseLayer; break; } } } } _stateMachines[cacheKey] = asm; return asm; } private static string MapPath(EditorCurveBinding binding, string basePath) { if (binding.type == typeof(Animator) && binding.path == "") { return ""; } else { var newPath = binding.path == "" ? basePath : basePath + binding.path; if (newPath.EndsWith("/")) { newPath = newPath.Substring(0, newPath.Length - 1); } return PathMappings.MapPath(newPath, binding.type == typeof(Transform)); } } private Object customClone(Object o, string basePath) { if (basePath == "") return null; if (o is AnimationClip clip) { AnimationClip newClip = new AnimationClip(); newClip.name = "rebased " + clip.name; AssetDatabase.AddObjectToAsset(newClip, _combined); foreach (var binding in AnimationUtility.GetCurveBindings(clip)) { var newBinding = binding; newBinding.path = MapPath(binding, basePath); newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName, AnimationUtility.GetEditorCurve(clip, binding)); } foreach (var objBinding in AnimationUtility.GetObjectReferenceCurveBindings(clip)) { var newBinding = objBinding; newBinding.path = MapPath(objBinding, basePath); AnimationUtility.SetObjectReferenceCurve(newClip, newBinding, AnimationUtility.GetObjectReferenceCurve(clip, objBinding)); } newClip.wrapMode = clip.wrapMode; newClip.legacy = clip.legacy; newClip.frameRate = clip.frameRate; newClip.localBounds = clip.localBounds; AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(clip)); return newClip; } else if (o is Texture) { return o; } else { return null; } } private T deepClone(T original, Func visitor, Dictionary cloneMap = null ) where T : Object { if (original == null) return null; // We want to avoid trying to copy assets not part of the animation system (eg - textures, meshes, // MonoScripts...), so check for the types we care about here switch (original) { // Any object referenced by an animator that we intend to mutate needs to be listed here. case Motion _: case AnimatorController _: case AnimatorState _: case AnimatorStateMachine _: case AnimatorTransitionBase _: case StateMachineBehaviour _: break; // We want to clone these types // Leave textures, materials, and script definitions alone case Texture2D _: case MonoScript _: case Material _: return original; // Also avoid copying unknown scriptable objects. // This ensures compatibility with e.g. avatar remote, which stores state information in a state // behaviour referencing a custom ScriptableObject case ScriptableObject _: return original; default: throw new Exception($"Unknown type referenced from animator: {original.GetType()}"); } if (cloneMap == null) cloneMap = new Dictionary(); if (cloneMap.ContainsKey(original)) { return (T) cloneMap[original]; } var obj = visitor(original); if (obj != null) { cloneMap[original] = obj; return (T) obj; } var ctor = original.GetType().GetConstructor(Type.EmptyTypes); if (ctor == null || original is ScriptableObject) { obj = Object.Instantiate(original); } else { obj = (T) ctor.Invoke(Array.Empty()); EditorUtility.CopySerialized(original, obj); } cloneMap[original] = obj; AssetDatabase.AddObjectToAsset(obj, _combined); SerializedObject so = new SerializedObject(obj); SerializedProperty prop = so.GetIterator(); bool enterChildren = true; while (prop.Next(enterChildren)) { enterChildren = true; switch (prop.propertyType) { case SerializedPropertyType.ObjectReference: prop.objectReferenceValue = deepClone(prop.objectReferenceValue, visitor, cloneMap); break; // Iterating strings can get super slow... case SerializedPropertyType.String: enterChildren = false; break; } } so.ApplyModifiedPropertiesWithoutUndo(); return (T) obj; } } }