diff --git a/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs b/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs index f68e8be8..91729b88 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/AvatarProcessor.cs @@ -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); diff --git a/Packages/net.fushizen.modular-avatar/Editor/BlendshapeSyncAnimationProcessor.cs b/Packages/net.fushizen.modular-avatar/Editor/BlendshapeSyncAnimationProcessor.cs new file mode 100644 index 00000000..35c6a7d3 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/BlendshapeSyncAnimationProcessor.cs @@ -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 _motionCache; + private Dictionary> _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(); + _bindingMappings = new Dictionary>(); + _motionCache = new Dictionary(); + + var components = avatarDescriptor.GetComponentsInChildren(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(); + 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(); + _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 AllStates(AnimatorController controller) + { + HashSet visitedStateMachines = new HashSet(); + Queue stateMachines = new Queue(); + + 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); + } + } + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/BlendshapeSyncAnimationProcessor.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/BlendshapeSyncAnimationProcessor.cs.meta new file mode 100644 index 00000000..2b14c73b --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/BlendshapeSyncAnimationProcessor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9af2c505aa6d417b9964078edd71131f +timeCreated: 1666226691 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs index 6744619c..19926db0 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs @@ -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; } } } diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs new file mode 100644 index 00000000..8d5da1be --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs @@ -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(); + _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); + } + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs.meta new file mode 100644 index 00000000..ac93420f --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e8771528af9b49509738c0939b1399fa +timeCreated: 1666232014 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/ShapekeySelectTreeView.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/ShapekeySelectTreeView.cs new file mode 100644 index 00000000..2c6dfa42 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/ShapekeySelectTreeView.cs @@ -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 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 _candidateBindings; + + internal Action 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(); + _candidateBindings.Add(null); + + var allItems = new List(); + + int createdDepth = 0; + List ObjectDisplayNames = new List(); + + WalkTree(_avatarRoot, allItems, ObjectDisplayNames, ref createdDepth); + + SetupParentsAndChildrenFromDepths(root, allItems); + + return root; + } + + private void WalkTree(GameObject node, List items, List objectDisplayNames, + ref int createdDepth) + { + objectDisplayNames.Add(node.name); + + var smr = node.GetComponent(); + 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 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 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--; + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/ShapekeySelectTreeView.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector/ShapekeySelectTreeView.cs.meta new file mode 100644 index 00000000..5fede14a --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/ShapekeySelectTreeView.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cde70f5d28c74132b955a5819545e264 +timeCreated: 1666231105 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/ZeroIndentScope.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/ZeroIndentScope.cs new file mode 100644 index 00000000..b10dfc20 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/ZeroIndentScope.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/ZeroIndentScope.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector/ZeroIndentScope.cs.meta new file mode 100644 index 00000000..20aa9304 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/ZeroIndentScope.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 556e4b16293c47ae8080a26c5206eb9e +timeCreated: 1666232888 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs b/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs index f0f5208c..051b328c 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/RenameParametersHook.cs @@ -205,9 +205,7 @@ namespace net.fushizen.modular_avatar.core.editor var queue = new Queue(); // 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++) diff --git a/Packages/net.fushizen.modular-avatar/Editor/Util.cs b/Packages/net.fushizen.modular-avatar/Editor/Util.cs index 1ea605c1..9712454f 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/Util.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/Util.cs @@ -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() + "/"); + } } } \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs b/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs index 2aa21551..837fc6ec 100644 --- a/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs +++ b/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs @@ -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); + } } } \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBlendshapeSync.cs b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBlendshapeSync.cs new file mode 100644 index 00000000..65c3fb22 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBlendshapeSync.cs @@ -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 Bindings = new List(); + + struct EditorBlendshapeBinding + { + public SkinnedMeshRenderer TargetMesh; + public int RemoteBlendshapeIndex; + public int LocalBlendshapeIndex; + } + + private List _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(); + + var localRenderer = GetComponent(); + 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(); + 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(); + 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); + } + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBlendshapeSync.cs.meta b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBlendshapeSync.cs.meta new file mode 100644 index 00000000..be0aee09 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBlendshapeSync.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6fd7cab7d93b403280f2f9da978d8a4f +timeCreated: 1666226053 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBoneProxy.cs b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBoneProxy.cs index ca7ca00d..d763b19d 100644 --- a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBoneProxy.cs +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBoneProxy.cs @@ -29,6 +29,7 @@ using UnityEngine; namespace net.fushizen.modular_avatar.core { [ExecuteInEditMode] + [DisallowMultipleComponent] public class ModularAvatarBoneProxy : AvatarTagComponent { private Transform _targetCache; diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMergeArmature.cs b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMergeArmature.cs index 940e007c..862446bb 100644 --- a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMergeArmature.cs +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMergeArmature.cs @@ -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; diff --git a/Packages/net.fushizen.modular-avatar/Runtime/RuntimeUtil.cs b/Packages/net.fushizen.modular-avatar/Runtime/RuntimeUtil.cs index ae60f04d..49bdf41d 100644 --- a/Packages/net.fushizen.modular-avatar/Runtime/RuntimeUtil.cs +++ b/Packages/net.fushizen.modular-avatar/Runtime/RuntimeUtil.cs @@ -35,11 +35,9 @@ namespace net.fushizen.modular_avatar.core { public static class RuntimeUtil { - public delegate void NullCallback(); - // Initialized in Util - public static Action delayCall = (_) => { }; - public static event NullCallback OnHierarchyChanged; + public static Action delayCall = (_) => { }; + public static event Action OnHierarchyChanged; public enum OnDemandSource {