WIP: VPM bone mapping support

This commit is contained in:
bd_ 2024-02-17 18:37:54 +09:00
parent 54d4e974db
commit 4aa46d2277
5 changed files with 131 additions and 32 deletions

View File

@ -138,7 +138,28 @@ namespace nadena.dev.modular_avatar.core.editor
merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject); merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject);
merge.LockMode = ArmatureLockMode.BaseToMerge; merge.LockMode = ArmatureLockMode.BaseToMerge;
merge.InferPrefixSuffix(); merge.InferPrefixSuffix();
HeuristicBoneMapper.RenameBonesByHeuristic(merge);
List<Transform> subRoots = new List<Transform>();
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<ModularAvatarMergeArmature>(subRoot.gameObject);
var parentConfig = subConfig.transform.parent.GetComponentInParent<ModularAvatarMergeArmature>();
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); var avatarRootMatchingArmature = avatarRoot.transform.Find(outfitArmature.gameObject.name);
if (merge.prefix == "" && merge.suffix == "" && avatarRootMatchingArmature != null) if (merge.prefix == "" && merge.suffix == "" && avatarRootMatchingArmature != null)

View File

@ -225,16 +225,15 @@ namespace nadena.dev.modular_avatar.core.editor
}, },
new[] {"UpperChest", "UChest"}, new[] {"UpperChest", "UChest"},
}; };
internal static readonly Regex Regex_VRM_Bone = new Regex(@"^([LRC])_(.*)$");
internal static string NormalizeName(string name) internal static string NormalizeName(string name)
{ {
name = name.ToLowerInvariant() name = name.ToLowerInvariant();
.Replace("_", "") name = Regex.Replace(name, "[0-9 ._]", "");
.Replace(".", "")
.Replace(" ", "");
name = Regex.Replace(name, "[0-9]", "");
return PAT_END_NUMBER.Replace(name, ""); return name;
} }
internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap; internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap;
@ -259,6 +258,12 @@ namespace nadena.dev.modular_avatar.core.editor
altName = match.Groups[1] + "." + altName; altName = match.Groups[1] + "." + altName;
RegisterNameForBone(NormalizeName(altName), bone); 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<Transform, Transform> AssignBoneMappings( internal static Dictionary<Transform, Transform> AssignBoneMappings(
ModularAvatarMergeArmature config, ModularAvatarMergeArmature config,
GameObject src, GameObject src,
GameObject newParent GameObject newParent,
List<Transform> skipped = null,
HashSet<Transform> unassigned = null
) )
{ {
HashSet<Transform> unassigned = new HashSet<Transform>();
Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>(); Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>();
List<Transform> heuristicAssignmentPass = new List<Transform>(); List<Transform> heuristicAssignmentPass = new List<Transform>();
foreach (Transform child in newParent.transform) if (unassigned == null)
{ {
unassigned.Add(child); unassigned = new HashSet<Transform>();
foreach (Transform child in newParent.transform)
{
unassigned.Add(child);
}
} }
foreach (Transform child in src.transform) foreach (Transform child in src.transform)
@ -339,9 +349,9 @@ namespace nadena.dev.modular_avatar.core.editor
var childName = child.gameObject.name; var childName = child.gameObject.name;
var targetObjectName = childName.Substring(config.prefix.Length, var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length); childName.Length - config.prefix.Length - config.suffix.Length);
if (!NameToBoneMap.TryGetValue( if (!NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName.ToLowerInvariant()), out var bodyBones)) NormalizeName(targetObjectName), out var bodyBones))
{ {
continue; continue;
} }
@ -356,21 +366,38 @@ namespace nadena.dev.modular_avatar.core.editor
break; 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; return mappings;
} }
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config) internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null)
{ {
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform)); var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform));
if (target == null) return; if (target == null) return;
if (skipped == null) skipped = new List<Transform>();
Traverse(config.transform, target.transform); Traverse(config.transform, target.transform);
void Traverse(Transform src, Transform dst) 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) foreach (var pair in mappings)
{ {

View File

@ -160,7 +160,12 @@ namespace nadena.dev.modular_avatar.core.editor
private void ForcePositionToBaseAvatar() 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 mergeTarget = mama.mergeTarget.Get(mama);
var xform_to_bone = new Dictionary<Transform, HumanBodyBones>(); var xform_to_bone = new Dictionary<Transform, HumanBodyBones>();
var bone_to_xform = new Dictionary<HumanBodyBones, Transform>(); var bone_to_xform = new Dictionary<HumanBodyBones, Transform>();
@ -183,7 +188,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
if (posReset_heuristicRootScale) if (posReset_heuristicRootScale && !suppressRootScale)
{ {
AdjustRootScale(); AdjustRootScale();
} }
@ -219,14 +224,9 @@ namespace nadena.dev.modular_avatar.core.editor
void Walk(Transform t_merge, Transform t_target) void Walk(Transform t_merge, Transform t_target)
{ {
Undo.RecordObject(t_merge, "Merge Armature: Force outfit position"); Undo.RecordObject(t_merge, "Merge Armature: Force outfit position");
Debug.Log("=== Processing: " + t_merge.gameObject.name); Debug.Log("Merge: " + t_merge.gameObject.name + " => " + t_target.gameObject.name);
if (!t_merge.IsChildOf(mama.transform))
{
throw new ArgumentException("t_merge not a child of mama.transform");
}
t_merge.position = t_target.position; t_merge.position = t_target.position;
if (posReset_adjustScale) if (posReset_adjustScale)
{ {
@ -241,11 +241,24 @@ namespace nadena.dev.modular_avatar.core.editor
t_merge.localRotation = t_target.localRotation; t_merge.localRotation = t_target.localRotation;
} }
foreach (Transform t_child in t_merge) Queue<Transform> traversalQueue = new Queue<Transform>();
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<ModularAvatarMergeArmature>();
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);
}
} }
} }
} }

View File

@ -121,8 +121,6 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var config in mergeArmatures) foreach (var config in mergeArmatures)
{ {
// TODO - assert that we're not nesting merge armatures?
var target = config.mergeTargetObject; var target = config.mergeTargetObject;
if (target == null) if (target == null)
{ {
@ -149,6 +147,11 @@ namespace nadena.dev.modular_avatar.core.editor
TopoLoop(next); TopoLoop(next);
} }
foreach (var next in mergeArmatures)
{
UnityEngine.Object.DestroyImmediate(next);
}
void TopoLoop(ModularAvatarMergeArmature config) void TopoLoop(ModularAvatarMergeArmature config)
{ {
if (visited.Contains(config)) return; if (visited.Contains(config)) return;
@ -196,7 +199,6 @@ namespace nadena.dev.modular_avatar.core.editor
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
PruneDuplicatePhysBones(); PruneDuplicatePhysBones();
#endif #endif
UnityEngine.Object.DestroyImmediate(config);
}); });
} }
@ -360,6 +362,11 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
foreach (Transform child in children) foreach (Transform child in children)
{ {
if (child.GetComponent <ModularAvatarMergeArmature>() != null)
{
continue;
}
var childGameObject = child.gameObject; var childGameObject = child.gameObject;
var childName = childGameObject.name; var childName = childGameObject.name;
GameObject childNewParent = mergedSrcBone; GameObject childNewParent = mergedSrcBone;

View File

@ -26,6 +26,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using nadena.dev.modular_avatar.core.armature_lock; using nadena.dev.modular_avatar.core.armature_lock;
using UnityEngine; using UnityEngine;
using UnityEngine.Analytics;
using UnityEngine.Serialization; using UnityEngine.Serialization;
namespace nadena.dev.modular_avatar.core namespace nadena.dev.modular_avatar.core
@ -59,6 +60,27 @@ namespace nadena.dev.modular_avatar.core
private ArmatureLockController _lockController; 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) internal Transform FindCorrespondingBone(Transform bone, Transform baseParent)
{ {
var childName = bone.gameObject.name; var childName = bone.gameObject.name;
@ -161,6 +183,9 @@ namespace nadena.dev.modular_avatar.core
{ {
foreach (Transform t in merge) foreach (Transform t in merge)
{ {
var subMerge = t.GetComponent<ModularAvatarMergeArmature>();
if (subMerge != null && subMerge != this) continue;
var baseChild = FindCorrespondingBone(t, baseBone); var baseChild = FindCorrespondingBone(t, baseBone);
if (baseChild != null) if (baseChild != null)
{ {
@ -196,6 +221,12 @@ namespace nadena.dev.modular_avatar.core
prefix = mergeName.Substring(0, prefixLength); prefix = mergeName.Substring(0, prefixLength);
suffix = mergeName.Substring(mergeName.Length - suffixLength); suffix = mergeName.Substring(mergeName.Length - suffixLength);
if (prefix == "J_Bip_C_")
{
// VRM workaround
prefix = "J_Bip_";
}
if (!string.IsNullOrEmpty(prefix) || !string.IsNullOrEmpty(suffix)) if (!string.IsNullOrEmpty(prefix) || !string.IsNullOrEmpty(suffix))
{ {
RuntimeUtil.MarkDirty(this); RuntimeUtil.MarkDirty(this);