port remaining MA passes to anim-api

This commit is contained in:
bd_ 2025-02-15 18:59:08 -08:00
parent dca6ad36ba
commit fda9878a48
5 changed files with 117 additions and 199 deletions

View File

@ -1,12 +1,14 @@
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
#nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using UnityEditor; using UnityEditor;
using UnityEditor.Animations;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.Components;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor namespace nadena.dev.modular_avatar.core.editor
{ {
@ -17,11 +19,16 @@ namespace nadena.dev.modular_avatar.core.editor
*/ */
internal class BlendshapeSyncAnimationProcessor internal class BlendshapeSyncAnimationProcessor
{ {
private BuildContext _context; private readonly ndmf.BuildContext _context;
private Dictionary<Motion, Motion> _motionCache;
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings; 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."; private const string PREFIX = "blendShape.";
public string path; public string path;
@ -33,71 +40,76 @@ namespace nadena.dev.modular_avatar.core.editor
this.propertyName = PREFIX + blendShape; 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)) if (binding.type != typeof(SkinnedMeshRenderer) || !binding.propertyName.StartsWith(PREFIX))
{ {
return new SummaryBinding(); return null;
} }
return new SummaryBinding(binding.path, binding.propertyName.Substring(PREFIX.Length)); 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 avatarGameObject = context.AvatarRootObject; var animDb = _context.Extension<AnimatorServicesContext>().AnimationIndex;
var animDb = _context.AnimationDatabase;
var avatarDescriptor = context.AvatarDescriptor;
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>(); _bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
_motionCache = new Dictionary<Motion, Motion>();
var components = avatarGameObject.GetComponentsInChildren<ModularAvatarBlendshapeSync>(true); var components = avatarGameObject.GetComponentsInChildren<ModularAvatarBlendshapeSync>(true);
if (components.Length == 0) return; 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) foreach (var component in components)
{ {
BuildReport.ReportingObject(component, () => ProcessComponent(avatarGameObject, component)); BuildReport.ReportingObject(component, () => ProcessComponent(avatarGameObject, component));
} }
// Walk and transform all clips var clips = new HashSet<VirtualClip>();
animDb.ForeachClip(clip => foreach (var key in _bindingMappings.Keys)
{ {
if (clip.CurrentClip is AnimationClip anim) var ecb = key.ToEditorCurveBinding();
{ clips.UnionWith(animDb.GetClipsForBinding(ecb));
BuildReport.ReportingObject(clip.CurrentClip, }
() => { clip.CurrentClip = TransformMotion(anim); });
} // Walk and transform all clips
}); foreach (var clip in clips)
{
ProcessClip(clip);
}
} }
private void ProcessComponent(GameObject avatarGameObject, ModularAvatarBlendshapeSync component) private void ProcessComponent(GameObject avatarGameObject, ModularAvatarBlendshapeSync component)
{ {
var targetObj = RuntimeUtil.RelativePath(avatarGameObject, component.gameObject); var targetObj = RuntimeUtil.RelativePath(avatarGameObject, component.gameObject);
if (targetObj == null) return;
foreach (var binding in component.Bindings) foreach (var binding in component.Bindings)
{ {
var refObj = binding.ReferenceMesh.Get(component); var refObj = binding.ReferenceMesh.Get(component);
@ -106,6 +118,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (refSmr == null) continue; if (refSmr == null) continue;
var refPath = RuntimeUtil.RelativePath(avatarGameObject, refObj); var refPath = RuntimeUtil.RelativePath(avatarGameObject, refObj);
if (refPath == null) continue;
var srcBinding = new SummaryBinding(refPath, binding.Blendshape); 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; foreach (var binding in clip.GetFloatCurveBindings().ToList())
if (_motionCache.TryGetValue(motion, out var cached)) return cached;
switch (motion)
{ {
case AnimationClip clip: var srcBinding = SummaryBinding.FromEditorBinding(binding);
{ if (srcBinding == null || !_bindingMappings.TryGetValue(srcBinding.Value, out var dstBindings))
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))
{ {
continue; continue;
} }
if (clip == origClip) var curve = clip.GetFloatCurve(binding);
{
clip = Object.Instantiate(clip);
}
foreach (var dst in dstBindings) foreach (var dst in dstBindings)
{ {
clip.SetCurve(dst.path, typeof(SkinnedMeshRenderer), dst.propertyName, clip.SetFloatCurve(dst.ToEditorCurveBinding(), curve);
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);
} }
} }
} }

View File

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

View File

@ -77,28 +77,29 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
#endif #endif
seq.Run(MergeArmaturePluginPass.Instance); 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);
#endif
seq.Run(ConstraintConverterPass.Instance);
seq.Run("Prune empty animator layers", seq.Run("Prune empty animator layers",
ctx => { ctx.Extension<AnimatorServicesContext>().RemoveEmptyLayers(); }); ctx => { ctx.Extension<AnimatorServicesContext>().RemoveEmptyLayers(); });
seq.Run("Harmonize animator parameter types", seq.Run("Harmonize animator parameter types",
ctx => { ctx.Extension<AnimatorServicesContext>().HarmonizeParameterTypes(); }); ctx => { ctx.Extension<AnimatorServicesContext>().HarmonizeParameterTypes(); });
}); });
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
{
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); - TODO, move back here
#endif
seq.Run(ConstraintConverterPass.Instance);
});
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
seq.Run(PhysbonesBlockerPluginPass.Instance); seq.Run(PhysbonesBlockerPluginPass.Instance);
seq.Run("Fixup Expressions Menu", ctx => seq.Run("Fixup Expressions Menu", ctx =>
@ -258,7 +259,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
{ {
protected override void Execute(ndmf.BuildContext context) protected override void Execute(ndmf.BuildContext context)
{ {
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(MAContext(context)); new BlendshapeSyncAnimationProcessor(context).OnPreprocessAvatar();
} }
} }

View File

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

View File

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