2022-10-20 10:42:33 +08:00
|
|
|
|
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;
|
|
|
|
|
|
2022-11-11 12:39:58 +08:00
|
|
|
|
namespace nadena.dev.modular_avatar.core.editor
|
2022-10-20 10:42:33 +08:00
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-31 17:40:57 +08:00
|
|
|
|
var targetBlendshapeName = string.IsNullOrWhiteSpace(binding.LocalBlendshape)
|
|
|
|
|
? binding.Blendshape
|
|
|
|
|
: binding.LocalBlendshape;
|
|
|
|
|
|
|
|
|
|
dstBindings.Add(new SummaryBinding(targetObj, targetBlendshapeName));
|
2022-10-20 10:42:33 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure we have a unique copy of the controller.
|
|
|
|
|
if (!Util.IsTemporaryAsset(controller))
|
|
|
|
|
{
|
|
|
|
|
controller = Util.DeepCloneAnimator(controller);
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|