chore: port MA to the new NDMF animation API (#1371)

This commit is contained in:
bd_ 2025-02-16 22:25:12 -08:00 committed by GitHub
parent d7e949239a
commit 7cafd314a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 1378 additions and 3519 deletions

View File

@ -4,7 +4,7 @@
"version": "3.7.4"
},
"nadena.dev.ndmf": {
"version": "1.6.0"
"version": "1.7.0-alpha.0"
}
},
"locked": {
@ -19,7 +19,7 @@
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.6.0"
"version": "1.7.0-alpha.0"
}
}
}

View File

@ -2,6 +2,7 @@ using System;
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 @@ namespace nadena.dev.modular_avatar.core.editor
{
private readonly BuildContext _context;
private readonly BoneDatabase _boneDatabase;
private readonly PathMappings _pathMappings;
private readonly AnimatorServicesContext _asc;
private readonly List<IntermediateObj> _intermediateObjs = new List<IntermediateObj>();
/// <summary>
@ -55,15 +56,15 @@ namespace nadena.dev.modular_avatar.core.editor
{
_context = context;
_boneDatabase = boneDatabase;
_pathMappings = context.PluginBuildContext.Extension<AnimationServicesContext>().PathMappings;
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
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 @@ namespace nadena.dev.modular_avatar.core.editor
// Ensure mesh retargeting looks through this
_boneDatabase.AddMergedBone(sourceBone.transform);
_boneDatabase.RetainMergedBone(sourceBone.transform);
_pathMappings.MarkTransformLookthrough(sourceBone);
}
return sourceBone;
@ -130,22 +130,14 @@ namespace nadena.dev.modular_avatar.core.editor
{
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 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
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"));
}
}
}

View File

@ -1,430 +0,0 @@
#region
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using UnityEngine.Profiling;
using BuildContext = nadena.dev.ndmf.BuildContext;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
/// <summary>
/// The animation database records the set of all clips which are used in the avatar, and which paths they
/// manipulate.
/// </summary>
internal class AnimationDatabase
{
internal class ClipHolder
{
private readonly AnimationDatabase ParentDatabase;
private Motion _currentClip;
internal Motion CurrentClip
{
get
{
ParentDatabase.InvalidateCaches();
return _currentClip;
}
set
{
ParentDatabase.InvalidateCaches();
_currentClip = value;
}
}
private Motion _originalClip;
internal Motion OriginalClip
{
get => _originalClip;
set
{
_originalClip = value;
var baseClip = ObjectRegistry.GetReference(value)?.Object as AnimationClip;
IsProxyAnimation = false;
if (value != null && Util.IsProxyAnimation(value))
{
IsProxyAnimation = true;
}
else if (baseClip != null && Util.IsProxyAnimation(baseClip))
{
// RenameParametersPass replaces proxy clips outside of the purview of the animation database,
// so trace this using ObjectRegistry and correct the reference.
IsProxyAnimation = true;
_originalClip = baseClip;
}
}
}
internal bool IsProxyAnimation { private set; get; }
internal ClipHolder(AnimationDatabase parentDatabase, Motion clip)
{
ParentDatabase = parentDatabase;
CurrentClip = OriginalClip = clip;
}
/// <summary>
/// Returns the current clip without invalidating caches. Do not modify this clip without taking extra
/// steps to invalidate caches on the AnimationDatabase.
/// </summary>
/// <returns></returns>
internal Motion GetCurrentClipUnsafe()
{
return _currentClip;
}
public void SetCurrentNoInvalidate(Motion newMotion)
{
_currentClip = newMotion;
}
}
private BuildContext _context;
private List<Action> _clipCommitActions = new List<Action>();
private List<ClipHolder> _clips = new List<ClipHolder>();
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
private HashSet<VRCAnimatorPlayAudio> _playAudios = new HashSet<VRCAnimatorPlayAudio>();
#endif
private Dictionary<string, HashSet<ClipHolder>> _pathToClip = null;
internal AnimationDatabase()
{
Debug.Log("Creating animation database");
}
internal void Commit()
{
Profiler.BeginSample("AnimationDatabase.Commit");
foreach (var clip in _clips)
{
if (clip.IsProxyAnimation) clip.CurrentClip = clip.OriginalClip;
}
Profiler.BeginSample("UpdateClipProperties");
foreach (var clip in _clips)
{
// Changing the "high quality curve" setting can result in behavior changes (but can happen accidentally
// as we manipulate curves)
if (clip.CurrentClip != clip.OriginalClip && clip.CurrentClip != null && clip.OriginalClip != null)
{
SerializedObject before = new SerializedObject(clip.OriginalClip);
SerializedObject after = new SerializedObject(clip.CurrentClip);
var before_prop = before.FindProperty("m_UseHighQualityCurve");
var after_prop = after.FindProperty("m_UseHighQualityCurve");
if (after_prop.boolValue != before_prop.boolValue)
{
after_prop.boolValue = before_prop.boolValue;
after.ApplyModifiedPropertiesWithoutUndo();
}
}
}
Profiler.EndSample();
Profiler.BeginSample("ClipCommitActions");
foreach (var action in _clipCommitActions)
{
action();
}
Profiler.EndSample();
Profiler.EndSample();
}
internal void OnActivate(BuildContext context)
{
_context = context;
AnimationUtil.CloneAllControllers(context);
#if MA_VRCSDK3_AVATARS
var avatarDescriptor = context.AvatarDescriptor;
if (!avatarDescriptor) return;
foreach (var layer in avatarDescriptor.baseAnimationLayers)
{
BootstrapLayer(layer);
}
foreach (var layer in avatarDescriptor.specialAnimationLayers)
{
BootstrapLayer(layer);
}
void BootstrapLayer(VRCAvatarDescriptor.CustomAnimLayer layer)
{
if (!layer.isDefault && layer.animatorController is AnimatorController ac &&
context.IsTemporaryAsset(ac))
{
BuildReport.ReportingObject(ac, () =>
{
foreach (var state in Util.States(ac))
{
RegisterState(state);
}
});
}
}
#endif
}
/// <summary>
/// Registers a motion and all its reachable submotions with the animation database. The processClip callback,
/// if provided, will be invoked for each newly discovered clip.
/// </summary>
/// <param name="state"></param>
/// <param name="processClip"></param>
/// <exception cref="Exception"></exception>
internal void RegisterState(AnimatorState state, Action<ClipHolder> processClip = null)
{
Dictionary<Motion, ClipHolder> _originalToHolder = new Dictionary<Motion, ClipHolder>();
if (processClip == null) processClip = (_) => { };
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
foreach (var behavior in state.behaviours)
{
if (behavior is VRCAnimatorPlayAudio playAudio)
{
_playAudios.Add(playAudio);
}
}
#endif
if (state.motion == null) return;
var clipHolder = RegisterMotion(state.motion, state, processClip, _originalToHolder);
state.motion = clipHolder.CurrentClip;
_clipCommitActions.Add(() =>
{
state.motion = clipHolder.CurrentClip;
MaybeSaveClip(clipHolder.CurrentClip);
});
}
internal void ForeachClip(Action<ClipHolder> processClip)
{
foreach (var clipHolder in _clips)
{
processClip(clipHolder);
}
}
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
internal void ForeachPlayAudio(Action<VRCAnimatorPlayAudio> processPlayAudio)
{
foreach (var playAudioHolder in _playAudios)
{
processPlayAudio(playAudioHolder);
}
}
#endif
/// <summary>
/// Returns a list of clips which touched the given _original_ path. This path is subject to basepath remapping,
/// but not object movement remapping.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
internal ImmutableArray<ClipHolder> ClipsForPath(string path)
{
HydrateCaches();
if (_pathToClip.TryGetValue(path, out var clips))
{
return clips.ToImmutableArray();
}
else
{
return ImmutableArray<ClipHolder>.Empty;
}
}
private ClipHolder RegisterMotion(
Motion motion,
AnimatorState state,
Action<ClipHolder> processClip,
Dictionary<Motion, ClipHolder> originalToHolder
)
{
if (motion == null)
{
return new ClipHolder(this, null);
}
if (originalToHolder.TryGetValue(motion, out var holder))
{
return holder;
}
InvalidateCaches();
Motion cloned = motion;
if (!_context.IsTemporaryAsset(motion))
{
// Protect the original animations from mutations by creating temporary clones; in the case of a proxy
// animation, we'll restore the original in a later pass
// cloned = Object.Instantiate(motion); - Object.Instantiate can't be used on AnimationClips and BlendTrees
cloned = (Motion)motion.GetType().GetConstructor(new Type[0]).Invoke(new object[0]);
EditorUtility.CopySerialized(motion, cloned);
ObjectRegistry.RegisterReplacedObject(motion, cloned);
}
switch (cloned)
{
case AnimationClip clip:
{
holder = new ClipHolder(this, clip);
processClip(holder);
_clips.Add(holder);
break;
}
case BlendTree tree:
{
holder = RegisterBlendtree(tree, state, processClip, originalToHolder);
break;
}
}
holder.OriginalClip = motion;
originalToHolder[motion] = holder;
return holder;
}
private void InvalidateCaches()
{
_pathToClip = null;
}
private void HydrateCaches()
{
if (_pathToClip == null)
{
_pathToClip = new Dictionary<string, HashSet<ClipHolder>>();
foreach (var clip in _clips)
{
RecordPaths(clip);
}
}
}
private void RecordPaths(ClipHolder holder)
{
var clip = holder.GetCurrentClipUnsafe() as AnimationClip;
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var path = binding.path;
AddPath(path);
}
foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var path = binding.path;
AddPath(path);
}
void AddPath(string p0)
{
if (!_pathToClip.TryGetValue(p0, out var clips))
{
clips = new HashSet<ClipHolder>();
_pathToClip[p0] = clips;
}
clips.Add(holder);
}
}
private ClipHolder RegisterBlendtree(
BlendTree tree,
AnimatorState state,
Action<ClipHolder> processClip,
Dictionary<Motion, ClipHolder> originalToHolder
)
{
if (!_context.IsTemporaryAsset(tree))
{
throw new Exception("Blendtree must be a temporary asset");
}
var treeHolder = new ClipHolder(this, tree);
var children = tree.children;
var holders = new ClipHolder[children.Length];
for (int i = 0; i < children.Length; i++)
{
holders[i] = RegisterMotion(children[i].motion, state, processClip, originalToHolder);
children[i].motion = holders[i].CurrentClip;
}
tree.children = children;
_clipCommitActions.Add(() =>
{
var dirty = false;
for (int i = 0; i < children.Length; i++)
{
var curClip = holders[i].CurrentClip;
if (children[i].motion != curClip)
{
children[i].motion = curClip;
dirty = true;
}
MaybeSaveClip(curClip);
}
if (dirty)
{
tree.children = children;
EditorUtility.SetDirty(tree);
}
});
return treeHolder;
}
private void MaybeSaveClip(Motion curClip)
{
Profiler.BeginSample("MaybeSaveClip");
if (curClip != null && !EditorUtility.IsPersistent(curClip) && EditorUtility.IsPersistent(_context.AssetContainer) && _context.AssetContainer != null)
{
try
{
_context.AssetSaver.SaveAsset(curClip);
}
catch (Exception e)
{
Debug.LogException(e);
throw;
}
}
Profiler.EndSample();
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 11130986120e452b8dc8db0d19aa71fc
timeCreated: 1671624207

View File

@ -1,125 +0,0 @@
#region
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
namespace nadena.dev.modular_avatar.animation
{
/// <summary>
/// This extension context amortizes a number of animation-related processing steps - notably,
/// collecting the set of all animation clips from the animators, and committing changes to them
/// in a deferred manner.
///
/// Restrictions: While this context is active, any changes to clips must be done by editing them via
/// ClipHolders in the AnimationDatabase. Any newly added clips must be registered in the AnimationDatabase,
/// and any new references to clips require setting appropriate ClipCommitActions.
///
/// New references to objects created in clips must use paths obtained from the
/// ObjectRenameTracker.GetObjectIdentifier method.
/// </summary>
internal sealed class AnimationServicesContext : IExtensionContext
{
private BuildContext _context;
private AnimationDatabase _animationDatabase;
private PathMappings _pathMappings;
private ReadableProperty _readableProperty;
private Dictionary<GameObject, string> _selfProxies = new();
public void OnActivate(BuildContext context)
{
_context = context;
_animationDatabase = new AnimationDatabase();
_animationDatabase.OnActivate(context);
_pathMappings = new PathMappings();
_pathMappings.OnActivate(context, _animationDatabase);
_readableProperty = new ReadableProperty(_context, _animationDatabase, this);
}
public void OnDeactivate(BuildContext context)
{
_pathMappings.OnDeactivate(context);
_animationDatabase.Commit();
_pathMappings = null;
_animationDatabase = null;
}
public AnimationDatabase AnimationDatabase
{
get
{
if (_animationDatabase == null)
{
throw new InvalidOperationException(
"AnimationDatabase is not available outside of the AnimationServicesContext");
}
return _animationDatabase;
}
}
public PathMappings PathMappings
{
get
{
if (_pathMappings == null)
{
throw new InvalidOperationException(
"ObjectRenameTracker is not available outside of the AnimationServicesContext");
}
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;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: c2c26040d44d4dacb838aceced3b3e52
timeCreated: 1696063949

View File

@ -1,224 +0,0 @@
#region
using System;
using System.Collections.Generic;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
internal static class AnimationUtil
{
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";
public static AnimatorController DeepCloneAnimator(BuildContext context, RuntimeAnimatorController controller)
{
if (controller == null) return null;
var merger = new AnimatorCombiner(context, controller.name + " (cloned)");
switch (controller)
{
case AnimatorController ac:
merger.AddController("", ac, null);
break;
case AnimatorOverrideController oac:
merger.AddOverrideController("", oac, null);
break;
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
}
var clone = merger.Finish();
ObjectRegistry.RegisterReplacedObject(controller, clone);
return clone;
}
internal static void CloneAllControllers(BuildContext context)
{
// Ensure all of the controllers on the avatar descriptor point to temporary assets.
// This helps reduce the risk that we'll accidentally modify the original assets.
#if MA_VRCSDK3_AVATARS
if (!context.AvatarDescriptor) return;
context.AvatarDescriptor.baseAnimationLayers =
CloneLayers(context, context.AvatarDescriptor.baseAnimationLayers);
context.AvatarDescriptor.specialAnimationLayers =
CloneLayers(context, context.AvatarDescriptor.specialAnimationLayers);
#endif
}
#if MA_VRCSDK3_AVATARS
private static VRCAvatarDescriptor.CustomAnimLayer[] CloneLayers(
BuildContext context,
VRCAvatarDescriptor.CustomAnimLayer[] layers
)
{
if (layers == null) return null;
for (int i = 0; i < layers.Length; i++)
{
var layer = layers[i];
if (layer.animatorController != null && !context.IsTemporaryAsset(layer.animatorController))
{
layer.animatorController = DeepCloneAnimator(context, layer.animatorController);
}
layers[i] = layer;
}
return layers;
}
public static AnimatorController GetOrInitializeController(
this BuildContext context,
VRCAvatarDescriptor.AnimLayerType type)
{
return FindLayer(context.AvatarDescriptor.baseAnimationLayers)
?? FindLayer(context.AvatarDescriptor.specialAnimationLayers);
AnimatorController FindLayer(VRCAvatarDescriptor.CustomAnimLayer[] layers)
{
for (int i = 0; i < layers.Length; i++)
{
var layer = layers[i];
if (layer.type == type)
{
if (layer.animatorController == null || layer.isDefault)
{
layer.animatorController = ResolveLayerController(layer);
if (type == VRCAvatarDescriptor.AnimLayerType.Gesture)
{
layer.mask = AssetDatabase.LoadAssetAtPath<AvatarMask>(
AssetDatabase.GUIDToAssetPath(GUID_GESTURE_HANDSONLY_MASK)
);
}
layers[i] = layer;
}
return layer.animatorController as AnimatorController;
}
}
return null;
}
}
private static AnimatorController ResolveLayerController(VRCAvatarDescriptor.CustomAnimLayer layer)
{
AnimatorController controller = null;
if (!layer.isDefault && layer.animatorController != null &&
layer.animatorController is AnimatorController c)
{
controller = c;
}
else
{
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<AnimatorController>(SAMPLE_PATH_PACKAGE + name);
if (controller == null)
{
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_LEGACY + name);
}
}
}
return controller;
}
#endif
public static bool IsProxyAnimation(this Motion m)
{
var path = AssetDatabase.GetAssetPath(m);
// This is a fairly wide condition in order to deal with:
// 1. Future additions of proxy animations (so GUIDs are out)
// 2. Unitypackage based installations of the VRCSDK
// 3. VCC based installations of the VRCSDK
// 4. Very old VCC based installations of the VRCSDK where proxy animations were copied into Assets
return path.Contains("/AV3 Demo Assets/Animation/ProxyAnim/proxy")
|| path.Contains("/VRCSDK/Examples3/Animation/ProxyAnim/proxy")
|| path.StartsWith("Packages/com.vrchat.");
}
/// <summary>
/// Enumerates all state machines and sub-state machines starting from a specific starting ASM
/// </summary>
/// <param name="ac"></param>
/// <returns></returns>
internal static IEnumerable<AnimatorStateMachine> ReachableStateMachines(this AnimatorStateMachine asm)
{
HashSet<AnimatorStateMachine> visitedStateMachines = new HashSet<AnimatorStateMachine>();
Queue<AnimatorStateMachine> pending = new Queue<AnimatorStateMachine>();
pending.Enqueue(asm);
while (pending.Count > 0)
{
var next = pending.Dequeue();
if (visitedStateMachines.Contains(next)) continue;
visitedStateMachines.Add(next);
foreach (var child in next.stateMachines)
{
if (child.stateMachine != null) pending.Enqueue(child.stateMachine);
}
yield return next;
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: af583e8ac3104fa4f8466741614219a0
timeCreated: 1691238553

View File

@ -1,625 +0,0 @@
/*
* 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.
*/
#region
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using nadena.dev.ndmf.util;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using Object = UnityEngine.Object;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase;
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
internal class AnimatorCombiner
{
private readonly BuildContext _context;
private readonly AnimatorController _combined;
private readonly DeepClone _deepClone;
private List<AnimatorControllerLayer> _layers = new List<AnimatorControllerLayer>();
private Dictionary<String, AnimatorControllerParameter> _parameters =
new Dictionary<string, AnimatorControllerParameter>();
private Dictionary<String, AnimatorController> _parameterSource =
new Dictionary<string, AnimatorController>();
private Dictionary<KeyValuePair<String, Motion>, Motion> _motions =
new Dictionary<KeyValuePair<string, Motion>, Motion>();
private Dictionary<KeyValuePair<String, AnimatorStateMachine>, AnimatorStateMachine> _stateMachines =
new Dictionary<KeyValuePair<string, AnimatorStateMachine>, AnimatorStateMachine>();
private Dictionary<Object, Object> _cloneMap;
private int _controllerBaseLayer = 0;
#if MA_VRCSDK3_AVATARS
public VRC_AnimatorLayerControl.BlendableLayer? BlendableLayer;
#endif
public AnimatorCombiner(BuildContext context, String assetName)
{
_combined = new AnimatorController();
if (context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer))
{
context.AssetSaver.SaveAsset(_combined);
}
_combined.name = assetName;
_context = context;
_deepClone = new DeepClone(context);
}
public AnimatorController Finish()
{
FixTransitionTypeConflicts();
PruneEmptyLayers();
_combined.parameters = _parameters.Values.ToArray();
_combined.layers = _layers.ToArray();
return _combined;
}
public void MergeTypes(Dictionary<string, AnimatorControllerParameterType> types)
{
foreach (var p in _parameters.ToList())
{
if (types.TryGetValue(p.Key, out var outerValue))
{
if (outerValue == p.Value.type) continue;
if (outerValue == AnimatorControllerParameterType.Trigger
|| p.Value.type == AnimatorControllerParameterType.Trigger)
{
BuildReport.LogFatal("error.merge_animator.param_type_mismatch",
p.Key,
p.Value.type,
outerValue
);
}
_parameters[p.Key].type = AnimatorControllerParameterType.Float;
types[p.Key] = AnimatorControllerParameterType.Float;
}
else
{
types.Add(p.Key, p.Value.type);
}
}
}
/// <summary>
/// When we merge multiple controllers with different types for the same parameter, we merge
/// them all into using floats; thanks to VRChat's implicit typecasting, we can do this even for
/// parameters registered as being ints or bools in the expressions parameter asset. However,
/// we do need to fix any transitions to use the right transition types after this conversion.
/// </summary>
private void FixTransitionTypeConflicts()
{
foreach (var layer in _layers)
{
foreach (var asm in layer.stateMachine.ReachableStateMachines())
{
foreach (ChildAnimatorState s in asm.states)
{
s.state.transitions = s.state.transitions.SelectMany(FixupTransition).ToArray();
}
asm.entryTransitions = asm.entryTransitions
.SelectMany(FixupTransition).ToArray();
asm.anyStateTransitions = asm.anyStateTransitions
.SelectMany(FixupTransition).ToArray();
foreach (var stateMachine in asm.stateMachines)
{
var ssm = stateMachine.stateMachine;
var stateMachineTransitions = asm.GetStateMachineTransitions(ssm);
if (stateMachineTransitions.Length > 0)
{
asm.SetStateMachineTransitions(ssm,
stateMachineTransitions.SelectMany(FixupTransition).ToArray());
}
}
}
}
}
private IEnumerable<T> FixupTransition<T>(T t) where T: AnimatorTransitionBase, new()
{
if (!NeedsFixing(t.conditions))
{
yield return t;
yield break;
}
AnimatorCondition[][][] combinations = t.conditions.Select(c => FixupCondition(c).ToArray()).ToArray();
// Generate the combinatorial explosion of conditions needed to emulate NotEquals with floats...
var conditions = ExplodeConditions(combinations).ToArray();
if (conditions.Length == 1)
{
t.conditions = conditions[0];
yield return t;
}
else
{
foreach (var conditionGroup in conditions)
{
t.conditions = conditionGroup;
yield return t;
var newTransition = new T();
EditorUtility.CopySerialized(t, newTransition);
if (_context.AssetContainer != null)
{
_context.AssetSaver.SaveAsset(newTransition);
}
t = newTransition;
}
}
}
private bool NeedsFixing(AnimatorCondition[] conditions)
{
return conditions.Any(c =>
{
if (!_parameters.TryGetValue(c.parameter, out var param)) return false;
switch (c.mode)
{
case AnimatorConditionMode.If when param.type != AnimatorControllerParameterType.Bool:
case AnimatorConditionMode.IfNot when param.type != AnimatorControllerParameterType.Bool:
case AnimatorConditionMode.Equals when param.type != AnimatorControllerParameterType.Int:
case AnimatorConditionMode.NotEqual when param.type != AnimatorControllerParameterType.Int:
return true;
default:
return false;
}
});
}
private IEnumerable<AnimatorCondition[]> ExplodeConditions(AnimatorCondition[][][] conditions)
{
int[] indices = new int[conditions.Length];
while (true)
{
yield return conditions.SelectMany((group, i_) => group[indices[i_]]).ToArray();
// Increment the rightmost possible counter
int i;
for (i = indices.Length - 1; i >= 0; i--)
{
if (indices[i] < conditions[i].Length - 1)
{
indices[i]++;
// Unity 2019.....
// System.Array.Fill(indices, 0, i + 1, indices.Length - i - 1);
for (int j = i + 1; j < indices.Length; j++)
{
indices[j] = 0;
}
break;
}
}
if (i < 0) break;
}
}
private IEnumerable<AnimatorCondition[]> FixupCondition(AnimatorCondition c)
{
if (!_parameters.TryGetValue(c.parameter, out var paramDef))
{
// Parameter is undefined, don't touch this condition
yield return new[] { c };
yield break;
}
switch (c.mode)
{
case AnimatorConditionMode.If when paramDef.type == AnimatorControllerParameterType.Float:
{
c.mode = AnimatorConditionMode.Greater;
c.threshold = 0.5f;
yield return new[] { c };
break;
}
case AnimatorConditionMode.IfNot when paramDef.type == AnimatorControllerParameterType.Float:
{
c.mode = AnimatorConditionMode.Less;
c.threshold = 0.5f;
yield return new[] { c };
break;
}
case AnimatorConditionMode.Equals when paramDef.type == AnimatorControllerParameterType.Float:
{
var c1 = c;
var c2 = c;
c1.mode = AnimatorConditionMode.Greater;
c1.threshold -= 0.1f;
c2.mode = AnimatorConditionMode.Less;
c2.threshold += 0.1f;
yield return new[] { c1, c2 };
break;
}
case AnimatorConditionMode.NotEqual when paramDef.type == AnimatorControllerParameterType.Float:
{
var origThresh = c.threshold;
c.mode = AnimatorConditionMode.Greater;
c.threshold = origThresh + 0.1f;
yield return new[] { c };
c.mode = AnimatorConditionMode.Less;
c.threshold = origThresh - 0.1f;
yield return new[] { c };
break;
}
default:
yield return new[] { c };
break;
}
}
private void PruneEmptyLayers()
{
#if MA_VRCSDK3_AVATARS
// We can't safely correct the layer index of a VRCAnimatorLayerControl without knowing if it refers to
// _this_ animator controller, so just skip this. We'll do the empty layer pruning later when we merge
// everything together.
if (BlendableLayer == null) return;
#endif
var originalLayers = _layers;
int[] layerIndexMappings = new int[originalLayers.Count];
List<AnimatorControllerLayer> newLayers = new List<AnimatorControllerLayer>();
for (int i = 0; i < originalLayers.Count; i++)
{
if (i > 0 && IsEmptyLayer(originalLayers[i]))
{
layerIndexMappings[i] = -1;
}
else
{
layerIndexMappings[i] = newLayers.Count;
newLayers.Add(originalLayers[i]);
}
}
foreach (var layer in newLayers)
{
if (layer.stateMachine == null) continue;
foreach (var asset in layer.stateMachine.ReferencedAssets(includeScene: false))
{
if (asset is AnimatorState alc)
{
alc.behaviours = AdjustStateBehaviors(alc.behaviours);
}
else if (asset is AnimatorStateMachine asm)
{
asm.behaviours = AdjustStateBehaviors(asm.behaviours);
}
}
}
_layers = newLayers;
StateMachineBehaviour[] AdjustStateBehaviors(StateMachineBehaviour[] behaviours)
{
if (behaviours.Length == 0) return behaviours;
var newBehaviors = new List<StateMachineBehaviour>();
foreach (var b in behaviours)
{
switch (b)
{
#if MA_VRCSDK3_AVATARS
case VRCAnimatorLayerControl alc when alc.playable == BlendableLayer:
int newLayer = -1;
if (alc.layer >= 0 && alc.layer < layerIndexMappings.Length)
{
newLayer = layerIndexMappings[alc.layer];
}
if (newLayer != -1)
{
alc.layer = newLayer;
newBehaviors.Add(alc);
}
break;
#endif
default:
newBehaviors.Add(b);
break;
}
}
return newBehaviors.ToArray();
}
}
private bool IsEmptyLayer(AnimatorControllerLayer layer)
{
if (layer.syncedLayerIndex >= 0) return false;
if (layer.avatarMask != null) return false;
return layer.stateMachine == null
|| (layer.stateMachine.states.Length == 0 && layer.stateMachine.stateMachines.Length == 0);
}
public void AddController(string basePath, AnimatorController controller, bool? writeDefaults,
bool forceFirstLayerWeight = false)
{
_controllerBaseLayer = _layers.Count;
_cloneMap = new Dictionary<Object, Object>();
foreach (var param in controller.parameters)
{
if (_parameters.TryGetValue(param.name, out var acp))
{
if (acp.type == param.type) continue;
if (acp.type != param.type &&
(acp.type == AnimatorControllerParameterType.Trigger ||
param.type == AnimatorControllerParameterType.Trigger))
{
BuildReport.LogFatal("error.merge_animator.param_type_mismatch",
param.name,
acp.type.ToString(),
param.type.ToString(),
controller,
_parameterSource[param.name]
);
}
acp.type = AnimatorControllerParameterType.Float;
continue;
}
var clonedParameter = new AnimatorControllerParameter()
{
name = param.name,
type = param.type,
defaultBool = param.defaultBool,
defaultFloat = param.defaultFloat,
defaultInt = param.defaultInt
};
_parameters.Add(param.name, clonedParameter);
_parameterSource.Add(param.name, controller);
}
bool first = true;
var layers = controller.layers;
foreach (var layer in layers)
{
insertLayer(basePath, layer, first, writeDefaults, layers);
if (first && forceFirstLayerWeight)
{
_layers[_layers.Count - 1].defaultWeight = 1;
}
first = false;
}
}
public void AddOverrideController(string basePath, AnimatorOverrideController overrideController,
bool? writeDefaults)
{
AnimatorController controller = overrideController.runtimeAnimatorController as AnimatorController;
if (controller == null) return;
_deepClone.OverrideController = overrideController;
try
{
this.AddController(basePath, controller, writeDefaults);
}
finally
{
}
}
private void insertLayer(
string basePath,
AnimatorControllerLayer layer,
bool first,
bool? writeDefaults,
AnimatorControllerLayer[] layers
)
{
var newLayer = new AnimatorControllerLayer()
{
name = layer.name,
avatarMask = _deepClone.DoClone(layer.avatarMask, basePath, _cloneMap),
blendingMode = layer.blendingMode,
defaultWeight = first ? 1 : layer.defaultWeight,
syncedLayerIndex = layer.syncedLayerIndex,
syncedLayerAffectsTiming = layer.syncedLayerAffectsTiming,
iKPass = layer.iKPass,
stateMachine = mapStateMachine(basePath, layer.stateMachine),
};
UpdateWriteDefaults(newLayer.stateMachine, writeDefaults);
if (newLayer.syncedLayerIndex != -1 && newLayer.syncedLayerIndex >= 0 &&
newLayer.syncedLayerIndex < layers.Length)
{
// Transfer any motion overrides onto the new synced layer
var baseLayer = layers[newLayer.syncedLayerIndex];
foreach (var state in WalkAllStates(baseLayer.stateMachine))
{
var overrideMotion = layer.GetOverrideMotion(state);
if (overrideMotion != null)
{
var newMotion = _deepClone.DoClone(overrideMotion, basePath, _cloneMap);
newLayer.SetOverrideMotion((AnimatorState)_cloneMap[state], newMotion);
}
var overrideBehaviors = (StateMachineBehaviour[])layer.GetOverrideBehaviours(state)?.Clone();
if (overrideBehaviors != null)
{
for (int i = 0; i < overrideBehaviors.Length; i++)
{
overrideBehaviors[i] = _deepClone.DoClone(overrideBehaviors[i]);
AdjustBehavior(overrideBehaviors[i], basePath);
}
newLayer.SetOverrideBehaviours((AnimatorState)_cloneMap[state], overrideBehaviors);
}
}
newLayer.syncedLayerIndex += _controllerBaseLayer;
}
_layers.Add(newLayer);
}
IEnumerable<AnimatorState> WalkAllStates(AnimatorStateMachine animatorStateMachine)
{
HashSet<Object> visited = new HashSet<Object>();
foreach (var state in VisitStateMachine(animatorStateMachine))
{
yield return state;
}
IEnumerable<AnimatorState> VisitStateMachine(AnimatorStateMachine layerStateMachine)
{
if (!visited.Add(layerStateMachine)) yield break;
foreach (var state in layerStateMachine.states)
{
if (state.state == null) continue;
yield return state.state;
}
foreach (var child in layerStateMachine.stateMachines)
{
if (child.stateMachine == null) continue;
if (visited.Contains(child.stateMachine)) continue;
foreach (var state in VisitStateMachine(child.stateMachine))
{
yield return state;
}
}
}
}
private void UpdateWriteDefaults(AnimatorStateMachine stateMachine, bool? writeDefaults)
{
if (!writeDefaults.HasValue) return;
var queue = new Queue<AnimatorStateMachine>();
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)
{
if (layerStateMachine == null) return null;
var cacheKey = new KeyValuePair<string, AnimatorStateMachine>(basePath, layerStateMachine);
if (_stateMachines.TryGetValue(cacheKey, out var asm))
{
return asm;
}
asm = _deepClone.DoClone(layerStateMachine, basePath, _cloneMap);
foreach (var state in WalkAllStates(asm))
{
foreach (var behavior in state.behaviours)
{
AdjustBehavior(behavior, basePath);
}
}
_stateMachines[cacheKey] = asm;
return asm;
}
private void AdjustBehavior(StateMachineBehaviour behavior, string basePath)
{
#if MA_VRCSDK3_AVATARS
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;
}
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
case VRCAnimatorPlayAudio playAudio:
{
if (!string.IsNullOrEmpty(playAudio.SourcePath) && !string.IsNullOrEmpty(basePath) && !playAudio.SourcePath.StartsWith(basePath))
{
playAudio.SourcePath = $"{basePath}{playAudio.SourcePath}";
}
break;
}
#endif
}
#endif
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 614457d82b1a4b109788029754c9fc1a
timeCreated: 1703674134

View File

@ -1,296 +0,0 @@
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using BuildContext = nadena.dev.ndmf.BuildContext;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.animation
{
using UnityObject = Object;
internal class DeepClone
{
private BuildContext _context;
private bool _isSaved;
private UnityObject _combined;
public AnimatorOverrideController OverrideController { get; set; }
public DeepClone(BuildContext context)
{
_context = context;
_isSaved = context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer);
_combined = context.AssetContainer;
}
public T DoClone<T>(T original,
string basePath = null,
Dictionary<UnityObject, UnityObject> cloneMap = null
) where T : UnityObject
{
if (original == null) return null;
if (cloneMap == null) cloneMap = new Dictionary<UnityObject, UnityObject>();
using var scope = _context.OpenSerializationScope();
Func<UnityObject, UnityObject> visitor = null;
if (basePath != null)
{
visitor = o => CloneWithPathMapping(o, basePath);
}
// 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 _:
case AvatarMask _:
break; // We want to clone these types
case AudioClip _: //Used in VRC Animator Play Audio State Behavior
// 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()}");
}
// When using AnimatorOverrideController, replace the original AnimationClip based on AnimatorOverrideController.
if (OverrideController != null && original is AnimationClip srcClip)
{
T overrideClip = OverrideController[srcClip] as T;
if (overrideClip != null)
{
original = overrideClip;
}
}
if (cloneMap.ContainsKey(original))
{
return (T)cloneMap[original];
}
var obj = visitor?.Invoke(original);
if (obj != null)
{
cloneMap[original] = obj;
if (obj != original)
{
ObjectRegistry.RegisterReplacedObject(original, obj);
}
if (_isSaved && !EditorUtility.IsPersistent(obj))
{
scope.SaveAsset(obj);
}
return (T)obj;
}
var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
if (ctor == null || original is ScriptableObject)
{
obj = UnityObject.Instantiate(original);
}
else
{
obj = (T)ctor.Invoke(Array.Empty<object>());
EditorUtility.CopySerialized(original, obj);
}
cloneMap[original] = obj;
ObjectRegistry.RegisterReplacedObject(original, obj);
if (_isSaved)
{
scope.SaveAsset(obj);
}
SerializedObject so = new SerializedObject(obj);
SerializedProperty prop = so.GetIterator();
bool enterChildren = true;
while (prop.Next(enterChildren))
{
enterChildren = true;
switch (prop.propertyType)
{
case SerializedPropertyType.ObjectReference:
{
if (prop.objectReferenceValue != null && prop.objectReferenceValue != obj)
{
var newObj = DoClone(prop.objectReferenceValue, basePath, cloneMap);
prop.objectReferenceValue = newObj;
}
break;
}
// Iterating strings can get super slow...
case SerializedPropertyType.String:
enterChildren = false;
break;
}
}
so.ApplyModifiedPropertiesWithoutUndo();
return (T)obj;
}
// internal for testing
internal static AvatarMask CloneAvatarMask(AvatarMask mask, string basePath)
{
if (basePath.EndsWith("/")) basePath = basePath.Substring(0, basePath.Length - 1);
var newMask = new AvatarMask();
// Transfer first the humanoid mask data
EditorUtility.CopySerialized(mask, newMask);
var srcSo = new SerializedObject(mask);
var dstSo = new SerializedObject(newMask);
var srcElements = srcSo.FindProperty("m_Elements");
if (basePath == "" || srcElements.arraySize == 0) return newMask; // no changes required
// We now need to prefix the elements of basePath (with weight zero)
var newElements = new List<string>();
var accum = "";
foreach (var element in basePath.Split("/"))
{
if (accum != "") accum += "/";
accum += element;
newElements.Add(accum);
}
var dstElements = dstSo.FindProperty("m_Elements");
// We'll need to create new array elements by using DuplicateCommand. We'll then rewrite the whole
// list to keep things in traversal order.
for (var i = 0; i < newElements.Count; i++) dstElements.GetArrayElementAtIndex(0).DuplicateCommand();
var totalElements = srcElements.arraySize + newElements.Count;
for (var i = 0; i < totalElements; i++)
{
var dstElem = dstElements.GetArrayElementAtIndex(i);
var dstPath = dstElem.FindPropertyRelative("m_Path");
var dstWeight = dstElem.FindPropertyRelative("m_Weight");
var srcIndex = i - newElements.Count;
if (srcIndex < 0)
{
dstPath.stringValue = newElements[i];
dstWeight.floatValue = 0;
}
else
{
var srcElem = srcElements.GetArrayElementAtIndex(srcIndex);
dstPath.stringValue = basePath + "/" + srcElem.FindPropertyRelative("m_Path").stringValue;
dstWeight.floatValue = srcElem.FindPropertyRelative("m_Weight").floatValue;
}
}
dstSo.ApplyModifiedPropertiesWithoutUndo();
return newMask;
}
private UnityObject CloneWithPathMapping(UnityObject o, string basePath)
{
if (o is AvatarMask mask)
{
return CloneAvatarMask(mask, basePath);
}
if (o is AnimationClip clip)
{
// We'll always rebase if the asset is non-persistent, because we can't reference a nonpersistent asset
// from a persistent asset. If the asset is persistent, skip cases where path editing isn't required,
// or where this is one of the special VRC proxy animations.
if (EditorUtility.IsPersistent(o) && (basePath == "" || Util.IsProxyAnimation(clip))) return clip;
AnimationClip newClip = new AnimationClip();
newClip.name = "rebased " + clip.name;
if (_isSaved)
{
_context.AssetSaver.SaveAsset(newClip);
}
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var newBinding = binding;
newBinding.path = MapPath(binding, basePath);
// https://github.com/bdunderscore/modular-avatar/issues/950
// It's reported that sometimes using SetObjectReferenceCurve right after SetCurve might cause the
// curves to be forgotten; use SetEditorCurve instead.
AnimationUtility.SetEditorCurve(newClip, newBinding,
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 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 newPath;
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: b33090a3e763464ab05f3efe07e0cbd3
timeCreated: 1703148770

View File

@ -1,18 +0,0 @@
using System.Collections.Generic;
using UnityEditor;
namespace nadena.dev.modular_avatar.animation
{
internal class EditorCurveBindingComparer : IEqualityComparer<EditorCurveBinding>
{
public bool Equals(UnityEditor.EditorCurveBinding x, UnityEditor.EditorCurveBinding y)
{
return x.path == y.path && x.type == y.type && x.propertyName == y.propertyName;
}
public int GetHashCode(UnityEditor.EditorCurveBinding obj)
{
return obj.path.GetHashCode() ^ obj.type.GetHashCode() ^ obj.propertyName.GetHashCode();
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: e751f7889323485bbe202285a47cb0d4
timeCreated: 1719196767

View File

@ -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 @@ namespace nadena.dev.modular_avatar.animation
{
protected override void Execute(BuildContext context)
{
var asc = context.Extension<AnimationServicesContext>();
if (!asc.BoundReadableProperties.Any()) return;
var fx = (AnimatorController)context.AvatarDescriptor.baseAnimationLayers
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController;
var asc = context.Extension<AnimatorServicesContext>();
var activeProxies = context.GetState<ReadablePropertyExtension.Retained>().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 @@ namespace nadena.dev.modular_avatar.animation
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;
var layer = fx.AddLayer(LayerPriority.Default, "DelayDisable");
var state = layer.StateMachine.AddState("DelayDisable");
layer.StateMachine.DefaultState = state;
asm.defaultState = state;
asm.states = new[]
{
new ChildAnimatorState
{
state = state,
position = Vector3.zero
}
};
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)

View File

@ -1,17 +0,0 @@
#region
using nadena.dev.ndmf;
#endregion
namespace nadena.dev.modular_avatar.animation
{
/// <summary>
/// This interface tags components which supply additional animation controllers for merging. They will be given
/// an opportunity to apply animation path updates when the TrackObjectRenamesContext is committed.
/// </summary>
internal interface IOnCommitObjectRenames
{
void OnCommitObjectRenames(BuildContext buildContext, PathMappings renameContext);
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6a66f552b8b334a45a986bfcf6767200
timeCreated: 1692511752

View File

@ -1,418 +0,0 @@
#region
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.ndmf;
using nadena.dev.ndmf.util;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using UnityEngine.Profiling;
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
#region
#endregion
/// <summary>
/// This extension context tracks when objects are renamed, and updates animations accordingly.
/// Users of this context need to be aware that, when creating new curves (or otherwise introducing new motions,
/// use context.ObjectPath to obtain a suitable path for the target objects).
/// </summary>
internal sealed class PathMappings
{
private AnimationDatabase _animationDatabase;
private Dictionary<GameObject, List<string>>
_objectToOriginalPaths = new Dictionary<GameObject, List<string>>();
private HashSet<GameObject> _transformLookthroughObjects = new HashSet<GameObject>();
private ImmutableDictionary<string, string> _originalPathToMappedPath = null;
private ImmutableDictionary<string, string> _transformOriginalPathToMappedPath = null;
private ImmutableDictionary<string, GameObject> _pathToObject = null;
internal void OnActivate(BuildContext context, AnimationDatabase animationDatabase)
{
_animationDatabase = animationDatabase;
_objectToOriginalPaths.Clear();
_transformLookthroughObjects.Clear();
ClearCache();
foreach (var xform in context.AvatarRootTransform.GetComponentsInChildren<Transform>(true))
{
_objectToOriginalPaths.Add(xform.gameObject, new List<string> {xform.gameObject.AvatarRootPath()});
}
}
public void ClearCache()
{
_originalPathToMappedPath = null;
_transformOriginalPathToMappedPath = null;
_pathToObject = null;
}
/// <summary>
/// Sets the "transform lookthrough" flag for an object. Any transform animations on this object will be
/// redirected to its parent. This is used in Modular Avatar as part of bone merging logic.
/// </summary>
/// <param name="obj"></param>
public void MarkTransformLookthrough(GameObject obj)
{
_transformLookthroughObjects.Add(obj);
}
/// <summary>
/// Returns a path for use in dynamically generated animations for a given object. This can include objects not
/// present at the time of context activation; in this case, they will be assigned a randomly-generated internal
/// path and replaced during path remapping with the true path.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public string GetObjectIdentifier(GameObject obj)
{
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
{
return paths[0];
}
else
{
var internalPath = "_NewlyCreatedObject/" + GUID.Generate() + "/" + obj.AvatarRootPath();
_objectToOriginalPaths.Add(obj, new List<string> {internalPath});
return internalPath;
}
}
/// <summary>
/// Marks an object as having been removed. Its paths will be remapped to its parent.
/// </summary>
/// <param name="obj"></param>
public void MarkRemoved(GameObject obj)
{
ClearCache();
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
{
var parent = obj.transform.parent.gameObject;
if (_objectToOriginalPaths.TryGetValue(parent, out var parentPaths))
{
parentPaths.AddRange(paths);
}
_objectToOriginalPaths.Remove(obj);
_transformLookthroughObjects.Remove(obj);
}
}
/// <summary>
/// Marks an object as having been replaced by another object. All references to the old object will be replaced
/// by the new object. References originally to the new object will continue to point to the new object.
/// </summary>
/// <param name="old"></param>
/// <param name="newObject"></param>
public void ReplaceObject(GameObject old, GameObject newObject)
{
ClearCache();
if (_objectToOriginalPaths.TryGetValue(old, out var paths))
{
if (!_objectToOriginalPaths.TryGetValue(newObject, out var newObjectPaths))
{
newObjectPaths = new List<string>();
_objectToOriginalPaths.Add(newObject, newObjectPaths);
}
newObjectPaths.AddRange(paths);
_objectToOriginalPaths.Remove(old);
}
if (_transformLookthroughObjects.Contains(old))
{
_transformLookthroughObjects.Remove(old);
_transformLookthroughObjects.Add(newObject);
}
}
private ImmutableDictionary<string, string> BuildMapping(ref ImmutableDictionary<string, string> cache,
bool transformLookup)
{
if (cache != null) return cache;
ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
foreach (var kvp in _objectToOriginalPaths)
{
var obj = kvp.Key;
var paths = kvp.Value;
if (transformLookup)
{
while (_transformLookthroughObjects.Contains(obj))
{
obj = obj.transform.parent.gameObject;
}
}
var newPath = obj.AvatarRootPath();
foreach (var origPath in paths)
{
if (!dict.ContainsKey(origPath))
{
dict = dict.Add(origPath, newPath);
}
}
}
cache = dict;
return cache;
}
public string MapPath(string path, bool isTransformMapping = false)
{
ImmutableDictionary<string, string> mappings;
if (isTransformMapping)
{
mappings = BuildMapping(ref _originalPathToMappedPath, true);
}
else
{
mappings = BuildMapping(ref _transformOriginalPathToMappedPath, false);
}
if (mappings.TryGetValue(path, out var mappedPath))
{
return mappedPath;
}
else
{
return path;
}
}
private string MapPath(EditorCurveBinding binding)
{
if (binding.type == typeof(Animator) && binding.path == "")
{
return "";
}
else
{
return MapPath(binding.path, binding.type == typeof(Transform));
}
}
private AnimationClip ApplyMappingsToClip(AnimationClip originalClip,
Dictionary<AnimationClip, AnimationClip> clipCache)
{
if (originalClip == null) return null;
if (clipCache != null && clipCache.TryGetValue(originalClip, out var cachedClip)) return cachedClip;
if (originalClip.IsProxyAnimation()) return originalClip;
var curveBindings = AnimationUtility.GetCurveBindings(originalClip);
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(originalClip);
bool hasMapping = false;
foreach (var binding in curveBindings.Concat(objectBindings))
{
if (MapPath(binding) != binding.path)
{
hasMapping = true;
break;
}
}
if (!hasMapping) return originalClip;
var newClip = new AnimationClip();
newClip.name = originalClip.name;
SerializedObject before = new SerializedObject(originalClip);
SerializedObject after = new SerializedObject(newClip);
var before_hqCurve = before.FindProperty("m_UseHighQualityCurve");
var after_hqCurve = after.FindProperty("m_UseHighQualityCurve");
after_hqCurve.boolValue = before_hqCurve.boolValue;
after.ApplyModifiedPropertiesWithoutUndo();
// TODO - should we use direct SerializedObject manipulation to avoid missing script issues?
foreach (var binding in curveBindings)
{
var newBinding = binding;
newBinding.path = MapPath(binding);
// https://github.com/bdunderscore/modular-avatar/issues/950
// It's reported that sometimes using SetObjectReferenceCurve right after SetCurve might cause the
// curves to be forgotten; use SetEditorCurve instead.
AnimationUtility.SetEditorCurve(newClip, newBinding,
AnimationUtility.GetEditorCurve(originalClip, binding));
}
foreach (var objBinding in objectBindings)
{
var newBinding = objBinding;
newBinding.path = MapPath(objBinding);
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
AnimationUtility.GetObjectReferenceCurve(originalClip, objBinding));
}
newClip.wrapMode = originalClip.wrapMode;
newClip.legacy = originalClip.legacy;
newClip.frameRate = originalClip.frameRate;
newClip.localBounds = originalClip.localBounds;
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(originalClip));
if (clipCache != null)
{
clipCache.Add(originalClip, newClip);
}
return newClip;
}
private void ApplyMappingsToAvatarMask(AvatarMask mask)
{
if (mask == null) return;
var maskSo = new SerializedObject(mask);
var seenTransforms = new Dictionary<string, float>();
var transformOrder = new List<string>();
var m_Elements = maskSo.FindProperty("m_Elements");
var elementCount = m_Elements.arraySize;
for (var i = 0; i < elementCount; i++)
{
var element = m_Elements.GetArrayElementAtIndex(i);
var path = element.FindPropertyRelative("m_Path").stringValue;
var weight = element.FindPropertyRelative("m_Weight").floatValue;
path = MapPath(path);
// ensure all parent elements are present
EnsureParentsPresent(path);
if (!seenTransforms.ContainsKey(path)) transformOrder.Add(path);
seenTransforms[path] = weight;
}
transformOrder.Sort();
m_Elements.arraySize = transformOrder.Count;
for (var i = 0; i < transformOrder.Count; i++)
{
var element = m_Elements.GetArrayElementAtIndex(i);
var path = transformOrder[i];
element.FindPropertyRelative("m_Path").stringValue = path;
element.FindPropertyRelative("m_Weight").floatValue = seenTransforms[path];
}
maskSo.ApplyModifiedPropertiesWithoutUndo();
void EnsureParentsPresent(string path)
{
var nextSlash = -1;
while ((nextSlash = path.IndexOf('/', nextSlash + 1)) != -1)
{
var parentPath = path.Substring(0, nextSlash);
if (!seenTransforms.ContainsKey(parentPath))
{
seenTransforms[parentPath] = 0;
transformOrder.Add(parentPath);
}
}
}
}
internal void OnDeactivate(BuildContext context)
{
Profiler.BeginSample("PathMappings.OnDeactivate");
Dictionary<AnimationClip, AnimationClip> clipCache = new Dictionary<AnimationClip, AnimationClip>();
Profiler.BeginSample("ApplyMappingsToClip");
_animationDatabase.ForeachClip(holder =>
{
if (holder.CurrentClip is AnimationClip clip)
{
holder.CurrentClip = ApplyMappingsToClip(clip, clipCache);
}
});
Profiler.EndSample();
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
Profiler.BeginSample("MapPlayAudio");
_animationDatabase.ForeachPlayAudio(playAudio =>
{
if (playAudio == null) return;
playAudio.SourcePath = MapPath(playAudio.SourcePath, true);
});
Profiler.EndSample();
#endif
Profiler.BeginSample("InvokeIOnCommitObjectRenamesCallbacks");
foreach (var listener in context.AvatarRootObject.GetComponentsInChildren<IOnCommitObjectRenames>())
{
listener.OnCommitObjectRenames(context, this);
}
Profiler.EndSample();
#if MA_VRCSDK3_AVATARS
if (context.AvatarDescriptor)
{
var layers = context.AvatarDescriptor.baseAnimationLayers
.Concat(context.AvatarDescriptor.specialAnimationLayers);
Profiler.BeginSample("ApplyMappingsToAvatarMasks");
foreach (var layer in layers)
{
ApplyMappingsToAvatarMask(layer.mask);
if (layer.animatorController is AnimatorController ac)
// By this point, all AnimationOverrideControllers have been collapsed into an ephemeral
// AnimatorController so we can safely modify the controller in-place.
foreach (var acLayer in ac.layers)
ApplyMappingsToAvatarMask(acLayer.avatarMask);
}
Profiler.EndSample();
}
#endif
Profiler.EndSample();
}
public GameObject PathToObject(string path)
{
if (_pathToObject == null)
{
var builder = ImmutableDictionary.CreateBuilder<string, GameObject>();
foreach (var kvp in _objectToOriginalPaths)
foreach (var p in kvp.Value)
builder[p] = kvp.Key;
_pathToObject = builder.ToImmutable();
}
if (_pathToObject.TryGetValue(path, out var obj))
{
return obj;
}
else
{
return null;
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f60ee78d127fda546a84d5396edfc8b2
timeCreated: 1691237971

View File

@ -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<EditorCurveBinding, string> _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));
/// <summary>
/// Creates an animator parameter which tracks the effective value of a property on a component. This only
/// tracks FX layer properties.
/// </summary>
/// <param name="ecb"></param>
/// <returns></returns>
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");
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1074339e2a59465ba585cb8cbbc4a88c
timeCreated: 1719195449

View File

@ -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<EditorCurveBinding, string> proxyProps = new();
}
private AnimatorServicesContext? _asc;
private Retained _retained = null!;
private AnimatorServicesContext asc =>
_asc ?? throw new InvalidOperationException("ActiveSelfProxyExtension is not active");
private Dictionary<EditorCurveBinding, string> 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<AnimatorServicesContext>();
_retained = context.GetState<Retained>();
}
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);
}
}
});
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 511cbc0373a2469192e0351e2222a203
timeCreated: 1732496091

View File

@ -5,6 +5,7 @@ using System;
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 @@ namespace nadena.dev.modular_avatar.core.editor
var values = context.GetState<DefaultValues>()?.InitialValueOverrides
?? ImmutableDictionary<string, float>.Empty;
foreach (var layer in context.AvatarDescriptor.baseAnimationLayers
.Concat(context.AvatarDescriptor.specialAnimationLayers))
var asc = context.Extension<AnimatorServicesContext>();
foreach (var controller in asc.ControllerContext.GetAllControllers())
{
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))
var parameters = controller.Parameters;
foreach (var (name, parameter) in parameters)
{
throw new Exception("Leaked unexpected controller: " + layer.animatorController + " (type " + layer.animatorController?.GetType() + ")");
}
if (!values.TryGetValue(name, out var defaultValue)) continue;
var parameters = controller.parameters;
for (int i = 0; i < parameters.Length; i++)
{
if (!values.TryGetValue(parameters[i].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;
}
}
}

View File

@ -1,12 +1,14 @@
#if MA_VRCSDK3_AVATARS
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -17,11 +19,16 @@ namespace nadena.dev.modular_avatar.core.editor
*/
internal class BlendshapeSyncAnimationProcessor
{
private BuildContext _context;
private Dictionary<Motion, Motion> _motionCache;
private readonly ndmf.BuildContext _context;
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
private struct SummaryBinding
internal BlendshapeSyncAnimationProcessor(ndmf.BuildContext context)
{
_context = context;
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
}
private struct SummaryBinding : IEquatable<SummaryBinding>
{
private const string PREFIX = "blendShape.";
public string path;
@ -33,71 +40,76 @@ namespace nadena.dev.modular_avatar.core.editor
this.propertyName = PREFIX + blendShape;
}
public static SummaryBinding FromEditorBinding(EditorCurveBinding binding)
public static SummaryBinding? FromEditorBinding(EditorCurveBinding binding)
{
if (binding.type != typeof(SkinnedMeshRenderer) || !binding.propertyName.StartsWith(PREFIX))
{
return new SummaryBinding();
return null;
}
return new SummaryBinding(binding.path, binding.propertyName.Substring(PREFIX.Length));
}
public EditorCurveBinding ToEditorCurveBinding()
{
return EditorCurveBinding.FloatCurve(
path,
typeof(SkinnedMeshRenderer),
propertyName
);
}
public bool Equals(SummaryBinding other)
{
return path == other.path && propertyName == other.propertyName;
}
public override bool Equals(object? obj)
{
return obj is SummaryBinding other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(path, propertyName);
}
}
public void OnPreprocessAvatar(BuildContext context)
public void OnPreprocessAvatar()
{
_context = context;
var avatarGameObject = context.AvatarRootObject;
var animDb = _context.AnimationDatabase;
var avatarDescriptor = context.AvatarDescriptor;
var avatarGameObject = _context.AvatarRootObject;
var animDb = _context.Extension<AnimatorServicesContext>().AnimationIndex;
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
_motionCache = new Dictionary<Motion, Motion>();
var components = avatarGameObject.GetComponentsInChildren<ModularAvatarBlendshapeSync>(true);
if (components.Length == 0) return;
var layers = avatarDescriptor.baseAnimationLayers;
var fxIndex = -1;
AnimatorController controller = null;
for (int i = 0; i < layers.Length; i++)
{
if (layers[i].type == VRCAvatarDescriptor.AnimLayerType.FX && !layers[i].isDefault)
{
if (layers[i].animatorController is AnimatorController c && c != null)
{
fxIndex = i;
controller = c;
break;
}
}
}
if (controller == null)
{
// Nothing to do, return
}
foreach (var component in components)
{
BuildReport.ReportingObject(component, () => ProcessComponent(avatarGameObject, component));
}
// Walk and transform all clips
animDb.ForeachClip(clip =>
var clips = new HashSet<VirtualClip>();
foreach (var key in _bindingMappings.Keys)
{
if (clip.CurrentClip is AnimationClip anim)
{
BuildReport.ReportingObject(clip.CurrentClip,
() => { clip.CurrentClip = TransformMotion(anim); });
}
});
var ecb = key.ToEditorCurveBinding();
clips.UnionWith(animDb.GetClipsForBinding(ecb));
}
// Walk and transform all clips
foreach (var clip in clips)
{
ProcessClip(clip);
}
}
private void ProcessComponent(GameObject avatarGameObject, ModularAvatarBlendshapeSync component)
{
var targetObj = RuntimeUtil.RelativePath(avatarGameObject, component.gameObject);
if (targetObj == null) return;
foreach (var binding in component.Bindings)
{
var refObj = binding.ReferenceMesh.Get(component);
@ -106,6 +118,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (refSmr == null) continue;
var refPath = RuntimeUtil.RelativePath(avatarGameObject, refObj);
if (refPath == null) continue;
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
@ -123,108 +136,20 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
Motion TransformMotion(Motion motion)
private void ProcessClip(VirtualClip clip)
{
if (motion == null) return null;
if (_motionCache.TryGetValue(motion, out var cached)) return cached;
switch (motion)
foreach (var binding in clip.GetFloatCurveBindings().ToList())
{
case AnimationClip clip:
{
motion = ProcessClip(clip);
break;
}
case BlendTree tree:
{
bool anyChanged = false;
var children = tree.children;
for (int i = 0; i < children.Length; i++)
{
var newM = TransformMotion(children[i].motion);
if (newM != children[i].motion)
{
anyChanged = true;
children[i].motion = newM;
}
}
if (anyChanged)
{
var newTree = new BlendTree();
EditorUtility.CopySerialized(tree, newTree);
_context.SaveAsset(newTree);
newTree.children = children;
motion = newTree;
}
break;
}
default:
Debug.LogWarning($"Ignoring unsupported motion type {motion.GetType()}");
break;
}
_motionCache[motion] = motion;
return motion;
}
AnimationClip ProcessClip(AnimationClip origClip)
{
var clip = origClip;
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip);
foreach (var binding in bindings)
{
if (!_bindingMappings.TryGetValue(SummaryBinding.FromEditorBinding(binding), out var dstBindings))
var srcBinding = SummaryBinding.FromEditorBinding(binding);
if (srcBinding == null || !_bindingMappings.TryGetValue(srcBinding.Value, out var dstBindings))
{
continue;
}
if (clip == origClip)
{
clip = Object.Instantiate(clip);
}
var curve = clip.GetFloatCurve(binding);
foreach (var dst in dstBindings)
{
clip.SetCurve(dst.path, typeof(SkinnedMeshRenderer), dst.propertyName,
AnimationUtility.GetEditorCurve(origClip, binding));
}
}
return clip;
}
IEnumerable<AnimatorState> AllStates(AnimatorController controller)
{
HashSet<AnimatorStateMachine> visitedStateMachines = new HashSet<AnimatorStateMachine>();
Queue<AnimatorStateMachine> stateMachines = new Queue<AnimatorStateMachine>();
foreach (var layer in controller.layers)
{
if (layer.stateMachine != null)
stateMachines.Enqueue(layer.stateMachine);
}
while (stateMachines.Count > 0)
{
var next = stateMachines.Dequeue();
if (visitedStateMachines.Contains(next)) continue;
visitedStateMachines.Add(next);
foreach (var state in next.states)
{
yield return state.state;
}
foreach (var sm in next.stateMachines)
{
stateMachines.Enqueue(sm.stateMachine);
clip.SetFloatCurve(dst.ToEditorCurveBinding(), curve);
}
}
}

View File

@ -4,10 +4,7 @@ using VRC.SDK3.Avatars.ScriptableObjects;
#endif
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using Object = UnityEngine.Object;
@ -23,14 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
internal GameObject AvatarRootObject => PluginBuildContext.AvatarRootObject;
internal Transform AvatarRootTransform => PluginBuildContext.AvatarRootTransform;
internal AnimationDatabase AnimationDatabase =>
PluginBuildContext.Extension<AnimationServicesContext>().AnimationDatabase;
internal PathMappings PathMappings =>
PluginBuildContext.Extension<AnimationServicesContext>().PathMappings;
internal Object AssetContainer => PluginBuildContext.AssetContainer;
private bool SaveImmediate = false;
#if MA_VRCSDK3_AVATARS
@ -71,61 +60,6 @@ namespace nadena.dev.modular_avatar.core.editor
PluginBuildContext.AssetSaver.SaveAsset(obj);
}
public AnimatorController CreateAnimator(AnimatorController toClone = null)
{
AnimatorController controller;
if (toClone != null)
{
controller = Object.Instantiate(toClone);
}
else
{
controller = new AnimatorController();
}
SaveAsset(controller);
return controller;
}
public AnimatorController DeepCloneAnimator(RuntimeAnimatorController controller)
{
if (controller == null) return null;
var merger = new AnimatorCombiner(PluginBuildContext, controller.name + " (clone)");
switch (controller)
{
case AnimatorController ac:
merger.AddController("", ac, null);
break;
case AnimatorOverrideController oac:
merger.AddOverrideController("", oac, null);
break;
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
}
var result = merger.Finish();
ObjectRegistry.RegisterReplacedObject(controller, result);
return result;
}
public AnimatorController ConvertAnimatorController(RuntimeAnimatorController anyController)
{
switch (anyController)
{
case AnimatorController ac:
return ac;
case AnimatorOverrideController aoc:
var merger = new AnimatorCombiner(PluginBuildContext, anyController.name + " (clone)");
merger.AddOverrideController("", aoc, null);
return merger.Finish();
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + anyController.GetType());
}
}
#if MA_VRCSDK3_AVATARS
public VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)

View File

@ -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<VRCAvatarDescriptor.AnimLayerType, AnimatorController> defaultControllers_ =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController>();
private Dictionary<VRCAvatarDescriptor.AnimLayerType, bool?> writeDefaults_ =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, bool?>();
Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner> mergeSessions =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner>();
private AnimatorServicesContext _asc;
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
{
_context = context;
defaultControllers_.Clear();
mergeSessions.Clear();
var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
if (!descriptor) return;
if (descriptor.baseAnimationLayers != null) InitSessions(descriptor.baseAnimationLayers);
if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers);
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
var toMerge = avatarGameObject.transform.GetComponentsInChildren<ModularAvatarMergeAnimator>(true);
Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>> byLayerType
= new Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>>();
@ -89,10 +60,6 @@ namespace nadena.dev.modular_avatar.core.editor
{
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 @@ namespace nadena.dev.modular_avatar.core.editor
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);
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 beforeOriginal)
bool? writeDefaults = null;
if (wdStateCounter.Count == 1) writeDefaults = wdStateCounter.First().Key;
foreach (var component in sorted)
{
MergeSingle(context, session, component);
}
if (defaultControllers_.TryGetValue(layerType, out var defaultController) &&
defaultController.layers.Length > 0)
{
session.AddController("", defaultController, null, forceFirstLayerWeight: true);
}
foreach (var component in afterOriginal)
{
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<RenamedMergeAnimators>();
var clonedController = stash.Controllers.GetValueOrDefault(merge)
?? _asc.ControllerContext.CloneContext.CloneDistinct(merge.animator);
string basePath;
if (merge.pathMode == MergeAnimatorPathMode.Relative)
{
@ -145,200 +112,60 @@ namespace nadena.dev.modular_avatar.core.editor
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);
foreach (var l in clonedController.Layers)
{
if (initialWriteDefaults != null)
{
foreach (var s in l.StateMachine.AllStates())
{
s.WriteDefaultValues = initialWriteDefaults.Value;
}
}
controller.AddLayer(new LayerPriority(merge.layerPriority), l);
}
foreach (var (name, parameter) in clonedController.Parameters)
{
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;
}
controller.Parameters = controller.Parameters.Add(name, parameter);
}
if (merge.deleteAttachedAnimator)
{
var animator = merge.GetComponent<Animator>();
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<string, AnimatorControllerParameterType> types =
new Dictionary<string, AnimatorControllerParameterType>();
// Learn types...
foreach (var session in mergeSessions.Values)
{
session.MergeTypes(types);
}
// And propagate them
foreach (var session in mergeSessions.Values)
{
session.MergeTypes(types);
}
for (int i = 0; i < layers.Length; i++)
{
if (mergeSessions.TryGetValue(layers[i].type, out var session))
{
if (layers[i].type == VRCAvatarDescriptor.AnimLayerType.Gesture && layers[i].isDefault)
{
// We need to set the mask field for the gesture layer on initial configuration
layers[i].mask = AssetDatabase.LoadAssetAtPath<AvatarMask>(
AssetDatabase.GUIDToAssetPath(GUID_GESTURE_HANDSONLY_MASK)
);
}
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);
}
}
}
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<AnimatorStateMachine>();
foreach (var layer in controller.layers)
{
// 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
)
{
continue;
}
stateMachineQueue.Enqueue(layer.stateMachine);
}
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
{
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<AnimatorController>(SAMPLE_PATH_PACKAGE + name);
if (controller == null)
{
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_LEGACY + name);
}
}
}
return controller;
}
}
}

View File

@ -31,8 +31,8 @@ using VRC.SDK3.Dynamics.PhysBone.Components;
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 @@ namespace nadena.dev.modular_avatar.core.editor
#endif
private BoneDatabase BoneDatabase = new BoneDatabase();
private PathMappings PathMappings => frameworkContext.Extension<AnimationServicesContext>()
.PathMappings;
private AnimatorServicesContext AnimatorServices => frameworkContext.Extension<AnimatorServicesContext>();
private HashSet<Transform> humanoidBones = new HashSet<Transform>();
private HashSet<Transform> mergedObjects = new HashSet<Transform>();
private HashSet<Transform> thisPassAdded = new HashSet<Transform>();
private HashSet<Transform> transformLookthrough = new HashSet<Transform>();
internal void OnPreprocessAvatar(ndmf.BuildContext context, GameObject avatarGameObject)
{
@ -135,7 +136,68 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings);
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, AnimatorServices);
ProcessTransformLookthrough();
}
private void ProcessTransformLookthrough()
{
var asc = frameworkContext.Extension<AnimatorServicesContext>();
transformLookthrough.RemoveWhere(t => !t);
var clipsToEdit = transformLookthrough.SelectMany(
xform =>
{
var path = asc.ObjectPathRemapper.GetVirtualPathForObject(xform);
return asc.AnimationIndex.GetClipsForObjectPath(path);
});
Dictionary<string, string> 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 @@ namespace nadena.dev.modular_avatar.core.editor
_activeRetargeter.FixupAnimations();
thisPassAdded.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform));
transformLookthrough.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform));
}
/**
@ -357,7 +420,7 @@ namespace nadena.dev.modular_avatar.core.editor
BoneDatabase.AddMergedBone(mergedSrcBone.transform);
BoneDatabase.RetainMergedBone(mergedSrcBone.transform);
PathMappings.MarkTransformLookthrough(mergedSrcBone);
transformLookthrough.Add(mergedSrcBone.transform);
thisPassAdded.Add(mergedSrcBone.transform);
}
@ -372,7 +435,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (zipMerge)
{
PathMappings.MarkTransformLookthrough(src);
transformLookthrough.Add(src.transform);
BoneDatabase.AddMergedBone(src.transform);
}

View File

@ -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 @@ namespace nadena.dev.modular_avatar.core.editor
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<string> _parameterNames;
protected override void Execute(ndmf.BuildContext context)
{
_asc = context.Extension<AnimatorServicesContext>();
_rootBlendTree = null;
_parameterNames = new HashSet<string>();
_controller = new AnimatorController();
var fx = _asc.ControllerContext[VRCAvatarDescriptor.AnimLayerType.FX];
foreach (var component in
context.AvatarRootObject.GetComponentsInChildren<ModularAvatarMergeBlendTree>(true))
{
ErrorReport.WithContextObject(component, () => ProcessComponent(context, component));
}
List<AnimatorControllerParameter> parameters = new List<AnimatorControllerParameter>(_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()
{
name = ALWAYS_ONE,
type = AnimatorControllerParameterType.Float,
defaultFloat = 1
});
foreach (var name in _parameterNames)
{
if (fx.Parameters.ContainsKey(name)) continue;
foreach (var name in _parameterNames)
fx.Parameters = fx.Parameters.SetItem(name, new AnimatorControllerParameter()
{
parameters.Add(new AnimatorControllerParameter()
{
name = name,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0
});
}
var paramsAnimator = new AnimatorController();
paramsAnimator.parameters = parameters.ToArray();
var paramsComponent = _mergeHost.AddComponent<ModularAvatarMergeAnimator>();
paramsComponent.animator = paramsAnimator;
paramsComponent.layerPriority = Int32.MaxValue;
name = name,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0.0f
});
}
}
private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlendTree component)
private void ProcessComponent(BuildContext context, ModularAvatarMergeBlendTree component)
{
var stash = context.PluginBuildContext.GetState<RenamedMergeAnimators>();
BlendTree componentBlendTree = component.BlendTree as BlendTree;
if (componentBlendTree == null)
@ -79,46 +70,60 @@ namespace nadena.dev.modular_avatar.core.editor
}
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 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
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<ModularAvatarMergeAnimator>();
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;
}
}

View File

@ -27,6 +27,7 @@ using System.Linq;
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 @@ namespace nadena.dev.modular_avatar.core.editor
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<SkinnedMeshRenderer>(true))
{
@ -153,7 +156,8 @@ namespace nadena.dev.modular_avatar.core.editor
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);
}
}

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
#nullable enable
using System.Collections.Generic;
using nadena.dev.ndmf;
using UnityEditor;
#if MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER
@ -6,7 +8,7 @@ using UnityEngine;
using UnityEngine.Animations;
using VRC.SDK3.Avatars;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf.animator;
using VRC.Dynamics;
#endif
@ -60,7 +62,7 @@ namespace nadena.dev.modular_avatar.core.editor
AvatarDynamicsSetup.DoConvertUnityConstraints(targetConstraintComponents, null, false);
var asc = context.Extension<AnimationServicesContext>();
var asc = context.Extension<AnimatorServicesContext>();
// Also look for preexisting VRCConstraints so we can go fix up any broken animation clips from people who
// clicked auto fix :(
@ -70,24 +72,20 @@ namespace nadena.dev.modular_avatar.core.editor
var targetPaths = constraintGameObjects
.Union(existingVRCConstraints)
.Select(c => asc.PathMappings.GetObjectIdentifier(c))
.Select(c => asc.ObjectPathRemapper.GetVirtualPathForObject(c))
.ToHashSet();
// Update animation clips
var clips = targetPaths.SelectMany(tp => asc.AnimationDatabase.ClipsForPath(tp))
var clips = targetPaths.SelectMany(tp => asc.AnimationIndex.GetClipsForObjectPath(tp))
.ToHashSet();
foreach (var clip in clips) RemapSingleClip(clip, targetPaths);
}
private void RemapSingleClip(AnimationDatabase.ClipHolder clip, HashSet<string> targetPaths)
private void RemapSingleClip(VirtualClip clip, HashSet<string> targetPaths)
{
var motion = clip.CurrentClip as AnimationClip;
if (motion == null) return;
var bindings = AnimationUtility.GetCurveBindings(motion);
var toUpdateBindings = new List<EditorCurveBinding>();
var toUpdateCurves = new List<AnimationCurve>();
var bindings = clip.GetFloatCurveBindings().ToList();
foreach (var ecb in bindings)
{
@ -102,20 +100,11 @@ namespace nadena.dev.modular_avatar.core.editor
type = newType,
propertyName = newProp
};
var curve = AnimationUtility.GetEditorCurve(motion, ecb);
if (curve != null)
{
toUpdateBindings.Add(newBinding);
toUpdateCurves.Add(curve);
toUpdateBindings.Add(ecb);
toUpdateCurves.Add(null);
}
var curve = clip.GetFloatCurve(ecb);
clip.SetFloatCurve(newBinding, curve);
clip.SetFloatCurve(newBinding, null);
}
}
if (toUpdateBindings.Count == 0) return;
AnimationUtility.SetEditorCurves(motion, toUpdateBindings.ToArray(), toUpdateCurves.ToArray());
}
#else

View File

@ -5,7 +5,9 @@ using nadena.dev.modular_avatar.animation;
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 nadena.dev.ndmf.util;
using UnityEngine;
using Object = UnityEngine.Object;
@ -35,8 +37,6 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
{
Sequence seq = InPhase(BuildPhase.Resolving);
seq.Run(ResolveObjectReferences.Instance);
// Protect against accidental destructive edits by cloning the input controllers ASAP
seq.Run("Clone animators", AnimationUtil.CloneAllControllers);
seq = InPhase(BuildPhase.Transforming);
seq.Run("Validate configuration",
@ -46,40 +46,57 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
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.Run(BoneProxyPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(VisibleHeadAccessoryPluginPass.Instance);
#endif
seq.Run("World Fixed Object",
ctx => new WorldFixedObjectProcessor().Process(ctx)
);
seq.Run(ReplaceObjectPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(BlendshapeSyncAnimationPluginPass.Instance);
seq.Run(GameObjectDelayDisablePass.Instance);
#endif
seq.Run(ConstraintConverterPass.Instance);
seq.Run("Prune empty animator layers",
ctx => { ctx.Extension<AnimatorServicesContext>().RemoveEmptyLayers(); });
seq.Run("Harmonize animator parameter types",
ctx => { ctx.Extension<AnimatorServicesContext>().HarmonizeParameterTypes(); });
});
#if MA_VRCSDK3_AVATARS
seq.Run(PhysbonesBlockerPluginPass.Instance);
@ -240,7 +257,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
{
protected override void Execute(ndmf.BuildContext context)
{
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(MAContext(context));
new BlendshapeSyncAnimationProcessor(context).OnPreprocessAvatar();
}
}

View File

@ -31,7 +31,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (_asc != null)
{
return _asc.GetActiveSelfProxy(obj);
return _rpe.GetActiveSelfProxy(obj);
}
else
{

View File

@ -4,6 +4,7 @@ using System.Collections.Immutable;
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 @@ namespace nadena.dev.modular_avatar.core.editor
{
private readonly ComputeContext _computeContext;
private readonly ndmf.BuildContext _context;
private readonly AnimationServicesContext _asc;
private readonly AnimatorServicesContext _asc;
private readonly ReadablePropertyExtension _rpe;
private Dictionary<string, float> _simulationInitialStates;
public const string BlendshapePrefix = "blendShape.";
@ -34,7 +37,8 @@ namespace nadena.dev.modular_avatar.core.editor
{
_computeContext = ComputeContext.NullContext;
_context = context;
_asc = context.Extension<AnimationServicesContext>();
_asc = context.Extension<AnimatorServicesContext>();
_rpe = context.Extension<ReadablePropertyExtension>();
_simulationInitialStates = null;
}
@ -145,7 +149,7 @@ namespace nadena.dev.modular_avatar.core.editor
/// <param name="shapes"></param>
private void AnalyzeConstants(Dictionary<TargetProp, AnimatedProperty> shapes)
{
var asc = _context?.Extension<AnimationServicesContext>();
var asc = _context?.Extension<AnimatorServicesContext>();
HashSet<GameObject> toggledObjects = new();
if (asc == null) return;
@ -160,7 +164,10 @@ namespace nadena.dev.modular_avatar.core.editor
{
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 @@ namespace nadena.dev.modular_avatar.core.editor
/// <param name="groups"></param>
private void ResolveToggleInitialStates(Dictionary<TargetProp, AnimatedProperty> groups)
{
var asc = _context?.Extension<AnimationServicesContext>();
var asc = _context?.Extension<AnimatorServicesContext>();
Dictionary<string, float> propStates = new();
Dictionary<string, float> nextPropStates = new();

View File

@ -5,6 +5,7 @@ using System;
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 @@ namespace nadena.dev.modular_avatar.core.editor
// Properties that are being driven, either by foreign animations or Object Toggles
private HashSet<string> activeProps = new();
private AnimationClip _initialStateClip;
private VirtualClip _initialStateClip;
private bool _writeDefaults;
public ReactiveObjectPass(ndmf.BuildContext context)
@ -38,7 +39,14 @@ namespace nadena.dev.modular_avatar.core.editor
// 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<AnimatorServicesContext>();
_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 @@ namespace nadena.dev.modular_avatar.core.editor
private void GenerateActiveSelfProxies(Dictionary<TargetProp, AnimatedProperty> shapes)
{
var asc = context.Extension<AnimationServicesContext>();
var rpe = context.Extension<ReadablePropertyExtension>();
foreach (var prop in shapes.Keys)
{
@ -68,7 +76,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
// 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 @@ namespace nadena.dev.modular_avatar.core.editor
private void ProcessInitialStates(Dictionary<TargetProp, object> initialStates,
Dictionary<TargetProp, AnimatedProperty> shapes)
{
var asc = context.Extension<AnimationServicesContext>();
var asc = context.Extension<AnimatorServicesContext>();
var rpe = context.Extension<ReadablePropertyExtension>();
// 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<AnimationServicesContext>().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 @@ namespace nadena.dev.modular_avatar.core.editor
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 @@ namespace nadena.dev.modular_avatar.core.editor
componentType,
key.PropertyName
);
AnimationUtility.SetObjectReferenceCurve(_initialStateClip, binding, new []
_initialStateClip.SetObjectCurve(binding, new[]
{
new ObjectReferenceKeyframe()
{
@ -308,7 +316,7 @@ namespace nadena.dev.modular_avatar.core.editor
private AnimatorStateMachine GenerateStateMachine(AnimatedProperty info)
{
var asc = context.Extension<AnimationServicesContext>();
var asc = context.Extension<AnimatorServicesContext>();
var asm = new AnimatorStateMachine();
// Workaround for the warning: "'.' is not allowed in State name"
@ -333,7 +341,6 @@ namespace nadena.dev.modular_avatar.core.editor
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<AnimatorStateTransition>)>();
@ -363,7 +370,7 @@ namespace nadena.dev.modular_avatar.core.editor
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 @@ namespace nadena.dev.modular_avatar.core.editor
position = new Vector3(x, y),
state = state
});
asc.AnimationDatabase.RegisterState(states[^1].state);
var transitionList = new List<AnimatorStateTransition>();
transitionBuffer.Add((state, transitionList));
@ -488,7 +494,7 @@ namespace nadena.dev.modular_avatar.core.editor
};
}
private AnimatorCondition[] GetTransitionConditions(AnimationServicesContext asc, ReactionRule group)
private AnimatorCondition[] GetTransitionConditions(ReactionRule group)
{
var conditions = new List<AnimatorCondition>();
@ -574,8 +580,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (key.TargetObject is GameObject targetObject && key.PropertyName == "m_IsActive")
{
var asc = context.Extension<AnimationServicesContext>();
var propName = asc.GetActiveSelfProxy(targetObject);
var rpe = context.Extension<ReadablePropertyExtension>();
var propName = rpe.GetActiveSelfProxy(targetObject);
binding = EditorCurveBinding.FloatCurve("", typeof(Animator), propName);
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
@ -586,47 +592,29 @@ namespace nadena.dev.modular_avatar.core.editor
private void ApplyController(AnimatorStateMachine asm, string layerName)
{
var fx = FindFxController();
if (fx.animatorController == null)
var asc = context.Extension<AnimatorServicesContext>();
var fx = asc.ControllerContext[
VRCAvatarDescriptor.AnimLayerType.FX
];
if (fx == null)
{
throw new InvalidOperationException("No FX layer found");
}
if (!context.IsTemporaryAsset(fx.animatorController))
{
throw new InvalidOperationException("FX layer is not a temporary asset");
}
if (!(fx.animatorController is AnimatorController animController))
foreach (var paramName in initialValues.Keys.Except(fx.Parameters.Keys))
{
throw new InvalidOperationException("FX layer is not an animator controller");
}
var paramList = animController.parameters.ToList();
var paramSet = paramList.Select(p => p.name).ToHashSet();
foreach (var paramName in initialValues.Keys.Except(paramSet))
{
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()

View File

@ -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 @@ namespace nadena.dev.modular_avatar.core.editor
if (mamiWithRC.Count > 0)
{
// This make sures the parameters are correctly merged into the FX layer.
var mergeAnimator = context.AvatarRootObject.AddComponent<ModularAvatarMergeAnimator>();
mergeAnimator.layerType = VRCAvatarDescriptor.AnimLayerType.FX;
mergeAnimator.deleteAttachedAnimator = false;
mergeAnimator.animator = new AnimatorController
var asc = context.Extension<AnimatorServicesContext>();
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,
});
}
}
}
}

View File

@ -6,13 +6,12 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
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;
using UnityEngine.Profiling;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using VRC.SDK3.Dynamics.Contact.Components;
@ -55,6 +54,43 @@ namespace nadena.dev.modular_avatar.core.editor
{
public ImmutableDictionary<string, float> InitialValueOverrides;
}
internal class RenamedMergeAnimators
{
public AnimatorServicesContext AnimatorServices;
public Dictionary<ModularAvatarMergeAnimator, VirtualAnimatorController> Controllers = new();
public Dictionary<ModularAvatarMergeBlendTree, VirtualBlendTree> 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.CloneDistinct(mama.animator, mama.layerType);
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 +199,10 @@ namespace nadena.dev.modular_avatar.core.editor
if (!context.AvatarDescriptor) return;
_context = context;
var stash = _context.PluginBuildContext.GetState<RenamedMergeAnimators>();
var asc = _context.PluginBuildContext.Extension<AnimatorServicesContext>();
stash.AnimatorServices = asc;
var syncParams = WalkTree(avatar);
@ -368,12 +408,6 @@ namespace nadena.dev.modular_avatar.core.editor
case ModularAvatarMergeAnimator merger:
{
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (merger.animator is AnimatorOverrideController overrideController)
{
merger.animator = _context.ConvertAnimatorController(overrideController);
}
var mappings = paramInfo.GetParameterRemappingsAt(obj);
var remap = mappings.SelectMany(item =>
{
@ -389,11 +423,13 @@ namespace nadena.dev.modular_avatar.core.editor
if (merger.animator != null)
{
Profiler.BeginSample("DeepCloneAnimator");
merger.animator = new DeepClone(_context.PluginBuildContext).DoClone(merger.animator);
Profiler.EndSample();
var stash = _context.PluginBuildContext.GetState<RenamedMergeAnimators>();
ProcessRuntimeAnimatorController(merger.animator, remap);
var controller = stash.Clone(merger);
ProcessVirtualAnimatorController(controller, remap);
stash.Controllers[merger] = controller;
}
break;
@ -404,8 +440,12 @@ namespace nadena.dev.modular_avatar.core.editor
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<RenamedMergeAnimators>();
var virtualbt = stash.Clone(merger);
ProcessBlendtree(virtualbt, paramInfo.GetParameterRemappingsAt(obj));
stash.BlendTrees[merger] = virtualbt;
}
break;
@ -497,28 +537,6 @@ namespace nadena.dev.modular_avatar.core.editor
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<KeyValuePair<AnimationClip, AnimationClip>>();
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 +555,70 @@ namespace nadena.dev.modular_avatar.core.editor
});
}
private void ProcessAnimator(AnimatorController controller,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessVirtualAnimatorController(VirtualAnimatorController controller,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remap)
{
if (remaps.IsEmpty) return;
foreach (var node in controller.AllReachableNodes())
{
switch (node)
{
case VirtualStateMachine vsm: ProcessStateMachine(vsm, remap); break;
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;
}
}
var newParameters = controller.Parameters.Clear();
foreach (var (name, parameter) in controller.Parameters)
{
if (remap.TryGetValue((ParameterNamespace.Animator, name), out var newParam))
{
newParameters = newParameters.Add(newParam.ParameterName, parameter);
}
else
{
newParameters = newParameters.Add(name, parameter);
}
}
var visited = new HashSet<AnimatorStateMachine>();
var queue = new Queue<AnimatorStateMachine>();
var parameters = controller.parameters;
for (int i = 0; i < parameters.Length; i++)
{
if (remaps.TryGetValue((ParameterNamespace.Animator, parameters[i].name), out var newName))
{
parameters[i].name = newName.ParameterName;
}
}
controller.parameters = parameters;
foreach (var layer in controller.layers)
{
if (layer.stateMachine != null)
{
queue.Enqueue(layer.stateMachine);
}
}
Profiler.BeginSample("Walk animator graph");
while (queue.Count > 0)
{
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)
{
queue.Enqueue(sub.stateMachine);
foreach (var t in sm.GetStateMachineTransitions(sub.stateMachine))
{
ProcessTransition(t, remaps);
}
}
foreach (var st in sm.states)
{
ProcessState(st.state, remaps);
}
}
Profiler.EndSample();
controller.Parameters = newParameters;
}
private void ProcessState(AnimatorState state, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessStateMachine(VirtualStateMachine vsm,
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);
foreach (var t in state.transitions)
{
ProcessTransition(t, remaps);
}
foreach (var behavior in state.behaviours)
foreach (var behavior in vsm.Behaviours)
{
if (behavior is VRCAvatarParameterDriver driver)
{
ProcessDriver(driver, remaps);
}
}
ProcessMotion(state.motion, remaps);
}
private void ProcessMotion(Motion motion,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessState(VirtualState state, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
if (motion is BlendTree blendTree) ProcessBlendtree(blendTree, 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);
if (motion is AnimationClip clip) ProcessClip(clip, remaps);
foreach (var behavior in state.Behaviours)
{
if (behavior is VRCAvatarParameterDriver driver)
{
ProcessDriver(driver, remaps);
}
}
}
private void ProcessClip(AnimationClip clip,
private void ProcessClip(VirtualClip clip,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
var curveBindings = AnimationUtility.GetCurveBindings(clip);
var curveBindings = clip.GetFloatCurveBindings();
var bindingsToUpdate = new List<EditorCurveBinding>();
var newCurves = new List<AnimationCurve>();
@ -653,48 +628,30 @@ namespace nadena.dev.modular_avatar.core.editor
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 +667,17 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
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<string, ParameterInfo> CollectParameters(ModularAvatarParameters p,

View File

@ -1,14 +1,15 @@
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 Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
using UnityObject = UnityEngine.Object;
using UnityObject = Object;
// ReSharper disable once RedundantUsingDirective
using Object = System.Object;
@ -128,7 +129,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
_buildContext.Extension<AnimationServicesContext>().PathMappings
_buildContext.Extension<AnimatorServicesContext>().ObjectPathRemapper
.ReplaceObject(original, replacement);
// Destroy original

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -164,33 +165,34 @@ namespace nadena.dev.modular_avatar.core.editor
private void ProcessAnimations()
{
var animdb = _context.AnimationDatabase;
var paths = _context.PathMappings;
var animdb = _context.PluginBuildContext.Extension<AnimatorServicesContext>();
var paths = animdb.ObjectPathRemapper;
Dictionary<string, string> pathMappings = new Dictionary<string, string>();
HashSet<VirtualClip> clips = new();
foreach (var kvp in _boneShims)
{
var orig = paths.GetObjectIdentifier(kvp.Key.gameObject);
var shim = paths.GetObjectIdentifier(kvp.Value.gameObject);
var orig = paths.GetVirtualPathForObject(kvp.Key.gameObject);
var shim = paths.GetVirtualPathForObject(kvp.Value.gameObject);
pathMappings[orig] = shim;
clips.UnionWith(animdb.AnimationIndex.GetClipsForObjectPath(orig));
}
animdb.ForeachClip(motion =>
foreach (var clip in clips)
{
if (!(motion.CurrentClip is AnimationClip clip)) return;
var bindings = AnimationUtility.GetCurveBindings(clip);
foreach (var binding in bindings)
foreach (var binding in clip.GetFloatCurveBindings())
{
if (binding.type != typeof(Transform)) continue;
if (!pathMappings.TryGetValue(binding.path, out var newPath)) continue;
var newBinding = binding;
newBinding.path = newPath;
AnimationUtility.SetEditorCurve(clip, newBinding, AnimationUtility.GetEditorCurve(clip, binding));
if (binding.type == typeof(Transform) && pathMappings.TryGetValue(binding.path, out var newPath))
{
clip.SetFloatCurve(
EditorCurveBinding.FloatCurve(newPath, typeof(Transform), binding.propertyName),
clip.GetFloatCurve(binding)
);
}
}
});
}
}
private Transform CreateShim(Transform target)

View File

@ -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 class ActiveAnimationRetargeterTests : TestBase
// initialize context
var buildContext = new BuildContext(avatar);
var pathMappings = buildContext.PluginBuildContext.ActivateExtensionContext<AnimationServicesContext>()
.PathMappings;
var asc = buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
// get game objects
var changedChild = avatar.transform.Find("Toggled/Child");
@ -29,18 +31,16 @@ public class ActiveAnimationRetargeterTests : TestBase
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")));
}
}

View File

@ -1,58 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using modular_avatar_tests;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using NUnit.Framework;
using UnityEditor.Animations;
using UnityEngine;
namespace _ModularAvatar.EditModeTests
{
public class AnimationDatabaseCloningTest : TestBase
{
[Test]
public void TestAnimationDatabaseCloningLogic()
{
var root = CreateRoot("root");
var context = CreateContext(root);
var origController = LoadAsset<AnimatorController>("ac.controller");
var state = origController.layers[0].stateMachine.defaultState;
var clonedState = Object.Instantiate(state);
var origAnimation = LoadAsset<AnimationClip>("anim.anim");
using (new ObjectRegistryScope(new ObjectRegistry(root.transform)))
{
var db = new AnimationDatabase();
db.OnActivate(context);
db.RegisterState(clonedState);
var newBlendTree = clonedState.motion as BlendTree;
var origBlendTree = state.motion as BlendTree;
Assert.NotNull(newBlendTree);
Assert.NotNull(origBlendTree);
Assert.AreNotSame(newBlendTree, origBlendTree);
Assert.AreNotSame(newBlendTree.children[1].motion, origBlendTree.children[1].motion);
// Before commit, proxy animations are replaced.
Assert.AreNotSame(newBlendTree.children[0].motion, origBlendTree.children[0].motion);
Assert.AreSame(ObjectRegistry.GetReference(origAnimation),
ObjectRegistry.GetReference(newBlendTree.children[1].motion));
db.Commit();
Assert.AreNotSame(newBlendTree, origBlendTree);
Assert.AreNotSame(newBlendTree.children[1].motion, origBlendTree.children[1].motion);
// After commit, proxy animations are restored to the original assets.
Assert.AreSame(newBlendTree.children[0].motion, origBlendTree.children[0].motion);
}
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: d86c7d257d78fff4d8fdf56e2954a5c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -108,9 +108,10 @@ namespace modular_avatar_tests
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

View File

@ -32,8 +32,12 @@ namespace modular_avatar_tests
Assert.AreEqual("L3", l3.name);
Assert.AreEqual("L3.a", l3a.name);
Assert.AreEqual(2, l3.stateMachine.defaultState.behaviours.Length);
Assert.AreEqual(4, ((VRCAnimatorLayerControl)l3.stateMachine.defaultState.behaviours[0]).layer);
// The layer control behavior referencing the deleted layer should be removed
Assert.AreEqual(3, l3.stateMachine.defaultState.behaviours.Length);
Assert.AreEqual("2", ((VRCAnimatorLayerControl)l3.stateMachine.defaultState.behaviours[0]).debugString);
Assert.IsTrue(l3.stateMachine.defaultState.behaviours[1] is VRCAnimatorTrackingControl);
Assert.AreEqual("3", ((VRCAnimatorLayerControl)l3.stateMachine.defaultState.behaviours[2]).debugString);
Assert.AreEqual(3, ((VRCAnimatorLayerControl)l3.stateMachine.defaultState.behaviours[0]).layer);
Assert.AreEqual(1, l3a.stateMachine.defaultState.behaviours.Length);
Assert.AreEqual(3, ((VRCAnimatorLayerControl)l3a.stateMachine.defaultState.behaviours[0]).layer);

View File

@ -1,5 +1,22 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-8943217745761122886
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -1936262289, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
m_Name:
m_EditorClassIdentifier:
playable: 0
layer: 0
goalWeight: 0
blendDuration: 0
debugString: 3
--- !u!1107 &-6599268793956612610
AnimatorStateMachine:
serializedVersion: 6
@ -76,6 +93,7 @@ AnimatorState:
- {fileID: 2645212092703721488}
- {fileID: -4263003778027536188}
- {fileID: 1637143755065747004}
- {fileID: -8943217745761122886}
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
@ -102,8 +120,8 @@ MonoBehaviour:
m_Script: {fileID: -1936262289, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
m_Name:
m_EditorClassIdentifier:
playable: 0
layer: 0
playable: 1
layer: 1
goalWeight: 0
blendDuration: 0
debugString: 2

View File

@ -3,6 +3,7 @@
using nadena.dev.ndmf;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.core;
using nadena.dev.ndmf.animator;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
@ -16,8 +17,9 @@ namespace modular_avatar_tests
{
var prefab = CreatePrefab("HighQualityCurvesSettingPreserved.prefab");
var context = new BuildContext(prefab, null);
context.ActivateExtensionContext<AnimationServicesContext>();
context.DeactivateExtensionContext<AnimationServicesContext>();
context.ActivateExtensionContextRecursive<AnimatorServicesContext>();
context.DeactivateExtensionContext<AnimatorServicesContext>();
context.DeactivateExtensionContext<VirtualControllerContext>();
var layer = findFxLayer(prefab, "Base Layer");

View File

@ -1,152 +0,0 @@
using System.Linq;
using nadena.dev.modular_avatar.animation;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
namespace modular_avatar_tests
{
using UnityObject = UnityEngine.Object;
public class TrackObjectRenamesContextTests : TestBase
{
[Test]
public void testBasicContextInitialization()
{
var av = CreateRoot("root");
var bc = CreateContext(av);
var toc = new AnimationServicesContext();
toc.OnActivate(bc);
toc.OnDeactivate(bc);
}
[Test]
public void TracksSimpleRenames()
{
var root = CreateRoot("root");
var a = CreateChild(root, "a");
var toc = new AnimationServicesContext();
toc.OnActivate(CreateContext(root));
Assert.AreEqual("a", toc.PathMappings.MapPath("a"));
a.name = "b";
toc.PathMappings.ClearCache();
Assert.AreEqual("b", toc.PathMappings.MapPath("a"));
}
[Test]
public void TracksObjectMoves()
{
var root = CreateRoot("root");
var a = CreateChild(root, "a");
var b = CreateChild(root, "b");
var toc = new AnimationServicesContext();
toc.OnActivate(CreateContext(root));
Assert.AreEqual("a", toc.PathMappings.MapPath("a"));
a.transform.parent = b.transform;
toc.PathMappings.ClearCache();
Assert.AreEqual("b/a", toc.PathMappings.MapPath("a"));
}
[Test]
public void TracksCollapses()
{
var root = CreateRoot("root");
var a = CreateChild(root, "a");
var b = CreateChild(a, "b");
var c = CreateChild(b, "c");
var toc = new AnimationServicesContext();
toc.OnActivate(CreateContext(root));
toc.PathMappings.MarkRemoved(b);
c.transform.parent = a.transform;
UnityObject.DestroyImmediate(b);
Assert.AreEqual("a/c", toc.PathMappings.MapPath("a/b/c"));
}
[Test]
public void TransformLookthrough()
{
var root = CreateRoot("root");
var a = CreateChild(root, "a");
var b = CreateChild(a, "b");
var c = CreateChild(b, "c");
var d = CreateChild(c, "d");
var toc = new AnimationServicesContext();
toc.OnActivate(CreateContext(root));
toc.PathMappings.MarkTransformLookthrough(b);
toc.PathMappings.MarkTransformLookthrough(c);
Assert.AreEqual("a/b/c", toc.PathMappings.MapPath("a/b/c"));
Assert.AreEqual("a", toc.PathMappings.MapPath("a/b/c", true));
Assert.AreEqual("a/b/c/d", toc.PathMappings.MapPath("a/b/c/d", true));
}
#if MA_VRCSDK3_AVATARS
[Test]
public void TestAnimatorControllerUpdates()
{
var root = CreatePrefab("BasicObjectReferenceTest.prefab");
var parent = root.transform.Find("parent").gameObject;
var child = parent.transform.Find("child").gameObject;
var descriptor = root.GetComponent<VRCAvatarDescriptor>();
var oldFx = descriptor.baseAnimationLayers.First(l =>
l.type == VRCAvatarDescriptor.AnimLayerType.FX);
var oldIk = descriptor.specialAnimationLayers.First(l =>
l.type == VRCAvatarDescriptor.AnimLayerType.IKPose);
var toc = new AnimationServicesContext();
var buildContext = CreateContext(root);
toc.OnActivate(buildContext);
toc.PathMappings.MarkTransformLookthrough(child);
parent.name = "p2";
toc.OnDeactivate(buildContext);
var newFx = buildContext.AvatarDescriptor.baseAnimationLayers.First(l =>
l.type == VRCAvatarDescriptor.AnimLayerType.FX);
var newIk = buildContext.AvatarDescriptor.specialAnimationLayers.First(l =>
l.type == VRCAvatarDescriptor.AnimLayerType.IKPose);
Assert.AreNotEqual(oldFx.animatorController, newFx.animatorController);
Assert.AreNotEqual(oldIk.animatorController, newIk.animatorController);
CheckClips(newFx.animatorController as AnimatorController);
CheckClips(newIk.animatorController as AnimatorController);
void CheckClips(AnimatorController controller)
{
var clip = controller.layers[0].stateMachine.states[0].state.motion
as AnimationClip;
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
if (binding.type == typeof(Transform))
{
Assert.AreEqual("p2", binding.path);
}
else
{
Assert.AreEqual("p2/child", binding.path);
}
}
}
}
#endif
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: e71c9edfe1c94c5c93603ce45a1e2310
timeCreated: 1692516261

View File

@ -5,6 +5,7 @@ using nadena.dev.modular_avatar.animation;
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 @@ namespace UnitTests.MergeAnimatorTests
var ctx = new BuildContext(av, null);
ctx.ActivateExtensionContext<ModularAvatarContext>();
ctx.ActivateExtensionContext<AnimationServicesContext>();
ctx.ActivateExtensionContextRecursive<AnimatorServicesContext>();
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 @@ namespace UnitTests.MergeAnimatorTests
var ctx = new BuildContext(av, null);
ctx.ActivateExtensionContext<ModularAvatarContext>();
ctx.ActivateExtensionContext<AnimationServicesContext>();
ctx.ActivateExtensionContextRecursive<AnimatorServicesContext>();
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);

View File

@ -25,6 +25,8 @@ public class PreexistingParamsTest : TestBase
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);

View File

@ -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 @@ namespace UnitTests.MergeAnimatorTests.SyncedLayerOverrideInSubStatemachine
var controller = LoadAsset<AnimatorController>("syncedlayer.controller");
var root = CreateRoot("root");
var vrc_descriptor = root.GetComponent<VRCAvatarDescriptor>();
vrc_descriptor.customizeAnimationLayers = true;
var layers = vrc_descriptor.baseAnimationLayers;
for (int i = 0; i < layers.Length; i++)
{

View File

@ -237,7 +237,10 @@ public class ConvertTransitionTypes : TestBase
var fx = (AnimatorController) FindFxController(prefab).animatorController;
Assert.AreEqual(fx.parameters[0].type, AnimatorControllerParameterType.Bool);
Assert.AreEqual(
AnimatorControllerParameterType.Bool,
fx.parameters.First(p => p.name == "bool").type
);
}
void AssertTransitions(AnimatorControllerLayer layer, string src, string dest, int index,

View File

@ -2,6 +2,7 @@ using System.Linq;
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 @@ namespace modular_avatar_tests.MergeArmatureTests
nadena.dev.ndmf.BuildContext context =
new nadena.dev.ndmf.BuildContext(root, null);
context.ActivateExtensionContext<ModularAvatarContext>();
context.ActivateExtensionContext<AnimationServicesContext>();
context.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new MergeArmatureHook().OnPreprocessAvatar(context, root);
Assert.IsTrue(bone.GetComponentInChildren<TestComponentA>() != null);
@ -82,7 +83,7 @@ namespace modular_avatar_tests.MergeArmatureTests
nadena.dev.ndmf.BuildContext context =
new nadena.dev.ndmf.BuildContext(root, null);
context.ActivateExtensionContext<ModularAvatarContext>();
context.ActivateExtensionContext<AnimationServicesContext>();
context.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new MergeArmatureHook().OnPreprocessAvatar(context, root);
Assert.IsTrue(m_bone == null); // destroyed by retargeting pass
@ -106,7 +107,7 @@ namespace modular_avatar_tests.MergeArmatureTests
nadena.dev.ndmf.BuildContext context =
new nadena.dev.ndmf.BuildContext(root, null);
context.ActivateExtensionContext<ModularAvatarContext>();
context.ActivateExtensionContext<AnimationServicesContext>();
context.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new MergeArmatureHook().OnPreprocessAvatar(context, root);
Assert.IsTrue(m_bone == null); // destroyed by retargeting pass

View File

@ -13,7 +13,7 @@ namespace UnitTests.ReactiveComponent
internal class ObjectToggleTests : TestBase
{
[Test]
public void WhenObjectIsAlwaysOn_CorrectProxyParameterIsGenerated()
public void WhenObjectIsAlwaysOn_CorrectObjectStateIsSelected()
{
var root = CreateRoot("root");
var obj = CreateChild(root, "obj");
@ -36,15 +36,6 @@ namespace UnitTests.ReactiveComponent
};
AvatarProcessor.ProcessAvatar(root);
// TODO: Ideally we should start using play mode testing for these things...
var fx = (AnimatorController)FindFxController(root).animatorController;
var readableProp = fx.parameters.FirstOrDefault(
p => p.name.StartsWith("__MA/ReadableProp/obj/UnityEngine.GameObject/m_IsActive")
);
Assert.IsNotNull(readableProp);
Assert.AreEqual(readableProp.defaultFloat, 0);
Assert.IsFalse(obj.activeSelf);
}

View File

@ -7,6 +7,7 @@ using System.Linq;
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 @@ namespace modular_avatar_tests.RenameParametersTests
var context = CreateContext(prefab);
var maContext = context.ActivateExtensionContext<ModularAvatarContext>().BuildContext;
context.ActivateExtensionContextRecursive<AnimatorServicesContext>();
var errors = ErrorReport.CaptureErrors(
() =>
@ -209,6 +211,7 @@ namespace modular_avatar_tests.RenameParametersTests
var context = CreateContext(av);
var maContext = context.ActivateExtensionContext<ModularAvatarContext>().BuildContext;
context.ActivateExtensionContextRecursive<AnimatorServicesContext>();
var errors = ErrorReport.CaptureErrors(() => new RenameParametersHook().OnPreprocessAvatar(av, maContext));
@ -243,6 +246,7 @@ namespace modular_avatar_tests.RenameParametersTests
var context = CreateContext(av);
var maContext = context.ActivateExtensionContext<ModularAvatarContext>().BuildContext;
context.ActivateExtensionContextRecursive<AnimatorServicesContext>();
var errors = ErrorReport.CaptureErrors(() => new RenameParametersHook().OnPreprocessAvatar(av, maContext));

View File

@ -5,6 +5,7 @@ using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using NUnit.Framework;
using UnityEngine;
@ -12,12 +13,12 @@ namespace modular_avatar_tests.ReplaceObject
{
public class ReplaceObjectTests : TestBase
{
private PathMappings pathMappings;
private ObjectPathRemapper pathMappings;
void Process(GameObject root)
{
var buildContext = new nadena.dev.ndmf.BuildContext(root, null);
pathMappings = buildContext.ActivateExtensionContext<AnimationServicesContext>().PathMappings;
pathMappings = buildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>().ObjectPathRemapper;
new ReplaceObjectPass(buildContext).Process();
}
@ -162,7 +163,7 @@ namespace modular_avatar_tests.ReplaceObject
Process(root);
Assert.AreEqual("replacement", pathMappings.MapPath("replacee"));
Assert.AreEqual("replacement", pathMappings.GetVirtualPathForObject(pathMappings.GetObjectForPath("replacee")!));
}
}
}

View File

@ -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 @@ namespace modular_avatar_tests
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<AnimatorServicesContext>();
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 @@ namespace modular_avatar_tests
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<AnimatorServicesContext>();
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)),

View File

@ -0,0 +1,218 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1107 &-1523926916476422360
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: sync
m_ChildStates: []
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 0}
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: AC2_SameLayerBehavior
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
m_StateMachine: {fileID: 487233783815393512}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
- serializedVersion: 5
m_Name: main
m_StateMachine: {fileID: 5038666263108125925}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
- serializedVersion: 5
m_Name: sync
m_StateMachine: {fileID: -1523926916476422360}
m_Mask: {fileID: 0}
m_Motions:
- serializedVersion: 2
m_State: {fileID: 3457079820388324162}
m_Motion: {fileID: 7400000, guid: bf660ccc19e7d6e45849be7bdb273645, type: 2}
- serializedVersion: 2
m_State: {fileID: 592244106348618204}
m_Motion: {fileID: 7400000, guid: b4473bdae48543347999d599bcef9efc, type: 2}
m_Behaviours:
- m_State: {fileID: 3457079820388324162}
m_StateMachineBehaviours:
- {fileID: 684586325619030559}
m_BlendingMode: 0
m_SyncedLayerIndex: 1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1107 &487233783815393512
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Base Layer
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: 7429053045055472530}
m_Position: {x: 310.35324, y: 17.679626, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 7429053045055472530}
--- !u!1102 &592244106348618204
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: m2
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 7104638764aea2b4682734684ce18d68, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!114 &684586325619030559
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -1936262289, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
m_Name:
m_EditorClassIdentifier:
playable: 1
layer: 1
goalWeight: 0.5
blendDuration: 0
debugString:
--- !u!1102 &3457079820388324162
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: m1
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 462fc0905374b744fb7a4ca1110e9c84, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &5038666263108125925
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: main
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: 3457079820388324162}
m_Position: {x: 360, y: 10, z: 0}
- serializedVersion: 1
m_State: {fileID: 592244106348618204}
m_Position: {x: 360, y: 120, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 3457079820388324162}
--- !u!1102 &7429053045055472530
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: New State
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 0}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 26ef9d769605a1e46a5ad2768e28ad73
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,373 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &521542568012153140
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2616869947308232229}
- component: {fileID: 8063907918941263456}
- component: {fileID: 4452914144664039463}
m_Layer: 0
m_Name: MergedController_AC2
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2616869947308232229
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 521542568012153140}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -1.6884441, y: 1.1147345, z: -4.914471}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children:
- {fileID: 6479369535091914718}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8063907918941263456
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 521542568012153140}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 542108242, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
m_Name:
m_EditorClassIdentifier:
Name:
ViewPosition: {x: 0, y: 1.6, z: 0.2}
Animations: 0
ScaleIPD: 1
lipSync: 0
lipSyncJawBone: {fileID: 0}
lipSyncJawClosed: {x: 0, y: 0, z: 0, w: 1}
lipSyncJawOpen: {x: 0, y: 0, z: 0, w: 1}
VisemeSkinnedMesh: {fileID: 0}
MouthOpenBlendShapeName: Facial_Blends.Jaw_Down
VisemeBlendShapes: []
unityVersion:
portraitCameraPositionOffset: {x: 0, y: 0, z: 0}
portraitCameraRotationOffset: {x: 0, y: 1, z: 0, w: -0.00000004371139}
customExpressions: 0
expressionsMenu: {fileID: 0}
expressionParameters: {fileID: 0}
enableEyeLook: 0
customEyeLookSettings:
eyeMovement:
confidence: 0.5
excitement: 0.5
leftEye: {fileID: 0}
rightEye: {fileID: 0}
eyesLookingStraight:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingUp:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingDown:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingLeft:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingRight:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidType: 0
upperLeftEyelid: {fileID: 0}
upperRightEyelid: {fileID: 0}
lowerLeftEyelid: {fileID: 0}
lowerRightEyelid: {fileID: 0}
eyelidsDefault:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsClosed:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsLookingUp:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsLookingDown:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsSkinnedMesh: {fileID: 0}
eyelidsBlendshapes:
customizeAnimationLayers: 1
baseAnimationLayers:
- isEnabled: 0
type: 0
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 2
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 3
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 4
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 5
animatorController: {fileID: 9100000, guid: 91f184d4fc3ff1f49afdd24f69e085f0,
type: 2}
mask: {fileID: 0}
isDefault: 0
specialAnimationLayers:
- isEnabled: 0
type: 6
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 7
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 8
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
AnimationPreset: {fileID: 0}
animationHashSet: []
autoFootsteps: 1
autoLocomotion: 1
collider_head:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_torso:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_footR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_footL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_handR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_handL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerIndexL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerMiddleL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerRingL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerLittleL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerIndexR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerMiddleR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerRingR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerLittleR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
--- !u!114 &4452914144664039463
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 521542568012153140}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3}
m_Name:
m_EditorClassIdentifier:
launchedFromSDKPipeline: 0
completedSDKPipeline: 0
blueprintId:
contentType: 0
assetBundleUnityVersion:
fallbackStatus: 0
--- !u!1 &7379231148671166614
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6479369535091914718}
- component: {fileID: 5229589187063892731}
- component: {fileID: 1179508555950567543}
m_Layer: 0
m_Name: GameObject
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &6479369535091914718
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7379231148671166614}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 2616869947308232229}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &5229589187063892731
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7379231148671166614}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a5bf908a199a4648845ebe2fd3b5a4bd, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1179508555950567543
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7379231148671166614}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bb122659f724ebf85fe095ac02dc339, type: 3}
m_Name:
m_EditorClassIdentifier:
animator: {fileID: 9100000, guid: 22f4686ec8638e243854be6fccb38820, type: 2}
layerType: 5
deleteAttachedAnimator: 0
pathMode: 0
matchAvatarWriteDefaults: 0

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3946772624f2d6a469547924e29e0f35
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -76,7 +76,7 @@ namespace modular_avatar_tests.SyncedLayerHandling
}
[Test]
public void WhenSyncedLayerIsOnMergedController_LayerControlBehaviorsAreAdjusted()
public void WhenSyncedLayerIsOnMergedController_LayerControlBehaviorsAreNotAdjusted_CrossPlayable()
{
var prefab = CreatePrefab("MergedController.prefab");
AvatarProcessor.ProcessAvatar(prefab);
@ -93,7 +93,28 @@ namespace modular_avatar_tests.SyncedLayerHandling
var layercontrol = overrides[0] as VRCAnimatorLayerControl;
Assert.NotNull(layercontrol);
Assert.AreEqual(2, layercontrol.layer);
Assert.AreEqual(1, layercontrol.layer);
}
[Test]
public void WhenSyncedLayerIsOnMergedController_LayerControlBehaviorsAreAdjusted_SamePlayable()
{
var prefab = CreatePrefab("MergedController_AC2.prefab");
AvatarProcessor.ProcessAvatar(prefab);
var mainLayer = findFxLayer(prefab, "main");
var syncLayer = findFxLayer(prefab, "sync");
Assert.AreEqual(2, syncLayer.syncedLayerIndex);
var m1State = FindStateInLayer(mainLayer, "m1");
var overrides = syncLayer.GetOverrideBehaviours(m1State);
Assert.AreEqual(1, overrides.Length);
var layercontrol = overrides[0] as VRCAnimatorLayerControl;
Assert.NotNull(layercontrol);
Assert.AreEqual(1, layercontrol.layer);
}
}
}

View File

@ -7,6 +7,7 @@ using nadena.dev.modular_avatar.core;
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 @@ namespace modular_avatar_tests.VirtualMenuTests
};
var buildContext = new BuildContext(av_root.GetComponent<VRCAvatarDescriptor>());
buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new RenameParametersHook().OnPreprocessAvatar(av_root, buildContext);
var virtualMenu = VirtualMenu.ForAvatar(av_root.GetComponent<VRCAvatarDescriptor>(), buildContext);
@ -663,6 +665,7 @@ namespace modular_avatar_tests.VirtualMenuTests
var root = CreatePrefab("InternalParameterTest.prefab");
BuildContext buildContext = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new RenameParametersHook().OnPreprocessAvatar(root, buildContext);
var virtualMenu = VirtualMenu.ForAvatar(root.GetComponent<VRCAvatarDescriptor>(), buildContext);
@ -676,6 +679,7 @@ namespace modular_avatar_tests.VirtualMenuTests
var root = CreatePrefab("UnusedSubParametersAreStripped.prefab");
BuildContext buildContext = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new RenameParametersHook().OnPreprocessAvatar(root, buildContext);
var virtualMenu = VirtualMenu.ForAvatar(root.GetComponent<VRCAvatarDescriptor>(), buildContext);

View File

@ -2,6 +2,7 @@ using modular_avatar_tests;
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 class WorldFixedObjectTest : TestBase
// initialize context
var buildContext = new BuildContext(avatar);
buildContext.PluginBuildContext.ActivateExtensionContext<AnimationServicesContext>();
buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new WorldFixedObjectProcessor().Process(buildContext);
@ -42,7 +43,7 @@ public class WorldFixedObjectTest : TestBase
// initialize context
var buildContext = new BuildContext(avatar);
buildContext.PluginBuildContext.ActivateExtensionContext<AnimationServicesContext>();
buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new WorldFixedObjectProcessor().Process(buildContext);
@ -75,7 +76,7 @@ public class WorldFixedObjectTest : TestBase
// initialize context
var buildContext = new BuildContext(avatar);
var animationServices = buildContext.PluginBuildContext.ActivateExtensionContext<AnimationServicesContext>();
var animationServices = buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
new WorldFixedObjectProcessor().Process(buildContext);

View File

@ -16,7 +16,7 @@
},
"vpmDependencies": {
"com.vrchat.avatars": ">=3.7.4",
"nadena.dev.ndmf": ">=1.6.0 <2.0.0-a"
"nadena.dev.ndmf": ">=1.7.0-alpha.0 <2.0.0-a"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}