Add support for mapping blendshapes with different names in source and target

Closes: #70
This commit is contained in:
bd_ 2022-11-07 19:37:20 -08:00
parent c634956569
commit bd9e3711ea
6 changed files with 202 additions and 20 deletions

View File

@ -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<Mesh, string[]> blendshapeNames = new Dictionary<Mesh, string[]>();
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<SkinnedMeshRenderer>()
?.sharedMesh;
DrawBlendshapePopup(sourceMesh, baseShapeNameRect, sourceBlendshape);
var localMesh =
(targets.Length == 1 ? target as ModularAvatarBlendshapeSync : null)?
.GetComponent<SkinnedMeshRenderer>()
?.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<GUIStyleState> 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();
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 775469f355c64ef2b3e3f5de10e2cde4
timeCreated: 1667876954

View File

@ -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"
}

View File

@ -41,5 +41,8 @@
"fpvisible.normal": "このオブジェクトは一人視点で表示されます。",
"fpvisible.quest": "このコンポーネントはクエスト単体非対応のため無効化となっています。",
"fpvisible.NotUnderHead": "このコンポーネントはヘッドボーン外では効果がありません。",
"fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneに制御されているため、一人視点で表示できません。PhysBoneの始点を指定してください。"
"fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneに制御されているため、一人視点で表示できません。PhysBoneの始点を指定してください。",
"blendshape.mesh": "メッシュ",
"blendshape.source": "元メッシュのブレンドシェープ",
"blendshape.target": "このメッシュのブレンドシェープ"
}

View File

@ -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;