mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-01 12:15:05 +08:00
Add blendshape sync component
This commit is contained in:
parent
7e2534ebe3
commit
232d0c43bc
@ -114,6 +114,7 @@ namespace net.fushizen.modular_avatar.core.editor
|
|||||||
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject);
|
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject);
|
||||||
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
|
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
|
||||||
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject);
|
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject);
|
||||||
|
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject);
|
||||||
|
|
||||||
AfterProcessing?.Invoke(avatarGameObject);
|
AfterProcessing?.Invoke(avatarGameObject);
|
||||||
|
|
||||||
|
@ -0,0 +1,225 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9af2c505aa6d417b9964078edd71131f
|
||||||
|
timeCreated: 1666226691
|
@ -18,14 +18,16 @@ namespace net.fushizen.modular_avatar.core.editor
|
|||||||
|
|
||||||
position = EditorGUI.PrefixLabel(position, label);
|
position = EditorGUI.PrefixLabel(position, label);
|
||||||
|
|
||||||
EditorGUI.LabelField(position,
|
using (var scope = new ZeroIndentScope())
|
||||||
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
|
{
|
||||||
|
EditorGUI.LabelField(position,
|
||||||
|
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CustomGUI(Rect position, SerializedProperty property, GUIContent label)
|
private bool CustomGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||||
{
|
{
|
||||||
var indentLevel = EditorGUI.indentLevel;
|
|
||||||
var color = GUI.contentColor;
|
var color = GUI.contentColor;
|
||||||
|
|
||||||
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
|
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
|
||||||
@ -56,73 +58,75 @@ namespace net.fushizen.modular_avatar.core.editor
|
|||||||
|
|
||||||
var nullContent = GUIContent.none;
|
var nullContent = GUIContent.none;
|
||||||
|
|
||||||
if (target != null || isNull)
|
using (var scope = new ZeroIndentScope())
|
||||||
{
|
{
|
||||||
EditorGUI.BeginChangeCheck();
|
if (target != null || isNull)
|
||||||
var newTarget = EditorGUI.ObjectField(position, nullContent, target, typeof(Transform), true);
|
|
||||||
if (EditorGUI.EndChangeCheck())
|
|
||||||
{
|
{
|
||||||
if (newTarget == null)
|
EditorGUI.BeginChangeCheck();
|
||||||
|
var newTarget = EditorGUI.ObjectField(position, nullContent, target, typeof(Transform), true);
|
||||||
|
if (EditorGUI.EndChangeCheck())
|
||||||
{
|
{
|
||||||
property.stringValue = "";
|
if (newTarget == null)
|
||||||
}
|
{
|
||||||
else if (newTarget == avatar.transform)
|
property.stringValue = "";
|
||||||
{
|
}
|
||||||
property.stringValue = AvatarObjectReference.AVATAR_ROOT;
|
else if (newTarget == avatar.transform)
|
||||||
}
|
{
|
||||||
else
|
property.stringValue = AvatarObjectReference.AVATAR_ROOT;
|
||||||
{
|
}
|
||||||
var relPath =
|
else
|
||||||
RuntimeUtil.RelativePath(avatar.gameObject, ((Transform) newTarget).gameObject);
|
{
|
||||||
if (relPath == null) return true;
|
var relPath =
|
||||||
|
RuntimeUtil.RelativePath(avatar.gameObject, ((Transform) newTarget).gameObject);
|
||||||
|
if (relPath == null) return true;
|
||||||
|
|
||||||
property.stringValue = relPath;
|
property.stringValue = relPath;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// For some reason, this color change retroactively affects the prefix label above, so draw our own
|
|
||||||
// label as well (we still want the prefix label for highlights, etc).
|
|
||||||
EditorGUI.LabelField(labelRect, label);
|
|
||||||
|
|
||||||
GUI.contentColor = new Color(0, 0, 0, 0);
|
|
||||||
EditorGUI.BeginChangeCheck();
|
|
||||||
var newTarget = EditorGUI.ObjectField(position, nullContent, target, typeof(Transform), true);
|
|
||||||
GUI.contentColor = color;
|
|
||||||
|
|
||||||
if (EditorGUI.EndChangeCheck())
|
|
||||||
{
|
|
||||||
if (newTarget == null)
|
|
||||||
{
|
|
||||||
property.stringValue = "";
|
|
||||||
}
|
|
||||||
else if (newTarget == avatar.transform)
|
|
||||||
{
|
|
||||||
property.stringValue = AvatarObjectReference.AVATAR_ROOT;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var relPath =
|
|
||||||
RuntimeUtil.RelativePath(avatar.gameObject, ((Transform) newTarget).gameObject);
|
|
||||||
if (relPath == null) return true;
|
|
||||||
|
|
||||||
property.stringValue = relPath;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
GUI.contentColor = Color.red;
|
// For some reason, this color change retroactively affects the prefix label above, so draw our own
|
||||||
EditorGUI.LabelField(position, property.stringValue);
|
// label as well (we still want the prefix label for highlights, etc).
|
||||||
}
|
EditorGUI.LabelField(labelRect, label);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
GUI.contentColor = new Color(0, 0, 0, 0);
|
||||||
|
EditorGUI.BeginChangeCheck();
|
||||||
|
var newTarget = EditorGUI.ObjectField(position, nullContent, target, typeof(Transform), true);
|
||||||
|
GUI.contentColor = color;
|
||||||
|
|
||||||
|
if (EditorGUI.EndChangeCheck())
|
||||||
|
{
|
||||||
|
if (newTarget == null)
|
||||||
|
{
|
||||||
|
property.stringValue = "";
|
||||||
|
}
|
||||||
|
else if (newTarget == avatar.transform)
|
||||||
|
{
|
||||||
|
property.stringValue = AvatarObjectReference.AVATAR_ROOT;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var relPath =
|
||||||
|
RuntimeUtil.RelativePath(avatar.gameObject, ((Transform) newTarget).gameObject);
|
||||||
|
if (relPath == null) return true;
|
||||||
|
|
||||||
|
property.stringValue = relPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GUI.contentColor = Color.red;
|
||||||
|
EditorGUI.LabelField(position, property.stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
GUI.contentColor = color;
|
GUI.contentColor = color;
|
||||||
EditorGUI.indentLevel = indentLevel;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace net.fushizen.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
[CustomEditor(typeof(ModularAvatarBlendshapeSync))]
|
||||||
|
internal class BlendshapeSyncEditor : Editor
|
||||||
|
{
|
||||||
|
private BlendshapeSelectWindow _window;
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
base.OnInspectorGUI();
|
||||||
|
|
||||||
|
if (GUILayout.Button("Add blendshape"))
|
||||||
|
{
|
||||||
|
if (_window != null) DestroyImmediate(_window);
|
||||||
|
_window = ScriptableObject.CreateInstance<BlendshapeSelectWindow>();
|
||||||
|
_window.AvatarRoot = RuntimeUtil.FindAvatarInParents(((ModularAvatarBlendshapeSync) target).transform)
|
||||||
|
.gameObject;
|
||||||
|
_window.OfferBinding += OfferBinding;
|
||||||
|
_window.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OfferBinding(BlendshapeBinding binding)
|
||||||
|
{
|
||||||
|
foreach (var obj in targets)
|
||||||
|
{
|
||||||
|
var sync = (ModularAvatarBlendshapeSync) obj;
|
||||||
|
Undo.RecordObject(sync, "Adding blendshape binding");
|
||||||
|
if (!sync.Bindings.Contains(binding)) sync.Bindings.Add(binding);
|
||||||
|
EditorUtility.SetDirty(sync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e8771528af9b49509738c0939b1399fa
|
||||||
|
timeCreated: 1666232014
|
@ -0,0 +1,149 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.Animations;
|
||||||
|
using UnityEditor.IMGUI.Controls;
|
||||||
|
using UnityEditor.PackageManager.UI;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace net.fushizen.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
public class BlendshapeSelectWindow : EditorWindow
|
||||||
|
{
|
||||||
|
internal GameObject AvatarRoot;
|
||||||
|
private BlendshapeTree _tree;
|
||||||
|
|
||||||
|
internal Action<BlendshapeBinding> OfferBinding;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
titleContent = new GUIContent("Select blendshapes");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
if (_tree == null)
|
||||||
|
{
|
||||||
|
_tree = new BlendshapeTree(AvatarRoot, new TreeViewState());
|
||||||
|
_tree.OfferBinding = (binding) => OfferBinding?.Invoke(binding);
|
||||||
|
_tree.Reload();
|
||||||
|
|
||||||
|
_tree.SetExpanded(0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tree.OnGUI(new Rect(0, 0, position.width, position.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class BlendshapeTree : TreeView
|
||||||
|
{
|
||||||
|
private readonly GameObject _avatarRoot;
|
||||||
|
private List<BlendshapeBinding?> _candidateBindings;
|
||||||
|
|
||||||
|
internal Action<BlendshapeBinding> OfferBinding;
|
||||||
|
|
||||||
|
public BlendshapeTree(GameObject avatarRoot, TreeViewState state) : base(state)
|
||||||
|
{
|
||||||
|
this._avatarRoot = avatarRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlendshapeTree(GameObject avatarRoot, TreeViewState state, MultiColumnHeader multiColumnHeader) : base(
|
||||||
|
state, multiColumnHeader)
|
||||||
|
{
|
||||||
|
this._avatarRoot = avatarRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DoubleClickedItem(int id)
|
||||||
|
{
|
||||||
|
var binding = _candidateBindings[id];
|
||||||
|
if (binding.HasValue)
|
||||||
|
{
|
||||||
|
OfferBinding.Invoke(binding.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TreeViewItem BuildRoot()
|
||||||
|
{
|
||||||
|
var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"};
|
||||||
|
_candidateBindings = new List<BlendshapeBinding?>();
|
||||||
|
_candidateBindings.Add(null);
|
||||||
|
|
||||||
|
var allItems = new List<TreeViewItem>();
|
||||||
|
|
||||||
|
int createdDepth = 0;
|
||||||
|
List<string> ObjectDisplayNames = new List<string>();
|
||||||
|
|
||||||
|
WalkTree(_avatarRoot, allItems, ObjectDisplayNames, ref createdDepth);
|
||||||
|
|
||||||
|
SetupParentsAndChildrenFromDepths(root, allItems);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WalkTree(GameObject node, List<TreeViewItem> items, List<string> objectDisplayNames,
|
||||||
|
ref int createdDepth)
|
||||||
|
{
|
||||||
|
objectDisplayNames.Add(node.name);
|
||||||
|
|
||||||
|
var smr = node.GetComponent<SkinnedMeshRenderer>();
|
||||||
|
if (smr != null && smr.sharedMesh != null && smr.sharedMesh.blendShapeCount > 0)
|
||||||
|
{
|
||||||
|
while (createdDepth < objectDisplayNames.Count)
|
||||||
|
{
|
||||||
|
items.Add(new TreeViewItem
|
||||||
|
{
|
||||||
|
id = _candidateBindings.Count, depth = createdDepth,
|
||||||
|
displayName = objectDisplayNames[createdDepth]
|
||||||
|
});
|
||||||
|
_candidateBindings.Add(null);
|
||||||
|
createdDepth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateBlendshapes(smr, items, ref createdDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Transform child in node.transform)
|
||||||
|
{
|
||||||
|
WalkTree(child.gameObject, items, objectDisplayNames, ref createdDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
objectDisplayNames.RemoveAt(objectDisplayNames.Count - 1);
|
||||||
|
createdDepth = Math.Min(createdDepth, objectDisplayNames.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateBlendshapes(SkinnedMeshRenderer smr, List<TreeViewItem> items, ref int createdDepth)
|
||||||
|
{
|
||||||
|
items.Add(new TreeViewItem
|
||||||
|
{id = _candidateBindings.Count, depth = createdDepth, displayName = "BlendShapes"});
|
||||||
|
_candidateBindings.Add(null);
|
||||||
|
createdDepth++;
|
||||||
|
|
||||||
|
var path = RuntimeUtil.RelativePath(_avatarRoot, smr.gameObject);
|
||||||
|
var mesh = smr.sharedMesh;
|
||||||
|
List<BlendshapeBinding> bindings = Enumerable.Range(0, mesh.blendShapeCount)
|
||||||
|
.Select(n =>
|
||||||
|
{
|
||||||
|
var name = mesh.GetBlendShapeName(n);
|
||||||
|
return new BlendshapeBinding()
|
||||||
|
{
|
||||||
|
Blendshape = name,
|
||||||
|
ReferenceMesh = new AvatarObjectReference()
|
||||||
|
{
|
||||||
|
referencePath = path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var binding in bindings)
|
||||||
|
{
|
||||||
|
items.Add(new TreeViewItem
|
||||||
|
{id = _candidateBindings.Count, depth = createdDepth, displayName = binding.Blendshape});
|
||||||
|
_candidateBindings.Add(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
createdDepth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cde70f5d28c74132b955a5819545e264
|
||||||
|
timeCreated: 1666231105
|
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace net.fushizen.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
public class ZeroIndentScope : IDisposable
|
||||||
|
{
|
||||||
|
private int oldIndentLevel;
|
||||||
|
|
||||||
|
public ZeroIndentScope()
|
||||||
|
{
|
||||||
|
oldIndentLevel = EditorGUI.indentLevel;
|
||||||
|
EditorGUI.indentLevel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel = oldIndentLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 556e4b16293c47ae8080a26c5206eb9e
|
||||||
|
timeCreated: 1666232888
|
@ -205,9 +205,7 @@ namespace net.fushizen.modular_avatar.core.editor
|
|||||||
var queue = new Queue<AnimatorStateMachine>();
|
var queue = new Queue<AnimatorStateMachine>();
|
||||||
|
|
||||||
// Deep clone the animator
|
// Deep clone the animator
|
||||||
var merger = new AnimatorCombiner();
|
controller = Util.DeepCloneAnimator(controller);
|
||||||
merger.AddController("", controller, null);
|
|
||||||
controller = merger.Finish();
|
|
||||||
|
|
||||||
var parameters = controller.parameters;
|
var parameters = controller.parameters;
|
||||||
for (int i = 0; i < parameters.Length; i++)
|
for (int i = 0; i < parameters.Length; i++)
|
||||||
|
@ -107,5 +107,19 @@ namespace net.fushizen.modular_avatar.core.editor
|
|||||||
FileUtil.DeleteFileOrDirectory(subdir);
|
FileUtil.DeleteFileOrDirectory(subdir);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AnimatorController DeepCloneAnimator(AnimatorController controller)
|
||||||
|
{
|
||||||
|
var merger = new AnimatorCombiner();
|
||||||
|
merger.AddController("", controller, null);
|
||||||
|
return merger.Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsTemporaryAsset(Object obj)
|
||||||
|
{
|
||||||
|
var path = AssetDatabase.GetAssetPath(obj);
|
||||||
|
|
||||||
|
return path != null && path.StartsWith(GetGeneratedAssetsFolder() + "/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ using UnityEngine;
|
|||||||
namespace net.fushizen.modular_avatar.core
|
namespace net.fushizen.modular_avatar.core
|
||||||
{
|
{
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public struct AvatarObjectReference
|
public class AvatarObjectReference
|
||||||
{
|
{
|
||||||
public static string AVATAR_ROOT = "$$$AVATAR_ROOT$$$";
|
public static string AVATAR_ROOT = "$$$AVATAR_ROOT$$$";
|
||||||
public string referencePath;
|
public string referencePath;
|
||||||
@ -46,5 +46,23 @@ namespace net.fushizen.modular_avatar.core
|
|||||||
RuntimeUtil.OnHierarchyChanged -= InvalidateCache;
|
RuntimeUtil.OnHierarchyChanged -= InvalidateCache;
|
||||||
_cacheValid = false;
|
_cacheValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected bool Equals(AvatarObjectReference other)
|
||||||
|
{
|
||||||
|
return referencePath == other.referencePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, obj)) return false;
|
||||||
|
if (ReferenceEquals(this, obj)) return true;
|
||||||
|
if (obj.GetType() != this.GetType()) return false;
|
||||||
|
return Equals((AvatarObjectReference) obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return (referencePath != null ? referencePath.GetHashCode() : 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using VRC.PackageManagement.Core;
|
||||||
|
|
||||||
|
namespace net.fushizen.modular_avatar.core
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public struct BlendshapeBinding
|
||||||
|
{
|
||||||
|
public AvatarObjectReference ReferenceMesh;
|
||||||
|
public string Blendshape;
|
||||||
|
|
||||||
|
public bool Equals(BlendshapeBinding other)
|
||||||
|
{
|
||||||
|
return Equals(ReferenceMesh, other.ReferenceMesh) && Blendshape == other.Blendshape;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is BlendshapeBinding other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
return ((ReferenceMesh != null ? ReferenceMesh.GetHashCode() : 0) * 397) ^
|
||||||
|
(Blendshape != null ? Blendshape.GetHashCode() : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RequireComponent(typeof(SkinnedMeshRenderer))]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[ExecuteInEditMode]
|
||||||
|
public class ModularAvatarBlendshapeSync : AvatarTagComponent
|
||||||
|
{
|
||||||
|
public List<BlendshapeBinding> Bindings = new List<BlendshapeBinding>();
|
||||||
|
|
||||||
|
struct EditorBlendshapeBinding
|
||||||
|
{
|
||||||
|
public SkinnedMeshRenderer TargetMesh;
|
||||||
|
public int RemoteBlendshapeIndex;
|
||||||
|
public int LocalBlendshapeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EditorBlendshapeBinding> _editorBindings;
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
RuntimeUtil.delayCall(Rebind);
|
||||||
|
RuntimeUtil.OnHierarchyChanged -= Rebind;
|
||||||
|
RuntimeUtil.OnHierarchyChanged += Rebind;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
RuntimeUtil.OnHierarchyChanged -= Rebind;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Rebind()
|
||||||
|
{
|
||||||
|
_editorBindings = new List<EditorBlendshapeBinding>();
|
||||||
|
|
||||||
|
var localRenderer = GetComponent<SkinnedMeshRenderer>();
|
||||||
|
var localMesh = localRenderer.sharedMesh;
|
||||||
|
if (localMesh == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var binding in Bindings)
|
||||||
|
{
|
||||||
|
var obj = binding.ReferenceMesh.Get(this);
|
||||||
|
if (obj == null)
|
||||||
|
continue;
|
||||||
|
var smr = obj.GetComponent<SkinnedMeshRenderer>();
|
||||||
|
if (smr == null)
|
||||||
|
continue;
|
||||||
|
var mesh = smr.sharedMesh;
|
||||||
|
if (mesh == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var localIndex = localMesh.GetBlendShapeIndex(binding.Blendshape);
|
||||||
|
var refIndex = mesh.GetBlendShapeIndex(binding.Blendshape);
|
||||||
|
if (localIndex == -1 || refIndex == -1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_editorBindings.Add(new EditorBlendshapeBinding()
|
||||||
|
{
|
||||||
|
TargetMesh = smr,
|
||||||
|
RemoteBlendshapeIndex = refIndex,
|
||||||
|
LocalBlendshapeIndex = localIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (RuntimeUtil.isPlaying) return;
|
||||||
|
|
||||||
|
if (_editorBindings == null) return;
|
||||||
|
var localRenderer = GetComponent<SkinnedMeshRenderer>();
|
||||||
|
if (localRenderer == null) return;
|
||||||
|
foreach (var binding in _editorBindings)
|
||||||
|
{
|
||||||
|
if (binding.TargetMesh == null) return;
|
||||||
|
var weight = binding.TargetMesh.GetBlendShapeWeight(binding.RemoteBlendshapeIndex);
|
||||||
|
localRenderer.SetBlendShapeWeight(binding.LocalBlendshapeIndex, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6fd7cab7d93b403280f2f9da978d8a4f
|
||||||
|
timeCreated: 1666226053
|
@ -29,6 +29,7 @@ using UnityEngine;
|
|||||||
namespace net.fushizen.modular_avatar.core
|
namespace net.fushizen.modular_avatar.core
|
||||||
{
|
{
|
||||||
[ExecuteInEditMode]
|
[ExecuteInEditMode]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
public class ModularAvatarBoneProxy : AvatarTagComponent
|
public class ModularAvatarBoneProxy : AvatarTagComponent
|
||||||
{
|
{
|
||||||
private Transform _targetCache;
|
private Transform _targetCache;
|
||||||
|
@ -32,6 +32,7 @@ using UnityEditor;
|
|||||||
namespace net.fushizen.modular_avatar.core
|
namespace net.fushizen.modular_avatar.core
|
||||||
{
|
{
|
||||||
[ExecuteInEditMode]
|
[ExecuteInEditMode]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
public class ModularAvatarMergeArmature : AvatarTagComponent
|
public class ModularAvatarMergeArmature : AvatarTagComponent
|
||||||
{
|
{
|
||||||
private const float POS_EPSILON = 0.01f;
|
private const float POS_EPSILON = 0.01f;
|
||||||
|
@ -35,11 +35,9 @@ namespace net.fushizen.modular_avatar.core
|
|||||||
{
|
{
|
||||||
public static class RuntimeUtil
|
public static class RuntimeUtil
|
||||||
{
|
{
|
||||||
public delegate void NullCallback();
|
|
||||||
|
|
||||||
// Initialized in Util
|
// Initialized in Util
|
||||||
public static Action<NullCallback> delayCall = (_) => { };
|
public static Action<Action> delayCall = (_) => { };
|
||||||
public static event NullCallback OnHierarchyChanged;
|
public static event Action OnHierarchyChanged;
|
||||||
|
|
||||||
public enum OnDemandSource
|
public enum OnDemandSource
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user