mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-02-07 06:12:47 +08:00
ui: improve setup outfit usability (#422)
Show a descriptive error when setup outfit fails (fixes #415). Use HeuristicBoneMapper to fuzzy-match hips (fixes #414)
This commit is contained in:
parent
75fe74da53
commit
b6537da650
@ -1,15 +1,119 @@
|
|||||||
using UnityEditor;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
public class EasySetupOutfit
|
internal class ESOErrorWindow : EditorWindow
|
||||||
|
{
|
||||||
|
private string header;
|
||||||
|
private string[] messageGroups;
|
||||||
|
private static readonly GUIStyle buttonStyle, labelStyle;
|
||||||
|
private const float SeparatorSize = 6f;
|
||||||
|
|
||||||
|
static ESOErrorWindow()
|
||||||
|
{
|
||||||
|
buttonStyle = EditorStyles.miniButtonRight;
|
||||||
|
labelStyle = EditorStyles.label;
|
||||||
|
labelStyle.wordWrap = true;
|
||||||
|
|
||||||
|
buttonStyle.fixedWidth = 40f;
|
||||||
|
buttonStyle.fixedHeight = EditorGUIUtility.singleLineHeight * 1.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Show(
|
||||||
|
string header,
|
||||||
|
string[] messageGroups
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var window = CreateInstance<ESOErrorWindow>();
|
||||||
|
window.titleContent = new GUIContent("Setup Outfit");
|
||||||
|
window.header = header;
|
||||||
|
window.messageGroups = messageGroups;
|
||||||
|
|
||||||
|
// Compute required window size
|
||||||
|
var height = 0f;
|
||||||
|
var width = 450f;
|
||||||
|
|
||||||
|
height += SeparatorSize;
|
||||||
|
height += EditorStyles.helpBox.CalcHeight(new GUIContent(header), width);
|
||||||
|
foreach (var message in messageGroups)
|
||||||
|
{
|
||||||
|
height += 6f; // TODO: constant
|
||||||
|
height += labelStyle.CalcHeight(new GUIContent(message), width);
|
||||||
|
}
|
||||||
|
|
||||||
|
height += buttonStyle.fixedHeight;
|
||||||
|
height += SeparatorSize;
|
||||||
|
|
||||||
|
window.minSize = new Vector2(width, height);
|
||||||
|
|
||||||
|
window.ShowModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(SeparatorSize);
|
||||||
|
|
||||||
|
EditorGUILayout.HelpBox(header, MessageType.Error);
|
||||||
|
|
||||||
|
foreach (var message in messageGroups)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(SeparatorSize);
|
||||||
|
EditorGUILayout.LabelField(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
if (GUILayout.Button("OK", buttonStyle))
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
var finalRect = GUILayoutUtility.GetRect(SeparatorSize, SeparatorSize, GUILayout.ExpandWidth(true));
|
||||||
|
|
||||||
|
var size = this.minSize;
|
||||||
|
size.y = finalRect.position.y + finalRect.height;
|
||||||
|
|
||||||
|
if (size.y > 10)
|
||||||
|
{
|
||||||
|
if (Vector2.Distance(this.minSize, size) > 1f)
|
||||||
|
{
|
||||||
|
this.minSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Vector2.Distance(this.maxSize, size) > 1f)
|
||||||
|
{
|
||||||
|
this.maxSize = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class EasySetupOutfit
|
||||||
{
|
{
|
||||||
private const int PRIORITY = 49;
|
private const int PRIORITY = 49;
|
||||||
|
private static string[] errorMessageGroups;
|
||||||
|
private static string errorHeader;
|
||||||
|
|
||||||
[MenuItem("GameObject/ModularAvatar/Setup Outfit", false, PRIORITY)]
|
[MenuItem("GameObject/ModularAvatar/Setup Outfit", false, PRIORITY)]
|
||||||
static void SetupOutfit(MenuCommand cmd)
|
static void SetupOutfit(MenuCommand cmd)
|
||||||
{
|
{
|
||||||
|
if (!ValidateSetupOutfit())
|
||||||
|
{
|
||||||
|
ESOErrorWindow.Show(errorHeader, errorMessageGroups);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!FindBones(cmd.context,
|
if (!FindBones(cmd.context,
|
||||||
out var avatarRoot, out var avatarHips, out var outfitHips)
|
out var avatarRoot, out var avatarHips, out var outfitHips)
|
||||||
) return;
|
) return;
|
||||||
@ -117,13 +221,21 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("GameObject/ModularAvatar/Setup Outfit", true, PRIORITY)]
|
|
||||||
static bool ValidateSetupOutfit()
|
static bool ValidateSetupOutfit()
|
||||||
{
|
{
|
||||||
if (Selection.objects.Length == 0) return false;
|
errorHeader = S("setup_outfit.err.header.notarget");
|
||||||
|
errorMessageGroups = new string[] { S("setup_outfit.err.unknown") };
|
||||||
|
|
||||||
|
if (Selection.objects.Length == 0)
|
||||||
|
{
|
||||||
|
errorMessageGroups = new string[] { S("setup_outfit.err.no_selection") };
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var obj in Selection.objects)
|
foreach (var obj in Selection.objects)
|
||||||
{
|
{
|
||||||
|
errorHeader = S_f("setup_outfit.err.header", obj.name);
|
||||||
|
|
||||||
if (!(obj is GameObject gameObj)) return false;
|
if (!(obj is GameObject gameObj)) return false;
|
||||||
var xform = gameObj.transform;
|
var xform = gameObj.transform;
|
||||||
|
|
||||||
@ -137,10 +249,22 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
// refusing to run if we detect multiple avatar descriptors above the current object (or if we're run on
|
// refusing to run if we detect multiple avatar descriptors above the current object (or if we're run on
|
||||||
// the avdesc object itself)
|
// the avdesc object itself)
|
||||||
var nearestAvatar = RuntimeUtil.FindAvatarInParents(xform);
|
var nearestAvatar = RuntimeUtil.FindAvatarInParents(xform);
|
||||||
if (nearestAvatar == null || nearestAvatar.transform == xform) return false;
|
if (nearestAvatar == null || nearestAvatar.transform == xform)
|
||||||
|
{
|
||||||
|
errorMessageGroups = new string[]
|
||||||
|
{S_f("setup_outfit.err.multiple_avatar_descriptors", xform.gameObject.name)};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var parent = nearestAvatar.transform.parent;
|
var parent = nearestAvatar.transform.parent;
|
||||||
if (parent != null && RuntimeUtil.FindAvatarInParents(parent) != null) return false;
|
if (parent != null && RuntimeUtil.FindAvatarInParents(parent) != null)
|
||||||
|
{
|
||||||
|
errorMessageGroups = new string[]
|
||||||
|
{
|
||||||
|
S_f("setup_outfit.err.no_avatar_descriptor", xform.gameObject.name)
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -157,10 +281,24 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
if (outfitRoot == null || avatarRoot == null) return false;
|
if (outfitRoot == null || avatarRoot == null) return false;
|
||||||
|
|
||||||
var avatarAnimator = avatarRoot.GetComponent<Animator>();
|
var avatarAnimator = avatarRoot.GetComponent<Animator>();
|
||||||
if (avatarAnimator == null) return false;
|
if (avatarAnimator == null)
|
||||||
|
{
|
||||||
|
errorMessageGroups = new string[]
|
||||||
|
{
|
||||||
|
S("setup_outfit.err.no_animator")
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
avatarHips = avatarAnimator.GetBoneTransform(HumanBodyBones.Hips)?.gameObject;
|
avatarHips = avatarAnimator.GetBoneTransform(HumanBodyBones.Hips)?.gameObject;
|
||||||
if (avatarHips == null) return false;
|
if (avatarHips == null)
|
||||||
|
{
|
||||||
|
errorMessageGroups = new string[]
|
||||||
|
{
|
||||||
|
S("setup_outfit.err.no_hips")
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var outfitAnimator = outfitRoot.GetComponent<Animator>();
|
var outfitAnimator = outfitRoot.GetComponent<Animator>();
|
||||||
if (outfitAnimator != null)
|
if (outfitAnimator != null)
|
||||||
@ -168,9 +306,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
outfitHips = outfitAnimator.GetBoneTransform(HumanBodyBones.Hips)?.gameObject;
|
outfitHips = outfitAnimator.GetBoneTransform(HumanBodyBones.Hips)?.gameObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hipsCandidates = new List<string>();
|
||||||
|
|
||||||
if (outfitHips == null)
|
if (outfitHips == null)
|
||||||
{
|
{
|
||||||
// Heuristic search - usually there'll be root -> Armature -> (single child) Hips
|
// Heuristic search - usually there'll be root -> Armature -> (single child) Hips.
|
||||||
|
// First, look for an exact match.
|
||||||
foreach (Transform child in outfitRoot.transform)
|
foreach (Transform child in outfitRoot.transform)
|
||||||
{
|
{
|
||||||
foreach (Transform tempHip in child)
|
foreach (Transform tempHip in child)
|
||||||
@ -181,6 +322,39 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
hipsCandidates.Add(avatarHips.name);
|
||||||
|
|
||||||
|
// If that doesn't work out, we'll check for heuristic bone mapper mappings.
|
||||||
|
foreach (var hbm in HeuristicBoneMapper.BoneToNameMap[HumanBodyBones.Hips])
|
||||||
|
{
|
||||||
|
if (hipsCandidates[0] != hbm)
|
||||||
|
{
|
||||||
|
hipsCandidates.Add(hbm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Transform child in outfitRoot.transform)
|
||||||
|
{
|
||||||
|
foreach (Transform tempHip in child)
|
||||||
|
{
|
||||||
|
foreach (var candidate in hipsCandidates)
|
||||||
|
{
|
||||||
|
if (HeuristicBoneMapper.NormalizeName(tempHip.name).Contains(candidate))
|
||||||
|
{
|
||||||
|
outfitHips = tempHip.gameObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outfitHips == null)
|
||||||
|
{
|
||||||
|
errorMessageGroups = new string[]
|
||||||
|
{
|
||||||
|
S("setup_outfit.err.no_outfit_hips"),
|
||||||
|
string.Join("\n", hipsCandidates.Select(c => "・ " + c).ToArray())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return avatarHips != null && outfitHips != null;
|
return avatarHips != null && outfitHips != null;
|
||||||
|
@ -93,6 +93,18 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
return S(key, key);
|
return S(key, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string S_f(string key, params string[] format)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return string.Format(S(key, key), format);
|
||||||
|
}
|
||||||
|
catch (FormatException e)
|
||||||
|
{
|
||||||
|
return S(key, key) + "(" + string.Join(", ", format) + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string S(string key, string defValue)
|
public static string S(string key, string defValue)
|
||||||
{
|
{
|
||||||
var info = GetLocalization(GetSelectedLocalization());
|
var info = GetLocalization(GetSelectedLocalization());
|
||||||
|
@ -151,5 +151,14 @@
|
|||||||
"animation_gen.duplicate_binding": "Controls from different control groups are trying to animate the same parameter. Parameter: {0}",
|
"animation_gen.duplicate_binding": "Controls from different control groups are trying to animate the same parameter. Parameter: {0}",
|
||||||
"animation_gen.multiple_defaults": "Multiple default menu items were found in the same control group.",
|
"animation_gen.multiple_defaults": "Multiple default menu items were found in the same control group.",
|
||||||
"menuitem.misc.add_item": "Add menu item",
|
"menuitem.misc.add_item": "Add menu item",
|
||||||
"replace_object.target_object": "Object to replace"
|
"replace_object.target_object": "Object to replace",
|
||||||
|
"setup_outfit.err.header.notarget": "Setup outfit failed",
|
||||||
|
"setup_outfit.err.header": "Setup Outfit failed to process {0}",
|
||||||
|
"setup_outfit.err.unknown": "Unknown error",
|
||||||
|
"setup_outfit.err.no_selection": "No object selected.",
|
||||||
|
"setup_outfit.err.multiple_avatar_descriptors": "Multiple avatar descriptors found in {0} and its parents.",
|
||||||
|
"setup_outfit.err.no_avatar_descriptor": "No avatar descriptor found in {0} or its parents.",
|
||||||
|
"setup_outfit.err.no_animator": "Your avatar does not have an Animator component.",
|
||||||
|
"setup_outfit.err.no_hips": "Your avatar does not have a Hips bone. Setup Outfit only works on humanoid avatars.",
|
||||||
|
"setup_outfit.err.no_outfit_hips": "Unable to identify the Hips object for the outfit. Searched for objects containing the following names:"
|
||||||
}
|
}
|
@ -149,5 +149,14 @@
|
|||||||
"animation_gen.duplicate_binding": "別々のコントロールグループから、同じパラメーターが操作されています。パラメーター:{0}",
|
"animation_gen.duplicate_binding": "別々のコントロールグループから、同じパラメーターが操作されています。パラメーター:{0}",
|
||||||
"animation_gen.multiple_defaults": "同じコントロールグループに初期設定に指定されたメニューアイテムが複数あります。",
|
"animation_gen.multiple_defaults": "同じコントロールグループに初期設定に指定されたメニューアイテムが複数あります。",
|
||||||
"menuitem.misc.add_item": "メニューアイテムを追加",
|
"menuitem.misc.add_item": "メニューアイテムを追加",
|
||||||
"replace_object.target_object": "上書き先"
|
"replace_object.target_object": "上書き先",
|
||||||
|
"setup_outfit.err.header.notarget": "Setup outfit の処理が失敗しました",
|
||||||
|
"setup_outfit.err.header": "Setup outfit が「{0}」を処理中に失敗しました。",
|
||||||
|
"setup_outfit.err.unknown": "原因不明のエラーが発生しました。",
|
||||||
|
"setup_outfit.err.no_selection": "オブジェクトが選択されていません。",
|
||||||
|
"setup_outfit.err.multiple_avatar_descriptors": "「{}」とその親に、複数のavatar descriptorを発見しました。",
|
||||||
|
"setup_outfit.err.no_avatar_descriptor": "「{}」とその親に、avatar descriptorが見つかりませんでした。",
|
||||||
|
"setup_outfit.err.no_animator": "アバターにAnimatorコンポーネントがありません。",
|
||||||
|
"setup_outfit.err.no_hips": "アバターにHipsボーンがありません。なお、Setup Outfitはヒューマノイドアバター以外には対応していません。",
|
||||||
|
"setup_outfit.err.no_outfit_hips": "衣装のHipsボーンを発見できませんでした。以下の名前を含むボーンを探しました:"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user