diff --git a/Editor/ActiveAnimationRetargeter.cs b/Editor/ActiveAnimationRetargeter.cs index 8c515b16..5e1f96cf 100644 --- a/Editor/ActiveAnimationRetargeter.cs +++ b/Editor/ActiveAnimationRetargeter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using nadena.dev.modular_avatar.animation; +using nadena.dev.ndmf.animator; using UnityEditor; using UnityEngine; using EditorCurveBinding = UnityEditor.EditorCurveBinding; @@ -16,7 +17,7 @@ internal class ActiveAnimationRetargeter { private readonly BuildContext _context; private readonly BoneDatabase _boneDatabase; - private readonly PathMappings _pathMappings; + private readonly AnimatorServicesContext _asc; private readonly List _intermediateObjs = new List(); /// @@ -55,15 +56,15 @@ Transform root { _context = context; _boneDatabase = boneDatabase; - _pathMappings = context.PluginBuildContext.Extension().PathMappings; + _asc = context.PluginBuildContext.Extension(); while (root != null && !RuntimeUtil.IsAvatarRoot(root)) { var originalPath = RuntimeUtil.AvatarRootPath(root.gameObject); System.Diagnostics.Debug.Assert(originalPath != null); - if (context.AnimationDatabase.ClipsForPath(originalPath).Any(clip => - GetActiveBinding(clip.CurrentClip as AnimationClip, originalPath) != null + if (_asc.AnimationIndex.GetClipsForObjectPath(originalPath).Any(clip => + GetActiveBinding(clip, originalPath) != null )) { _intermediateObjs.Add(new IntermediateObj @@ -118,7 +119,6 @@ public GameObject CreateIntermediateObjects(GameObject sourceBone) // Ensure mesh retargeting looks through this _boneDatabase.AddMergedBone(sourceBone.transform); _boneDatabase.RetainMergedBone(sourceBone.transform); - _pathMappings.MarkTransformLookthrough(sourceBone); } return sourceBone; @@ -130,22 +130,14 @@ public void FixupAnimations() { var path = intermediate.OriginalPath; - foreach (var holder in _context.AnimationDatabase.ClipsForPath(path)) + foreach (var clip in _asc.AnimationIndex.GetClipsForObjectPath(path)) { - if (!_context.PluginBuildContext.IsTemporaryAsset(holder.CurrentClip)) - { - holder.CurrentClip = Object.Instantiate(holder.CurrentClip); - } - - var clip = holder.CurrentClip as AnimationClip; - if (clip == null) continue; - var curve = GetActiveBinding(clip, path); if (curve != null) { foreach (var mapping in intermediate.Created) { - clip.SetCurve(_pathMappings.GetObjectIdentifier(mapping), typeof(GameObject), "m_IsActive", + clip.SetFloatCurve(_asc.ObjectPathRemapper.GetVirtualPathForObject(mapping), typeof(GameObject), "m_IsActive", curve); } } @@ -153,10 +145,9 @@ public void FixupAnimations() } } - private AnimationCurve GetActiveBinding(AnimationClip clip, string path) + private AnimationCurve GetActiveBinding(VirtualClip clip, string path) { - return AnimationUtility.GetEditorCurve(clip, - EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive")); + return clip.GetFloatCurve(EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive")); } } } \ No newline at end of file diff --git a/Editor/Animation/AnimationServicesContext.cs b/Editor/Animation/AnimationServicesContext.cs index 57ad5dd0..642f7573 100644 --- a/Editor/Animation/AnimationServicesContext.cs +++ b/Editor/Animation/AnimationServicesContext.cs @@ -2,13 +2,9 @@ using System; using System.Collections.Generic; -using System.Linq; using nadena.dev.ndmf; -using UnityEditor; -using UnityEditor.Animations; using UnityEngine; #if MA_VRCSDK3_AVATARS -using VRC.SDK3.Avatars.Components; #endif #endregion @@ -32,7 +28,6 @@ internal sealed class AnimationServicesContext : IExtensionContext private BuildContext _context; private AnimationDatabase _animationDatabase; private PathMappings _pathMappings; - private ReadableProperty _readableProperty; private Dictionary _selfProxies = new(); @@ -45,8 +40,6 @@ public void OnActivate(BuildContext context) _pathMappings = new PathMappings(); _pathMappings.OnActivate(context, _animationDatabase); - - _readableProperty = new ReadableProperty(_context, _animationDatabase, this); } public void OnDeactivate(BuildContext context) @@ -85,41 +78,5 @@ public PathMappings PathMappings return _pathMappings; } } - - public IEnumerable<(EditorCurveBinding, string)> BoundReadableProperties => _readableProperty.BoundProperties; - - // HACK: This is a temporary crutch until we rework the entire animator services system - public void AddPropertyDefinition(AnimatorControllerParameter paramDef) - { -#if MA_VRCSDK3_AVATARS - if (!_context.AvatarDescriptor) return; - - var fx = (AnimatorController) - _context.AvatarDescriptor.baseAnimationLayers - .First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX) - .animatorController; - - fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray(); -#endif - } - - public string GetActiveSelfProxy(GameObject obj) - { - if (_selfProxies.TryGetValue(obj, out var paramName) && !string.IsNullOrEmpty(paramName)) return paramName; - - var path = PathMappings.GetObjectIdentifier(obj); - - paramName = _readableProperty.ForActiveSelf(path); - _selfProxies[obj] = paramName; - - return paramName; - } - - public bool ObjectHasAnimations(GameObject obj) - { - var path = PathMappings.GetObjectIdentifier(obj); - var clips = AnimationDatabase.ClipsForPath(path); - return clips != null && !clips.IsEmpty; - } } } \ No newline at end of file diff --git a/Editor/Animation/GameObjectDisableDelayPass.cs b/Editor/Animation/GameObjectDisableDelayPass.cs index 87692657..33e5b37f 100644 --- a/Editor/Animation/GameObjectDisableDelayPass.cs +++ b/Editor/Animation/GameObjectDisableDelayPass.cs @@ -2,6 +2,7 @@ using System.Linq; using nadena.dev.modular_avatar.core.editor; using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; using UnityEditor; using UnityEditor.Animations; using UnityEngine; @@ -18,14 +19,13 @@ internal class GameObjectDelayDisablePass : Pass { protected override void Execute(BuildContext context) { - var asc = context.Extension(); - if (!asc.BoundReadableProperties.Any()) return; - - var fx = (AnimatorController)context.AvatarDescriptor.baseAnimationLayers - .FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController; + var asc = context.Extension(); + var activeProxies = context.GetState().proxyProps; + if (activeProxies.Count == 0) return; + var fx = asc.ControllerContext[VRCAvatarDescriptor.AnimLayerType.FX]; if (fx == null) return; - + var nullMotion = new AnimationClip(); nullMotion.name = "NullMotion"; @@ -33,48 +33,31 @@ protected override void Execute(BuildContext context) blendTree.blendType = BlendTreeType.Direct; blendTree.useAutomaticThresholds = false; - blendTree.children = asc.BoundReadableProperties - .Select(prop => GenerateDelayChild(nullMotion, prop)) + blendTree.children = activeProxies + .Select(prop => GenerateDelayChild(nullMotion, (prop.Key, prop.Value))) .ToArray(); - var asm = new AnimatorStateMachine(); - var state = new AnimatorState(); - state.name = "DelayDisable"; - state.motion = blendTree; - state.writeDefaultValues = true; - - asm.defaultState = state; - asm.states = new[] - { - new ChildAnimatorState - { - state = state, - position = Vector3.zero - } - }; + var layer = fx.AddLayer(LayerPriority.Default, "DelayDisable"); + var state = layer.StateMachine.AddState("DelayDisable"); + layer.StateMachine.DefaultState = state; - fx.layers = fx.layers.Append(new AnimatorControllerLayer - { - name = "DelayDisable", - stateMachine = asm, - defaultWeight = 1, - blendingMode = AnimatorLayerBlendingMode.Override - }).ToArray(); + state.WriteDefaultValues = true; + state.Motion = asc.ControllerContext.Clone(blendTree); // Ensure the initial state of readable props matches the actual state of the gameobject - var parameters = fx.parameters; - var paramToIndex = parameters.Select((p, i) => (p, i)).ToDictionary(x => x.p.name, x => x.i); - foreach (var (binding, prop) in asc.BoundReadableProperties) + foreach (var controller in asc.ControllerContext.GetAllControllers()) { - var obj = asc.PathMappings.PathToObject(binding.path); - - if (obj != null && paramToIndex.TryGetValue(prop, out var index)) + foreach (var (binding, prop) in activeProxies) { - parameters[index].defaultFloat = obj.activeSelf ? 1 : 0; + var obj = asc.ObjectPathRemapper.GetObjectForPath(binding.path); + + if (obj != null && controller.Parameters.TryGetValue(prop, out var p)) + { + p.defaultFloat = obj.activeSelf ? 1 : 0; + controller.Parameters = controller.Parameters.SetItem(prop, p); + } } } - - fx.parameters = parameters; } private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding) diff --git a/Editor/Animation/ReadableProperty.cs b/Editor/Animation/ReadableProperty.cs deleted file mode 100644 index 5931f6b6..00000000 --- a/Editor/Animation/ReadableProperty.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using nadena.dev.ndmf; -using UnityEditor; -using UnityEditor.Animations; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace nadena.dev.modular_avatar.animation -{ - internal class ReadableProperty - { - private readonly BuildContext _context; - private readonly AnimationDatabase _animDB; - private readonly AnimationServicesContext _asc; - private readonly Dictionary _alreadyBound = new(); - private long _nextIndex; - - public ReadableProperty(BuildContext context, AnimationDatabase animDB, AnimationServicesContext asc) - { - _context = context; - _animDB = animDB; - _asc = asc; - } - - public IEnumerable<(EditorCurveBinding, string)> BoundProperties => - _alreadyBound.Select(kv => (kv.Key, kv.Value)); - - /// - /// Creates an animator parameter which tracks the effective value of a property on a component. This only - /// tracks FX layer properties. - /// - /// - /// - public string ForBinding(string path, Type componentType, string property) - { - var ecb = new EditorCurveBinding - { - path = path, - type = componentType, - propertyName = property - }; - - if (_alreadyBound.TryGetValue(ecb, out var reader)) - { - return reader; - } - - var lastComponent = path.Split("/")[^1]; - var emuPropName = $"__MA/ReadableProp/{lastComponent}/{componentType}/{property}#{_nextIndex++}"; - - float initialValue = 0; - var gameObject = _asc.PathMappings.PathToObject(path); - Object component = componentType == typeof(GameObject) - ? gameObject - : gameObject?.GetComponent(componentType); - if (component != null) - { - var so = new SerializedObject(component); - var prop = so.FindProperty(property); - if (prop != null) - switch (prop.propertyType) - { - case SerializedPropertyType.Boolean: - initialValue = prop.boolValue ? 1 : 0; - break; - case SerializedPropertyType.Float: - initialValue = prop.floatValue; - break; - case SerializedPropertyType.Integer: - initialValue = prop.intValue; - break; - default: throw new NotImplementedException($"Property type {prop.type} not supported"); - } - } - - - _asc.AddPropertyDefinition(new AnimatorControllerParameter - { - defaultFloat = initialValue, - name = emuPropName, - type = AnimatorControllerParameterType.Float - }); - - BindProperty(ecb, emuPropName); - - _alreadyBound[ecb] = emuPropName; - - return emuPropName; - } - - private void BindProperty(EditorCurveBinding ecb, string propertyName) - { - var boundProp = new EditorCurveBinding - { - path = "", - type = typeof(Animator), - propertyName = propertyName - }; - - foreach (var clip in _animDB.ClipsForPath(ecb.path)) ProcessAnyClip(clip); - - void ProcessBlendTree(BlendTree blendTree) - { - foreach (var child in blendTree.children) - switch (child.motion) - { - case AnimationClip animationClip: - ProcessAnimationClip(animationClip); - break; - - case BlendTree subBlendTree: - ProcessBlendTree(subBlendTree); - break; - } - } - - void ProcessAnimationClip(AnimationClip animationClip) - { - var curve = AnimationUtility.GetEditorCurve(animationClip, ecb); - if (curve == null) return; - - AnimationUtility.SetEditorCurve(animationClip, boundProp, curve); - } - - void ProcessAnyClip(AnimationDatabase.ClipHolder clip) - { - switch (clip.CurrentClip) - { - case AnimationClip animationClip: - ProcessAnimationClip(animationClip); - break; - - case BlendTree blendTree: - ProcessBlendTree(blendTree); - break; - } - } - } - - public string ForActiveSelf(string path) - { - return ForBinding(path, typeof(GameObject), "m_IsActive"); - } - } -} \ No newline at end of file diff --git a/Editor/Animation/ReadableProperty.cs.meta b/Editor/Animation/ReadableProperty.cs.meta deleted file mode 100644 index e4399110..00000000 --- a/Editor/Animation/ReadableProperty.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 1074339e2a59465ba585cb8cbbc4a88c -timeCreated: 1719195449 \ No newline at end of file diff --git a/Editor/Animation/ReadablePropertyExtension.cs b/Editor/Animation/ReadablePropertyExtension.cs new file mode 100644 index 00000000..a4807d8b --- /dev/null +++ b/Editor/Animation/ReadablePropertyExtension.cs @@ -0,0 +1,82 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; +using UnityEditor; +using UnityEngine; + +namespace nadena.dev.modular_avatar.animation +{ + [DependsOnContext(typeof(AnimatorServicesContext))] + internal class ReadablePropertyExtension : IExtensionContext + { + // This is a temporary hack for GameObjectDelayDisablePass + public class Retained + { + public Dictionary proxyProps = new(); + } + + private AnimatorServicesContext? _asc; + private Retained _retained = null!; + + private AnimatorServicesContext asc => + _asc ?? throw new InvalidOperationException("ActiveSelfProxyExtension is not active"); + + private Dictionary proxyProps => _retained.proxyProps; + private int index; + + public IEnumerable<(EditorCurveBinding, string)> ActiveProxyProps => + proxyProps.Select(kvp => (kvp.Key, kvp.Value)); + + public string GetActiveSelfProxy(GameObject obj) + { + var path = asc.ObjectPathRemapper.GetVirtualPathForObject(obj); + var ecb = EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"); + + if (proxyProps.TryGetValue(ecb, out var prop)) return prop; + + prop = $"__MA/ActiveSelfProxy/{obj.name}##{index++}"; + proxyProps[ecb] = prop; + + // Add prop to all animators + foreach (var animator in asc.ControllerContext.GetAllControllers()) + { + animator.Parameters = animator.Parameters.SetItem( + prop, + new AnimatorControllerParameter + { + name = prop, + type = AnimatorControllerParameterType.Float, + defaultFloat = obj.activeSelf ? 1 : 0 + } + ); + } + + return prop; + } + + public void OnActivate(BuildContext context) + { + _asc = context.Extension(); + _retained = context.GetState(); + } + + public void OnDeactivate(BuildContext context) + { + asc.AnimationIndex.EditClipsByBinding(proxyProps.Keys, clip => + { + foreach (var b in clip.GetFloatCurveBindings().ToList()) + { + if (proxyProps.TryGetValue(b, out var proxyProp)) + { + var curve = clip.GetFloatCurve(b); + clip.SetFloatCurve("", typeof(Animator), proxyProp, curve); + } + } + }); + } + } +} \ No newline at end of file diff --git a/Editor/Animation/ReadablePropertyExtension.cs.meta b/Editor/Animation/ReadablePropertyExtension.cs.meta new file mode 100644 index 00000000..93a503c5 --- /dev/null +++ b/Editor/Animation/ReadablePropertyExtension.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 511cbc0373a2469192e0351e2222a203 +timeCreated: 1732496091 \ No newline at end of file diff --git a/Editor/ApplyAnimatorDefaultValuesPass.cs b/Editor/ApplyAnimatorDefaultValuesPass.cs index 0f9cb15e..84056413 100644 --- a/Editor/ApplyAnimatorDefaultValuesPass.cs +++ b/Editor/ApplyAnimatorDefaultValuesPass.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Linq; using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; using UnityEditor.Animations; using UnityEngine; @@ -21,40 +22,34 @@ protected override void Execute(ndmf.BuildContext context) var values = context.GetState()?.InitialValueOverrides ?? ImmutableDictionary.Empty; - foreach (var layer in context.AvatarDescriptor.baseAnimationLayers - .Concat(context.AvatarDescriptor.specialAnimationLayers)) - { - if (layer.isDefault || layer.animatorController == null) continue; - - // We should have converted anything that's not an AnimationController by now - var controller = layer.animatorController as AnimatorController; - if (controller == null || !context.IsTemporaryAsset(controller)) - { - throw new Exception("Leaked unexpected controller: " + layer.animatorController + " (type " + layer.animatorController?.GetType() + ")"); - } + var asc = context.Extension(); - var parameters = controller.parameters; - for (int i = 0; i < parameters.Length; i++) + foreach (var controller in asc.ControllerContext.GetAllControllers()) + { + var parameters = controller.Parameters; + foreach (var (name, parameter) in parameters) { - if (!values.TryGetValue(parameters[i].name, out var defaultValue)) continue; + if (!values.TryGetValue(name, out var defaultValue)) continue; - switch (parameters[i].type) + switch (parameter.type) { case AnimatorControllerParameterType.Bool: - parameters[i].defaultBool = defaultValue != 0.0f; + parameter.defaultBool = defaultValue != 0.0f; break; case AnimatorControllerParameterType.Int: - parameters[i].defaultInt = Mathf.RoundToInt(defaultValue); + parameter.defaultInt = Mathf.RoundToInt(defaultValue); break; case AnimatorControllerParameterType.Float: - parameters[i].defaultFloat = defaultValue; + parameter.defaultFloat = defaultValue; break; default: continue; // unhandled type, e.g. trigger } + + parameters = parameters.SetItem(name, parameter); } - controller.parameters = parameters; + controller.Parameters = parameters; } } } diff --git a/Editor/MergeAnimatorProcessor.cs b/Editor/MergeAnimatorProcessor.cs index aa4dbcbc..1002360f 100644 --- a/Editor/MergeAnimatorProcessor.cs +++ b/Editor/MergeAnimatorProcessor.cs @@ -24,52 +24,23 @@ #if MA_VRCSDK3_AVATARS -using System; using System.Collections.Generic; using System.Linq; -using nadena.dev.modular_avatar.animation; -using UnityEditor; -using UnityEditor.Animations; +using nadena.dev.ndmf.animator; using UnityEngine; using VRC.SDK3.Avatars.Components; -using VRC.SDKBase; using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.core.editor { internal class MergeAnimatorProcessor { - private const string SAMPLE_PATH_PACKAGE = - "Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Animation/Controllers"; - - private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers"; - - private const string GUID_GESTURE_HANDSONLY_MASK = "b2b8bad9583e56a46a3e21795e96ad92"; - - private BuildContext _context; - - private Dictionary defaultControllers_ = - new Dictionary(); - - private Dictionary writeDefaults_ = - new Dictionary(); - - Dictionary mergeSessions = - new Dictionary(); + private AnimatorServicesContext _asc; internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context) { - _context = context; - - defaultControllers_.Clear(); - mergeSessions.Clear(); - - var descriptor = avatarGameObject.GetComponent(); - if (!descriptor) return; - - if (descriptor.baseAnimationLayers != null) InitSessions(descriptor.baseAnimationLayers); - if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers); - + _asc = context.PluginBuildContext.Extension(); + var toMerge = avatarGameObject.transform.GetComponentsInChildren(true); Dictionary> byLayerType = new Dictionary>(); @@ -89,10 +60,6 @@ internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext conte { ProcessLayerType(context, entry.Key, entry.Value); } - - descriptor.baseAnimationLayers = FinishSessions(descriptor.baseAnimationLayers); - descriptor.specialAnimationLayers = FinishSessions(descriptor.specialAnimationLayers); - descriptor.customizeAnimationLayers = true; } private void ProcessLayerType( @@ -109,34 +76,34 @@ List toMerge var afterOriginal = sorted.Where(x => x.layerPriority >= 0) .ToList(); - var session = new AnimatorCombiner(context.PluginBuildContext, layerType.ToString() + " (merged)"); - mergeSessions[layerType] = session; - mergeSessions[layerType].BlendableLayer = BlendableLayerFor(layerType); - - foreach (var component in beforeOriginal) - { - MergeSingle(context, session, component); - } - - if (defaultControllers_.TryGetValue(layerType, out var defaultController) && - defaultController.layers.Length > 0) - { - session.AddController("", defaultController, null, forceFirstLayerWeight: true); - } + var controller = _asc.ControllerContext[layerType]; + + var wdStateCounter = controller.Layers.SelectMany(l => l.StateMachine.AllStates()) + .Select(s => s.WriteDefaultValues) + .GroupBy(b => b) + .ToDictionary(g => g.Key, g => g.Count()); - foreach (var component in afterOriginal) + bool? writeDefaults = null; + if (wdStateCounter.Count == 1) writeDefaults = wdStateCounter.First().Key; + + foreach (var component in sorted) { - MergeSingle(context, session, component); + MergeSingle(context, controller, component, writeDefaults); } } - private void MergeSingle(BuildContext context, AnimatorCombiner session, ModularAvatarMergeAnimator merge) + private void MergeSingle(BuildContext context, VirtualAnimatorController controller, ModularAvatarMergeAnimator merge, bool? initialWriteDefaults) { if (merge.animator == null) { return; } + var stash = context.PluginBuildContext.GetState(); + + var clonedController = stash.Controllers.GetValueOrDefault(merge) + ?? _asc.ControllerContext.CloneContext.Clone(merge.animator); + string basePath; if (merge.pathMode == MergeAnimatorPathMode.Relative) { @@ -145,199 +112,59 @@ private void MergeSingle(BuildContext context, AnimatorCombiner session, Modular var relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject); basePath = relativePath != "" ? relativePath + "/" : ""; + + var animationIndex = new AnimationIndex(new[] { clonedController }); + animationIndex.RewritePaths(p => p == "" ? relativePath : basePath + p); } else { basePath = ""; } - var writeDefaults = merge.matchAvatarWriteDefaults - ? writeDefaults_.GetValueOrDefault(merge.layerType) - : null; - var controller = _context.ConvertAnimatorController(merge.animator); - session.AddController(basePath, controller, writeDefaults); - - if (merge.deleteAttachedAnimator) - { - var animator = merge.GetComponent(); - if (animator != null) Object.DestroyImmediate(animator); - } - } - - private VRCAvatarDescriptor.CustomAnimLayer[] FinishSessions( - VRCAvatarDescriptor.CustomAnimLayer[] layers - ) - { - layers = (VRCAvatarDescriptor.CustomAnimLayer[])layers.Clone(); - - // Ensure types are consistent across layers - Dictionary types = - new Dictionary(); - // Learn types... - foreach (var session in mergeSessions.Values) - { - session.MergeTypes(types); - } - // And propagate them - foreach (var session in mergeSessions.Values) + foreach (var l in clonedController.Layers) { - session.MergeTypes(types); - } - - for (int i = 0; i < layers.Length; i++) - { - if (mergeSessions.TryGetValue(layers[i].type, out var session)) + if (initialWriteDefaults != null) { - if (layers[i].type == VRCAvatarDescriptor.AnimLayerType.Gesture && layers[i].isDefault) + foreach (var s in l.StateMachine.AllStates()) { - // We need to set the mask field for the gesture layer on initial configuration - layers[i].mask = AssetDatabase.LoadAssetAtPath( - AssetDatabase.GUIDToAssetPath(GUID_GESTURE_HANDSONLY_MASK) - ); + s.WriteDefaultValues = initialWriteDefaults.Value; } - - layers[i].isDefault = false; - layers[i].animatorController = session.Finish(); - } - } - - return layers; - } - - private void InitSessions(VRCAvatarDescriptor.CustomAnimLayer[] layers) - { - foreach (var layer in layers) - { - var controller = ResolveLayerController(layer); - if (controller == null) controller = new AnimatorController(); - - defaultControllers_[layer.type] = controller; - writeDefaults_[layer.type] = ProbeWriteDefaults(controller); - if (!layer.isDefault) - { - // For non-default layers, ensure we always clone the controller for the benefit of subsequent - // processing phases - mergeSessions[layer.type] = - new AnimatorCombiner(_context.PluginBuildContext, layer.type.ToString()); - mergeSessions[layer.type].BlendableLayer = BlendableLayerFor(layer.type); - mergeSessions[layer.type].AddController("", controller, null); } + controller.AddLayer(new LayerPriority(merge.layerPriority), l); } - } - - private VRC_AnimatorLayerControl.BlendableLayer? BlendableLayerFor(VRCAvatarDescriptor.AnimLayerType layerType) - { - if (Enum.TryParse(layerType.ToString(), out VRC_AnimatorLayerControl.BlendableLayer result)) - { - return result; - } - else - { - return null; - } - } - - internal static bool? ProbeWriteDefaults(AnimatorController controller) - { - if (controller == null) return null; - - bool hasWDOn = false; - bool hasWDOff = false; - var stateMachineQueue = new Queue(); - foreach (var layer in controller.layers) + foreach (var (name, parameter) in clonedController.Parameters) { - // Special case: A layer with a single state, which contains a blend tree, is ignored for WD analysis. - // This is because WD ON blend trees have different behavior from most WD ON states, and can be safely - // used in a WD OFF animator. - - if (layer.stateMachine.states.Length == 1 - && layer.stateMachine.states[0].state.motion is BlendTree - && layer.stateMachine.stateMachines.Length == 0 - ) + if (controller.Parameters.TryGetValue(name, out var existingParam)) { + if (existingParam.type != parameter.type) + { + // Force to float + switch (parameter.type) + { + case AnimatorControllerParameterType.Bool: + existingParam.defaultFloat = existingParam.defaultBool ? 1.0f : 0.0f; + break; + case AnimatorControllerParameterType.Int: + existingParam.defaultFloat = existingParam.defaultInt; + break; + } + + existingParam.type = AnimatorControllerParameterType.Float; + + controller.Parameters = controller.Parameters.SetItem(name, existingParam); + } continue; } - stateMachineQueue.Enqueue(layer.stateMachine); + controller.Parameters = controller.Parameters.Add(name, parameter); } - - while (stateMachineQueue.Count > 0) - { - var stateMachine = stateMachineQueue.Dequeue(); - foreach (var state in stateMachine.states) - { - if (state.state.writeDefaultValues) hasWDOn = true; - else hasWDOff = true; - } - - foreach (var child in stateMachine.stateMachines) - { - stateMachineQueue.Enqueue(child.stateMachine); - } - } - - if (hasWDOn == hasWDOff) return null; - return hasWDOn; - } - - - private static AnimatorController ResolveLayerController(VRCAvatarDescriptor.CustomAnimLayer layer) - { - AnimatorController controller = null; - - if (!layer.isDefault && layer.animatorController != null && - layer.animatorController is AnimatorController c) - { - controller = c; - } - else + + if (merge.deleteAttachedAnimator) { - string name; - switch (layer.type) - { - case VRCAvatarDescriptor.AnimLayerType.Action: - name = "Action"; - break; - case VRCAvatarDescriptor.AnimLayerType.Additive: - name = "Idle"; - break; - case VRCAvatarDescriptor.AnimLayerType.Base: - name = "Locomotion"; - break; - case VRCAvatarDescriptor.AnimLayerType.Gesture: - name = "Hands"; - break; - case VRCAvatarDescriptor.AnimLayerType.Sitting: - name = "Sitting"; - break; - case VRCAvatarDescriptor.AnimLayerType.FX: - name = "Face"; - break; - case VRCAvatarDescriptor.AnimLayerType.TPose: - name = "UtilityTPose"; - break; - case VRCAvatarDescriptor.AnimLayerType.IKPose: - name = "UtilityIKPose"; - break; - default: - name = null; - break; - } - - if (name != null) - { - name = "/vrc_AvatarV3" + name + "Layer.controller"; - - controller = AssetDatabase.LoadAssetAtPath(SAMPLE_PATH_PACKAGE + name); - if (controller == null) - { - controller = AssetDatabase.LoadAssetAtPath(SAMPLE_PATH_LEGACY + name); - } - } + var animator = merge.GetComponent(); + if (animator != null) Object.DestroyImmediate(animator); } - - return controller; } } } diff --git a/Editor/MergeArmatureHook.cs b/Editor/MergeArmatureHook.cs index e9896fdb..b6ba5870 100644 --- a/Editor/MergeArmatureHook.cs +++ b/Editor/MergeArmatureHook.cs @@ -31,8 +31,8 @@ using System; using System.Collections.Generic; using System.Linq; -using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.editor.ErrorReporting; +using nadena.dev.ndmf.animator; using UnityEditor; using UnityEngine; using UnityEngine.Animations; @@ -54,12 +54,13 @@ internal class #endif private BoneDatabase BoneDatabase = new BoneDatabase(); - private PathMappings PathMappings => frameworkContext.Extension() - .PathMappings; + private AnimatorServicesContext AnimatorServices => frameworkContext.Extension(); private HashSet humanoidBones = new HashSet(); private HashSet mergedObjects = new HashSet(); private HashSet thisPassAdded = new HashSet(); + + private HashSet transformLookthrough = new HashSet(); internal void OnPreprocessAvatar(ndmf.BuildContext context, GameObject avatarGameObject) { @@ -135,7 +136,68 @@ internal void OnPreprocessAvatar(ndmf.BuildContext context, GameObject avatarGam } } - new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings); + new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, AnimatorServices); + + ProcessTransformLookthrough(); + } + + private void ProcessTransformLookthrough() + { + var asc = frameworkContext.Extension(); + + transformLookthrough.RemoveWhere(t => !t); + + var clipsToEdit = transformLookthrough.SelectMany( + xform => + { + var path = asc.ObjectPathRemapper.GetVirtualPathForObject(xform); + return asc.AnimationIndex.GetClipsForObjectPath(path); + }); + + Dictionary parentCache = new(); + + foreach (var clip in clipsToEdit) + { + foreach (var binding in clip.GetFloatCurveBindings()) + { + if (binding.type == typeof(Transform)) + { + var newPath = GetReplacementPath(binding.path); + + var newBinding = EditorCurveBinding.FloatCurve(newPath, binding.type, binding.propertyName); + clip.SetFloatCurve(newBinding, clip.GetFloatCurve(binding)); + clip.SetFloatCurve(binding, null); + } + } + } + + string GetReplacementPath(string bindingPath) + { + if (parentCache.TryGetValue(bindingPath, out var cached)) + { + return cached; + } + + var obj = asc.ObjectPathRemapper.GetObjectForPath(bindingPath)!.transform; + while (obj != null && transformLookthrough.Contains(obj)) + { + obj = obj.parent; + } + + string path; + if (obj == null) + { + path = bindingPath; + } + else + { + path = asc.ObjectPathRemapper.GetVirtualPathForObject(obj); + } + + parentCache[bindingPath] = path; + + return path; + } } private void TopoProcessMergeArmatures(ModularAvatarMergeArmature[] mergeArmatures) @@ -294,6 +356,7 @@ private void MergeArmature(ModularAvatarMergeArmature mergeArmature, GameObject _activeRetargeter.FixupAnimations(); thisPassAdded.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform)); + transformLookthrough.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform)); } /** @@ -357,7 +420,7 @@ private void RecursiveMerge(ModularAvatarMergeArmature config, BoneDatabase.AddMergedBone(mergedSrcBone.transform); BoneDatabase.RetainMergedBone(mergedSrcBone.transform); - PathMappings.MarkTransformLookthrough(mergedSrcBone); + transformLookthrough.Add(mergedSrcBone.transform); thisPassAdded.Add(mergedSrcBone.transform); } @@ -372,7 +435,7 @@ private void RecursiveMerge(ModularAvatarMergeArmature config, if (zipMerge) { - PathMappings.MarkTransformLookthrough(src); + transformLookthrough.Add(src.transform); BoneDatabase.AddMergedBone(src.transform); } diff --git a/Editor/MergeBlendTreePass.cs b/Editor/MergeBlendTreePass.cs index 89e5ffe5..fa8b113e 100644 --- a/Editor/MergeBlendTreePass.cs +++ b/Editor/MergeBlendTreePass.cs @@ -2,11 +2,9 @@ #region -using System; using System.Collections.Generic; -using nadena.dev.modular_avatar.animation; using nadena.dev.ndmf; -using nadena.dev.ndmf.util; +using nadena.dev.ndmf.animator; using UnityEditor.Animations; using UnityEngine; using VRC.SDK3.Avatars.Components; @@ -20,56 +18,49 @@ internal class MergeBlendTreePass : Pass internal const string ALWAYS_ONE = "__ModularAvatarInternal/One"; internal const string BlendTreeLayerName = "ModularAvatar: Merge Blend Tree"; - private AnimatorController _controller; - private BlendTree _rootBlendTree; - private GameObject _mergeHost; + private AnimatorServicesContext _asc; + private VirtualBlendTree _rootBlendTree; private HashSet _parameterNames; protected override void Execute(ndmf.BuildContext context) { + _asc = context.Extension(); _rootBlendTree = null; _parameterNames = new HashSet(); - _controller = new AnimatorController(); + var fx = _asc.ControllerContext[VRCAvatarDescriptor.AnimLayerType.FX]; + foreach (var component in context.AvatarRootObject.GetComponentsInChildren(true)) { ErrorReport.WithContextObject(component, () => ProcessComponent(context, component)); } - - List parameters = new List(_parameterNames.Count + 1); - if (_mergeHost != null) + + // always add the ALWAYS_ONE parameter + fx.Parameters = fx.Parameters.SetItem(ALWAYS_ONE, new AnimatorControllerParameter() { - _parameterNames.Remove(ALWAYS_ONE); + name = ALWAYS_ONE, + type = AnimatorControllerParameterType.Float, + defaultFloat = 1 + }); - parameters.Add(new AnimatorControllerParameter() + foreach (var name in _parameterNames) + { + if (fx.Parameters.ContainsKey(name)) continue; + + fx.Parameters = fx.Parameters.SetItem(name, new AnimatorControllerParameter() { - name = ALWAYS_ONE, + name = name, type = AnimatorControllerParameterType.Float, - defaultFloat = 1 + defaultFloat = 0.0f }); - - foreach (var name in _parameterNames) - { - parameters.Add(new AnimatorControllerParameter() - { - name = name, - type = AnimatorControllerParameterType.Float, - defaultFloat = 0 - }); - } - - var paramsAnimator = new AnimatorController(); - paramsAnimator.parameters = parameters.ToArray(); - - var paramsComponent = _mergeHost.AddComponent(); - paramsComponent.animator = paramsAnimator; - paramsComponent.layerPriority = Int32.MaxValue; } } - private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlendTree component) + private void ProcessComponent(BuildContext context, ModularAvatarMergeBlendTree component) { + var stash = context.PluginBuildContext.GetState(); + BlendTree componentBlendTree = component.BlendTree as BlendTree; if (componentBlendTree == null) @@ -79,46 +70,60 @@ private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlend } string basePath = null; + string rootPath = null; if (component.PathMode == MergeAnimatorPathMode.Relative) { var root = component.RelativePathRoot.Get(context.AvatarRootTransform); if (root == null) root = component.gameObject; - basePath = RuntimeUtil.AvatarRootPath(root) + "/"; + rootPath = RuntimeUtil.AvatarRootPath(root); + basePath = rootPath + "/"; + } + + var bt = stash.BlendTrees.GetValueOrDefault(component) + ?? _asc.ControllerContext.CloneContext.Clone(componentBlendTree); + + if (basePath != null) + { + var animationIndex = new AnimationIndex(new[] { bt }); + animationIndex.RewritePaths(p => p == "" ? rootPath : basePath + p); } - var bt = new DeepClone(context).DoClone(componentBlendTree, basePath); - var rootBlend = GetRootBlendTree(context); + var rootBlend = GetRootBlendTree(); - rootBlend.AddChild(bt); - var children = rootBlend.children; - children[children.Length - 1].directBlendParameter = ALWAYS_ONE; - rootBlend.children = children; - - foreach (var asset in bt.ReferencedAssets(includeScene: false)) + rootBlend.Children = rootBlend.Children.Add(new() { - if (asset is BlendTree bt2) + Motion = bt, + DirectBlendParameter = ALWAYS_ONE, + Threshold = 1, + CycleOffset = 1, + TimeScale = 1, + }); + + foreach (var asset in bt.AllReachableNodes()) + { + if (asset is VirtualBlendTree bt2) { - if (!string.IsNullOrEmpty(bt2.blendParameter) && bt2.blendType != BlendTreeType.Direct) + if (!string.IsNullOrEmpty(bt2.BlendParameter) && bt2.BlendType != BlendTreeType.Direct) { - _parameterNames.Add(bt2.blendParameter); + _parameterNames.Add(bt2.BlendParameter); } - if (bt2.blendType != BlendTreeType.Direct && bt2.blendType != BlendTreeType.Simple1D) + if (bt2.BlendType != BlendTreeType.Direct && bt2.BlendType != BlendTreeType.Simple1D) { - if (!string.IsNullOrEmpty(bt2.blendParameterY)) + if (!string.IsNullOrEmpty(bt2.BlendParameterY)) { - _parameterNames.Add(bt2.blendParameterY); + _parameterNames.Add(bt2.BlendParameterY); } } - if (bt2.blendType == BlendTreeType.Direct) + if (bt2.BlendType == BlendTreeType.Direct) { - foreach (var childMotion in bt2.children) + foreach (var childMotion in bt2.Children) { - if (!string.IsNullOrEmpty(childMotion.directBlendParameter)) + if (!string.IsNullOrEmpty(childMotion.DirectBlendParameter)) { - _parameterNames.Add(childMotion.directBlendParameter); + _parameterNames.Add(childMotion.DirectBlendParameter); } } } @@ -126,59 +131,22 @@ private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlend } } - private BlendTree GetRootBlendTree(ndmf.BuildContext context) + private VirtualBlendTree GetRootBlendTree() { if (_rootBlendTree != null) return _rootBlendTree; - var newController = new AnimatorController(); - var newStateMachine = new AnimatorStateMachine(); - var newState = new AnimatorState(); - - _rootBlendTree = new BlendTree(); - _controller = newController; - - newController.layers = new[] - { - new AnimatorControllerLayer - { - blendingMode = AnimatorLayerBlendingMode.Override, - defaultWeight = 1, - name = BlendTreeLayerName, - stateMachine = newStateMachine - } - }; - - newStateMachine.name = "ModularAvatarMergeBlendTree"; - newStateMachine.states = new[] - { - new ChildAnimatorState - { - state = newState, - position = Vector3.zero - } - }; - newStateMachine.defaultState = newState; + var fx = _asc.ControllerContext[VRCAvatarDescriptor.AnimLayerType.FX]; + var controller = fx.AddLayer(new LayerPriority(int.MinValue), BlendTreeLayerName); + var stateMachine = controller.StateMachine; - newState.writeDefaultValues = true; - newState.motion = _rootBlendTree; - - _rootBlendTree.blendType = BlendTreeType.Direct; - _rootBlendTree.blendParameter = ALWAYS_ONE; + _rootBlendTree = VirtualBlendTree.Create("Root"); + var state = stateMachine.AddState("State", _rootBlendTree); + stateMachine.DefaultState = state; + state.WriteDefaultValues = true; - var mergeObject = new GameObject("ModularAvatarMergeBlendTree"); - var merger = mergeObject.AddComponent(); - merger.animator = newController; - merger.pathMode = MergeAnimatorPathMode.Absolute; - merger.matchAvatarWriteDefaults = false; - merger.layerType = VRCAvatarDescriptor.AnimLayerType.FX; - merger.deleteAttachedAnimator = false; - merger.layerPriority = Int32.MinValue; + _rootBlendTree.BlendType = BlendTreeType.Direct; + _rootBlendTree.BlendParameter = ALWAYS_ONE; - mergeObject.transform.SetParent(context.AvatarRootTransform, false); - mergeObject.transform.SetSiblingIndex(0); - - _mergeHost = mergeObject; - return _rootBlendTree; } } diff --git a/Editor/MeshRetargeter.cs b/Editor/MeshRetargeter.cs index 96e6cb91..3a1e34cc 100644 --- a/Editor/MeshRetargeter.cs +++ b/Editor/MeshRetargeter.cs @@ -27,6 +27,7 @@ using JetBrains.Annotations; using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.editor.ErrorReporting; +using nadena.dev.ndmf.animator; using UnityEngine; namespace nadena.dev.modular_avatar.core.editor @@ -84,13 +85,15 @@ public Transform GetRetargetedBone(Transform bone, bool fallbackToOriginal) internal class RetargetMeshes { private BoneDatabase _boneDatabase; - private PathMappings _pathTracker; + private AnimationIndex _animationIndex; + private ObjectPathRemapper _pathRemapper; internal void OnPreprocessAvatar(GameObject avatarGameObject, BoneDatabase boneDatabase, - PathMappings pathMappings) + AnimatorServicesContext pathMappings) { this._boneDatabase = boneDatabase; - this._pathTracker = pathMappings; + this._animationIndex = pathMappings.AnimationIndex; + this._pathRemapper = pathMappings.ObjectPathRemapper; foreach (var renderer in avatarGameObject.GetComponentsInChildren(true)) { @@ -153,7 +156,8 @@ internal void OnPreprocessAvatar(GameObject avatarGameObject, BoneDatabase boneD child.SetParent(destBone, true); } - _pathTracker.MarkRemoved(sourceBone.gameObject); + // Remap any animation clips that reference this bone into its parent + _pathRemapper.ReplaceObject(sourceBone.gameObject, sourceBone.transform.parent.gameObject); UnityEngine.Object.DestroyImmediate(sourceBone.gameObject); } } diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs index ffc77033..b73e5a32 100644 --- a/Editor/PluginDefinition/PluginDefinition.cs +++ b/Editor/PluginDefinition/PluginDefinition.cs @@ -5,6 +5,7 @@ using nadena.dev.modular_avatar.core.editor.plugin; using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; using nadena.dev.ndmf.fluent; using UnityEngine; using Object = UnityEngine.Object; @@ -46,27 +47,39 @@ protected override void Configure() seq.Run(ClearEditorOnlyTags.Instance); seq.Run(MeshSettingsPluginPass.Instance); seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview()); + + // All these need to move to the new ASC #if MA_VRCSDK3_AVATARS seq.Run(ReactiveObjectPrepass.Instance); - seq.Run(RenameParametersPluginPass.Instance); - seq.Run(ParameterAssignerPass.Instance); - seq.Run(MergeBlendTreePass.Instance); - seq.Run(MergeAnimatorPluginPass.Instance); - seq.Run(ApplyAnimatorDefaultValuesPass.Instance); #endif - seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 => + seq.WithRequiredExtension(typeof(AnimatorServicesContext), _s2 => { #if MA_VRCSDK3_AVATARS - seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute()) - .PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(), new MaterialSetterPreview()); + seq.Run(RenameParametersPluginPass.Instance); + seq.Run(ParameterAssignerPass.Instance); + seq.Run(MergeBlendTreePass.Instance); + seq.Run(MergeAnimatorPluginPass.Instance); + seq.Run(ApplyAnimatorDefaultValuesPass.Instance); + + seq.WithRequiredExtension(typeof(ReadablePropertyExtension), _s3 => + { + seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute()) + .PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(), + new MaterialSetterPreview()); + }); + seq.Run(GameObjectDelayDisablePass.Instance); // TODO: We currently run this above MergeArmaturePlugin, because Merge Armature might destroy // game objects which contain Menu Installers. It'd probably be better however to teach Merge Armature // to retain those objects? maybe? seq.Run(MenuInstallPluginPass.Instance); #endif - seq.Run(MergeArmaturePluginPass.Instance); + + }); + + seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 => + { seq.Run(BoneProxyPluginPass.Instance); #if MA_VRCSDK3_AVATARS seq.Run(VisibleHeadAccessoryPluginPass.Instance); @@ -77,7 +90,7 @@ protected override void Configure() seq.Run(ReplaceObjectPluginPass.Instance); #if MA_VRCSDK3_AVATARS seq.Run(BlendshapeSyncAnimationPluginPass.Instance); - seq.Run(GameObjectDelayDisablePass.Instance); + // seq.Run(GameObjectDelayDisablePass.Instance); - TODO, move back here #endif seq.Run(ConstraintConverterPass.Instance); }); diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs index 2f804945..d5f76056bb9 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs @@ -31,7 +31,7 @@ private string GetActiveSelfProxy(GameObject obj) { if (_asc != null) { - return _asc.GetActiveSelfProxy(obj); + return _rpe.GetActiveSelfProxy(obj); } else { diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs index 228f215c..986499fe 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs @@ -4,6 +4,7 @@ using System.Linq; using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.core.editor.Simulator; +using nadena.dev.ndmf.animator; using nadena.dev.ndmf.preview; using UnityEngine; @@ -17,7 +18,9 @@ internal partial class ReactiveObjectAnalyzer { private readonly ComputeContext _computeContext; private readonly ndmf.BuildContext _context; - private readonly AnimationServicesContext _asc; + private readonly AnimatorServicesContext _asc; + private readonly ReadablePropertyExtension _rpe; + private Dictionary _simulationInitialStates; public const string BlendshapePrefix = "blendShape."; @@ -34,7 +37,8 @@ public ReactiveObjectAnalyzer(ndmf.BuildContext context) { _computeContext = ComputeContext.NullContext; _context = context; - _asc = context.Extension(); + _asc = context.Extension(); + _rpe = context.Extension(); _simulationInitialStates = null; } @@ -145,7 +149,7 @@ private void ApplyInitialStateOverrides(Dictionary /// private void AnalyzeConstants(Dictionary shapes) { - var asc = _context?.Extension(); + var asc = _context?.Extension(); HashSet toggledObjects = new(); if (asc == null) return; @@ -160,7 +164,10 @@ private void AnalyzeConstants(Dictionary shapes) { foreach (var condition in actionGroup.ControllingConditions) if (condition.ReferenceObject != null && !toggledObjects.Contains(condition.ReferenceObject)) - condition.IsConstant = asc.AnimationDatabase.ClipsForPath(asc.PathMappings.GetObjectIdentifier(condition.ReferenceObject)).IsEmpty; + condition.IsConstant = !asc.AnimationIndex.GetClipsForObjectPath( + asc.ObjectPathRemapper.GetVirtualPathForObject(condition.ReferenceObject) ?? + "___NONEXISTENT___" + ).Any(); // Remove redundant active conditions. actionGroup.ControllingConditions.RemoveAll(c => c.IsConstant && c.InitiallyActive); @@ -187,7 +194,7 @@ private void AnalyzeConstants(Dictionary shapes) /// private void ResolveToggleInitialStates(Dictionary groups) { - var asc = _context?.Extension(); + var asc = _context?.Extension(); Dictionary propStates = new(); Dictionary nextPropStates = new(); diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs index 00b4173f..4ecc35b2 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using nadena.dev.modular_avatar.animation; +using nadena.dev.ndmf.animator; using UnityEditor; using UnityEditor.Animations; using UnityEngine; @@ -23,8 +24,8 @@ internal partial class ReactiveObjectPass // Properties that are being driven, either by foreign animations or Object Toggles private HashSet activeProps = new(); - - private AnimationClip _initialStateClip; + + private VirtualClip _initialStateClip; private bool _writeDefaults; public ReactiveObjectPass(ndmf.BuildContext context) @@ -38,7 +39,14 @@ internal void Execute() // Having a WD OFF layer after WD ON layers can break WD. We match the behavior of the existing states, // and if mixed, use WD ON to maximize compatibility. - _writeDefaults = MergeAnimatorProcessor.ProbeWriteDefaults(FindFxController().animatorController as AnimatorController) ?? true; + var asc = context.Extension(); + _writeDefaults = asc.ControllerContext[VRCAvatarDescriptor.AnimLayerType.FX]?.Layers.Any( + l => l.StateMachine.StateMachines.Any( + sm => sm.StateMachine.AllStates().Any( + s => s.WriteDefaultValues && s.Motion is not VirtualBlendTree + ) + ) + ) ?? true; var analysis = new ReactiveObjectAnalyzer(context).Analyze(context.AvatarRootObject); @@ -60,7 +68,7 @@ internal void Execute() private void GenerateActiveSelfProxies(Dictionary shapes) { - var asc = context.Extension(); + var rpe = context.Extension(); foreach (var prop in shapes.Keys) { @@ -68,7 +76,7 @@ private void GenerateActiveSelfProxies(Dictionary { // Ensure a proxy exists for each object we're going to be toggling. // TODO: is this still needed? - asc.GetActiveSelfProxy(go); + rpe.GetActiveSelfProxy(go); } } } @@ -91,19 +99,19 @@ private void ProcessInitialAnimatorVariables(Dictionary initialStates, Dictionary shapes) { - var asc = context.Extension(); + var asc = context.Extension(); + var rpe = context.Extension(); // We need to track _two_ initial states: the initial state we'll apply at build time (which applies // when animations are disabled) and the animation base state. Confusingly, the animation base state // should be the state that is currently applied to the object... - - var clips = context.Extension().AnimationDatabase; - var initialStateHolder = clips.ClipsForPath(ReactiveObjectPrepass.TAG_PATH).FirstOrDefault(); - if (initialStateHolder == null) return; - _initialStateClip = new AnimationClip(); - _initialStateClip.name = "Reactive Component Defaults"; - initialStateHolder.CurrentClip = _initialStateClip; + var clips = asc.AnimationIndex; + _initialStateClip = clips.GetClipsForObjectPath(ReactiveObjectPrepass.TAG_PATH).FirstOrDefault(); + + if (_initialStateClip == null) return; + + _initialStateClip.Name = "Reactive Component Defaults"; foreach (var (key, initialState) in initialStates) { @@ -186,17 +194,17 @@ private void ProcessInitialStates(Dictionary initialStates, curve.AddKey(0, f); curve.AddKey(1, f); - AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve); + _initialStateClip.SetFloatCurve(binding, curve); if (componentType == typeof(GameObject) && key.PropertyName == "m_IsActive") { binding = EditorCurveBinding.FloatCurve( "", typeof(Animator), - asc.GetActiveSelfProxy((GameObject)key.TargetObject) + rpe.GetActiveSelfProxy((GameObject)key.TargetObject) ); - AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve); + _initialStateClip.SetFloatCurve(binding, curve); } } else if (animBaseState is Object obj) @@ -206,8 +214,8 @@ private void ProcessInitialStates(Dictionary initialStates, componentType, key.PropertyName ); - - AnimationUtility.SetObjectReferenceCurve(_initialStateClip, binding, new [] + + _initialStateClip.SetObjectCurve(binding, new[] { new ObjectReferenceKeyframe() { @@ -308,7 +316,7 @@ private void ProcessShapeKey(AnimatedProperty info) private AnimatorStateMachine GenerateStateMachine(AnimatedProperty info) { - var asc = context.Extension(); + var asc = context.Extension(); var asm = new AnimatorStateMachine(); // Workaround for the warning: "'.' is not allowed in State name" @@ -333,7 +341,6 @@ private AnimatorStateMachine GenerateStateMachine(AnimatedProperty info) position = new Vector3(x, y), state = initialState }); - asc.AnimationDatabase.RegisterState(states[^1].state); var lastConstant = info.actionGroups.FindLastIndex(agk => agk.IsConstant); var transitionBuffer = new List<(AnimatorState, List)>(); @@ -363,7 +370,7 @@ private AnimatorStateMachine GenerateStateMachine(AnimatedProperty info) clip.name = "Property Overlay controlled by " + group.ControllingConditions[0].DebugName + " " + group.Value; - var conditions = GetTransitionConditions(asc, group); + var conditions = GetTransitionConditions(group); foreach (var (st, transitions) in transitionBuffer) { @@ -407,7 +414,6 @@ private AnimatorStateMachine GenerateStateMachine(AnimatedProperty info) position = new Vector3(x, y), state = state }); - asc.AnimationDatabase.RegisterState(states[^1].state); var transitionList = new List(); transitionBuffer.Add((state, transitionList)); @@ -488,7 +494,7 @@ private static AnimatorCondition InvertCondition(AnimatorCondition cond) }; } - private AnimatorCondition[] GetTransitionConditions(AnimationServicesContext asc, ReactionRule group) + private AnimatorCondition[] GetTransitionConditions(ReactionRule group) { var conditions = new List(); @@ -574,8 +580,8 @@ private Motion AnimResult(TargetProp key, object value) if (key.TargetObject is GameObject targetObject && key.PropertyName == "m_IsActive") { - var asc = context.Extension(); - var propName = asc.GetActiveSelfProxy(targetObject); + var rpe = context.Extension(); + var propName = rpe.GetActiveSelfProxy(targetObject); binding = EditorCurveBinding.FloatCurve("", typeof(Animator), propName); AnimationUtility.SetEditorCurve(clip, binding, curve); } @@ -586,47 +592,29 @@ private Motion AnimResult(TargetProp key, object value) private void ApplyController(AnimatorStateMachine asm, string layerName) { - var fx = FindFxController(); - - if (fx.animatorController == null) - { - throw new InvalidOperationException("No FX layer found"); - } - - if (!context.IsTemporaryAsset(fx.animatorController)) - { - throw new InvalidOperationException("FX layer is not a temporary asset"); - } + var asc = context.Extension(); + var fx = asc.ControllerContext[ + VRCAvatarDescriptor.AnimLayerType.FX + ]; - if (!(fx.animatorController is AnimatorController animController)) + if (fx == null) { - throw new InvalidOperationException("FX layer is not an animator controller"); + throw new InvalidOperationException("No FX layer found"); } - var paramList = animController.parameters.ToList(); - var paramSet = paramList.Select(p => p.name).ToHashSet(); - - foreach (var paramName in initialValues.Keys.Except(paramSet)) + foreach (var paramName in initialValues.Keys.Except(fx.Parameters.Keys)) { - paramList.Add(new AnimatorControllerParameter() + var parameter = new AnimatorControllerParameter { name = paramName, type = AnimatorControllerParameterType.Float, defaultFloat = initialValues[paramName], // TODO - }); - paramSet.Add(paramName); + }; + fx.Parameters = fx.Parameters.SetItem(paramName, parameter); } - animController.parameters = paramList.ToArray(); - - animController.layers = animController.layers.Append( - new AnimatorControllerLayer - { - stateMachine = asm, - name = "RC " + layerName, - defaultWeight = 1 - } - ).ToArray(); + fx.AddLayer(LayerPriority.Default, "RC " + layerName).StateMachine = + asc.ControllerContext.Clone(asm); } private VRCAvatarDescriptor.CustomAnimLayer FindFxController() diff --git a/Editor/ReactiveObjects/ParameterAssignerPass.cs b/Editor/ReactiveObjects/ParameterAssignerPass.cs index f7512050..cb0b039f 100644 --- a/Editor/ReactiveObjects/ParameterAssignerPass.cs +++ b/Editor/ReactiveObjects/ParameterAssignerPass.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; using UnityEditor.Animations; using UnityEngine; using VRC.SDK3.Avatars.Components; @@ -201,18 +202,20 @@ protected override void Execute(ndmf.BuildContext context) if (mamiWithRC.Count > 0) { - // This make sures the parameters are correctly merged into the FX layer. - var mergeAnimator = context.AvatarRootObject.AddComponent(); - mergeAnimator.layerType = VRCAvatarDescriptor.AnimLayerType.FX; - mergeAnimator.deleteAttachedAnimator = false; - mergeAnimator.animator = new AnimatorController + var asc = context.Extension(); + var fx = asc.ControllerContext[VRCAvatarDescriptor.AnimLayerType.FX]; + + foreach (var (name, _) in mamiWithRC) { - parameters = mamiWithRC.Select(kvp => new AnimatorControllerParameter + if (!fx.Parameters.ContainsKey(name)) { - name = kvp.Key, - type = AnimatorControllerParameterType.Float, - }).ToArray(), - }; + fx.Parameters = fx.Parameters.SetItem(name, new() + { + name = name, + type = AnimatorControllerParameterType.Float, + }); + } + } } } diff --git a/Editor/RenameParametersHook.cs b/Editor/RenameParametersHook.cs index 02de614c..ee33b64f 100644 --- a/Editor/RenameParametersHook.cs +++ b/Editor/RenameParametersHook.cs @@ -9,6 +9,7 @@ using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; using UnityEditor; using UnityEditor.Animations; using UnityEngine; @@ -55,6 +56,43 @@ internal class DefaultValues { public ImmutableDictionary InitialValueOverrides; } + + internal class RenamedMergeAnimators + { + public AnimatorServicesContext AnimatorServices; + public Dictionary Controllers = new(); + public Dictionary BlendTrees = new(); + + public VirtualAnimatorController Clone(ModularAvatarMergeAnimator mama) + { + if (Controllers.TryGetValue(mama, out var controller)) + { + return controller; + } + + if (mama.animator == null) return null; + + var cloned = AnimatorServices.ControllerContext.CloneContext.Clone(mama.animator); + Controllers[mama] = cloned; + + return cloned; + } + + public VirtualBlendTree Clone(ModularAvatarMergeBlendTree mbt) + { + if (BlendTrees.TryGetValue(mbt, out var blendTree)) + { + return blendTree; + } + + if (mbt.BlendTree is not BlendTree bt) return null; + + var cloned = (VirtualBlendTree)AnimatorServices.ControllerContext.CloneContext.Clone(bt); + BlendTrees[mbt] = cloned; + + return cloned; + } + } internal class RenameParametersHook { @@ -163,6 +201,10 @@ public void OnPreprocessAvatar(GameObject avatar, BuildContext context) if (!context.AvatarDescriptor) return; _context = context; + + var stash = _context.PluginBuildContext.GetState(); + var asc = _context.PluginBuildContext.Extension(); + stash.AnimatorServices = asc; var syncParams = WalkTree(avatar); @@ -389,11 +431,13 @@ GameObject obj if (merger.animator != null) { - Profiler.BeginSample("DeepCloneAnimator"); - merger.animator = new DeepClone(_context.PluginBuildContext).DoClone(merger.animator); - Profiler.EndSample(); + var stash = _context.PluginBuildContext.GetState(); - ProcessRuntimeAnimatorController(merger.animator, remap); + var controller = stash.Clone(merger); + + ProcessVirtualAnimatorController(controller, remap); + + stash.Controllers[merger] = controller; } break; @@ -404,8 +448,12 @@ GameObject obj var bt = merger.BlendTree as BlendTree; if (bt != null) { - merger.BlendTree = bt = new DeepClone(_context.PluginBuildContext).DoClone(bt); - ProcessBlendtree(bt, paramInfo.GetParameterRemappingsAt(obj)); + var stash = _context.PluginBuildContext.GetState(); + + var virtualbt = stash.Clone(merger); + ProcessBlendtree(virtualbt, paramInfo.GetParameterRemappingsAt(obj)); + + stash.BlendTrees[merger] = virtualbt; } break; @@ -497,28 +545,6 @@ GameObject obj return rv; } - private void ProcessRuntimeAnimatorController(RuntimeAnimatorController controller, - ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remap) - { - if (controller is AnimatorController ac) - { - ProcessAnimator(ac, remap); - } - else if (controller is AnimatorOverrideController aoc) - { - var list = new List>(); - aoc.GetOverrides(list); - - for (var i = 0; i < list.Count; i++) - { - var kvp = list[i]; - if (kvp.Value != null) ProcessClip(kvp.Value, remap); - } - - ProcessRuntimeAnimatorController(aoc.runtimeAnimatorController, remap); - } - } - private void ProcessMenuInstaller(ModularAvatarMenuInstaller installer, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { @@ -537,113 +563,57 @@ private void ProcessMenuInstaller(ModularAvatarMenuInstaller installer, }); } - private void ProcessAnimator(AnimatorController controller, - ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) + private void ProcessVirtualAnimatorController(VirtualAnimatorController controller, + ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remap) { - if (remaps.IsEmpty) return; - - var visited = new HashSet(); - var queue = new Queue(); - - - var parameters = controller.parameters; - for (int i = 0; i < parameters.Length; i++) + foreach (var node in controller.AllReachableNodes()) { - if (remaps.TryGetValue((ParameterNamespace.Animator, parameters[i].name), out var newName)) + switch (node) { - parameters[i].name = newName.ParameterName; + case VirtualState vs: ProcessState(vs, remap); break; + case VirtualTransition vt: ProcessTransition(vt, remap); break; + case VirtualClip vc: ProcessClip(vc, remap); break; + case VirtualBlendTree bt: ProcessBlendtree(bt, remap); break; } } - controller.parameters = parameters; - - foreach (var layer in controller.layers) - { - if (layer.stateMachine != null) - { - queue.Enqueue(layer.stateMachine); - } - } + var newParameters = controller.Parameters.Clear(); - Profiler.BeginSample("Walk animator graph"); - while (queue.Count > 0) + foreach (var (name, parameter) in controller.Parameters) { - var sm = queue.Dequeue(); - if (visited.Contains(sm)) continue; - visited.Add(sm); - - foreach (var behavior in sm.behaviours) - { - if (behavior is VRCAvatarParameterDriver driver) - { - ProcessDriver(driver, remaps); - } - } - - foreach (var t in sm.anyStateTransitions) - { - ProcessTransition(t, remaps); - } - - foreach (var t in sm.entryTransitions) - { - ProcessTransition(t, remaps); - } - - foreach (var sub in sm.stateMachines) + if (remap.TryGetValue((ParameterNamespace.Animator, name), out var newParam)) { - queue.Enqueue(sub.stateMachine); - - - foreach (var t in sm.GetStateMachineTransitions(sub.stateMachine)) - { - ProcessTransition(t, remaps); - } + newParameters = newParameters.Add(newParam.ParameterName, parameter); } - - foreach (var st in sm.states) + else { - ProcessState(st.state, remaps); + newParameters = newParameters.Add(name, parameter); } } - Profiler.EndSample(); + + controller.Parameters = newParameters; } - private void ProcessState(AnimatorState state, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) + private void ProcessState(VirtualState state, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { - state.mirrorParameter = remap(remaps, state.mirrorParameter); - state.timeParameter = remap(remaps, state.timeParameter); - state.speedParameter = remap(remaps, state.speedParameter); - state.cycleOffsetParameter = remap(remaps, state.cycleOffsetParameter); + state.MirrorParameter = remap(remaps, state.MirrorParameter); + state.TimeParameter = remap(remaps, state.TimeParameter); + state.SpeedParameter = remap(remaps, state.SpeedParameter); + state.CycleOffsetParameter = remap(remaps, state.CycleOffsetParameter); - foreach (var t in state.transitions) - { - ProcessTransition(t, remaps); - } - - foreach (var behavior in state.behaviours) + foreach (var behavior in state.Behaviours) { if (behavior is VRCAvatarParameterDriver driver) { ProcessDriver(driver, remaps); } } - - ProcessMotion(state.motion, remaps); } - private void ProcessMotion(Motion motion, + private void ProcessClip(VirtualClip clip, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { - if (motion is BlendTree blendTree) ProcessBlendtree(blendTree, remaps); - - if (motion is AnimationClip clip) ProcessClip(clip, remaps); - } - - private void ProcessClip(AnimationClip clip, - ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) - { - var curveBindings = AnimationUtility.GetCurveBindings(clip); + var curveBindings = clip.GetFloatCurveBindings(); var bindingsToUpdate = new List(); var newCurves = new List(); @@ -653,48 +623,30 @@ private void ProcessClip(AnimationClip clip, if (binding.path != "" || binding.type != typeof(Animator)) continue; if (remaps.TryGetValue((ParameterNamespace.Animator, binding.propertyName), out var newBinding)) { - var curCurve = AnimationUtility.GetEditorCurve(clip, binding); - - bindingsToUpdate.Add(binding); - newCurves.Add(null); - - bindingsToUpdate.Add(new EditorCurveBinding + var curCurve = clip.GetFloatCurve(binding); + var newECB = new EditorCurveBinding { path = "", type = typeof(Animator), propertyName = newBinding.ParameterName - }); - newCurves.Add(curCurve); + }; + + clip.SetFloatCurve(binding, null); + clip.SetFloatCurve(newECB, curCurve); } } - - if (bindingsToUpdate.Any()) - { - AnimationUtility.SetEditorCurves(clip, bindingsToUpdate.ToArray(), newCurves.ToArray()); - - // Workaround apparent unity bug where the clip's curves are not deleted - for (var i = 0; i < bindingsToUpdate.Count; i++) - if (newCurves[i] == null && AnimationUtility.GetEditorCurve(clip, bindingsToUpdate[i]) != null) - AnimationUtility.SetEditorCurve(clip, bindingsToUpdate[i], newCurves[i]); - } } - private void ProcessBlendtree(BlendTree blendTree, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) + private void ProcessBlendtree(VirtualBlendTree blendTree, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { - blendTree.blendParameter = remap(remaps, blendTree.blendParameter); - blendTree.blendParameterY = remap(remaps, blendTree.blendParameterY); + blendTree.BlendParameter = remap(remaps, blendTree.BlendParameter); + blendTree.BlendParameterY = remap(remaps, blendTree.BlendParameterY); - var children = blendTree.children; - for (int i = 0; i < children.Length; i++) + var children = blendTree.Children; + foreach (var child in children) { - var childMotion = children[i]; - ProcessMotion(childMotion.motion, remaps); - - childMotion.directBlendParameter = remap(remaps, childMotion.directBlendParameter); - children[i] = childMotion; + child.DirectBlendParameter = remap(remaps, child.DirectBlendParameter); } - - blendTree.children = children; } private void ProcessDriver(VRCAvatarParameterDriver driver, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) @@ -710,19 +662,17 @@ private void ProcessDriver(VRCAvatarParameterDriver driver, ImmutableDictionary< } } - private void ProcessTransition(AnimatorTransitionBase t, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) + private void ProcessTransition(VirtualTransitionBase t, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { bool dirty = false; - var conditions = t.conditions; - - for (int i = 0; i < conditions.Length; i++) - { - var cond = conditions[i]; - cond.parameter = remap(remaps, cond.parameter, ref dirty); - conditions[i] = cond; - } - - if (dirty) t.conditions = conditions; + var conditions = t.Conditions + .Select(cond => + { + cond.parameter = remap(remaps, cond.parameter, ref dirty); + return cond; + }) + .ToImmutableList(); + t.Conditions = conditions; } private ImmutableDictionary CollectParameters(ModularAvatarParameters p, diff --git a/UnitTests~/ActiveAnimationRetargeterTests/ActiveAnimationRetargeterTests.cs b/UnitTests~/ActiveAnimationRetargeterTests/ActiveAnimationRetargeterTests.cs index e689cffd..ffa29e98 100644 --- a/UnitTests~/ActiveAnimationRetargeterTests/ActiveAnimationRetargeterTests.cs +++ b/UnitTests~/ActiveAnimationRetargeterTests/ActiveAnimationRetargeterTests.cs @@ -1,11 +1,14 @@ #if MA_VRCSDK3_AVATARS +using System.Linq; using modular_avatar_tests; using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.core.editor; +using nadena.dev.ndmf.animator; using NUnit.Framework; using UnityEditor; using UnityEngine; +using VRC.SDK3.Avatars.Components; using EditorCurveBinding = UnityEditor.EditorCurveBinding; public class ActiveAnimationRetargeterTests : TestBase @@ -17,8 +20,7 @@ public void SimpleRetarget() // initialize context var buildContext = new BuildContext(avatar); - var pathMappings = buildContext.PluginBuildContext.ActivateExtensionContext() - .PathMappings; + var asc = buildContext.PluginBuildContext.ActivateExtensionContextRecursive(); // get game objects var changedChild = avatar.transform.Find("Toggled/Child"); @@ -29,18 +31,16 @@ public void SimpleRetarget() var created = retargeter.CreateIntermediateObjects(newParent.gameObject); retargeter.FixupAnimations(); - // commit - buildContext.AnimationDatabase.Commit(); - - var clip = findFxClip(avatar, layerName: "retarget"); - var curveBindings = AnimationUtility.GetCurveBindings(clip); + var fx = asc.ControllerContext[VRCAvatarDescriptor.AnimLayerType.FX]!; + var clip = (VirtualClip) fx.Layers.First(l => l.Name == "retarget").StateMachine.DefaultState!.Motion; + var curveBindings = clip!.GetFloatCurveBindings(); // Intermediate object must be created Assert.That(created, Is.Not.EqualTo(newParent.gameObject)); // The created animation must have m_IsActive of intermediate object Assert.That(curveBindings, Does.Contain(EditorCurveBinding.FloatCurve( - pathMappings.GetObjectIdentifier(created), typeof(GameObject), "m_IsActive"))); + asc.ObjectPathRemapper.GetVirtualPathForObject(created), typeof(GameObject), "m_IsActive"))); } } diff --git a/UnitTests~/Animation/AvatarMask/AvatarMaskTest.cs b/UnitTests~/Animation/AvatarMask/AvatarMaskTest.cs index 09bf1825..1d4bc8ad 100644 --- a/UnitTests~/Animation/AvatarMask/AvatarMaskTest.cs +++ b/UnitTests~/Animation/AvatarMask/AvatarMaskTest.cs @@ -108,9 +108,10 @@ public void whenAvatarMaskIsPresentOnMergedAnimator_rewritesPathsByPrefix() Assert.Greater(animRootIndex, parentIndex); Assert.Greater(bodyIndex, animRootIndex); - // Body is still enabled; the injected parent and parent/anim-root are not + // Body is still enabled; the injected parent is not. anim-root should be enabled, since the test mask has + // the root element enabled. Assert.IsTrue(state.transformMaskElements[parentIndex].Item2 < 0.5f); - Assert.IsTrue(state.transformMaskElements[animRootIndex].Item2 < 0.5f); + Assert.IsTrue(state.transformMaskElements[animRootIndex].Item2 > 0.5f); Assert.IsTrue(state.transformMaskElements[bodyIndex].Item2 > 0.5f); // Original paths are removed diff --git a/UnitTests~/MergeAnimatorTests/MergeSingleTests.cs b/UnitTests~/MergeAnimatorTests/MergeSingleTests.cs index 2da5afbf..71c5bb8e 100644 --- a/UnitTests~/MergeAnimatorTests/MergeSingleTests.cs +++ b/UnitTests~/MergeAnimatorTests/MergeSingleTests.cs @@ -5,6 +5,7 @@ using nadena.dev.modular_avatar.core; using nadena.dev.modular_avatar.core.editor; using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; using NUnit.Framework; using UnityEngine; using BuildContext = nadena.dev.ndmf.BuildContext; @@ -22,9 +23,13 @@ public void NoErrorWhenAnimatorIsNull() var ctx = new BuildContext(av, null); ctx.ActivateExtensionContext(); - ctx.ActivateExtensionContext(); + ctx.ActivateExtensionContextRecursive(); - var errors = ErrorReport.CaptureErrors(() => new MergeAnimatorProcessor().OnPreprocessAvatar(av, ctx)); + var errors = ErrorReport.CaptureErrors(() => + { + new MergeAnimatorProcessor().OnPreprocessAvatar(av, ctx); + ctx.DeactivateAllExtensionContexts(); + }); Assert.IsEmpty(errors); } @@ -39,9 +44,15 @@ public void MergeAnimationOverrideController() var ctx = new BuildContext(av, null); ctx.ActivateExtensionContext(); - ctx.ActivateExtensionContext(); + ctx.ActivateExtensionContextRecursive(); - var errors = ErrorReport.CaptureErrors(() => new MergeAnimatorProcessor().OnPreprocessAvatar(av, ctx)); + var errors = ErrorReport.CaptureErrors(() => + { + new MergeAnimatorProcessor().OnPreprocessAvatar(av, ctx); + ctx.DeactivateAllExtensionContexts(); + }); + + ctx.DeactivateAllExtensionContexts(); Assert.IsEmpty(errors); diff --git a/UnitTests~/MergeAnimatorTests/PreexistingAnimatorParams/PreexistingParamsTest.cs b/UnitTests~/MergeAnimatorTests/PreexistingAnimatorParams/PreexistingParamsTest.cs index 3f1739d7..7c4bce15 100644 --- a/UnitTests~/MergeAnimatorTests/PreexistingAnimatorParams/PreexistingParamsTest.cs +++ b/UnitTests~/MergeAnimatorTests/PreexistingAnimatorParams/PreexistingParamsTest.cs @@ -25,6 +25,8 @@ public void TestPreexistingParameterOverwritePolicy() foreach (var kvp in paramDict) { + if (kvp.Key.StartsWith("__ModularAvatarInternal/")) continue; + if (kvp.Key == "default_override" || kvp.Key == "animator_only") { Assert.AreEqual(1, kvp.Value); diff --git a/UnitTests~/MergeAnimatorTests/SyncedLayerOverrideInSubStatemachine/SyncedLayerOverrideInSubStateMachine.cs b/UnitTests~/MergeAnimatorTests/SyncedLayerOverrideInSubStatemachine/SyncedLayerOverrideInSubStateMachine.cs index ecf3c88a..7c24c171 100644 --- a/UnitTests~/MergeAnimatorTests/SyncedLayerOverrideInSubStatemachine/SyncedLayerOverrideInSubStateMachine.cs +++ b/UnitTests~/MergeAnimatorTests/SyncedLayerOverrideInSubStatemachine/SyncedLayerOverrideInSubStateMachine.cs @@ -1,8 +1,10 @@ #if MA_VRCSDK3_AVATARS +using HarmonyLib; using modular_avatar_tests; using nadena.dev.ndmf; using NUnit.Framework; +using UnityEditor; using UnityEditor.Animations; using VRC.SDK3.Avatars.Components; @@ -16,7 +18,9 @@ public void SyncedLayerOverride_usesCorrectStateOverride() var controller = LoadAsset("syncedlayer.controller"); var root = CreateRoot("root"); var vrc_descriptor = root.GetComponent(); - + + vrc_descriptor.customizeAnimationLayers = true; + var layers = vrc_descriptor.baseAnimationLayers; for (int i = 0; i < layers.Length; i++) { diff --git a/UnitTests~/MergeArmatureTests/MultiLevelMergeTest.cs b/UnitTests~/MergeArmatureTests/MultiLevelMergeTest.cs index dc65c5d1..3020d64e 100644 --- a/UnitTests~/MergeArmatureTests/MultiLevelMergeTest.cs +++ b/UnitTests~/MergeArmatureTests/MultiLevelMergeTest.cs @@ -2,6 +2,7 @@ using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.core; using nadena.dev.modular_avatar.core.editor; +using nadena.dev.ndmf.animator; using NUnit.Framework; using UnityEngine; @@ -54,7 +55,7 @@ public void mergeProcessesInTopoOrder() nadena.dev.ndmf.BuildContext context = new nadena.dev.ndmf.BuildContext(root, null); context.ActivateExtensionContext(); - context.ActivateExtensionContext(); + context.ActivateExtensionContextRecursive(); new MergeArmatureHook().OnPreprocessAvatar(context, root); Assert.IsTrue(bone.GetComponentInChildren() != null); @@ -82,7 +83,7 @@ public void canDisableNameMangling() nadena.dev.ndmf.BuildContext context = new nadena.dev.ndmf.BuildContext(root, null); context.ActivateExtensionContext(); - context.ActivateExtensionContext(); + context.ActivateExtensionContextRecursive(); new MergeArmatureHook().OnPreprocessAvatar(context, root); Assert.IsTrue(m_bone == null); // destroyed by retargeting pass @@ -106,7 +107,7 @@ public void manglesByDefault() nadena.dev.ndmf.BuildContext context = new nadena.dev.ndmf.BuildContext(root, null); context.ActivateExtensionContext(); - context.ActivateExtensionContext(); + context.ActivateExtensionContextRecursive(); new MergeArmatureHook().OnPreprocessAvatar(context, root); Assert.IsTrue(m_bone == null); // destroyed by retargeting pass diff --git a/UnitTests~/RenameParametersTests/RenameParametersTests.cs b/UnitTests~/RenameParametersTests/RenameParametersTests.cs index 8942b5e4..8fb914e0 100644 --- a/UnitTests~/RenameParametersTests/RenameParametersTests.cs +++ b/UnitTests~/RenameParametersTests/RenameParametersTests.cs @@ -7,6 +7,7 @@ using nadena.dev.modular_avatar.core; using nadena.dev.modular_avatar.core.editor; using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; using NUnit.Framework; using UnityEditor.Animations; using UnityEngine; @@ -79,6 +80,7 @@ public void TestParameterConflicts() var context = CreateContext(prefab); var maContext = context.ActivateExtensionContext().BuildContext; + context.ActivateExtensionContextRecursive(); var errors = ErrorReport.CaptureErrors( () => @@ -209,6 +211,7 @@ public void TestMultipleRemappings() var context = CreateContext(av); var maContext = context.ActivateExtensionContext().BuildContext; + context.ActivateExtensionContextRecursive(); var errors = ErrorReport.CaptureErrors(() => new RenameParametersHook().OnPreprocessAvatar(av, maContext)); @@ -243,6 +246,7 @@ public void TestMultipleRemappings_WithConflict() var context = CreateContext(av); var maContext = context.ActivateExtensionContext().BuildContext; + context.ActivateExtensionContextRecursive(); var errors = ErrorReport.CaptureErrors(() => new RenameParametersHook().OnPreprocessAvatar(av, maContext)); diff --git a/UnitTests~/RetargetMeshesTest.cs b/UnitTests~/RetargetMeshesTest.cs index b3fc1d57..38c31573 100644 --- a/UnitTests~/RetargetMeshesTest.cs +++ b/UnitTests~/RetargetMeshesTest.cs @@ -1,5 +1,6 @@ using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.core.editor; +using nadena.dev.ndmf.animator; using NUnit.Framework; using UnityEngine; @@ -21,13 +22,12 @@ public void RootBoneOnly() Debug.Assert(skinnedMeshRenderer.bones.Length == 0); var build_context = new nadena.dev.ndmf.BuildContext(root, null); - var torc = new AnimationServicesContext(); - torc.OnActivate(build_context); + var asc = build_context.ActivateExtensionContextRecursive(); var bonedb = new BoneDatabase(); bonedb.AddMergedBone(b.transform); - new RetargetMeshes().OnPreprocessAvatar(root, bonedb, torc.PathMappings); + new RetargetMeshes().OnPreprocessAvatar(root, bonedb, asc); Assert.AreEqual(a.transform, skinnedMeshRenderer.rootBone); } @@ -47,13 +47,12 @@ public void NoMeshRootBoneOnly() Debug.Assert(skinnedMeshRenderer.bones.Length == 0); var build_context = new nadena.dev.ndmf.BuildContext(root, null); - var torc = new AnimationServicesContext(); - torc.OnActivate(build_context); + var asc = build_context.ActivateExtensionContextRecursive(); var bonedb = new BoneDatabase(); bonedb.AddMergedBone(b.transform); - new RetargetMeshes().OnPreprocessAvatar(root, bonedb, torc.PathMappings); + new RetargetMeshes().OnPreprocessAvatar(root, bonedb, asc); Assert.AreEqual(a.transform, skinnedMeshRenderer.rootBone); Assert.AreEqual(new Bounds(new Vector3(0, 0, 0), new Vector3(2, 2, 2)), diff --git a/UnitTests~/VirtualMenuTests/VirtualMenuTests.cs b/UnitTests~/VirtualMenuTests/VirtualMenuTests.cs index 5450dba0..fa7d46c3 100644 --- a/UnitTests~/VirtualMenuTests/VirtualMenuTests.cs +++ b/UnitTests~/VirtualMenuTests/VirtualMenuTests.cs @@ -7,6 +7,7 @@ using nadena.dev.modular_avatar.core.editor; using nadena.dev.modular_avatar.core.editor.menu; using nadena.dev.modular_avatar.core.menu; +using nadena.dev.ndmf.animator; using NUnit.Framework; using UnityEditor; using UnityEngine; @@ -644,6 +645,7 @@ public void remapParams_isAppliedSeparatelyForEachDedup() }; var buildContext = new BuildContext(av_root.GetComponent()); + buildContext.PluginBuildContext.ActivateExtensionContextRecursive(); new RenameParametersHook().OnPreprocessAvatar(av_root, buildContext); var virtualMenu = VirtualMenu.ForAvatar(av_root.GetComponent(), buildContext); @@ -663,6 +665,7 @@ public void internalParameterTest() var root = CreatePrefab("InternalParameterTest.prefab"); BuildContext buildContext = new BuildContext(root.GetComponent()); + buildContext.PluginBuildContext.ActivateExtensionContextRecursive(); new RenameParametersHook().OnPreprocessAvatar(root, buildContext); var virtualMenu = VirtualMenu.ForAvatar(root.GetComponent(), buildContext); @@ -676,6 +679,7 @@ public void UnusedSubParametersAreStripped() var root = CreatePrefab("UnusedSubParametersAreStripped.prefab"); BuildContext buildContext = new BuildContext(root.GetComponent()); + buildContext.PluginBuildContext.ActivateExtensionContextRecursive(); new RenameParametersHook().OnPreprocessAvatar(root, buildContext); var virtualMenu = VirtualMenu.ForAvatar(root.GetComponent(), buildContext); diff --git a/UnitTests~/WorldFixedObjectTest/WorldFixedObjectTest.cs b/UnitTests~/WorldFixedObjectTest/WorldFixedObjectTest.cs index ba838bda..89d59df1 100644 --- a/UnitTests~/WorldFixedObjectTest/WorldFixedObjectTest.cs +++ b/UnitTests~/WorldFixedObjectTest/WorldFixedObjectTest.cs @@ -2,6 +2,7 @@ using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.core; using nadena.dev.modular_avatar.core.editor; +using nadena.dev.ndmf.animator; using NUnit.Framework; using UnityEngine.Animations; @@ -16,7 +17,7 @@ public void SimpleTest() // initialize context var buildContext = new BuildContext(avatar); - buildContext.PluginBuildContext.ActivateExtensionContext(); + buildContext.PluginBuildContext.ActivateExtensionContextRecursive(); new WorldFixedObjectProcessor().Process(buildContext); @@ -42,7 +43,7 @@ public void NestedTest() // initialize context var buildContext = new BuildContext(avatar); - buildContext.PluginBuildContext.ActivateExtensionContext(); + buildContext.PluginBuildContext.ActivateExtensionContextRecursive(); new WorldFixedObjectProcessor().Process(buildContext); @@ -75,7 +76,7 @@ public void NameCollisions() // initialize context var buildContext = new BuildContext(avatar); - var animationServices = buildContext.PluginBuildContext.ActivateExtensionContext(); + var animationServices = buildContext.PluginBuildContext.ActivateExtensionContextRecursive(); new WorldFixedObjectProcessor().Process(buildContext);