Merge branch 'main' into enhancement/AddSubMenuCreatorModule

This commit is contained in:
raiti-chan 2022-12-11 22:07:17 +09:00
commit 18cad7c7ee
19 changed files with 392 additions and 19 deletions

View File

@ -36,6 +36,8 @@ namespace nadena.dev.modular_avatar.core.editor
internal class AnimatorCombiner
{
private readonly AnimatorController _combined;
private AnimatorOverrideController _overrideController;
private List<AnimatorControllerLayer> _layers = new List<AnimatorControllerLayer>();
@ -90,6 +92,20 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
public void AddOverrideController(string basePath, AnimatorOverrideController overrideController, bool? writeDefaults)
{
AnimatorController controller = overrideController.runtimeAnimatorController as AnimatorController;
if (controller == null) return;
_overrideController = overrideController;
try
{
this.AddController(basePath, controller, writeDefaults);
} finally
{
_overrideController = null;
}
}
private void insertLayer(string basePath, AnimatorControllerLayer layer, bool first, bool? writeDefaults)
{
var newLayer = new AnimatorControllerLayer()
@ -260,6 +276,16 @@ namespace nadena.dev.modular_avatar.core.editor
throw new Exception($"Unknown type referenced from animator: {original.GetType()}");
}
// When using AnimatorOverrideController, replace the original AnimationClip based on AnimatorOverrideController.
if (_overrideController != null && original is AnimationClip srcClip)
{
T overrideClip = _overrideController[srcClip] as T;
if (overrideClip != null)
{
original = overrideClip;
}
}
if (cloneMap == null) cloneMap = new Dictionary<Object, Object>();
if (cloneMap.ContainsKey(original))

View File

@ -37,6 +37,9 @@ namespace nadena.dev.modular_avatar.core.editor
[InitializeOnLoad]
public class AvatarProcessor : IVRCSDKPreprocessAvatarCallback, IVRCSDKPostprocessAvatarCallback
{
// Place after EditorOnly processing (which runs at -1024) but hopefully before most other user callbacks
public int callbackOrder => -25;
public delegate void AvatarProcessorCallback(GameObject obj);
/// <summary>
@ -93,8 +96,6 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
public int callbackOrder => -9000;
public void OnPostprocessAvatar()
{
Util.DeleteTemporaryAssets();

View File

@ -33,15 +33,19 @@ namespace nadena.dev.modular_avatar.core.editor
[InitializeOnLoad]
internal static class ComponentAllowlistPatch
{
internal static readonly bool PATCH_OK;
static ComponentAllowlistPatch()
{
try
{
PatchAllowlist();
PATCH_OK = true;
}
catch (Exception e)
{
Debug.LogException(e);
PATCH_OK = false;
}
}

View File

@ -18,22 +18,26 @@ namespace nadena.dev.modular_avatar.core.editor
var avatarArmature = avatarHips.transform.parent;
var outfitArmature = outfitHips.transform.parent;
if (outfitArmature.GetComponent<ModularAvatarMergeArmature>() != null) return;
var merge = Undo.AddComponent<ModularAvatarMergeArmature>(outfitArmature.gameObject);
merge.mergeTarget = new AvatarObjectReference();
merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject);
merge.InferPrefixSuffix();
HeuristicBoneMapper.RenameBonesByHeuristic(merge);
}
[MenuItem("GameObject/[ModularAvatar] Setup Outfit", true, PRIORITY)]
static bool ValidateSetupOutfit()
{
if (Selection.objects.Length == 0) return false;
foreach (var obj in Selection.objects)
{
if (!(obj is GameObject gameObj)) return false;
var xform = gameObj.transform;
if (!FindBones(obj, out var _, out var _, out var outfitHips)
|| outfitHips.transform.parent.GetComponent<ModularAvatarMergeArmature>() != null)
if (!FindBones(obj, out var _, out var _, out var outfitHips))
{
return false;
}

View File

@ -0,0 +1,219 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using UnityEditor;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
internal class HeuristicBoneMapper
{
// 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
private static string[][] boneNamePatterns = new[]
{
new[] {"Hips", "Hip"},
new[] {"LeftUpperLeg", "UpperLeg_Left", "UpperLeg_L", "Leg_Left", "Leg_L"},
new[] {"RightUpperLeg", "UpperLeg_Right", "UpperLeg_R", "Leg_Right", "Leg_R"},
new[] {"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L"},
new[] {"RightLowerLeg", "LowerLeg_Right", "LowerLeg_R", "Knee_Right", "Knee_R"},
new[] {"LeftFoot", "Foot_Left", "Foot_L"},
new[] {"RightFoot", "Foot_Right", "Foot_R"},
new[] {"Spine"},
new[] {"Chest"},
new[] {"Neck"},
new[] {"Head"},
new[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"},
new[] {"RightShoulder", "Shoulder_Right", "Shoulder_R"},
new[] {"LeftUpperArm", "UpperArm_Left", "UpperArm_L", "Arm_Left", "Arm_L"},
new[] {"RightUpperArm", "UpperArm_Right", "UpperArm_R", "Arm_Right", "Arm_R"},
new[] {"LeftLowerArm", "LowerArm_Left", "LowerArm_L"},
new[] {"RightLowerArm", "LowerArm_Right", "LowerArm_R"},
new[] {"LeftHand", "Hand_Left", "Hand_L"},
new[] {"RightHand", "Hand_Right", "Hand_R"},
new[] {"LeftToes", "Toes_Left", "Toe_Left", "ToeIK_L", "Toes_L", "Toe_L"},
new[] {"RightToes", "Toes_Right", "Toe_Right", "ToeIK_R", "Toes_R", "Toe_R"},
new[] {"LeftEye", "Eye_Left", "Eye_L"},
new[] {"RightEye", "Eye_Right", "Eye_R"},
new[] {"Jaw"},
new[] {"LeftThumbProximal", "ProximalThumb_Left", "ProximalThumb_L"},
new[] {"LeftThumbIntermediate", "IntermediateThumb_Left", "IntermediateThumb_L"},
new[] {"LeftThumbDistal", "DistalThumb_Left", "DistalThumb_L"},
new[] {"LeftIndexProximal", "ProximalIndex_Left", "ProximalIndex_L"},
new[] {"LeftIndexIntermediate", "IntermediateIndex_Left", "IntermediateIndex_L"},
new[] {"LeftIndexDistal", "DistalIndex_Left", "DistalIndex_L"},
new[] {"LeftMiddleProximal", "ProximalMiddle_Left", "ProximalMiddle_L"},
new[] {"LeftMiddleIntermediate", "IntermediateMiddle_Left", "IntermediateMiddle_L"},
new[] {"LeftMiddleDistal", "DistalMiddle_Left", "DistalMiddle_L"},
new[] {"LeftRingProximal", "ProximalRing_Left", "ProximalRing_L"},
new[] {"LeftRingIntermediate", "IntermediateRing_Left", "IntermediateRing_L"},
new[] {"LeftRingDistal", "DistalRing_Left", "DistalRing_L"},
new[] {"LeftLittleProximal", "ProximalLittle_Left", "ProximalLittle_L"},
new[] {"LeftLittleIntermediate", "IntermediateLittle_Left", "IntermediateLittle_L"},
new[] {"LeftLittleDistal", "DistalLittle_Left", "DistalLittle_L"},
new[] {"RightThumbProximal", "ProximalThumb_Right", "ProximalThumb_R"},
new[] {"RightThumbIntermediate", "IntermediateThumb_Right", "IntermediateThumb_R"},
new[] {"RightThumbDistal", "DistalThumb_Right", "DistalThumb_R"},
new[] {"RightIndexProximal", "ProximalIndex_Right", "ProximalIndex_R"},
new[] {"RightIndexIntermediate", "IntermediateIndex_Right", "IntermediateIndex_R"},
new[] {"RightIndexDistal", "DistalIndex_Right", "DistalIndex_R"},
new[] {"RightMiddleProximal", "ProximalMiddle_Right", "ProximalMiddle_R"},
new[] {"RightMiddleIntermediate", "IntermediateMiddle_Right", "IntermediateMiddle_R"},
new[] {"RightMiddleDistal", "DistalMiddle_Right", "DistalMiddle_R"},
new[] {"RightRingProximal", "ProximalRing_Right", "ProximalRing_R"},
new[] {"RightRingIntermediate", "IntermediateRing_Right", "IntermediateRing_R"},
new[] {"RightRingDistal", "DistalRing_Right", "DistalRing_R"},
new[] {"RightLittleProximal", "ProximalLittle_Right", "ProximalLittle_R"},
new[] {"RightLittleIntermediate", "IntermediateLittle_Right", "IntermediateLittle_R"},
new[] {"RightLittleDistal", "DistalLittle_Right", "DistalLittle_R"},
new[] {"UpperChest"},
};
internal static string NormalizeName(string name)
{
return name.ToLowerInvariant()
.Replace("_", "")
.Replace(".", "")
.Replace(" ", "");
}
internal static readonly ImmutableDictionary<string, HumanBodyBones> NameToBoneMap;
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
static HeuristicBoneMapper()
{
var nameToBoneMap = new Dictionary<string, HumanBodyBones>();
var boneToNameMap = new Dictionary<HumanBodyBones, ImmutableList<string>>();
for (int i = 0; i < boneNamePatterns.Length; i++)
{
var bone = (HumanBodyBones) i;
foreach (var name in boneNamePatterns[i])
{
RegisterNameForBone(NormalizeName(name), bone);
}
}
void RegisterNameForBone(string name, HumanBodyBones bone)
{
nameToBoneMap[name] = bone;
if (!boneToNameMap.TryGetValue(bone, out var names))
{
names = ImmutableList<string>.Empty;
}
if (!names.Contains(name))
{
boneToNameMap[bone] = names.Add(name);
}
}
NameToBoneMap = nameToBoneMap.ToImmutableDictionary();
BoneToNameMap = boneToNameMap.ToImmutableDictionary();
}
/// <summary>
/// Examines the children of src, and tries to map them to the corresponding child of newParent.
/// Unmappable bones will not be added to the resulting dictionary. Ensures that each parent bone is only mapped
/// once.
/// </summary>
internal static Dictionary<Transform, Transform> AssignBoneMappings(
ModularAvatarMergeArmature config,
GameObject src,
GameObject newParent
)
{
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)
{
unassigned.Add(child);
}
foreach (Transform child in src.transform)
{
var childName = child.gameObject.name;
if (childName.StartsWith(config.prefix) && childName.EndsWith(config.suffix))
{
var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length);
var targetObject = newParent.transform.Find(targetObjectName);
if (targetObject != null && unassigned.Contains(targetObject))
{
mappings[child] = targetObject;
unassigned.Remove(targetObject);
}
else
{
heuristicAssignmentPass.Add(child);
}
}
}
Dictionary<string, Transform> lcNameToXform = new Dictionary<string, Transform>();
foreach (var target in unassigned)
{
lcNameToXform[NormalizeName(target.gameObject.name)] = target;
}
foreach (var child in heuristicAssignmentPass)
{
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 bodyBone))
{
continue;
}
foreach (var otherName in BoneToNameMap[bodyBone])
{
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
{
mappings[child] = targetObject;
unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant());
break;
}
}
}
return mappings;
}
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config)
{
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarInParents(config.transform));
if (target == null) return;
Traverse(config.transform, target.transform);
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 srcGameObj = pair.Key.gameObject;
var oldName = srcGameObj.name;
if (oldName != newName)
{
Undo.RecordObject(srcGameObj, "Applying heuristic mapping");
srcGameObj.name = newName;
PrefabUtility.RecordPrefabInstancePropertyModifications(srcGameObj);
}
Traverse(pair.Key, pair.Value);
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 51d014cbf5f24b3db8316c98e75d7efd
timeCreated: 1670642736

View File

@ -1,6 +1,7 @@
using UnityEditor;
using UnityEditor.Experimental.SceneManagement;
using UnityEngine;
using UnityEngine.SocialPlatforms;
namespace nadena.dev.modular_avatar.core.editor
{
@ -19,5 +20,10 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.HelpBox(Localization.S("hint.not_in_avatar"), MessageType.Warning);
}
}
public static void DisplayVRCSDKVersionWarning()
{
EditorGUILayout.HelpBox(Localization.S("hint.bad_vrcsdk"), MessageType.Error);
}
}
}

View File

@ -116,6 +116,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
InspectorCommon.DisplayOutOfAvatarWarning(targets);
if (!ComponentAllowlistPatch.PATCH_OK) InspectorCommon.DisplayVRCSDKVersionWarning();
OnInnerInspectorGUI();
}

View File

@ -49,6 +49,16 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
var enable_name_assignment =
target.mergeTarget.Get(RuntimeUtil.FindAvatarInParents(target.transform)) != null;
using (var scope = new EditorGUI.DisabledScope(!enable_name_assignment))
{
if (GUILayout.Button(G("merge_armature.adjust_names")))
{
HeuristicBoneMapper.RenameBonesByHeuristic(target);
}
}
Localization.ShowLanguageUI();
}
}

View File

@ -30,6 +30,8 @@
"merge_armature.suffix.tooltip": "Suffix expected on bones in this merged armature",
"merge_armature.locked": "Lock position",
"merge_armature.locked.tooltip": "Lock the position of this armature's bones to the target armature (and vice versa). Useful for creating animations.",
"merge_armature.adjust_names": "Adjust bone names to match target",
"merge_armature.adjust_names.tooltip": "Changes bone names to match the target avatar. Useful for porting outfits from one avatar to another.",
"path_mode.Relative": "Relative to this object",
"path_mode.Absolute": "Absolute (based on avatar root)",
"merge_animator.animator": "Animator to merge",
@ -53,5 +55,6 @@
"boneproxy.attachment": "Attachment mode",
"boneproxy.attachment.AsChildAtRoot": "As child; at root",
"boneproxy.attachment.AsChildKeepWorldPosition": "As child; keep position",
"pb_blocker.help": "This object will not be affected by PhysBones attached to parents."
"pb_blocker.help": "This object will not be affected by PhysBones attached to parents.",
"hint.bad_vrcsdk": "Incompatible version of VRCSDK detected.\n\nPlease try upgrading your VRCSDK; if this does not work, check for a newer version of Modular Avatar as well."
}

View File

@ -30,14 +30,16 @@
"merge_armature.suffix.tooltip": "このオブジェクトの子に付く後置詞",
"merge_armature.locked": "位置を固定",
"merge_armature.locked.tooltip": "このオブジェクトのボーンを統合先のボーンに常に相互的に位置を合わせる。アニメーション制作向け",
"path_mode.Relative": "相対的(このオブジェクトからのパースを使用)",
"path_mode.Absolute": "絶対的(アバタールートからのパースを使用)",
"merge_armature.adjust_names": "ボーン名を統合先に合わせる",
"merge_armature.adjust_names.tooltip": "統合先のボーン名に合わせて、衣装のボーン名を合わせて変更します。統合先アバターに非対応の衣装導入向け機能です。",
"path_mode.Relative": "相対的(このオブジェクトからのパスを使用)",
"path_mode.Absolute": "絶対的(アバタールートからのパスを使用)",
"merge_animator.animator": "統合されるアニメーター",
"merge_animator.layer_type": "レイヤー種別",
"merge_animator.delete_attached_animator": "付属アニメーターを削除",
"merge_animator.delete_attached_animator.tooltip": "統合後、このオブジェクトについているアニメーターを削除します",
"merge_animator.path_mode": "パスモード",
"merge_animator.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
"merge_animator.path_mode": "パスモード",
"merge_animator.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
"merge_animator.match_avatar_write_defaults": "アバターのWrite Defaults設定に合わせる",
"merge_animator.match_avatar_write_defaults.tooltip": "アバターの該当アニメーターのWrite Defaults設定に合わせます。アバター側の設定が矛盾する場合は、統合されるアニメーターのWD値がそのまま採用されます。",
"fpvisible.normal": "このオブジェクトは一人視点で表示されます。",
@ -53,5 +55,6 @@
"boneproxy.attachment": "配置モード",
"boneproxy.attachment.AsChildAtRoot": "子として・ルートに配置",
"boneproxy.attachment.AsChildKeepWorldPosition": "子として・ワールド位置を維持",
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。"
}
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。",
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでもだめでしたら、Modular Avatarにも最新版が出てないかチェックしてください。"
}

View File

@ -61,7 +61,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (!_installTargets.TryGetValue(installer.installTargetMenu, out var targetMenu)) return;
if (_installTargets.ContainsKey(installer.menuToAppend)) return;
targetMenu.controls.AddRange(installer.menuToAppend.controls);
// Clone before appending to sanitize menu icons
targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls);
while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
{
@ -100,9 +101,23 @@ namespace nadena.dev.modular_avatar.core.editor
newMenu = Object.Instantiate(menu);
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
_clonedMenus[menu] = newMenu;
foreach (var control in newMenu.controls)
{
if (Util.ValidateExpressionMenuIcon(control.icon) != Util.ValidateExpressionMenuIconResult.Success)
control.icon = null;
for (int i = 0; i < control.labels.Length; i++)
{
var label = control.labels[i];
var labelResult = Util.ValidateExpressionMenuIcon(label.icon);
if (labelResult != Util.ValidateExpressionMenuIconResult.Success)
{
label.icon = null;
control.labels[i] = label;
}
}
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
{
control.subMenu = CloneMenu(control.subMenu);

View File

@ -283,6 +283,8 @@ namespace nadena.dev.modular_avatar.core.editor
mergedSrcBone.transform.localScale = src.transform.localScale;
mergedSrcBone.transform.SetParent(newParent.transform, true);
if (zipMerge) PruneDuplicatePhysBones(newParent, src);
bool retain = HasAdditionalComponents(src, out var constraintType);
if (constraintType != null)
{
@ -359,5 +361,40 @@ namespace nadena.dev.modular_avatar.core.editor
return retain;
}
/**
* Sometimes outfit authors copy the entire armature, including PhysBones components. If we merge these and
* end up with multiple PB components referencing the same target, PB refuses to animate the bone. So detect
* and prune this case.
*
* For simplicity - we currently only detect the case where the physbone references the component it's on.
* TODO - detect duplicate colliders, contacts, et - these can cause perf issues but usually not quite as large
* of a correctness issue.
*/
private void PruneDuplicatePhysBones(GameObject baseBone, GameObject mergeBone)
{
bool hasSelfReferencePB = false;
foreach (var pb in baseBone.GetComponents<VRCPhysBone>())
{
var target = pb.rootTransform;
if (target == null || target == baseBone.transform)
{
hasSelfReferencePB = true;
break;
}
}
if (!hasSelfReferencePB) return;
foreach (var pb in mergeBone.GetComponents<VRCPhysBone>())
{
var target = pb.rootTransform;
if (target == null || target == baseBone.transform)
{
Object.DestroyImmediate(pb);
}
}
}
}
}

View File

@ -197,14 +197,24 @@ namespace nadena.dev.modular_avatar.core.editor
newBindPoses[i] = Bp;
}
var rootBone = renderer.rootBone;
var scaleBone = rootBone;
if (rootBone == null)
{
// Sometimes meshes have no root bone set. This is usually not ideal, but let's make sure we don't
// choke on the scale computation below.
scaleBone = renderer.bones[0];
}
dst.bindposes = newBindPoses;
renderer.bones = newBones;
renderer.sharedMesh = dst;
var newRootBone = BoneDatabase.GetRetargetedBone(renderer.rootBone, true);
var newRootBone = BoneDatabase.GetRetargetedBone(rootBone, true);
var newScaleBone = BoneDatabase.GetRetargetedBone(scaleBone, true);
var oldLossyScale = renderer.rootBone.transform.lossyScale;
var newLossyScale = newRootBone.transform.lossyScale;
var oldLossyScale = scaleBone.transform.lossyScale;
var newLossyScale = newScaleBone.transform.lossyScale;
var bounds = renderer.localBounds;
bounds.extents = new Vector3(

View File

@ -126,6 +126,12 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (willPurgeAnimators) break; // animator will be deleted in subsequent processing
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (anim.runtimeAnimatorController is AnimatorOverrideController overrideController)
{
anim.runtimeAnimatorController = Util.ConvertAnimatorController(overrideController);
}
var controller = anim.runtimeAnimatorController as AnimatorController;
if (controller != null)
{
@ -138,6 +144,12 @@ namespace nadena.dev.modular_avatar.core.editor
case ModularAvatarMergeAnimator merger:
{
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (merger.animator is AnimatorOverrideController overrideController)
{
merger.animator = Util.ConvertAnimatorController(overrideController);
}
var controller = merger.animator as AnimatorController;
if (controller != null)
{

View File

@ -118,6 +118,13 @@ namespace nadena.dev.modular_avatar.core.editor
return merger.Finish();
}
public static AnimatorController ConvertAnimatorController(AnimatorOverrideController overrideController)
{
var merger = new AnimatorCombiner();
merger.AddOverrideController("", overrideController, null);
return merger.Finish();
}
public static bool IsTemporaryAsset(Object obj)
{
var path = AssetDatabase.GetAssetPath(obj);

View File

@ -41,4 +41,10 @@ This is intended for use when animating non-humanoid bones. For example, you cou
## Object references
Although the editor UI allows you to drag in a target object for the merge armature component, internally this is saved as a path reference.
This allows the merge armature component to automatically restore its Merge Target after it is saved in a prefab.
This allows the merge armature component to automatically restore its Merge Target after it is saved in a prefab.
## Matching bone names
Since Merge Animator will attempt to match bones by name, just attaching it won't always work to make an outfit designed for one avatar work with another avatar.
You can click the "Adjust bone names to match target" button to attempt to rename bones in the outfit to match the base avatar it's currently attached to.
This will be done automatically if you added the Merge Armature component using the "Setup Outfit" menu item.

View File

@ -11,7 +11,7 @@ Merge Animatorコンポーネントは、指定したアニメーターをアバ
## 非推奨の場合
既存のレイヤーをそのままにして、指定したコントローラーを追加するだけです。完全に既存のアニメーターを置き換える場合は来通り
既存のレイヤーをそのままにして、指定したコントローラーを追加するだけです。完全に既存のアニメーターを置き換える場合は来通り
ユーザーに差し替えてもらいましょう。
## セットアップ方法

View File

@ -44,4 +44,10 @@ Transform以外のコンポーネントが入っているボーンがある場
## オブジェクト引用
エディタ上では統合先をドラッグアンドドロップで指定しますが、内部ではパスで保存されます。プレハブ化してもちゃんと統合先を保存できるということです。
エディタ上では統合先をドラッグアンドドロップで指定しますが、内部ではパスで保存されます。プレハブ化してもちゃんと統合先を保存できるということです。
## ボーン名合わせ
Merge Animatorがボーンを名前で照合するので、つけるだけでは非対応衣装がうまく動かない場合があります。
対策として、「ボーン名を統合先に合わせる」ボタンを押すことで、衣装側のボーン名を自動的にアバターのボーン名に合わせようとします。
なお、「Setup outfit」でMerge Armatureをつける場合はこの処理が自動的に走ります。