feat: add support for VRoid Hub armatures in Setup Outfit (#709)

This commit is contained in:
bd_ 2024-02-27 02:02:04 -08:00 committed by GitHub
parent 8ec1d92b6f
commit 97fe8075a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 131 additions and 33 deletions

View File

@ -1,10 +1,14 @@
using System.Collections.Generic;
#region
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
using static nadena.dev.modular_avatar.core.editor.Localization;
#endregion
namespace nadena.dev.modular_avatar.core.editor
{
internal class ESOErrorWindow : EditorWindow
@ -138,7 +142,27 @@ 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<Transform> subRoots = new List<Transform>();
HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots);
// 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)
{
var subConfig = Undo.AddComponent<ModularAvatarMergeArmature>(subRoot.gameObject);
var parentTransform = subConfig.transform.parent;
var parentConfig = parentTransform.GetComponentInParent<ModularAvatarMergeArmature>();
var parentMapping = parentConfig.MapBone(parentTransform);
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)

View File

@ -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<string, List<HumanBodyBones>> 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<Transform, Transform> AssignBoneMappings(
ModularAvatarMergeArmature config,
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>();
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)
@ -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,34 @@ namespace nadena.dev.modular_avatar.core.editor
break;
}
}
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<Transform> skipped = null)
{
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform));
if (target == null) return;
if (skipped == null) skipped = new List<Transform>();
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)
{

View File

@ -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<Transform, HumanBodyBones>();
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();
}
@ -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<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)
{
// 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 <ModularAvatarMergeArmature>() != null)
{
continue;
}
var childGameObject = child.gameObject;
var childName = childGameObject.name;
GameObject childNewParent = mergedSrcBone;

View File

@ -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;
@ -162,6 +184,9 @@ namespace nadena.dev.modular_avatar.core
{
foreach (Transform t in merge)
{
var subMerge = t.GetComponent<ModularAvatarMergeArmature>();
if (subMerge != null && subMerge != this) continue;
var baseChild = FindCorrespondingBone(t, baseBone);
if (baseChild != null)
{
@ -197,6 +222,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);