mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 18:55:06 +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 BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
|
||||
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject);
|
||||
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(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);
|
||||
|
||||
EditorGUI.LabelField(position,
|
||||
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
|
||||
using (var scope = new ZeroIndentScope())
|
||||
{
|
||||
EditorGUI.LabelField(position,
|
||||
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CustomGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
var color = GUI.contentColor;
|
||||
|
||||
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
|
||||
@ -56,73 +58,75 @@ namespace net.fushizen.modular_avatar.core.editor
|
||||
|
||||
var nullContent = GUIContent.none;
|
||||
|
||||
if (target != null || isNull)
|
||||
using (var scope = new ZeroIndentScope())
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newTarget = EditorGUI.ObjectField(position, nullContent, target, typeof(Transform), true);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
if (target != null || isNull)
|
||||
{
|
||||
if (newTarget == null)
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newTarget = EditorGUI.ObjectField(position, nullContent, target, typeof(Transform), true);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
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;
|
||||
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
|
||||
{
|
||||
// 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;
|
||||
property.stringValue = relPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.contentColor = Color.red;
|
||||
EditorGUI.LabelField(position, property.stringValue);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
|
||||
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
|
||||
{
|
||||
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>();
|
||||
|
||||
// Deep clone the animator
|
||||
var merger = new AnimatorCombiner();
|
||||
merger.AddController("", controller, null);
|
||||
controller = merger.Finish();
|
||||
controller = Util.DeepCloneAnimator(controller);
|
||||
|
||||
var parameters = controller.parameters;
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
|
@ -107,5 +107,19 @@ namespace net.fushizen.modular_avatar.core.editor
|
||||
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
|
||||
{
|
||||
[Serializable]
|
||||
public struct AvatarObjectReference
|
||||
public class AvatarObjectReference
|
||||
{
|
||||
public static string AVATAR_ROOT = "$$$AVATAR_ROOT$$$";
|
||||
public string referencePath;
|
||||
@ -46,5 +46,23 @@ namespace net.fushizen.modular_avatar.core
|
||||
RuntimeUtil.OnHierarchyChanged -= InvalidateCache;
|
||||
_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
|
||||
{
|
||||
[ExecuteInEditMode]
|
||||
[DisallowMultipleComponent]
|
||||
public class ModularAvatarBoneProxy : AvatarTagComponent
|
||||
{
|
||||
private Transform _targetCache;
|
||||
|
@ -32,6 +32,7 @@ using UnityEditor;
|
||||
namespace net.fushizen.modular_avatar.core
|
||||
{
|
||||
[ExecuteInEditMode]
|
||||
[DisallowMultipleComponent]
|
||||
public class ModularAvatarMergeArmature : AvatarTagComponent
|
||||
{
|
||||
private const float POS_EPSILON = 0.01f;
|
||||
|
@ -35,11 +35,9 @@ namespace net.fushizen.modular_avatar.core
|
||||
{
|
||||
public static class RuntimeUtil
|
||||
{
|
||||
public delegate void NullCallback();
|
||||
|
||||
// Initialized in Util
|
||||
public static Action<NullCallback> delayCall = (_) => { };
|
||||
public static event NullCallback OnHierarchyChanged;
|
||||
public static Action<Action> delayCall = (_) => { };
|
||||
public static event Action OnHierarchyChanged;
|
||||
|
||||
public enum OnDemandSource
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user