diff --git a/Editor/EasySetupOutfit.cs b/Editor/EasySetupOutfit.cs index 96c54288..e052aa01 100644 --- a/Editor/EasySetupOutfit.cs +++ b/Editor/EasySetupOutfit.cs @@ -138,7 +138,28 @@ namespace nadena.dev.modular_avatar.core.editor merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject); merge.LockMode = ArmatureLockMode.BaseToMerge; merge.InferPrefixSuffix(); - HeuristicBoneMapper.RenameBonesByHeuristic(merge); + + List subRoots = new List(); + HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots); + Debug.Log("Skipped: " + subRoots.Count); + + // If the outfit has an UpperChest bone but the avatar doesn't, add an additional MergeArmature to + // help with this + foreach (var subRoot in subRoots) + { + Debug.Log("$$$ Add subconfig to " + RuntimeUtil.RelativePath(avatarRoot, subRoot.gameObject)); + var subConfig = Undo.AddComponent(subRoot.gameObject); + var parentConfig = subConfig.transform.parent.GetComponentInParent(); + var parentMapping = parentConfig.MapBone(parentConfig.transform); + + subConfig.mergeTarget = new AvatarObjectReference(); + subConfig.mergeTarget.referencePath = + RuntimeUtil.RelativePath(avatarRoot, parentMapping.gameObject); + subConfig.LockMode = ArmatureLockMode.BaseToMerge; + subConfig.prefix = merge.prefix; + subConfig.suffix = merge.suffix; + subConfig.mangleNames = false; + } var avatarRootMatchingArmature = avatarRoot.transform.Find(outfitArmature.gameObject.name); if (merge.prefix == "" && merge.suffix == "" && avatarRootMatchingArmature != null) diff --git a/Editor/HeuristicBoneMapper.cs b/Editor/HeuristicBoneMapper.cs index 83d63c69..c6d8fb17 100644 --- a/Editor/HeuristicBoneMapper.cs +++ b/Editor/HeuristicBoneMapper.cs @@ -225,16 +225,15 @@ namespace nadena.dev.modular_avatar.core.editor }, new[] {"UpperChest", "UChest"}, }; + + internal static readonly Regex Regex_VRM_Bone = new Regex(@"^([LRC])_(.*)$"); internal static string NormalizeName(string name) { - name = name.ToLowerInvariant() - .Replace("_", "") - .Replace(".", "") - .Replace(" ", ""); - name = Regex.Replace(name, "[0-9]", ""); + name = name.ToLowerInvariant(); + name = Regex.Replace(name, "[0-9 ._]", ""); - return PAT_END_NUMBER.Replace(name, ""); + return name; } internal static readonly ImmutableDictionary> NameToBoneMap; @@ -259,6 +258,12 @@ namespace nadena.dev.modular_avatar.core.editor altName = match.Groups[1] + "." + altName; RegisterNameForBone(NormalizeName(altName), bone); } + else + { + // VRM pattern: J_Bip_C_[non-sided bone, e.g. hips] + var altName = "C." + name; + RegisterNameForBone(NormalizeName(altName), bone); + } } } @@ -295,16 +300,21 @@ namespace nadena.dev.modular_avatar.core.editor internal static Dictionary AssignBoneMappings( ModularAvatarMergeArmature config, GameObject src, - GameObject newParent + GameObject newParent, + List skipped = null, + HashSet unassigned = null ) { - HashSet unassigned = new HashSet(); Dictionary mappings = new Dictionary(); List heuristicAssignmentPass = new List(); - foreach (Transform child in newParent.transform) + if (unassigned == null) { - unassigned.Add(child); + unassigned = new HashSet(); + foreach (Transform child in newParent.transform) + { + unassigned.Add(child); + } } foreach (Transform child in src.transform) @@ -339,9 +349,9 @@ namespace nadena.dev.modular_avatar.core.editor 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 bodyBones)) + NormalizeName(targetObjectName), out var bodyBones)) { continue; } @@ -356,21 +366,38 @@ namespace nadena.dev.modular_avatar.core.editor break; } } + + if (!mappings.ContainsKey(child)) + { + Debug.Log("Unassigned: " + childName + " bodyBones=" + string.Join(", ", bodyBones) + " skipped=" + (skipped != null ? string.Join(", ", skipped.Select(x => x.name)) : "null")); + } + if (!mappings.ContainsKey(child) && bodyBones.Contains(HumanBodyBones.UpperChest) && skipped != null) + { + // Avatars are often missing UpperChest bones, try skipping over this... + skipped.Add(child); + + foreach (var kvp in AssignBoneMappings(config, child.gameObject, newParent, skipped, unassigned)) + { + mappings.Add(kvp.Key, kvp.Value); + } + } } return mappings; } - internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config) + internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List skipped = null) { var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform)); if (target == null) return; + if (skipped == null) skipped = new List(); + Traverse(config.transform, target.transform); - + void Traverse(Transform src, Transform dst) { - var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject); + var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped); foreach (var pair in mappings) { diff --git a/Editor/Inspector/MergeArmatureEditor.cs b/Editor/Inspector/MergeArmatureEditor.cs index d6acad52..3785a608 100644 --- a/Editor/Inspector/MergeArmatureEditor.cs +++ b/Editor/Inspector/MergeArmatureEditor.cs @@ -160,7 +160,12 @@ namespace nadena.dev.modular_avatar.core.editor private void ForcePositionToBaseAvatar() { - var mama = (ModularAvatarMergeArmature) target; + var mama = (ModularAvatarMergeArmature)target; + + ForcePositionToBaseAvatar(mama); + } + + private void ForcePositionToBaseAvatar(ModularAvatarMergeArmature mama, bool suppressRootScale = false) { var mergeTarget = mama.mergeTarget.Get(mama); var xform_to_bone = new Dictionary(); var bone_to_xform = new Dictionary(); @@ -183,7 +188,7 @@ namespace nadena.dev.modular_avatar.core.editor } } - if (posReset_heuristicRootScale) + if (posReset_heuristicRootScale && !suppressRootScale) { AdjustRootScale(); } @@ -219,14 +224,9 @@ namespace nadena.dev.modular_avatar.core.editor void Walk(Transform t_merge, Transform t_target) { Undo.RecordObject(t_merge, "Merge Armature: Force outfit position"); - - Debug.Log("=== Processing: " + t_merge.gameObject.name); - - if (!t_merge.IsChildOf(mama.transform)) - { - throw new ArgumentException("t_merge not a child of mama.transform"); - } - + + Debug.Log("Merge: " + t_merge.gameObject.name + " => " + t_target.gameObject.name); + t_merge.position = t_target.position; if (posReset_adjustScale) { @@ -241,11 +241,24 @@ namespace nadena.dev.modular_avatar.core.editor t_merge.localRotation = t_target.localRotation; } - foreach (Transform t_child in t_merge) + Queue traversalQueue = new Queue(); + traversalQueue.Enqueue(t_merge); + + while (traversalQueue.Count > 0) { - if (TryMatchChildBone(t_target, t_child, out var t_target_child)) + foreach (Transform t_child in traversalQueue.Dequeue()) { - Walk(t_child, t_target_child); + var mama_child = t_child.GetComponent(); + if (mama_child != null) + { + traversalQueue.Enqueue(t_child); + continue; + } + + if (TryMatchChildBone(t_target, t_child, out var t_target_child)) + { + Walk(t_child, t_target_child); + } } } } diff --git a/Editor/MergeArmatureHook.cs b/Editor/MergeArmatureHook.cs index 1ede08c5..db8e4f2a 100644 --- a/Editor/MergeArmatureHook.cs +++ b/Editor/MergeArmatureHook.cs @@ -121,8 +121,6 @@ namespace nadena.dev.modular_avatar.core.editor foreach (var config in mergeArmatures) { - // TODO - assert that we're not nesting merge armatures? - var target = config.mergeTargetObject; if (target == null) { @@ -149,6 +147,11 @@ namespace nadena.dev.modular_avatar.core.editor TopoLoop(next); } + foreach (var next in mergeArmatures) + { + UnityEngine.Object.DestroyImmediate(next); + } + void TopoLoop(ModularAvatarMergeArmature config) { if (visited.Contains(config)) return; @@ -196,7 +199,6 @@ namespace nadena.dev.modular_avatar.core.editor #if MA_VRCSDK3_AVATARS PruneDuplicatePhysBones(); #endif - UnityEngine.Object.DestroyImmediate(config); }); } @@ -360,6 +362,11 @@ namespace nadena.dev.modular_avatar.core.editor { foreach (Transform child in children) { + if (child.GetComponent () != null) + { + continue; + } + var childGameObject = child.gameObject; var childName = childGameObject.name; GameObject childNewParent = mergedSrcBone; diff --git a/Runtime/ModularAvatarMergeArmature.cs b/Runtime/ModularAvatarMergeArmature.cs index 27877b37..ff4960ad 100644 --- a/Runtime/ModularAvatarMergeArmature.cs +++ b/Runtime/ModularAvatarMergeArmature.cs @@ -26,6 +26,7 @@ using System; using System.Collections.Generic; using nadena.dev.modular_avatar.core.armature_lock; using UnityEngine; +using UnityEngine.Analytics; using UnityEngine.Serialization; namespace nadena.dev.modular_avatar.core @@ -59,6 +60,27 @@ namespace nadena.dev.modular_avatar.core private ArmatureLockController _lockController; + internal Transform MapBone(Transform bone) + { + var relPath = RuntimeUtil.RelativePath(gameObject, bone.gameObject); + + if (relPath == null) throw new ArgumentException("Bone is not a child of this component"); + if (relPath == "") return mergeTarget.Get(this).transform; + + var segments = relPath.Split('/'); + + var pointer = mergeTarget.Get(this).transform; + foreach (var segment in segments) + { + if (!segment.StartsWith(prefix) || !segment.EndsWith(suffix)) return null; + var targetObjectName = segment.Substring(prefix.Length, + segment.Length - prefix.Length - suffix.Length); + pointer = pointer.Find(targetObjectName); + } + + return pointer; + } + internal Transform FindCorrespondingBone(Transform bone, Transform baseParent) { var childName = bone.gameObject.name; @@ -161,6 +183,9 @@ namespace nadena.dev.modular_avatar.core { foreach (Transform t in merge) { + var subMerge = t.GetComponent(); + if (subMerge != null && subMerge != this) continue; + var baseChild = FindCorrespondingBone(t, baseBone); if (baseChild != null) { @@ -196,6 +221,12 @@ namespace nadena.dev.modular_avatar.core prefix = mergeName.Substring(0, prefixLength); suffix = mergeName.Substring(mergeName.Length - suffixLength); + if (prefix == "J_Bip_C_") + { + // VRM workaround + prefix = "J_Bip_"; + } + if (!string.IsNullOrEmpty(prefix) || !string.IsNullOrEmpty(suffix)) { RuntimeUtil.MarkDirty(this);