From 223f96d04ec2702c0cff8eababcf8ef76a1dbe56 Mon Sep 17 00:00:00 2001 From: bd_ Date: Fri, 9 Dec 2022 20:33:05 -0800 Subject: [PATCH] Initial implementation of heuristic bone matching Closes: #105 --- .../Editor/BoneNameMappings.cs | 68 ------ .../Editor/EasySetupOutfit.cs | 1 + .../Editor/HeuristicBoneMapper.cs | 219 ++++++++++++++++++ ...gs.cs.meta => HeuristicBoneMapper.cs.meta} | 0 .../Editor/Inspector/MergeArmatureEditor.cs | 10 + .../Editor/Localization/en.json | 2 + .../Editor/Localization/ja.json | 2 + 7 files changed, 234 insertions(+), 68 deletions(-) delete mode 100644 Packages/nadena.dev.modular-avatar/Editor/BoneNameMappings.cs create mode 100644 Packages/nadena.dev.modular-avatar/Editor/HeuristicBoneMapper.cs rename Packages/nadena.dev.modular-avatar/Editor/{BoneNameMappings.cs.meta => HeuristicBoneMapper.cs.meta} (100%) diff --git a/Packages/nadena.dev.modular-avatar/Editor/BoneNameMappings.cs b/Packages/nadena.dev.modular-avatar/Editor/BoneNameMappings.cs deleted file mode 100644 index 9ef2451e..00000000 --- a/Packages/nadena.dev.modular-avatar/Editor/BoneNameMappings.cs +++ /dev/null @@ -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させたい - }; - } -} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/EasySetupOutfit.cs b/Packages/nadena.dev.modular-avatar/Editor/EasySetupOutfit.cs index 5722841f..edc6aaa6 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/EasySetupOutfit.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/EasySetupOutfit.cs @@ -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)] diff --git a/Packages/nadena.dev.modular-avatar/Editor/HeuristicBoneMapper.cs b/Packages/nadena.dev.modular-avatar/Editor/HeuristicBoneMapper.cs new file mode 100644 index 00000000..82a2345c --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/HeuristicBoneMapper.cs @@ -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 NameToBoneMap; + internal static readonly ImmutableDictionary> BoneToNameMap; + + static HeuristicBoneMapper() + { + var nameToBoneMap = new Dictionary(); + var boneToNameMap = new Dictionary>(); + + 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.Empty; + } + + if (!names.Contains(name)) + { + boneToNameMap[bone] = names.Add(name); + } + } + + NameToBoneMap = nameToBoneMap.ToImmutableDictionary(); + BoneToNameMap = boneToNameMap.ToImmutableDictionary(); + } + + + /// + /// 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. + /// + internal static Dictionary AssignBoneMappings( + ModularAvatarMergeArmature config, + GameObject src, + GameObject newParent + ) + { + HashSet unassigned = new HashSet(); + Dictionary mappings = new Dictionary(); + List heuristicAssignmentPass = new List(); + + 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 lcNameToXform = new Dictionary(); + 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); + } + } + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/BoneNameMappings.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/HeuristicBoneMapper.cs.meta similarity index 100% rename from Packages/nadena.dev.modular-avatar/Editor/BoneNameMappings.cs.meta rename to Packages/nadena.dev.modular-avatar/Editor/HeuristicBoneMapper.cs.meta diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MergeArmatureEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MergeArmatureEditor.cs index c512762c..cfdd5a77 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MergeArmatureEditor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MergeArmatureEditor.cs @@ -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(); } } diff --git a/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json b/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json index 5de6d42d..5e3002dd 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json +++ b/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json @@ -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", diff --git a/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json b/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json index dc561d6b..9943c6d4 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json +++ b/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json @@ -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": "統合されるアニメーター",