Initial implementation of heuristic bone matching

Closes: #105
This commit is contained in:
bd_ 2022-12-09 20:33:05 -08:00
parent c70e821271
commit 223f96d04e
7 changed files with 234 additions and 68 deletions

View File

@ -1,68 +0,0 @@
namespace nadena.dev.modular_avatar.core.editor
{
internal class BoneNameMappings
{
// This list is originally from https://github.com/HhotateA/AvatarModifyTools/blob/d8ae75fed8577707253d6b63a64d6053eebbe78b/Assets/HhotateA/AvatarModifyTool/Editor/EnvironmentVariable.cs#L81-L139
// Copyright (c) 2021 @HhotateA_xR
// Licensed under the MIT License
public static string[][] boneNamePatterns = new string[][]
{
new string[] {"Hips", "Hip"},
new string[] {"LeftUpperLeg", "UpperLeg_Left", "UpperLeg_L", "Leg_Left", "Leg_L"},
new string[] {"RightUpperLeg", "UpperLeg_Right", "UpperLeg_R", "Leg_Right", "Leg_R"},
new string[] {"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L"},
new string[] {"RightLowerLeg", "LowerLeg_Right", "LowerLeg_R", "Knee_Right", "Knee_R"},
new string[] {"LeftFoot", "Foot_Left", "Foot_L"},
new string[] {"RightFoot", "Foot_Right", "Foot_R"},
new string[] {"Spine"},
new string[] {"Chest"},
new string[] {"Neck"},
new string[] {"Head"},
new string[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"},
new string[] {"RightShoulder", "Shoulder_Right", "Shoulder_R"},
new string[] {"LeftUpperArm", "UpperArm_Left", "UpperArm_L", "Arm_Left", "Arm_L"},
new string[] {"RightUpperArm", "UpperArm_Right", "UpperArm_R", "Arm_Right", "Arm_R"},
new string[] {"LeftLowerArm", "LowerArm_Left", "LowerArm_L"},
new string[] {"RightLowerArm", "LowerArm_Right", "LowerArm_R"},
new string[] {"LeftHand", "Hand_Left", "Hand_L"},
new string[] {"RightHand", "Hand_Right", "Hand_R"},
new string[] {"LeftToes", "Toes_Left", "Toe_Left", "ToeIK_L", "Toes_L", "Toe_L"},
new string[] {"RightToes", "Toes_Right", "Toe_Right", "ToeIK_R", "Toes_R", "Toe_R"},
new string[] {"LeftEye", "Eye_Left", "Eye_L"},
new string[] {"RightEye", "Eye_Right", "Eye_R"},
new string[] {"Jaw"},
new string[] {"LeftThumbProximal", "ProximalThumb_Left", "ProximalThumb_L"},
new string[] {"LeftThumbIntermediate", "IntermediateThumb_Left", "IntermediateThumb_L"},
new string[] {"LeftThumbDistal", "DistalThumb_Left", "DistalThumb_L"},
new string[] {"LeftIndexProximal", "ProximalIndex_Left", "ProximalIndex_L"},
new string[] {"LeftIndexIntermediate", "IntermediateIndex_Left", "IntermediateIndex_L"},
new string[] {"LeftIndexDistal", "DistalIndex_Left", "DistalIndex_L"},
new string[] {"LeftMiddleProximal", "ProximalMiddle_Left", "ProximalMiddle_L"},
new string[] {"LeftMiddleIntermediate", "IntermediateMiddle_Left", "IntermediateMiddle_L"},
new string[] {"LeftMiddleDistal", "DistalMiddle_Left", "DistalMiddle_L"},
new string[] {"LeftRingProximal", "ProximalRing_Left", "ProximalRing_L"},
new string[] {"LeftRingIntermediate", "IntermediateRing_Left", "IntermediateRing_L"},
new string[] {"LeftRingDistal", "DistalRing_Left", "DistalRing_L"},
new string[] {"LeftLittleProximal", "ProximalLittle_Left", "ProximalLittle_L"},
new string[] {"LeftLittleIntermediate", "IntermediateLittle_Left", "IntermediateLittle_L"},
new string[] {"LeftLittleDistal", "DistalLittle_Left", "DistalLittle_L"},
new string[] {"RightThumbProximal", "ProximalThumb_Right", "ProximalThumb_R"},
new string[] {"RightThumbIntermediate", "IntermediateThumb_Right", "IntermediateThumb_R"},
new string[] {"RightThumbDistal", "DistalThumb_Right", "DistalThumb_R"},
new string[] {"RightIndexProximal", "ProximalIndex_Right", "ProximalIndex_R"},
new string[] {"RightIndexIntermediate", "IntermediateIndex_Right", "IntermediateIndex_R"},
new string[] {"RightIndexDistal", "DistalIndex_Right", "DistalIndex_R"},
new string[] {"RightMiddleProximal", "ProximalMiddle_Right", "ProximalMiddle_R"},
new string[] {"RightMiddleIntermediate", "IntermediateMiddle_Right", "IntermediateMiddle_R"},
new string[] {"RightMiddleDistal", "DistalMiddle_Right", "DistalMiddle_R"},
new string[] {"RightRingProximal", "ProximalRing_Right", "ProximalRing_R"},
new string[] {"RightRingIntermediate", "IntermediateRing_Right", "IntermediateRing_R"},
new string[] {"RightRingDistal", "DistalRing_Right", "DistalRing_R"},
new string[] {"RightLittleProximal", "ProximalLittle_Right", "ProximalLittle_R"},
new string[] {"RightLittleIntermediate", "IntermediateLittle_Right", "IntermediateLittle_R"},
new string[] {"RightLittleDistal", "DistalLittle_Right", "DistalLittle_R"},
new string[] {"UpperChest"},
new string[] {"LastBone", "Armature"}, // 本来的ではないけどRootもhitさせたい
};
}
}

View File

@ -24,6 +24,7 @@ namespace nadena.dev.modular_avatar.core.editor
merge.mergeTarget = new AvatarObjectReference();
merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject);
merge.InferPrefixSuffix();
HeuristicBoneMapper.RenameBonesByHeuristic(merge);
}
[MenuItem("GameObject/[ModularAvatar] Setup Outfit", true, PRIORITY)]

View File

@ -0,0 +1,219 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using UnityEditor;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
internal class HeuristicBoneMapper
{
// This list is originally from https://github.com/HhotateA/AvatarModifyTools/blob/d8ae75fed8577707253d6b63a64d6053eebbe78b/Assets/HhotateA/AvatarModifyTool/Editor/EnvironmentVariable.cs#L81-L139
// Copyright (c) 2021 @HhotateA_xR
// Licensed under the MIT License
private static string[][] boneNamePatterns = new[]
{
new[] {"Hips", "Hip"},
new[] {"LeftUpperLeg", "UpperLeg_Left", "UpperLeg_L", "Leg_Left", "Leg_L"},
new[] {"RightUpperLeg", "UpperLeg_Right", "UpperLeg_R", "Leg_Right", "Leg_R"},
new[] {"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L"},
new[] {"RightLowerLeg", "LowerLeg_Right", "LowerLeg_R", "Knee_Right", "Knee_R"},
new[] {"LeftFoot", "Foot_Left", "Foot_L"},
new[] {"RightFoot", "Foot_Right", "Foot_R"},
new[] {"Spine"},
new[] {"Chest"},
new[] {"Neck"},
new[] {"Head"},
new[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"},
new[] {"RightShoulder", "Shoulder_Right", "Shoulder_R"},
new[] {"LeftUpperArm", "UpperArm_Left", "UpperArm_L", "Arm_Left", "Arm_L"},
new[] {"RightUpperArm", "UpperArm_Right", "UpperArm_R", "Arm_Right", "Arm_R"},
new[] {"LeftLowerArm", "LowerArm_Left", "LowerArm_L"},
new[] {"RightLowerArm", "LowerArm_Right", "LowerArm_R"},
new[] {"LeftHand", "Hand_Left", "Hand_L"},
new[] {"RightHand", "Hand_Right", "Hand_R"},
new[] {"LeftToes", "Toes_Left", "Toe_Left", "ToeIK_L", "Toes_L", "Toe_L"},
new[] {"RightToes", "Toes_Right", "Toe_Right", "ToeIK_R", "Toes_R", "Toe_R"},
new[] {"LeftEye", "Eye_Left", "Eye_L"},
new[] {"RightEye", "Eye_Right", "Eye_R"},
new[] {"Jaw"},
new[] {"LeftThumbProximal", "ProximalThumb_Left", "ProximalThumb_L"},
new[] {"LeftThumbIntermediate", "IntermediateThumb_Left", "IntermediateThumb_L"},
new[] {"LeftThumbDistal", "DistalThumb_Left", "DistalThumb_L"},
new[] {"LeftIndexProximal", "ProximalIndex_Left", "ProximalIndex_L"},
new[] {"LeftIndexIntermediate", "IntermediateIndex_Left", "IntermediateIndex_L"},
new[] {"LeftIndexDistal", "DistalIndex_Left", "DistalIndex_L"},
new[] {"LeftMiddleProximal", "ProximalMiddle_Left", "ProximalMiddle_L"},
new[] {"LeftMiddleIntermediate", "IntermediateMiddle_Left", "IntermediateMiddle_L"},
new[] {"LeftMiddleDistal", "DistalMiddle_Left", "DistalMiddle_L"},
new[] {"LeftRingProximal", "ProximalRing_Left", "ProximalRing_L"},
new[] {"LeftRingIntermediate", "IntermediateRing_Left", "IntermediateRing_L"},
new[] {"LeftRingDistal", "DistalRing_Left", "DistalRing_L"},
new[] {"LeftLittleProximal", "ProximalLittle_Left", "ProximalLittle_L"},
new[] {"LeftLittleIntermediate", "IntermediateLittle_Left", "IntermediateLittle_L"},
new[] {"LeftLittleDistal", "DistalLittle_Left", "DistalLittle_L"},
new[] {"RightThumbProximal", "ProximalThumb_Right", "ProximalThumb_R"},
new[] {"RightThumbIntermediate", "IntermediateThumb_Right", "IntermediateThumb_R"},
new[] {"RightThumbDistal", "DistalThumb_Right", "DistalThumb_R"},
new[] {"RightIndexProximal", "ProximalIndex_Right", "ProximalIndex_R"},
new[] {"RightIndexIntermediate", "IntermediateIndex_Right", "IntermediateIndex_R"},
new[] {"RightIndexDistal", "DistalIndex_Right", "DistalIndex_R"},
new[] {"RightMiddleProximal", "ProximalMiddle_Right", "ProximalMiddle_R"},
new[] {"RightMiddleIntermediate", "IntermediateMiddle_Right", "IntermediateMiddle_R"},
new[] {"RightMiddleDistal", "DistalMiddle_Right", "DistalMiddle_R"},
new[] {"RightRingProximal", "ProximalRing_Right", "ProximalRing_R"},
new[] {"RightRingIntermediate", "IntermediateRing_Right", "IntermediateRing_R"},
new[] {"RightRingDistal", "DistalRing_Right", "DistalRing_R"},
new[] {"RightLittleProximal", "ProximalLittle_Right", "ProximalLittle_R"},
new[] {"RightLittleIntermediate", "IntermediateLittle_Right", "IntermediateLittle_R"},
new[] {"RightLittleDistal", "DistalLittle_Right", "DistalLittle_R"},
new[] {"UpperChest"},
};
internal static string NormalizeName(string name)
{
return name.ToLowerInvariant()
.Replace("_", "")
.Replace(".", "")
.Replace(" ", "");
}
internal static readonly ImmutableDictionary<string, HumanBodyBones> NameToBoneMap;
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
static HeuristicBoneMapper()
{
var nameToBoneMap = new Dictionary<string, HumanBodyBones>();
var boneToNameMap = new Dictionary<HumanBodyBones, ImmutableList<string>>();
for (int i = 0; i < boneNamePatterns.Length; i++)
{
var bone = (HumanBodyBones) i;
foreach (var name in boneNamePatterns[i])
{
RegisterNameForBone(NormalizeName(name), bone);
}
}
void RegisterNameForBone(string name, HumanBodyBones bone)
{
nameToBoneMap[name] = bone;
if (!boneToNameMap.TryGetValue(bone, out var names))
{
names = ImmutableList<string>.Empty;
}
if (!names.Contains(name))
{
boneToNameMap[bone] = names.Add(name);
}
}
NameToBoneMap = nameToBoneMap.ToImmutableDictionary();
BoneToNameMap = boneToNameMap.ToImmutableDictionary();
}
/// <summary>
/// Examines the children of src, and tries to map them to the corresponding child of newParent.
/// Unmappable bones will not be added to the resulting dictionary. Ensures that each parent bone is only mapped
/// once.
/// </summary>
internal static Dictionary<Transform, Transform> AssignBoneMappings(
ModularAvatarMergeArmature config,
GameObject src,
GameObject newParent
)
{
HashSet<Transform> unassigned = new HashSet<Transform>();
Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>();
List<Transform> heuristicAssignmentPass = new List<Transform>();
foreach (Transform child in newParent.transform)
{
unassigned.Add(child);
}
foreach (Transform child in src.transform)
{
var childName = child.gameObject.name;
if (childName.StartsWith(config.prefix) && childName.EndsWith(config.suffix))
{
var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length);
var targetObject = newParent.transform.Find(targetObjectName);
if (targetObject != null && unassigned.Contains(targetObject))
{
mappings[child] = targetObject;
unassigned.Remove(targetObject);
}
else
{
heuristicAssignmentPass.Add(child);
}
}
}
Dictionary<string, Transform> lcNameToXform = new Dictionary<string, Transform>();
foreach (var target in unassigned)
{
lcNameToXform[NormalizeName(target.gameObject.name)] = target;
}
foreach (var child in heuristicAssignmentPass)
{
var childName = child.gameObject.name;
var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length);
if (!NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName.ToLowerInvariant()), out var bodyBone))
{
continue;
}
foreach (var otherName in BoneToNameMap[bodyBone])
{
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
{
mappings[child] = targetObject;
unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant());
break;
}
}
}
return mappings;
}
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config)
{
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarInParents(config.transform));
if (target == null) return;
Traverse(config.transform, target.transform);
void Traverse(Transform src, Transform dst)
{
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject);
foreach (var pair in mappings)
{
var newName = config.prefix + pair.Value.gameObject.name + config.suffix;
var srcGameObj = pair.Key.gameObject;
var oldName = srcGameObj.name;
if (oldName != newName)
{
Undo.RecordObject(srcGameObj, "Applying heuristic mapping");
srcGameObj.name = newName;
PrefabUtility.RecordPrefabInstancePropertyModifications(srcGameObj);
}
Traverse(pair.Key, pair.Value);
}
}
}
}
}

View File

@ -49,6 +49,16 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
var enable_name_assignment =
target.mergeTarget.Get(RuntimeUtil.FindAvatarInParents(target.transform)) != null;
using (var scope = new EditorGUI.DisabledScope(!enable_name_assignment))
{
if (GUILayout.Button(G("merge_armature.adjust_names")))
{
HeuristicBoneMapper.RenameBonesByHeuristic(target);
}
}
Localization.ShowLanguageUI();
}
}

View File

@ -30,6 +30,8 @@
"merge_armature.suffix.tooltip": "Suffix expected on bones in this merged armature",
"merge_armature.locked": "Lock position",
"merge_armature.locked.tooltip": "Lock the position of this armature's bones to the target armature (and vice versa). Useful for creating animations.",
"merge_armature.adjust_names": "Adjust bone names to match target",
"merge_armature.adjust_names.tooltip": "Changes bone names to match the target avatar. Useful for porting outfits from one avatar to another.",
"path_mode.Relative": "Relative to this object",
"path_mode.Absolute": "Absolute (based on avatar root)",
"merge_animator.animator": "Animator to merge",

View File

@ -30,6 +30,8 @@
"merge_armature.suffix.tooltip": "このオブジェクトの子に付く後置詞",
"merge_armature.locked": "位置を固定",
"merge_armature.locked.tooltip": "このオブジェクトのボーンを統合先のボーンに常に相互的に位置を合わせる。アニメーション制作向け",
"merge_armature.adjust_names": "ボーン名を統合先に合わせる",
"merge_armature.adjust_names.tooltip": "統合先のボーン名に合わせて、衣装のボーン名を合わせて変更します。統合先アバターに非対応の衣装導入向け機能です。",
"path_mode.Relative": "相対的(このオブジェクトからのパスを使用)",
"path_mode.Absolute": "絶対的(アバタールートからのパスを使用)",
"merge_animator.animator": "統合されるアニメーター",