fix: avoid name collisions in setup outfit (#435)

The human avatar mapping system seems to use bone _names_ rather than full
_paths_ to identify bones. When the avatar armature and an outfit armature
are both present under the avatar root, this can result in misidentification
of outfit bones as avatar bones on the avatar animator. This in turn results
in issues with Bone Proxy's editor-side tracking logic.

This change adjusts setup outfit to ensure that there is always a prefix
and/or suffix set, renaming bones if necessary.

Note that this does not fully use outfit human avatar data to map bones yet;
this is mostly intended as a patch to resolve the issues that have been
reported recently, particularly around the stricter validations in SDK
3.3.0.
This commit is contained in:
bd_ 2023-09-14 21:33:22 +09:00 committed by GitHub
parent ebda9cf7d5
commit 6cbcde05f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 201 additions and 11 deletions

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using UnityEditor;
using UnityEngine;
@ -122,14 +124,15 @@ namespace nadena.dev.modular_avatar.core.editor
var avatarArmature = avatarHips.transform.parent;
var outfitArmature = outfitHips.transform.parent;
if (outfitArmature.GetComponent<ModularAvatarMergeArmature>() == null)
var merge = outfitArmature.GetComponent<ModularAvatarMergeArmature>();
if (merge == null)
{
var merge = Undo.AddComponent<ModularAvatarMergeArmature>(outfitArmature.gameObject);
merge = Undo.AddComponent<ModularAvatarMergeArmature>(outfitArmature.gameObject);
merge.mergeTarget = new AvatarObjectReference();
merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject);
merge.InferPrefixSuffix();
HeuristicBoneMapper.RenameBonesByHeuristic(merge);
}
HeuristicBoneMapper.RenameBonesByHeuristic(merge);
if (outfitRoot != null
&& outfitRoot.GetComponent<ModularAvatarMeshSettings>() == null
@ -277,8 +280,7 @@ namespace nadena.dev.modular_avatar.core.editor
return true;
}
private static bool FindBones(Object obj, out GameObject avatarRoot, out GameObject avatarHips,
out GameObject outfitHips)
private static bool FindBones(Object obj, out GameObject avatarRoot, out GameObject avatarHips, out GameObject outfitHips)
{
avatarHips = outfitHips = null;
var outfitRoot = obj as GameObject;
@ -286,7 +288,7 @@ namespace nadena.dev.modular_avatar.core.editor
? RuntimeUtil.FindAvatarInParents(outfitRoot.transform)?.gameObject
: null;
if (outfitRoot == null || avatarRoot == null) return false;
var avatarAnimator = avatarRoot.GetComponent<Animator>();
if (avatarAnimator == null)
{
@ -297,7 +299,27 @@ namespace nadena.dev.modular_avatar.core.editor
return false;
}
avatarHips = avatarAnimator.GetBoneTransform(HumanBodyBones.Hips)?.gameObject;
var avatarBoneMappings = GetAvatarBoneMappings(avatarAnimator);
if (!avatarBoneMappings.ContainsKey(HumanBodyBones.Hips))
{
errorMessageGroups = new string[]
{
S("setup_outfit.err.no_hips")
};
return false;
}
// We do an explicit search for the hips bone rather than invoking the animator, as we want to control
// traversal order.
foreach (var maybeHips in avatarRoot.GetComponentsInChildren<Transform>())
{
if (maybeHips.name == avatarBoneMappings[HumanBodyBones.Hips] && !maybeHips.IsChildOf(outfitRoot.transform))
{
avatarHips = maybeHips.gameObject;
break;
}
}
if (avatarHips == null)
{
errorMessageGroups = new string[]
@ -323,13 +345,13 @@ namespace nadena.dev.modular_avatar.core.editor
{
foreach (Transform tempHip in child)
{
if (tempHip.name.Contains(avatarHips.name))
if (tempHip.name.Contains(avatarBoneMappings[HumanBodyBones.Hips]))
{
outfitHips = tempHip.gameObject;
}
}
}
hipsCandidates.Add(avatarHips.name);
hipsCandidates.Add(avatarBoneMappings[HumanBodyBones.Hips]);
// If that doesn't work out, we'll check for heuristic bone mapper mappings.
foreach (var hbm in HeuristicBoneMapper.BoneToNameMap[HumanBodyBones.Hips])
@ -366,5 +388,17 @@ namespace nadena.dev.modular_avatar.core.editor
return avatarHips != null && outfitHips != null;
}
private static ImmutableDictionary<HumanBodyBones, string> GetAvatarBoneMappings(Animator avatarAnimator)
{
var avatarHuman = avatarAnimator.avatar?.humanDescription.human ?? new HumanBone[0];
return avatarHuman
.Where(hb => !string.IsNullOrEmpty(hb.boneName))
.Select(hb => new KeyValuePair<HumanBodyBones, string>(
(HumanBodyBones) Enum.Parse(typeof(HumanBodyBones), hb.humanName.Replace(" ", "")),
hb.boneName
))
.ToImmutableDictionary();
}
}
}

View File

@ -10,6 +10,146 @@ namespace nadena.dev.modular_avatar.core.editor
{
private static readonly Regex PAT_END_NUMBER = new Regex(@"[_\.][0-9]+");
private static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<HumanBodyBones>> BoneChildren =
ImmutableDictionary<HumanBodyBones, ImmutableList<HumanBodyBones>>.Empty
.Add(HumanBodyBones.Hips, ImmutableList.Create(
HumanBodyBones.LeftUpperLeg,
HumanBodyBones.RightUpperLeg,
HumanBodyBones.Spine
))
.Add(HumanBodyBones.LeftUpperLeg, ImmutableList.Create(
HumanBodyBones.LeftLowerLeg
))
.Add(HumanBodyBones.RightUpperLeg, ImmutableList.Create(
HumanBodyBones.RightLowerLeg
))
.Add(HumanBodyBones.LeftLowerLeg, ImmutableList.Create(
HumanBodyBones.LeftFoot
))
.Add(HumanBodyBones.RightLowerLeg, ImmutableList.Create(
HumanBodyBones.RightFoot
))
.Add(HumanBodyBones.LeftFoot, ImmutableList.Create(
HumanBodyBones.LeftToes
))
.Add(HumanBodyBones.RightFoot, ImmutableList.Create(
HumanBodyBones.RightToes
))
.Add(HumanBodyBones.Spine, ImmutableList.Create(
HumanBodyBones.Chest,
HumanBodyBones.UpperChest
))
.Add(HumanBodyBones.Chest, ImmutableList.Create(
HumanBodyBones.Neck,
HumanBodyBones.LeftShoulder,
HumanBodyBones.RightShoulder
))
.Add(HumanBodyBones.UpperChest, ImmutableList.Create(
HumanBodyBones.Neck,
HumanBodyBones.LeftShoulder,
HumanBodyBones.RightShoulder
))
.Add(HumanBodyBones.Neck, ImmutableList.Create(
HumanBodyBones.Head
))
.Add(HumanBodyBones.Head, ImmutableList.Create(
HumanBodyBones.LeftEye,
HumanBodyBones.RightEye,
HumanBodyBones.Jaw
))
.Add(HumanBodyBones.LeftShoulder, ImmutableList.Create(
HumanBodyBones.LeftUpperArm
))
.Add(HumanBodyBones.RightShoulder, ImmutableList.Create(
HumanBodyBones.RightUpperArm
))
.Add(HumanBodyBones.LeftUpperArm, ImmutableList.Create(
HumanBodyBones.LeftLowerArm
))
.Add(HumanBodyBones.RightUpperArm, ImmutableList.Create(
HumanBodyBones.RightLowerArm
))
.Add(HumanBodyBones.LeftLowerArm, ImmutableList.Create(
HumanBodyBones.LeftHand
))
.Add(HumanBodyBones.RightLowerArm, ImmutableList.Create(
HumanBodyBones.RightHand
))
.Add(HumanBodyBones.LeftHand, ImmutableList.Create(
HumanBodyBones.LeftThumbProximal,
HumanBodyBones.LeftIndexProximal,
HumanBodyBones.LeftMiddleProximal,
HumanBodyBones.LeftRingProximal,
HumanBodyBones.LeftLittleProximal
))
.Add(HumanBodyBones.RightHand, ImmutableList.Create(
HumanBodyBones.RightThumbProximal,
HumanBodyBones.RightIndexProximal,
HumanBodyBones.RightMiddleProximal,
HumanBodyBones.RightRingProximal,
HumanBodyBones.RightLittleProximal
))
.Add(HumanBodyBones.LeftThumbProximal, ImmutableList.Create(
HumanBodyBones.LeftThumbIntermediate
))
.Add(HumanBodyBones.RightThumbProximal, ImmutableList.Create(
HumanBodyBones.RightThumbIntermediate
))
.Add(HumanBodyBones.LeftThumbIntermediate, ImmutableList.Create(
HumanBodyBones.LeftThumbDistal
))
.Add(HumanBodyBones.RightThumbIntermediate, ImmutableList.Create(
HumanBodyBones.RightThumbDistal
))
.Add(HumanBodyBones.LeftIndexProximal, ImmutableList.Create(
HumanBodyBones.LeftIndexIntermediate
))
.Add(HumanBodyBones.RightIndexProximal, ImmutableList.Create(
HumanBodyBones.RightIndexIntermediate
))
.Add(HumanBodyBones.LeftIndexIntermediate, ImmutableList.Create(
HumanBodyBones.LeftIndexDistal
))
.Add(HumanBodyBones.RightIndexIntermediate, ImmutableList.Create(
HumanBodyBones.RightIndexDistal
))
.Add(HumanBodyBones.LeftMiddleProximal, ImmutableList.Create(
HumanBodyBones.LeftMiddleIntermediate
))
.Add(HumanBodyBones.RightMiddleProximal, ImmutableList.Create(
HumanBodyBones.RightMiddleIntermediate
))
.Add(HumanBodyBones.LeftMiddleIntermediate, ImmutableList.Create(
HumanBodyBones.LeftMiddleDistal
))
.Add(HumanBodyBones.RightMiddleIntermediate, ImmutableList.Create(
HumanBodyBones.RightMiddleDistal
))
.Add(HumanBodyBones.LeftRingProximal, ImmutableList.Create(
HumanBodyBones.LeftRingIntermediate
))
.Add(HumanBodyBones.RightRingProximal, ImmutableList.Create(
HumanBodyBones.RightRingIntermediate
))
.Add(HumanBodyBones.LeftRingIntermediate, ImmutableList.Create(
HumanBodyBones.LeftRingDistal
))
.Add(HumanBodyBones.RightRingIntermediate, ImmutableList.Create(
HumanBodyBones.RightRingDistal
))
.Add(HumanBodyBones.LeftLittleProximal, ImmutableList.Create(
HumanBodyBones.LeftLittleIntermediate
))
.Add(HumanBodyBones.RightLittleProximal, ImmutableList.Create(
HumanBodyBones.RightLittleIntermediate
))
.Add(HumanBodyBones.LeftLittleIntermediate, ImmutableList.Create(
HumanBodyBones.LeftLittleDistal
))
.Add(HumanBodyBones.RightLittleIntermediate, ImmutableList.Create(
HumanBodyBones.RightLittleDistal
));
// 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
@ -359,15 +499,31 @@ namespace nadena.dev.modular_avatar.core.editor
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarInParents(config.transform));
if (target == null) return;
bool changedSuffix = false;
var newSuffix = config.suffix;
if (config.prefix == "" && config.suffix == "")
{
newSuffix = ".1";
changedSuffix = true;
}
Traverse(config.transform, target.transform);
config.suffix = newSuffix;
if (changedSuffix)
{
Undo.RecordObject(config, "Applying heuristic mapping");
PrefabUtility.RecordPrefabInstancePropertyModifications(config);
}
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 newName = config.prefix + pair.Value.gameObject.name + newSuffix;
var srcGameObj = pair.Key.gameObject;
var oldName = srcGameObj.name;