diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs index 195139ec..4b31e220 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using UnityEditor; using UnityEditorInternal; using UnityEngine; using UnityEngine.PlayerLoop; +using VRC.Core; +using static net.fushizen.modular_avatar.core.editor.Localization; namespace net.fushizen.modular_avatar.core.editor { @@ -17,6 +21,8 @@ namespace net.fushizen.modular_avatar.core.editor private ReorderableList _list; private SerializedProperty _bindings; + private Dictionary blendshapeNames = new Dictionary(); + static BlendshapeSyncEditor() { f_m_SerializedObject = @@ -59,17 +65,43 @@ namespace net.fushizen.modular_avatar.core.editor _list.elementHeight += 2; } + private float elementWidth = 0; + + private void ComputeRects( + Rect rect, + out Rect meshFieldRect, + out Rect baseShapeNameRect, + out Rect targetShapeNameRect + ) + { + if (elementWidth > 1 && elementWidth < rect.width) + { + rect.x += rect.width - elementWidth; + rect.width = elementWidth; + } + + meshFieldRect = rect; + meshFieldRect.width /= 3; + + baseShapeNameRect = rect; + baseShapeNameRect.width /= 3; + baseShapeNameRect.x = meshFieldRect.x + meshFieldRect.width; + + targetShapeNameRect = rect; + targetShapeNameRect.width /= 3; + targetShapeNameRect.x = baseShapeNameRect.x + baseShapeNameRect.width; + + meshFieldRect.width -= 12; + baseShapeNameRect.width -= 12; + } + private void DrawHeader(Rect rect) { - var leftHalf = rect; - leftHalf.width /= 2; + ComputeRects(rect, out var meshFieldRect, out var baseShapeNameRect, out var targetShapeNameRect); - var rightHalf = rect; - rightHalf.width /= 2; - rightHalf.x += rightHalf.width; - - EditorGUI.LabelField(leftHalf, "Mesh"); - EditorGUI.LabelField(rightHalf, "Blendshape"); + EditorGUI.LabelField(meshFieldRect, G("blendshape.mesh")); + EditorGUI.LabelField(baseShapeNameRect, G("blendshape.source")); + EditorGUI.LabelField(targetShapeNameRect, G("blendshape.target")); } private void DrawElement(Rect rect, int index, bool isactive, bool isfocused) @@ -77,25 +109,139 @@ namespace net.fushizen.modular_avatar.core.editor rect.height -= 2; rect.y += 1; - var leftHalf = rect; - leftHalf.width /= 2; - leftHalf.width -= 12; + if (Math.Abs(elementWidth - rect.width) > 0.5f && rect.width > 1) + { + elementWidth = rect.width; + Repaint(); + } - var rightHalf = rect; - rightHalf.width /= 2; - rightHalf.x += rightHalf.width; + ComputeRects(rect, out var meshFieldRect, out var baseShapeNameRect, out var targetShapeNameRect); var item = _bindings.GetArrayElementAtIndex(index); var mesh = item.FindPropertyRelative(nameof(BlendshapeBinding.ReferenceMesh)); - var blendshape = item.FindPropertyRelative(nameof(BlendshapeBinding.Blendshape)); + var sourceBlendshape = item.FindPropertyRelative(nameof(BlendshapeBinding.Blendshape)); + var localBlendshape = item.FindPropertyRelative(nameof(BlendshapeBinding.LocalBlendshape)); using (var scope = new ZeroIndentScope()) { - EditorGUI.PropertyField(leftHalf, mesh, GUIContent.none); - EditorGUI.PropertyField(rightHalf, blendshape, GUIContent.none); + EditorGUI.PropertyField(meshFieldRect, mesh, GUIContent.none); + + var sourceMesh = + (targets.Length == 1 ? target as ModularAvatarBlendshapeSync : null)?.Bindings[index] + .ReferenceMesh.Get((Component) target) + ?.GetComponent() + ?.sharedMesh; + DrawBlendshapePopup(sourceMesh, baseShapeNameRect, sourceBlendshape); + + var localMesh = + (targets.Length == 1 ? target as ModularAvatarBlendshapeSync : null)? + .GetComponent() + ?.sharedMesh; + + DrawBlendshapePopup(localMesh, targetShapeNameRect, localBlendshape, sourceBlendshape.stringValue); } } + private void DrawBlendshapePopup(Mesh targetMesh, Rect rect, SerializedProperty prop, + string defaultValue = null) + { + var style = new GUIStyle(EditorStyles.popup); + + style.fixedHeight = rect.height; + + if (targetMesh == null) + { + EditorGUI.PropertyField(rect, prop, GUIContent.none); + } + else + { + string[] selections = GetBlendshapeNames(targetMesh); + + int shapeIndex = Array.FindIndex(selections, s => s == prop.stringValue); + + EditorGUI.BeginChangeCheck(); + int newShapeIndex = EditorGUI.Popup(rect, shapeIndex, selections, style); + if (EditorGUI.EndChangeCheck()) + { + prop.stringValue = selections[newShapeIndex]; + } + else if (shapeIndex < 0) + { + var toDisplay = prop.stringValue; + bool colorRed = true; + + if (string.IsNullOrEmpty(toDisplay) && defaultValue != null) + { + toDisplay = defaultValue; + colorRed = Array.FindIndex(selections, s => s == toDisplay) < 0; + } + + if (!colorRed) + { + UpdateAllStates(style, s => s.textColor = Color.Lerp(s.textColor, Color.clear, 0.2f)); + style.fontStyle = FontStyle.Italic; + } + else + { + UpdateAllStates(style, s => s.textColor = Color.Lerp(s.textColor, Color.red, 0.85f)); + } + + GUI.Label(rect, toDisplay, style); + } + } + } + + private static void UpdateAllStates(GUIStyle style, Action action) + { + var state = style.normal; + action(state); + style.normal = state; + + state = style.hover; + action(state); + style.hover = state; + + state = style.active; + action(state); + style.active = state; + + state = style.focused; + action(state); + style.focused = state; + + state = style.onNormal; + action(state); + style.onNormal = state; + + state = style.onHover; + action(state); + style.onHover = state; + + state = style.onActive; + action(state); + style.onActive = state; + + state = style.onFocused; + action(state); + style.onFocused = state; + } + + private string[] GetBlendshapeNames(Mesh targetMesh) + { + if (!blendshapeNames.TryGetValue(targetMesh, out var selections)) + { + selections = new string[targetMesh.blendShapeCount]; + for (int i = 0; i < targetMesh.blendShapeCount; i++) + { + selections[i] = targetMesh.GetBlendShapeName(i); + } + + blendshapeNames[targetMesh] = selections; + } + + return selections; + } + public override void OnInspectorGUI() { LogoDisplay.DisplayLogo(); @@ -104,6 +250,8 @@ namespace net.fushizen.modular_avatar.core.editor _list.DoLayoutList(); + ShowLanguageUI(); + serializedObject.ApplyModifiedProperties(); } diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/GUIColorScope.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/GUIColorScope.cs new file mode 100644 index 00000000..4bf93df1 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/GUIColorScope.cs @@ -0,0 +1,21 @@ +using System; +using UnityEngine; + +namespace net.fushizen.modular_avatar.core.editor +{ + public class GUIColorScope : IDisposable + { + private readonly Color _oldColor; + + public GUIColorScope(Color color) + { + _oldColor = GUI.color; + GUI.color = color; + } + + public void Dispose() + { + GUI.color = _oldColor; + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/GUIColorScope.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector/GUIColorScope.cs.meta new file mode 100644 index 00000000..41b05a82 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/GUIColorScope.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 775469f355c64ef2b3e3f5de10e2cde4 +timeCreated: 1667876954 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Localization/en.json b/Packages/net.fushizen.modular-avatar/Editor/Localization/en.json index 3ca34bdf..500e308b 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/Localization/en.json +++ b/Packages/net.fushizen.modular-avatar/Editor/Localization/en.json @@ -41,5 +41,8 @@ "fpvisible.normal": "This object will be visible in your first person view.", "fpvisible.NotUnderHead": "This component has no effect when not placed under the head bone.", "fpvisible.quest": "This component is not compatible with the standalone Oculus Quest and will have no effect.", - "fpvisible.InPhysBoneChain": "This object is controlled by a Physics Bone chain and cannot be made visible in first person safely. Select the start of the chain instead." + "fpvisible.InPhysBoneChain": "This object is controlled by a Physics Bone chain and cannot be made visible in first person safely. Select the start of the chain instead.", + "blendshape.mesh": "Mesh", + "blendshape.source": "Source blendshape", + "blendshape.target": "Target blendshape" } \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Localization/ja.json b/Packages/net.fushizen.modular-avatar/Editor/Localization/ja.json index f801520f..4337c1da 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/Localization/ja.json +++ b/Packages/net.fushizen.modular-avatar/Editor/Localization/ja.json @@ -41,5 +41,8 @@ "fpvisible.normal": "このオブジェクトは一人視点で表示されます。", "fpvisible.quest": "このコンポーネントはクエスト単体非対応のため無効化となっています。", "fpvisible.NotUnderHead": "このコンポーネントはヘッドボーン外では効果がありません。", - "fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneに制御されているため、一人視点で表示できません。PhysBoneの始点を指定してください。" + "fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneに制御されているため、一人視点で表示できません。PhysBoneの始点を指定してください。", + "blendshape.mesh": "メッシュ", + "blendshape.source": "元メッシュのブレンドシェープ", + "blendshape.target": "このメッシュのブレンドシェープ" } \ 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 index 410550ad..bdf46424 100644 --- a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBlendshapeSync.cs +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarBlendshapeSync.cs @@ -9,6 +9,7 @@ namespace net.fushizen.modular_avatar.core { public AvatarObjectReference ReferenceMesh; public string Blendshape; + public string LocalBlendshape; public bool Equals(BlendshapeBinding other) { @@ -82,7 +83,10 @@ namespace net.fushizen.modular_avatar.core if (mesh == null) continue; - var localIndex = localMesh.GetBlendShapeIndex(binding.Blendshape); + var localShape = string.IsNullOrWhiteSpace(binding.LocalBlendshape) + ? binding.Blendshape + : binding.LocalBlendshape; + var localIndex = localMesh.GetBlendShapeIndex(localShape); var refIndex = mesh.GetBlendShapeIndex(binding.Blendshape); if (localIndex == -1 || refIndex == -1) continue;