mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 18:55:06 +08:00
225 lines
7.8 KiB
C#
225 lines
7.8 KiB
C#
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Linq;
|
|||
|
using UnityEditor;
|
|||
|
using UnityEditor.Animations;
|
|||
|
using UnityEngine;
|
|||
|
using VRC.SDK3.Avatars.Components;
|
|||
|
using Object = UnityEngine.Object;
|
|||
|
|
|||
|
namespace net.fushizen.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 Object _container;
|
|||
|
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(GameObject avatar)
|
|||
|
{
|
|||
|
var avatarDescriptor = avatar.GetComponent<VRCAvatarDescriptor>();
|
|||
|
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
|
|||
|
_motionCache = new Dictionary<Motion, Motion>();
|
|||
|
|
|||
|
var components = avatarDescriptor.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)
|
|||
|
{
|
|||
|
var targetObj = RuntimeUtil.RelativePath(avatarDescriptor.gameObject, 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(avatarDescriptor.gameObject, refObj);
|
|||
|
|
|||
|
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
|
|||
|
|
|||
|
if (!_bindingMappings.TryGetValue(srcBinding, out var dstBindings))
|
|||
|
{
|
|||
|
dstBindings = new List<SummaryBinding>();
|
|||
|
_bindingMappings[srcBinding] = dstBindings;
|
|||
|
}
|
|||
|
|
|||
|
dstBindings.Add(new SummaryBinding(targetObj, binding.Blendshape));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Ensure we have a unique copy of the controller.
|
|||
|
if (!Util.IsTemporaryAsset(controller))
|
|||
|
{
|
|||
|
controller = Util.DeepCloneAnimator(controller);
|
|||
|
AssetDatabase.CreateAsset(controller, Util.GenerateAssetPath());
|
|||
|
layers[fxIndex].animatorController = controller;
|
|||
|
avatarDescriptor.baseAnimationLayers = layers;
|
|||
|
}
|
|||
|
|
|||
|
_container = controller;
|
|||
|
|
|||
|
// Walk and transform all clips
|
|||
|
foreach (var state in AllStates(controller))
|
|||
|
{
|
|||
|
state.motion = TransformMotion(state.motion);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
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);
|
|||
|
AssetDatabase.AddObjectToAsset(newTree, _container);
|
|||
|
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);
|
|||
|
AssetDatabase.AddObjectToAsset(clip, _container);
|
|||
|
}
|
|||
|
|
|||
|
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);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|