fix: fix issues with nested armature confusion in Easy Setup Outfit

This fixes issues with nested armature confusion by changing the name of the
Armature object (only).
This commit is contained in:
bd_ 2023-10-02 20:22:12 +09:00
parent 56119c0779
commit cfd25ccd08
10 changed files with 2770 additions and 4 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e8745411e14ca634e87ffbf19723aab7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,60 @@
using modular_avatar_tests;
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using UnityObject = UnityEngine.Object;
public class ArmatureConfusionTest : TestBase
{
[TearDown]
public void TearDown()
{
ESOErrorWindow.Suppress = false;
}
[Test]
public void TestArmatureConfusionWorkaround()
{
ESOErrorWindow.Suppress = true;
// Arrange for a confused armature
var outer = CreatePrefab("shapell.fbx");
var inner = CreatePrefab("shapell.fbx");
var outerAnimator = outer.GetComponent<Animator>();
outer.AddComponent<VRCAvatarDescriptor>();
inner.gameObject.name = "inner";
inner.transform.parent = outer.transform;
// Unity seems to determine which armature is the "true" armature by counting the number of bones that match
// the humanoid description, and finding the root which has the most matches. Let's confuse it a bit by removing
// some non-humanoid bones from the outer armature.
var outerTarget = outer.transform.Find("Armature/Hips/Tail");
UnityObject.DestroyImmediate(outerTarget.gameObject);
// Clear animator cache
var avatar = outerAnimator.avatar;
outerAnimator.avatar = null;
// ReSharper disable once Unity.InefficientPropertyAccess
outerAnimator.avatar = avatar;
// Verify that we're well and confused now
Assert.AreSame(
outerAnimator.GetBoneTransform(HumanBodyBones.Hips),
inner.transform.Find("Armature/Hips")
);
// Now do a setup outfit operation
Selection.activeGameObject = inner;
EasySetupOutfit.SetupOutfit(new MenuCommand(inner));
// Verify that we're not confused anymore
Assert.AreSame(
outerAnimator.GetBoneTransform(HumanBodyBones.Hips),
outer.transform.Find("Armature/Hips")
);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 01ff1e86623a409ba5248c5fb6a0b53e
timeCreated: 1696244575

View File

@ -0,0 +1,2 @@
shapell.fbx is from Shapell (https://booth.pm/ja/items/1349366) by lowteq
Released under CC0 1.0 Universal (CC0 1.0) (https://creativecommons.org/publicdomain/zero/1.0/deed.ja)

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c5e311d0ce414f56afac9ccff53b16c4
timeCreated: 1696245763

View File

@ -0,0 +1,77 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: material
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 14fe6d8ede57a1a45955b9a0181223ae
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,8 @@ namespace nadena.dev.modular_avatar.core.editor
private static readonly GUIStyle buttonStyle, labelStyle; private static readonly GUIStyle buttonStyle, labelStyle;
private const float SeparatorSize = 6f; private const float SeparatorSize = 6f;
internal static bool Suppress = false;
static ESOErrorWindow() static ESOErrorWindow()
{ {
buttonStyle = EditorStyles.miniButtonRight; buttonStyle = EditorStyles.miniButtonRight;
@ -33,6 +35,8 @@ namespace nadena.dev.modular_avatar.core.editor
string[] messageGroups string[] messageGroups
) )
{ {
if (Suppress) return;
var window = CreateInstance<ESOErrorWindow>(); var window = CreateInstance<ESOErrorWindow>();
window.titleContent = new GUIContent("Setup Outfit"); window.titleContent = new GUIContent("Setup Outfit");
window.header = header; window.header = header;
@ -99,14 +103,14 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
internal class EasySetupOutfit internal static class EasySetupOutfit
{ {
private const int PRIORITY = 49; private const int PRIORITY = 49;
private static string[] errorMessageGroups; private static string[] errorMessageGroups;
private static string errorHeader; private static string errorHeader;
[MenuItem("GameObject/ModularAvatar/Setup Outfit", false, PRIORITY)] [MenuItem("GameObject/ModularAvatar/Setup Outfit", false, PRIORITY)]
static void SetupOutfit(MenuCommand cmd) internal static void SetupOutfit(MenuCommand cmd)
{ {
if (!ValidateSetupOutfit()) if (!ValidateSetupOutfit())
{ {
@ -130,6 +134,23 @@ namespace nadena.dev.modular_avatar.core.editor
merge.LockMode = ArmatureLockMode.BaseToMerge; merge.LockMode = ArmatureLockMode.BaseToMerge;
merge.InferPrefixSuffix(); merge.InferPrefixSuffix();
HeuristicBoneMapper.RenameBonesByHeuristic(merge); HeuristicBoneMapper.RenameBonesByHeuristic(merge);
var avatarRootMatchingArmature = avatarRoot.transform.Find(outfitArmature.gameObject.name);
if (merge.prefix == "" && merge.suffix == "" && avatarRootMatchingArmature != null)
{
// We have an armature whose names exactly match the root armature - this can cause some serious
// confusion in Unity's humanoid armature matching system. Fortunately, we can avoid this by
// renaming a bone close to the root; this will ensure the number of matching bones is small, and
// Unity's heuristics (apparently) will choose the base avatar's armature as the "true" armature.
outfitArmature.name += ".1";
// Also make sure to refresh the avatar's animator humanoid bone cache.
var avatarAnimator = avatarRoot.GetComponent<Animator>();
var humanDescription = avatarAnimator.avatar;
avatarAnimator.avatar = null;
// ReSharper disable once Unity.InefficientPropertyAccess
avatarAnimator.avatar = humanDescription;
}
} }
if (outfitRoot != null if (outfitRoot != null