mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-28 10:15:06 +08:00
3667dc319a
* add version defines * refactor: prefer BuildContext.ctor() overload taking GameObject * wrap unit tests entirely with MA_VRCSDK3_AVATARS * wrap unit tests smart with MA_VRCSDK3_AVATARS * wrap runtime entirely with MA_VRCSDK3_AVATARS * wrap VRC.SDKBase.IEditorOnly with MA_VRCSDK3_AVATARS * wrap editor entirely with MA_VRCSDK3_AVATARS * fix AvatarObjectReference.Get(Component) * wrap editor smart with MA_VRCSDK3_AVATARS * wrap BuildContext smart with MA_VRCSDK3_AVATARS * wrap PluginDefinition smart with MA_VRCSDK3_AVATARS * style: move conditional compiles one step outside
234 lines
7.9 KiB
C#
234 lines
7.9 KiB
C#
#if MA_VRCSDK3_AVATARS
|
|
|
|
using System.Collections.Generic;
|
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
|
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEngine;
|
|
using VRC.SDK3.Avatars.Components;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace nadena.dev.modular_avatar.core.editor
|
|
{
|
|
/**
|
|
* Ensures that any blendshapes marked for syncing by BlendshapeSync propagate values in all animation clips.
|
|
*
|
|
* Note that we only look at the FX layer, as any other layer won't work properly with mirror reflections anyway.
|
|
*/
|
|
internal class BlendshapeSyncAnimationProcessor
|
|
{
|
|
private BuildContext _context;
|
|
private Dictionary<Motion, Motion> _motionCache;
|
|
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
|
|
|
|
private struct SummaryBinding
|
|
{
|
|
private const string PREFIX = "blendShape.";
|
|
public string path;
|
|
public string propertyName;
|
|
|
|
public SummaryBinding(string path, string blendShape)
|
|
{
|
|
this.path = path;
|
|
this.propertyName = PREFIX + blendShape;
|
|
}
|
|
|
|
public static SummaryBinding FromEditorBinding(EditorCurveBinding binding)
|
|
{
|
|
if (binding.type != typeof(SkinnedMeshRenderer) || !binding.propertyName.StartsWith(PREFIX))
|
|
{
|
|
return new SummaryBinding();
|
|
}
|
|
|
|
return new SummaryBinding(binding.path, binding.propertyName.Substring(PREFIX.Length));
|
|
}
|
|
}
|
|
|
|
public void OnPreprocessAvatar(BuildContext context)
|
|
{
|
|
_context = context;
|
|
var avatarGameObject = context.AvatarRootObject;
|
|
var animDb = _context.AnimationDatabase;
|
|
|
|
var avatarDescriptor = context.AvatarDescriptor;
|
|
_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 =>
|
|
{
|
|
if (clip.CurrentClip is AnimationClip anim)
|
|
{
|
|
BuildReport.ReportingObject(clip.CurrentClip,
|
|
() => { clip.CurrentClip = TransformMotion(anim); });
|
|
}
|
|
});
|
|
}
|
|
|
|
private void ProcessComponent(GameObject avatarGameObject, ModularAvatarBlendshapeSync component)
|
|
{
|
|
var targetObj = RuntimeUtil.RelativePath(avatarGameObject, component.gameObject);
|
|
|
|
foreach (var binding in component.Bindings)
|
|
{
|
|
var refObj = binding.ReferenceMesh.Get(component);
|
|
if (refObj == null) continue;
|
|
var refSmr = refObj.GetComponent<SkinnedMeshRenderer>();
|
|
if (refSmr == null) continue;
|
|
|
|
var refPath = RuntimeUtil.RelativePath(avatarGameObject, refObj);
|
|
|
|
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
|
|
|
|
if (!_bindingMappings.TryGetValue(srcBinding, out var dstBindings))
|
|
{
|
|
dstBindings = new List<SummaryBinding>();
|
|
_bindingMappings[srcBinding] = dstBindings;
|
|
}
|
|
|
|
var targetBlendshapeName = string.IsNullOrWhiteSpace(binding.LocalBlendshape)
|
|
? binding.Blendshape
|
|
: binding.LocalBlendshape;
|
|
|
|
dstBindings.Add(new SummaryBinding(targetObj, targetBlendshapeName));
|
|
}
|
|
}
|
|
|
|
Motion TransformMotion(Motion motion)
|
|
{
|
|
if (motion == null) return null;
|
|
if (_motionCache.TryGetValue(motion, out var cached)) return cached;
|
|
|
|
switch (motion)
|
|
{
|
|
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))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (clip == origClip)
|
|
{
|
|
clip = Object.Instantiate(clip);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif |