mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-04-11 23:19:00 +08:00
Merge branch 'main' into enhancement/AddSubMenuCreatorModule
This commit is contained in:
commit
18cad7c7ee
@ -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))
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
219
Packages/nadena.dev.modular-avatar/Editor/HeuristicBoneMapper.cs
Normal file
219
Packages/nadena.dev.modular-avatar/Editor/HeuristicBoneMapper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51d014cbf5f24b3db8316c98e75d7efd
|
||||
timeCreated: 1670642736
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -116,6 +116,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
|
||||
InspectorCommon.DisplayOutOfAvatarWarning(targets);
|
||||
if (!ComponentAllowlistPatch.PATCH_OK) InspectorCommon.DisplayVRCSDKVersionWarning();
|
||||
|
||||
OnInnerInspectorGUI();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
@ -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にも最新版が出てないかチェックしてください。"
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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.
|
@ -11,7 +11,7 @@ Merge Animatorコンポーネントは、指定したアニメーターをアバ
|
||||
|
||||
## 非推奨の場合
|
||||
|
||||
既存のレイヤーをそのままにして、指定したコントローラーを追加するだけです。完全に既存のアニメーターを置き換える場合は将来通り
|
||||
既存のレイヤーをそのままにして、指定したコントローラーを追加するだけです。完全に既存のアニメーターを置き換える場合は従来通り
|
||||
ユーザーに差し替えてもらいましょう。
|
||||
|
||||
## セットアップ方法
|
||||
|
@ -44,4 +44,10 @@ Transform以外のコンポーネントが入っているボーンがある場
|
||||
|
||||
## オブジェクト引用
|
||||
|
||||
エディタ上では統合先をドラッグアンドドロップで指定しますが、内部ではパスで保存されます。プレハブ化してもちゃんと統合先を保存できるということです。
|
||||
エディタ上では統合先をドラッグアンドドロップで指定しますが、内部ではパスで保存されます。プレハブ化してもちゃんと統合先を保存できるということです。
|
||||
|
||||
## ボーン名合わせ
|
||||
|
||||
Merge Animatorがボーンを名前で照合するので、つけるだけでは非対応衣装がうまく動かない場合があります。
|
||||
対策として、「ボーン名を統合先に合わせる」ボタンを押すことで、衣装側のボーン名を自動的にアバターのボーン名に合わせようとします。
|
||||
なお、「Setup outfit」でMerge Armatureをつける場合はこの処理が自動的に走ります。
|
Loading…
x
Reference in New Issue
Block a user