mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 18:55:06 +08:00
feat: Unidirectional armature sync mode (#416)
This commit is contained in:
parent
f38eb55010
commit
f9e319bf49
@ -25,10 +25,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
buttonStyle.fixedWidth = 40f;
|
||||
buttonStyle.fixedHeight = EditorGUIUtility.singleLineHeight * 1.5f;
|
||||
}
|
||||
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal static void Show(
|
||||
@ -40,7 +39,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
window.titleContent = new GUIContent("Setup Outfit");
|
||||
window.header = header;
|
||||
window.messageGroups = messageGroups;
|
||||
|
||||
|
||||
// Compute required window size
|
||||
var height = 0f;
|
||||
var width = 450f;
|
||||
@ -55,9 +54,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
height += buttonStyle.fixedHeight;
|
||||
height += SeparatorSize;
|
||||
|
||||
|
||||
window.minSize = new Vector2(width, height);
|
||||
|
||||
|
||||
window.ShowModal();
|
||||
}
|
||||
|
||||
@ -79,6 +78,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
var finalRect = GUILayoutUtility.GetRect(SeparatorSize, SeparatorSize, GUILayout.ExpandWidth(true));
|
||||
@ -99,8 +99,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class EasySetupOutfit
|
||||
{
|
||||
private const int PRIORITY = 49;
|
||||
@ -115,7 +115,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
ESOErrorWindow.Show(errorHeader, errorMessageGroups);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!FindBones(cmd.context,
|
||||
out var avatarRoot, out var avatarHips, out var outfitHips)
|
||||
) return;
|
||||
@ -130,8 +130,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
merge = Undo.AddComponent<ModularAvatarMergeArmature>(outfitArmature.gameObject);
|
||||
merge.mergeTarget = new AvatarObjectReference();
|
||||
merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject);
|
||||
merge.LockMode = ArmatureLockMode.BaseToMerge;
|
||||
merge.InferPrefixSuffix();
|
||||
}
|
||||
|
||||
HeuristicBoneMapper.RenameBonesByHeuristic(merge);
|
||||
|
||||
if (outfitRoot != null
|
||||
@ -235,18 +237,18 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
static bool ValidateSetupOutfit()
|
||||
{
|
||||
errorHeader = S("setup_outfit.err.header.notarget");
|
||||
errorMessageGroups = new string[] { S("setup_outfit.err.unknown") };
|
||||
|
||||
errorMessageGroups = new string[] {S("setup_outfit.err.unknown")};
|
||||
|
||||
if (Selection.objects.Length == 0)
|
||||
{
|
||||
errorMessageGroups = new string[] { S("setup_outfit.err.no_selection") };
|
||||
errorMessageGroups = new string[] {S("setup_outfit.err.no_selection")};
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var obj in Selection.objects)
|
||||
{
|
||||
errorHeader = S_f("setup_outfit.err.header", obj.name);
|
||||
|
||||
|
||||
if (!(obj is GameObject gameObj)) return false;
|
||||
var xform = gameObj.transform;
|
||||
|
||||
@ -281,7 +283,8 @@ 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;
|
||||
@ -289,7 +292,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)
|
||||
{
|
||||
@ -309,18 +312,19 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
};
|
||||
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))
|
||||
if (maybeHips.name == avatarBoneMappings[HumanBodyBones.Hips] &&
|
||||
!maybeHips.IsChildOf(outfitRoot.transform))
|
||||
{
|
||||
avatarHips = maybeHips.gameObject;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (avatarHips == null)
|
||||
{
|
||||
errorMessageGroups = new string[]
|
||||
@ -352,8 +356,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
{
|
||||
@ -362,7 +367,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
hipsCandidates.Add(hbm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach (Transform child in outfitRoot.transform)
|
||||
{
|
||||
foreach (Transform tempHip in child)
|
||||
|
@ -7,14 +7,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
[CustomEditor(typeof(ModularAvatarMergeArmature))]
|
||||
internal class MergeArmatureEditor : MAEditorBase
|
||||
{
|
||||
private SerializedProperty prop_mergeTarget, prop_prefix, prop_suffix, prop_locked, prop_mangleNames;
|
||||
private SerializedProperty prop_mergeTarget, prop_prefix, prop_suffix, prop_lock_mode, prop_mangleNames;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
prop_mergeTarget = serializedObject.FindProperty(nameof(ModularAvatarMergeArmature.mergeTarget));
|
||||
prop_prefix = serializedObject.FindProperty(nameof(ModularAvatarMergeArmature.prefix));
|
||||
prop_suffix = serializedObject.FindProperty(nameof(ModularAvatarMergeArmature.suffix));
|
||||
prop_locked = serializedObject.FindProperty(nameof(ModularAvatarMergeArmature.locked));
|
||||
prop_lock_mode = serializedObject.FindProperty(nameof(ModularAvatarMergeArmature.LockMode));
|
||||
prop_mangleNames = serializedObject.FindProperty(nameof(ModularAvatarMergeArmature.mangleNames));
|
||||
}
|
||||
|
||||
@ -26,11 +26,60 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
EditorGUILayout.PropertyField(prop_prefix, G("merge_armature.prefix"));
|
||||
EditorGUILayout.PropertyField(prop_suffix, G("merge_armature.suffix"));
|
||||
EditorGUILayout.PropertyField(prop_mangleNames, G("merge_armature.mangle_names"));
|
||||
EditorGUILayout.PropertyField(prop_locked, G("merge_armature.locked"));
|
||||
|
||||
EditorGUILayout.Separator();
|
||||
EditorGUILayout.LabelField(S("merge_armature.lockmode"), EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.BeginVertical();
|
||||
|
||||
FakeEnum(prop_lock_mode, ArmatureLockMode.NotLocked, S("merge_armature.lockmode.not_locked.title"),
|
||||
S("merge_armature.lockmode.not_locked.body"));
|
||||
FakeEnum(prop_lock_mode, ArmatureLockMode.BaseToMerge, S("merge_armature.lockmode.base_to_merge.title"),
|
||||
S("merge_armature.lockmode.base_to_merge.body"));
|
||||
FakeEnum(prop_lock_mode, ArmatureLockMode.BidirectionalExact,
|
||||
S("merge_armature.lockmode.bidirectional.title"),
|
||||
S("merge_armature.lockmode.bidirectional.body"));
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private void FakeEnum(SerializedProperty propLockMode, ArmatureLockMode index, string label, string desc)
|
||||
{
|
||||
var val = !propLockMode.hasMultipleDifferentValues && propLockMode.enumValueIndex == (int) index;
|
||||
|
||||
var selectionStyle = val ? (GUIStyle) "flow node 1" : (GUIStyle) "flow node 0";
|
||||
selectionStyle.padding = new RectOffset(0, 0, 0, 0);
|
||||
selectionStyle.margin = new RectOffset(0, 0, 5, 5);
|
||||
|
||||
var boldLabel = EditorStyles.boldLabel;
|
||||
boldLabel.wordWrap = true;
|
||||
|
||||
var normalLabel = EditorStyles.label;
|
||||
normalLabel.wordWrap = true;
|
||||
|
||||
EditorGUILayout.BeginVertical(selectionStyle);
|
||||
|
||||
EditorGUILayout.LabelField(label, boldLabel);
|
||||
var l1 = GUILayoutUtility.GetLastRect();
|
||||
EditorGUILayout.Separator();
|
||||
EditorGUILayout.LabelField(desc, normalLabel);
|
||||
var l2 = GUILayoutUtility.GetLastRect();
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
var rect = GUILayoutUtility.GetLastRect();
|
||||
|
||||
if (GUI.Button(rect, GUIContent.none, selectionStyle))
|
||||
{
|
||||
propLockMode.enumValueIndex = (int) index;
|
||||
}
|
||||
|
||||
EditorGUI.LabelField(l1, label, boldLabel);
|
||||
EditorGUI.LabelField(l2, desc, normalLabel);
|
||||
}
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
var target = (ModularAvatarMergeArmature) this.target;
|
||||
@ -50,6 +99,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Separator();
|
||||
|
||||
var enable_name_assignment =
|
||||
target.mergeTarget.Get(RuntimeUtil.FindAvatarInParents(target.transform)) != null;
|
||||
using (var scope = new EditorGUI.DisabledScope(!enable_name_assignment))
|
||||
|
@ -51,6 +51,15 @@
|
||||
"merge_animator.path_mode.tooltip": "How to interpret paths in animations. Using relative mode lets you record animations from an animator on this object.",
|
||||
"merge_animator.match_avatar_write_defaults": "Match Avatar Write Defaults",
|
||||
"merge_animator.match_avatar_write_defaults.tooltip": "Match the write defaults setting used on the avatar's animator. If the avatar's write defaults settings are inconsistent, the settings on the animator will be left alone.",
|
||||
|
||||
"merge_armature.lockmode": "Position sync mode",
|
||||
"merge_armature.lockmode.not_locked.title": "Not locked",
|
||||
"merge_armature.lockmode.not_locked.body": "Merged armature does not sync its position with the base avatar.",
|
||||
"merge_armature.lockmode.base_to_merge.title": "Avatar =====> Target (Unidirectional)",
|
||||
"merge_armature.lockmode.base_to_merge.body": "Moving the base avatar will move the merge armature. If you move the merged armature, it will not affect the base avatar. This is useful when adding normal outfits, where you might want to adjust the position of bones in the outfit.",
|
||||
"merge_armature.lockmode.bidirectional.title": "Avatar <=====> Target (Bidirectional)",
|
||||
"merge_armature.lockmode.bidirectional.body": "The base armature and the merged armature will always have the same position. This is useful when creating animations that are meant to target the base armature. In order to activate this, your armatures must already be in the exact same position.",
|
||||
|
||||
"worldfixed.quest": "This component is not compatible with the standalone Oculus Quest and will have no effect.",
|
||||
"worldfixed.normal": "This object will be fixed to world unless you fixed to avatar with constraint.",
|
||||
"fpvisible.normal": "This object will be visible in your first person view.",
|
||||
|
@ -40,6 +40,14 @@
|
||||
"merge_armature.adjust_names.tooltip": "統合先のボーン名に合わせて、衣装のボーン名を合わせて変更します。統合先アバターに非対応の衣装導入向け機能です。",
|
||||
"merge_armature.mangle_names": "名前かぶりを回避",
|
||||
"merge_armature.mangle_names.tooltip": "ほかのアセットとの名前かぶりを裂けるため、新規ボーンの名前を自動で変更する",
|
||||
"merge_armature.lockmode": "位置追従モード",
|
||||
"merge_armature.lockmode.not_locked.title": "追従なし",
|
||||
"merge_armature.lockmode.not_locked.body": "統合されるアーマチュアは、統合先のアーマチュアに追従しません。",
|
||||
"merge_armature.lockmode.base_to_merge.title": "アバター =====> オブジェクト (一方的)",
|
||||
"merge_armature.lockmode.base_to_merge.body": "アバターを動かすと、統合されるアーマチュアも追従しますが、統合されるアーマチュアを動いたらアバターが動きません。通常の衣装追加なら、この設定のほうはボーンの位置調整を保持するのでお勧めです。",
|
||||
"merge_armature.lockmode.bidirectional.title": "アバター <=====> オブジェクト (双方向)",
|
||||
"merge_armature.lockmode.bidirectional.body": "アバターと統合されるアーマチュアは常に同じ位置になります。元のアバターを操作するアニメーションを作る時に便利かもしれません。なお、起動するには、すでに全く同位置にする必要があります。",
|
||||
|
||||
"path_mode.Relative": "相対的(このオブジェクトからのパスを使用)",
|
||||
"path_mode.Absolute": "絶対的(アバタールートからのパスを使用)",
|
||||
"merge_animator.animator": "統合されるアニメーター",
|
||||
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 077f9b1712f9416a9bb0143c3ad5e934
|
||||
timeCreated: 1693673568
|
@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.armature_lock
|
||||
{
|
||||
internal class ArmatureLockController : IDisposable
|
||||
{
|
||||
// Undo operations can reinitialize the MAMA component, which destroys critical lock controller state.
|
||||
// Avoid this issue by keeping a static reference to the controller for each MAMA component.
|
||||
private static Dictionary<ModularAvatarMergeArmature, ArmatureLockController>
|
||||
_controllers = new Dictionary<ModularAvatarMergeArmature, ArmatureLockController>();
|
||||
|
||||
public delegate IReadOnlyList<(Transform, Transform)> GetTransformsDelegate();
|
||||
|
||||
private readonly ModularAvatarMergeArmature _mama;
|
||||
private readonly GetTransformsDelegate _getTransforms;
|
||||
private IArmatureLock _lock;
|
||||
|
||||
private bool _updateActive;
|
||||
|
||||
private bool UpdateActive
|
||||
{
|
||||
get => _updateActive;
|
||||
set
|
||||
{
|
||||
if (UpdateActive == value) return;
|
||||
#if UNITY_EDITOR
|
||||
if (value)
|
||||
{
|
||||
EditorApplication.update += VoidUpdate;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorApplication.update -= VoidUpdate;
|
||||
}
|
||||
|
||||
_updateActive = value;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private ArmatureLockMode _curMode, _mode;
|
||||
|
||||
public ArmatureLockMode Mode
|
||||
{
|
||||
get => _mode;
|
||||
set
|
||||
{
|
||||
if (value == _mode) return;
|
||||
|
||||
_mode = value;
|
||||
|
||||
UpdateActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _enabled;
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
if (Enabled == value) return;
|
||||
_enabled = value;
|
||||
if (_enabled) UpdateActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
public ArmatureLockController(ModularAvatarMergeArmature mama, GetTransformsDelegate getTransforms)
|
||||
{
|
||||
AssemblyReloadEvents.beforeAssemblyReload += Dispose;
|
||||
|
||||
this._mama = mama;
|
||||
this._getTransforms = getTransforms;
|
||||
}
|
||||
|
||||
public static ArmatureLockController ForMerge(ModularAvatarMergeArmature mama,
|
||||
GetTransformsDelegate getTransforms)
|
||||
{
|
||||
if (!_controllers.TryGetValue(mama, out var controller))
|
||||
{
|
||||
controller = new ArmatureLockController(mama, getTransforms);
|
||||
_controllers[mama] = controller;
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
public bool IsStable()
|
||||
{
|
||||
if (Mode == ArmatureLockMode.NotLocked) return true;
|
||||
|
||||
if (_curMode == _mode && _lock?.IsStable() == true) return true;
|
||||
return RebuildLock() && (_lock?.IsStable() ?? false);
|
||||
}
|
||||
|
||||
private void VoidUpdate()
|
||||
{
|
||||
Update();
|
||||
}
|
||||
|
||||
internal bool Update()
|
||||
{
|
||||
LockResult result;
|
||||
if (!Enabled)
|
||||
{
|
||||
UpdateActive = false;
|
||||
_lock?.Dispose();
|
||||
_lock = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_curMode == _mode)
|
||||
{
|
||||
result = _lock?.Execute() ?? LockResult.Failed;
|
||||
if (result != LockResult.Failed) return true;
|
||||
}
|
||||
|
||||
if (!RebuildLock()) return false;
|
||||
|
||||
result = (_lock?.Execute() ?? LockResult.Failed);
|
||||
|
||||
return result != LockResult.Failed;
|
||||
}
|
||||
|
||||
private bool RebuildLock()
|
||||
{
|
||||
_lock?.Dispose();
|
||||
_lock = null;
|
||||
|
||||
var xforms = _getTransforms();
|
||||
if (xforms == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case ArmatureLockMode.BidirectionalExact:
|
||||
_lock = new BidirectionalArmatureLock(_getTransforms());
|
||||
break;
|
||||
case ArmatureLockMode.BaseToMerge:
|
||||
_lock = new OnewayArmatureLock(_getTransforms());
|
||||
break;
|
||||
default:
|
||||
UpdateActive = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_lock = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_curMode = _mode;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_lock?.Dispose();
|
||||
_lock = null;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AssemblyReloadEvents.beforeAssemblyReload -= Dispose;
|
||||
#endif
|
||||
|
||||
_controllers.Remove(_mama);
|
||||
UpdateActive = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 323d19fa721747ecbc3bc9c71805f630
|
||||
timeCreated: 1693713247
|
@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Jobs;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.armature_lock
|
||||
{
|
||||
internal class BidirectionalArmatureLock : IDisposable, IArmatureLock
|
||||
{
|
||||
private bool _disposed;
|
||||
private TransformAccessArray _baseBoneAccess, _mergeBoneAccess;
|
||||
private readonly Transform[] _baseBones, _mergeBones, _baseParentBones, _mergeParentBones;
|
||||
|
||||
private NativeArray<TransformState> BaseBones, MergeBones, SavedMerge;
|
||||
private NativeArray<bool> ShouldWriteBase, ShouldWriteMerge;
|
||||
private NativeIntPtr WroteAny;
|
||||
|
||||
private JobHandle LastOp;
|
||||
|
||||
public BidirectionalArmatureLock(IReadOnlyList<(Transform, Transform)> bones)
|
||||
{
|
||||
_baseBones = new Transform[bones.Count];
|
||||
_mergeBones = new Transform[bones.Count];
|
||||
_baseParentBones = new Transform[bones.Count];
|
||||
_mergeParentBones = new Transform[bones.Count];
|
||||
|
||||
BaseBones = new NativeArray<TransformState>(_baseBones.Length, Allocator.Persistent);
|
||||
MergeBones = new NativeArray<TransformState>(_baseBones.Length, Allocator.Persistent);
|
||||
SavedMerge = new NativeArray<TransformState>(_baseBones.Length, Allocator.Persistent);
|
||||
|
||||
for (int i = 0; i < _baseBones.Length; i++)
|
||||
{
|
||||
var (mergeBone, baseBone) = bones[i];
|
||||
_baseBones[i] = baseBone;
|
||||
_mergeBones[i] = mergeBone;
|
||||
_baseParentBones[i] = baseBone.parent;
|
||||
_mergeParentBones[i] = mergeBone.parent;
|
||||
|
||||
var mergeState = TransformState.FromTransform(mergeBone);
|
||||
SavedMerge[i] = mergeState;
|
||||
MergeBones[i] = mergeState;
|
||||
BaseBones[i] = TransformState.FromTransform(baseBone);
|
||||
}
|
||||
|
||||
_baseBoneAccess = new TransformAccessArray(_baseBones);
|
||||
_mergeBoneAccess = new TransformAccessArray(_mergeBones);
|
||||
|
||||
ShouldWriteBase = new NativeArray<bool>(_baseBones.Length, Allocator.Persistent);
|
||||
ShouldWriteMerge = new NativeArray<bool>(_baseBones.Length, Allocator.Persistent);
|
||||
WroteAny = new NativeIntPtr(Allocator.Persistent);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
struct Compute : IJobParallelForTransform
|
||||
{
|
||||
public NativeArray<TransformState> BaseBones, SavedMerge;
|
||||
|
||||
[WriteOnly] public NativeArray<TransformState> MergeBones;
|
||||
|
||||
[WriteOnly] public NativeArray<bool> ShouldWriteBase, ShouldWriteMerge;
|
||||
|
||||
[WriteOnly] public NativeIntPtr.Parallel WroteAny;
|
||||
|
||||
public void Execute(int index, TransformAccess mergeTransform)
|
||||
{
|
||||
var baseBone = BaseBones[index];
|
||||
var mergeBone = new TransformState()
|
||||
{
|
||||
localPosition = mergeTransform.localPosition,
|
||||
localRotation = mergeTransform.localRotation,
|
||||
localScale = mergeTransform.localScale,
|
||||
};
|
||||
MergeBones[index] = mergeBone;
|
||||
|
||||
var saved = SavedMerge[index];
|
||||
|
||||
if (TransformState.Differs(saved, mergeBone))
|
||||
{
|
||||
ShouldWriteBase[index] = true;
|
||||
ShouldWriteMerge[index] = false;
|
||||
|
||||
var mergeToBase = mergeBone;
|
||||
BaseBones[index] = mergeToBase;
|
||||
SavedMerge[index] = mergeBone;
|
||||
WroteAny.SetOne();
|
||||
}
|
||||
else if (TransformState.Differs(saved, baseBone))
|
||||
{
|
||||
ShouldWriteMerge[index] = true;
|
||||
ShouldWriteBase[index] = false;
|
||||
|
||||
MergeBones[index] = baseBone;
|
||||
SavedMerge[index] = baseBone;
|
||||
WroteAny.SetOne();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShouldWriteBase[index] = false;
|
||||
ShouldWriteMerge[index] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
struct Commit : IJobParallelForTransform
|
||||
{
|
||||
[ReadOnly] public NativeArray<TransformState> BoneState;
|
||||
[ReadOnly] public NativeArray<bool> ShouldWrite;
|
||||
|
||||
public void Execute(int index, TransformAccess transform)
|
||||
{
|
||||
if (ShouldWrite[index])
|
||||
{
|
||||
var boneState = BoneState[index];
|
||||
|
||||
transform.localPosition = boneState.localPosition;
|
||||
transform.localRotation = boneState.localRotation;
|
||||
transform.localScale = boneState.localScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
LastOp.Complete();
|
||||
|
||||
_baseBoneAccess.Dispose();
|
||||
_mergeBoneAccess.Dispose();
|
||||
BaseBones.Dispose();
|
||||
MergeBones.Dispose();
|
||||
SavedMerge.Dispose();
|
||||
ShouldWriteBase.Dispose();
|
||||
ShouldWriteMerge.Dispose();
|
||||
WroteAny.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private bool DoCompute(out JobHandle handle)
|
||||
{
|
||||
handle = default;
|
||||
|
||||
if (_disposed) return false;
|
||||
|
||||
WroteAny.Value = 0;
|
||||
|
||||
LastOp.Complete();
|
||||
|
||||
var readBase = new ReadBone()
|
||||
{
|
||||
_state = BaseBones,
|
||||
}.Schedule(_baseBoneAccess);
|
||||
LastOp = handle = new Compute()
|
||||
{
|
||||
BaseBones = BaseBones,
|
||||
MergeBones = MergeBones,
|
||||
SavedMerge = SavedMerge,
|
||||
ShouldWriteBase = ShouldWriteBase,
|
||||
ShouldWriteMerge = ShouldWriteMerge,
|
||||
WroteAny = WroteAny.GetParallel(),
|
||||
}.Schedule(_mergeBoneAccess, readBase);
|
||||
|
||||
// Check parents haven't changed
|
||||
for (int i = 0; i < _baseBones.Length; i++)
|
||||
{
|
||||
if (_baseBones[i].parent != _baseParentBones[i] || _mergeBones[i].parent != _mergeParentBones[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsStable()
|
||||
{
|
||||
if (!DoCompute(out var compute)) return false;
|
||||
|
||||
compute.Complete();
|
||||
|
||||
return WroteAny.Value == 0;
|
||||
}
|
||||
|
||||
public LockResult Execute()
|
||||
{
|
||||
if (!DoCompute(out var compute)) return LockResult.Failed;
|
||||
|
||||
var commitBase = new Commit()
|
||||
{
|
||||
BoneState = BaseBones,
|
||||
ShouldWrite = ShouldWriteBase,
|
||||
}.Schedule(_baseBoneAccess, compute);
|
||||
var commitMerge = new Commit()
|
||||
{
|
||||
BoneState = MergeBones,
|
||||
ShouldWrite = ShouldWriteMerge,
|
||||
}.Schedule(_mergeBoneAccess, compute);
|
||||
|
||||
commitBase.Complete();
|
||||
commitMerge.Complete();
|
||||
|
||||
if (WroteAny.Value == 0)
|
||||
{
|
||||
return LockResult.NoOp;
|
||||
}
|
||||
else
|
||||
{
|
||||
return LockResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e5ed2de355448b0934da7691cd6b584
|
||||
timeCreated: 1693712306
|
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.armature_lock
|
||||
{
|
||||
internal interface IArmatureLock : IDisposable
|
||||
{
|
||||
LockResult Execute();
|
||||
bool IsStable();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b4b88c94c2144128ffbe7f271b28ba2
|
||||
timeCreated: 1693712261
|
@ -0,0 +1,9 @@
|
||||
namespace nadena.dev.modular_avatar.core.armature_lock
|
||||
{
|
||||
internal enum LockResult
|
||||
{
|
||||
Failed,
|
||||
Success,
|
||||
NoOp,
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 361faa0a05e34f7b8fbd1b2ae73d27bf
|
||||
timeCreated: 1693713933
|
@ -0,0 +1,271 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
using UnityEngine.Jobs;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.armature_lock
|
||||
{
|
||||
internal class OnewayArmatureLock : IDisposable, IArmatureLock
|
||||
{
|
||||
struct BoneStaticData
|
||||
{
|
||||
public Matrix4x4 _mat_l, _mat_r;
|
||||
}
|
||||
|
||||
private NativeArray<BoneStaticData> _boneStaticData;
|
||||
private NativeArray<TransformState> _mergeSavedState;
|
||||
private NativeArray<TransformState> _baseState, _mergeState;
|
||||
|
||||
private NativeIntPtr _fault, _wroteAny;
|
||||
|
||||
private readonly Transform[] _baseBones, _mergeBones, _baseParentBones, _mergeParentBones;
|
||||
private TransformAccessArray _baseBonesAccessor, _mergeBonesAccessor;
|
||||
|
||||
private bool _disposed;
|
||||
private JobHandle LastOp;
|
||||
|
||||
[BurstCompile]
|
||||
struct WriteBone : IJobParallelForTransform
|
||||
{
|
||||
[ReadOnly] public NativeIntPtr _fault, _shouldWrite;
|
||||
|
||||
[ReadOnly] public NativeArray<TransformState> _values;
|
||||
|
||||
public void Execute(int index, TransformAccess transform)
|
||||
{
|
||||
if (_fault.Value == 0 && _shouldWrite.Value != 0)
|
||||
{
|
||||
var val = _values[index];
|
||||
transform.localPosition = val.localPosition;
|
||||
transform.localRotation = val.localRotation;
|
||||
transform.localScale = val.localScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
struct ComputePosition : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<BoneStaticData> _boneStatic;
|
||||
|
||||
[ReadOnly] public NativeArray<TransformState> _mergeState;
|
||||
[ReadOnly] public NativeArray<TransformState> _baseState;
|
||||
|
||||
public NativeArray<TransformState> _mergeSavedState;
|
||||
|
||||
public NativeIntPtr.Parallel _fault, _wroteAny;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var boneStatic = _boneStatic[index];
|
||||
var mergeState = _mergeState[index];
|
||||
var baseState = _baseState[index];
|
||||
var mergeSaved = _mergeSavedState[index];
|
||||
|
||||
var basePos = baseState.localPosition;
|
||||
var baseRot = baseState.localRotation;
|
||||
var baseScale = baseState.localScale;
|
||||
|
||||
if (TransformState.Differs(mergeSaved, mergeState))
|
||||
{
|
||||
_fault.Increment();
|
||||
}
|
||||
|
||||
var relTransform = boneStatic._mat_l * Matrix4x4.TRS(basePos, baseRot, baseScale) * boneStatic._mat_r;
|
||||
|
||||
var targetMergePos = relTransform.MultiplyPoint(Vector3.zero);
|
||||
var targetMergeRot = relTransform.rotation;
|
||||
var targetMergeScale = relTransform.lossyScale;
|
||||
|
||||
var newState = new TransformState
|
||||
{
|
||||
localPosition = targetMergePos,
|
||||
localRotation = targetMergeRot,
|
||||
localScale = targetMergeScale
|
||||
};
|
||||
|
||||
if (TransformState.Differs(mergeSaved, newState))
|
||||
{
|
||||
_wroteAny.SetOne();
|
||||
_mergeSavedState[index] = newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public OnewayArmatureLock(IReadOnlyList<(Transform, Transform)> mergeToBase)
|
||||
{
|
||||
_boneStaticData = new NativeArray<BoneStaticData>(mergeToBase.Count, Allocator.Persistent);
|
||||
_mergeSavedState = new NativeArray<TransformState>(mergeToBase.Count, Allocator.Persistent);
|
||||
_baseState = new NativeArray<TransformState>(mergeToBase.Count, Allocator.Persistent);
|
||||
_mergeState = new NativeArray<TransformState>(mergeToBase.Count, Allocator.Persistent);
|
||||
|
||||
_fault = new NativeIntPtr(Allocator.Persistent);
|
||||
_wroteAny = new NativeIntPtr(Allocator.Persistent);
|
||||
|
||||
_baseBones = new Transform[mergeToBase.Count];
|
||||
_mergeBones = new Transform[mergeToBase.Count];
|
||||
_baseParentBones = new Transform[mergeToBase.Count];
|
||||
_mergeParentBones = new Transform[mergeToBase.Count];
|
||||
|
||||
for (int i = 0; i < mergeToBase.Count; i++)
|
||||
{
|
||||
var (mergeBone, baseBone) = mergeToBase[i];
|
||||
var mergeParent = mergeBone.parent;
|
||||
var baseParent = baseBone.parent;
|
||||
|
||||
if (mergeParent == null || baseParent == null)
|
||||
{
|
||||
throw new Exception("Can't handle root objects");
|
||||
}
|
||||
|
||||
_baseBones[i] = baseBone;
|
||||
_mergeBones[i] = mergeBone;
|
||||
_baseParentBones[i] = baseParent;
|
||||
_mergeParentBones[i] = mergeParent;
|
||||
|
||||
_baseState[i] = TransformState.FromTransform(baseBone);
|
||||
_mergeSavedState[i] = _mergeState[i] = TransformState.FromTransform(mergeBone);
|
||||
|
||||
// We want to emulate the hierarchy:
|
||||
// baseParent
|
||||
// - baseBone
|
||||
// - v_mergeBone
|
||||
//
|
||||
// However our hierarchy actually is:
|
||||
// mergeParent
|
||||
// - mergeBone
|
||||
//
|
||||
// Our question is: What is the local affine transform of mergeBone -> mergeParent space, given a new
|
||||
// baseBone -> baseParent affine transform?
|
||||
|
||||
// First, relative to baseBone, what is the local affine transform of mergeBone?
|
||||
var mat_l = baseBone.worldToLocalMatrix * mergeBone.localToWorldMatrix;
|
||||
// We also find parent -> mergeParent
|
||||
var mat_r = mergeParent.worldToLocalMatrix * baseParent.localToWorldMatrix;
|
||||
// Now we can multiply:
|
||||
// (baseParent -> mergeParent) * (baseBone -> baseParent) * (mergeBone -> baseBone)
|
||||
// = (baseParent -> mergeParent) * (mergeBone -> baseParent)
|
||||
// = (mergeBone -> mergeParent)
|
||||
|
||||
_boneStaticData[i] = new BoneStaticData()
|
||||
{
|
||||
_mat_l = mat_r,
|
||||
_mat_r = mat_l
|
||||
};
|
||||
}
|
||||
|
||||
_baseBonesAccessor = new TransformAccessArray(_baseBones);
|
||||
_mergeBonesAccessor = new TransformAccessArray(_mergeBones);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AssemblyReloadEvents.beforeAssemblyReload += Dispose;
|
||||
#endif
|
||||
}
|
||||
|
||||
private bool DoCompute(out JobHandle handle)
|
||||
{
|
||||
handle = default;
|
||||
if (_disposed) return false;
|
||||
|
||||
_fault.Value = 0;
|
||||
_wroteAny.Value = 0;
|
||||
|
||||
var jobReadBase = new ReadBone
|
||||
{
|
||||
_state = _baseState
|
||||
}.Schedule(_baseBonesAccessor);
|
||||
var jobReadMerged = new ReadBone
|
||||
{
|
||||
_state = _mergeState
|
||||
}.Schedule(_mergeBonesAccessor);
|
||||
var readAll = JobHandle.CombineDependencies(jobReadBase, jobReadMerged);
|
||||
LastOp = handle = new ComputePosition
|
||||
{
|
||||
_boneStatic = _boneStaticData,
|
||||
_mergeState = _mergeState,
|
||||
_baseState = _baseState,
|
||||
_mergeSavedState = _mergeSavedState,
|
||||
_fault = _fault.GetParallel(),
|
||||
_wroteAny = _wroteAny.GetParallel(),
|
||||
}.Schedule(_baseBones.Length, 32, readAll);
|
||||
|
||||
// Validate parents while that job is running
|
||||
for (int i = 0; i < _baseBones.Length; i++)
|
||||
{
|
||||
if (_baseBones[i].parent != _baseParentBones[i] || _mergeBones[i].parent != _mergeParentBones[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsStable()
|
||||
{
|
||||
if (!DoCompute(out var compute)) return false;
|
||||
|
||||
compute.Complete();
|
||||
|
||||
return _fault.Value == 0 && _wroteAny.Value == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the armature lock job.
|
||||
/// </summary>
|
||||
/// <returns>True if successful, false if cached data was invalidated and needs recreating</returns>
|
||||
public LockResult Execute()
|
||||
{
|
||||
if (!DoCompute(out var compute)) return LockResult.Failed;
|
||||
|
||||
var commit = new WriteBone()
|
||||
{
|
||||
_fault = _fault,
|
||||
_values = _mergeSavedState,
|
||||
_shouldWrite = _wroteAny
|
||||
}.Schedule(_mergeBonesAccessor, compute);
|
||||
|
||||
commit.Complete();
|
||||
|
||||
if (_fault.Value != 0)
|
||||
{
|
||||
return LockResult.Failed;
|
||||
}
|
||||
else if (_wroteAny.Value == 0)
|
||||
{
|
||||
return LockResult.NoOp;
|
||||
}
|
||||
else
|
||||
{
|
||||
return LockResult.Success;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
LastOp.Complete();
|
||||
_boneStaticData.Dispose();
|
||||
_mergeSavedState.Dispose();
|
||||
_baseState.Dispose();
|
||||
_mergeState.Dispose();
|
||||
_fault.Dispose();
|
||||
_wroteAny.Dispose();
|
||||
_baseBonesAccessor.Dispose();
|
||||
_mergeBonesAccessor.Dispose();
|
||||
_disposed = true;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AssemblyReloadEvents.beforeAssemblyReload -= Dispose;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 869a3bad24fd44d8964b6a7dca81bd26
|
||||
timeCreated: 1693640255
|
@ -0,0 +1,22 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using UnityEngine.Jobs;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.armature_lock
|
||||
{
|
||||
[BurstCompile]
|
||||
internal struct ReadBone : IJobParallelForTransform
|
||||
{
|
||||
public NativeArray<TransformState> _state;
|
||||
|
||||
public void Execute(int index, TransformAccess transform)
|
||||
{
|
||||
_state[index] = new TransformState
|
||||
{
|
||||
localPosition = transform.localPosition,
|
||||
localRotation = transform.localRotation,
|
||||
localScale = transform.localScale
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df590c12d16249608a9d8a8204b154bf
|
||||
timeCreated: 1693712551
|
@ -0,0 +1,49 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Unity.Burst;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.armature_lock
|
||||
{
|
||||
internal struct TransformState
|
||||
{
|
||||
private const float POS_EPSILON = 0.00001f;
|
||||
private const float ROT_EPSILON = 0.00001f;
|
||||
private const float SCALE_EPSILON = 0.00001f;
|
||||
|
||||
public Vector3 localPosition;
|
||||
public Quaternion localRotation;
|
||||
public Vector3 localScale;
|
||||
|
||||
public static TransformState FromTransform(Transform mergeBone)
|
||||
{
|
||||
return new TransformState
|
||||
{
|
||||
localPosition = mergeBone.localPosition,
|
||||
localRotation = mergeBone.localRotation,
|
||||
localScale = mergeBone.localScale
|
||||
};
|
||||
}
|
||||
|
||||
public void ToTransform(Transform bone)
|
||||
{
|
||||
Undo.RecordObject(bone, Undo.GetCurrentGroupName());
|
||||
bone.localPosition = localPosition;
|
||||
bone.localRotation = localRotation;
|
||||
bone.localScale = localScale;
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Differs(TransformState self, TransformState other)
|
||||
{
|
||||
var deltaMergePos = (self.localPosition - other.localPosition).sqrMagnitude;
|
||||
var deltaMergeRot = self.localRotation * Quaternion.Inverse(other.localRotation);
|
||||
var deltaMergeScale = (self.localScale - other.localScale).sqrMagnitude;
|
||||
|
||||
return (deltaMergePos > POS_EPSILON
|
||||
|| Quaternion.Angle(deltaMergeRot, Quaternion.identity) > ROT_EPSILON
|
||||
|| deltaMergeScale > SCALE_EPSILON);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47655dad667d40afa5b75d67913c069c
|
||||
timeCreated: 1693712494
|
@ -24,69 +24,102 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.core.armature_lock;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
[Serializable]
|
||||
public enum ArmatureLockMode
|
||||
{
|
||||
Legacy,
|
||||
NotLocked,
|
||||
BaseToMerge,
|
||||
BidirectionalExact
|
||||
}
|
||||
|
||||
[ExecuteInEditMode]
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Modular Avatar/MA Merge Armature")]
|
||||
public class ModularAvatarMergeArmature : AvatarTagComponent
|
||||
{
|
||||
private const float POS_EPSILON = 0.001f * 0.001f;
|
||||
private const float ROT_EPSILON = 0.001f * 0.001f;
|
||||
|
||||
public AvatarObjectReference mergeTarget = new AvatarObjectReference();
|
||||
public GameObject mergeTargetObject => mergeTarget.Get(this);
|
||||
|
||||
public string prefix = "";
|
||||
public string suffix = "";
|
||||
public bool locked = false;
|
||||
|
||||
[FormerlySerializedAs("locked")] public bool legacyLocked;
|
||||
|
||||
public ArmatureLockMode LockMode = ArmatureLockMode.Legacy;
|
||||
|
||||
public bool mangleNames = true;
|
||||
|
||||
private class BoneBinding
|
||||
private ArmatureLockController _lockController;
|
||||
|
||||
internal Transform FindCorrespondingBone(Transform bone, Transform baseParent)
|
||||
{
|
||||
public Transform baseBone;
|
||||
public Transform mergeBone;
|
||||
var childName = bone.gameObject.name;
|
||||
|
||||
public Vector3 lastLocalPos;
|
||||
public Vector3 lastLocalScale;
|
||||
public Quaternion lastLocalRot;
|
||||
if (!childName.StartsWith(prefix) || !childName.EndsWith(suffix)) return null;
|
||||
var targetObjectName = childName.Substring(prefix.Length,
|
||||
childName.Length - prefix.Length - suffix.Length);
|
||||
return baseParent.Find(targetObjectName);
|
||||
}
|
||||
|
||||
private List<BoneBinding> lockedBones;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
MigrateLockConfig();
|
||||
RuntimeUtil.delayCall(SetLockMode);
|
||||
}
|
||||
|
||||
RuntimeUtil.delayCall(() =>
|
||||
private void SetLockMode()
|
||||
{
|
||||
if (_lockController == null)
|
||||
{
|
||||
if (this == null) return;
|
||||
_lockController = ArmatureLockController.ForMerge(this, GetBonesForLock);
|
||||
}
|
||||
|
||||
CheckLock();
|
||||
});
|
||||
if (_lockController.Mode != LockMode)
|
||||
{
|
||||
_lockController.Mode = LockMode;
|
||||
|
||||
if (!_lockController.IsStable())
|
||||
{
|
||||
_lockController.Mode = LockMode = ArmatureLockMode.NotLocked;
|
||||
}
|
||||
}
|
||||
|
||||
_lockController.Enabled = isActiveAndEnabled;
|
||||
}
|
||||
|
||||
private void MigrateLockConfig()
|
||||
{
|
||||
if (LockMode == ArmatureLockMode.Legacy)
|
||||
{
|
||||
LockMode = legacyLocked ? ArmatureLockMode.BidirectionalExact : ArmatureLockMode.BaseToMerge;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
RuntimeUtil.delayCall(CheckLock);
|
||||
MigrateLockConfig();
|
||||
|
||||
SetLockMode();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
RuntimeUtil.delayCall(CheckLock);
|
||||
_lockController.Enabled = false;
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.update -= EditorUpdate;
|
||||
#endif
|
||||
_lockController?.Dispose();
|
||||
_lockController = null;
|
||||
}
|
||||
|
||||
public override void ResolveReferences()
|
||||
@ -94,116 +127,32 @@ namespace nadena.dev.modular_avatar.core
|
||||
mergeTarget?.Get(this);
|
||||
}
|
||||
|
||||
void EditorUpdate()
|
||||
private List<(Transform, Transform)> GetBonesForLock()
|
||||
{
|
||||
if (this == null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.update -= EditorUpdate;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
var mergeRoot = this.transform;
|
||||
var baseRoot = mergeTarget.Get(this);
|
||||
|
||||
if (!locked || lockedBones == null)
|
||||
{
|
||||
CheckLock();
|
||||
}
|
||||
if (baseRoot == null) return null;
|
||||
|
||||
if (lockedBones != null)
|
||||
List<(Transform, Transform)> mergeBones = new List<(Transform, Transform)>();
|
||||
|
||||
ScanHierarchy(mergeRoot, baseRoot.transform);
|
||||
|
||||
return mergeBones;
|
||||
|
||||
|
||||
void ScanHierarchy(Transform merge, Transform baseBone)
|
||||
{
|
||||
foreach (var bone in lockedBones)
|
||||
foreach (Transform t in merge)
|
||||
{
|
||||
if (bone.baseBone == null || bone.mergeBone == null)
|
||||
var baseChild = FindCorrespondingBone(t, baseBone);
|
||||
if (baseChild != null)
|
||||
{
|
||||
lockedBones = null;
|
||||
break;
|
||||
}
|
||||
|
||||
var mergeBone = bone.mergeBone;
|
||||
var correspondingObject = bone.baseBone;
|
||||
bool lockBasePosition = bone.baseBone == mergeTargetObject.transform;
|
||||
|
||||
if ((mergeBone.localPosition - bone.lastLocalPos).sqrMagnitude > POS_EPSILON
|
||||
|| (mergeBone.localScale - bone.lastLocalScale).sqrMagnitude > POS_EPSILON
|
||||
|| Quaternion.Angle(bone.lastLocalRot, mergeBone.localRotation) > ROT_EPSILON)
|
||||
{
|
||||
if (lockBasePosition) mergeBone.position = correspondingObject.position;
|
||||
else correspondingObject.localPosition = mergeBone.localPosition;
|
||||
|
||||
correspondingObject.localScale = mergeBone.localScale;
|
||||
correspondingObject.localRotation = mergeBone.localRotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lockBasePosition) mergeBone.position = correspondingObject.position;
|
||||
else mergeBone.localPosition = correspondingObject.localPosition;
|
||||
mergeBone.localScale = correspondingObject.localScale;
|
||||
mergeBone.localRotation = correspondingObject.localRotation;
|
||||
}
|
||||
|
||||
bone.lastLocalPos = mergeBone.localPosition;
|
||||
bone.lastLocalScale = mergeBone.localScale;
|
||||
bone.lastLocalRot = mergeBone.localRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckLock()
|
||||
{
|
||||
if (RuntimeUtil.isPlaying) return;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.update -= EditorUpdate;
|
||||
#endif
|
||||
|
||||
bool shouldLock = locked && isActiveAndEnabled;
|
||||
bool wasLocked = lockedBones != null;
|
||||
if (shouldLock != wasLocked)
|
||||
{
|
||||
if (!shouldLock)
|
||||
{
|
||||
lockedBones = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mergeTargetObject == null) return;
|
||||
lockedBones = new List<BoneBinding>();
|
||||
|
||||
foreach (var xform in GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
Transform baseObject = FindCorresponding(xform);
|
||||
|
||||
if (baseObject == null) continue;
|
||||
|
||||
lockedBones.Add(new BoneBinding()
|
||||
{
|
||||
baseBone = baseObject,
|
||||
mergeBone = xform,
|
||||
lastLocalPos = xform.localPosition,
|
||||
lastLocalScale = xform.localScale,
|
||||
lastLocalRot = xform.localRotation
|
||||
});
|
||||
mergeBones.Add((t, baseChild));
|
||||
ScanHierarchy(t, baseChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (shouldLock)
|
||||
{
|
||||
EditorApplication.update += EditorUpdate;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private Transform FindCorresponding(Transform xform)
|
||||
{
|
||||
if (xform == null) return null;
|
||||
if (xform == transform) return mergeTargetObject.transform;
|
||||
|
||||
var correspondingParent = FindCorresponding(xform.parent);
|
||||
if (correspondingParent == null) return null;
|
||||
|
||||
return correspondingParent.Find(prefix + xform.name + suffix);
|
||||
}
|
||||
|
||||
public void InferPrefixSuffix()
|
||||
|
3
Packages/nadena.dev.modular-avatar/Runtime/Util.meta
Normal file
3
Packages/nadena.dev.modular-avatar/Runtime/Util.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6599bbb2aea142b1896d8db008b3759e
|
||||
timeCreated: 1693642951
|
392
Packages/nadena.dev.modular-avatar/Runtime/Util/NativeIntPtr.cs
Normal file
392
Packages/nadena.dev.modular-avatar/Runtime/Util/NativeIntPtr.cs
Normal file
@ -0,0 +1,392 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="NativeIntPtr.cs" company="Jackson Dunstan">
|
||||
// Copyright 2018 Jackson Dunstan
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
|
||||
namespace nadena.dev.modular_avatar.JacksonDunstan.NativeCollections
|
||||
{
|
||||
/// <summary>
|
||||
/// A pointer to an int stored in native (i.e. unmanaged) memory
|
||||
/// </summary>
|
||||
[NativeContainer]
|
||||
[NativeContainerSupportsDeallocateOnJobCompletion]
|
||||
[DebuggerTypeProxy(typeof(NativeIntPtrDebugView))]
|
||||
[DebuggerDisplay("Value = {Value}")]
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct NativeIntPtr : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// An atomic write-only version of the object suitable for use in a
|
||||
/// ParallelFor job
|
||||
/// </summary>
|
||||
[NativeContainer]
|
||||
[NativeContainerIsAtomicWriteOnly]
|
||||
public struct Parallel
|
||||
{
|
||||
/// <summary>
|
||||
/// Pointer to the value in native memory
|
||||
/// </summary>
|
||||
[NativeDisableUnsafePtrRestriction] internal readonly int* m_Buffer;
|
||||
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
/// <summary>
|
||||
/// A handle to information about what operations can be safely
|
||||
/// performed on the object at any given time.
|
||||
/// </summary>
|
||||
internal AtomicSafetyHandle m_Safety;
|
||||
|
||||
/// <summary>
|
||||
/// Create a parallel version of the object
|
||||
/// </summary>
|
||||
///
|
||||
/// <param name="value">
|
||||
/// Pointer to the value
|
||||
/// </param>
|
||||
///
|
||||
/// <param name="safety">
|
||||
/// Atomic safety handle for the object
|
||||
/// </param>
|
||||
internal Parallel(int* value, AtomicSafetyHandle safety)
|
||||
{
|
||||
m_Buffer = value;
|
||||
m_Safety = safety;
|
||||
}
|
||||
#else
|
||||
/// <summary>
|
||||
/// Create a parallel version of the object
|
||||
/// </summary>
|
||||
///
|
||||
/// <param name="value">
|
||||
/// Pointer to the value
|
||||
/// </param>
|
||||
internal Parallel(int* value)
|
||||
{
|
||||
m_Buffer = value;
|
||||
}
|
||||
#endif
|
||||
/// <summary>
|
||||
/// Sets this flag value to one.
|
||||
/// </summary>
|
||||
/// (added in Modular Avatar)
|
||||
[WriteAccessRequired]
|
||||
public void SetOne()
|
||||
{
|
||||
RequireWriteAccess();
|
||||
*m_Buffer = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increment the stored value
|
||||
/// </summary>
|
||||
///
|
||||
/// <returns>
|
||||
/// This object
|
||||
/// </returns>
|
||||
[WriteAccessRequired]
|
||||
public void Increment()
|
||||
{
|
||||
RequireWriteAccess();
|
||||
Interlocked.Increment(ref *m_Buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrement the stored value
|
||||
/// </summary>
|
||||
///
|
||||
/// <returns>
|
||||
/// This object
|
||||
/// </returns>
|
||||
[WriteAccessRequired]
|
||||
public void Decrement()
|
||||
{
|
||||
RequireWriteAccess();
|
||||
Interlocked.Decrement(ref *m_Buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add to the stored value
|
||||
/// </summary>
|
||||
///
|
||||
/// <param name="value">
|
||||
/// Value to add. Use negative values for subtraction.
|
||||
/// </param>
|
||||
///
|
||||
/// <returns>
|
||||
/// This object
|
||||
/// </returns>
|
||||
[WriteAccessRequired]
|
||||
public void Add(int value)
|
||||
{
|
||||
RequireWriteAccess();
|
||||
Interlocked.Add(ref *m_Buffer, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throw an exception if the object isn't writable
|
||||
/// </summary>
|
||||
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
|
||||
private void RequireWriteAccess()
|
||||
{
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pointer to the value in native memory. Must be named exactly this
|
||||
/// way to allow for [NativeContainerSupportsDeallocateOnJobCompletion]
|
||||
/// </summary>
|
||||
[NativeDisableUnsafePtrRestriction] internal int* m_Buffer;
|
||||
|
||||
/// <summary>
|
||||
/// Allocator used to create the backing memory
|
||||
///
|
||||
/// This field must be named this way to comply with
|
||||
/// [NativeContainerSupportsDeallocateOnJobCompletion]
|
||||
/// </summary>
|
||||
internal readonly Allocator m_AllocatorLabel;
|
||||
|
||||
// These fields are all required when safety checks are enabled
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
/// <summary>
|
||||
/// A handle to information about what operations can be safely
|
||||
/// performed on the object at any given time.
|
||||
/// </summary>
|
||||
private AtomicSafetyHandle m_Safety;
|
||||
|
||||
/// <summary>
|
||||
/// A handle that can be used to tell if the object has been disposed
|
||||
/// yet or not, which allows for error-checking double disposal.
|
||||
/// </summary>
|
||||
[NativeSetClassTypeToNullOnSchedule] private DisposeSentinel m_DisposeSentinel;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Allocate memory and set the initial value
|
||||
/// </summary>
|
||||
///
|
||||
/// <param name="allocator">
|
||||
/// Allocator to allocate and deallocate with. Must be valid.
|
||||
/// </param>
|
||||
///
|
||||
/// <param name="initialValue">
|
||||
/// Initial value of the allocated memory
|
||||
/// </param>
|
||||
public NativeIntPtr(Allocator allocator, int initialValue = 0)
|
||||
{
|
||||
// Require a valid allocator
|
||||
if (allocator <= Allocator.None)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Allocator must be Temp, TempJob or Persistent",
|
||||
"allocator");
|
||||
}
|
||||
|
||||
// Allocate the memory for the value
|
||||
m_Buffer = (int*) UnsafeUtility.Malloc(
|
||||
sizeof(int),
|
||||
UnsafeUtility.AlignOf<int>(),
|
||||
allocator);
|
||||
|
||||
// Store the allocator to use when deallocating
|
||||
m_AllocatorLabel = allocator;
|
||||
|
||||
// Create the dispose sentinel
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
#if UNITY_2018_3_OR_NEWER
|
||||
DisposeSentinel.Create(out m_Safety, out m_DisposeSentinel, 0, allocator);
|
||||
#else
|
||||
DisposeSentinel.Create(out m_Safety, out m_DisposeSentinel, 0);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Set the initial value
|
||||
*m_Buffer = initialValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or set the contained value
|
||||
///
|
||||
/// This operation requires read access to the node for 'get' and write
|
||||
/// access to the node for 'set'.
|
||||
/// </summary>
|
||||
///
|
||||
/// <value>
|
||||
/// The contained value
|
||||
/// </value>
|
||||
public int Value
|
||||
{
|
||||
get
|
||||
{
|
||||
RequireReadAccess();
|
||||
return *m_Buffer;
|
||||
}
|
||||
|
||||
[WriteAccessRequired]
|
||||
set
|
||||
{
|
||||
RequireWriteAccess();
|
||||
*m_Buffer = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a version of this object suitable for use in a ParallelFor job
|
||||
/// </summary>
|
||||
///
|
||||
/// <returns>
|
||||
/// A version of this object suitable for use in a ParallelFor job
|
||||
/// </returns>
|
||||
public Parallel GetParallel()
|
||||
{
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
Parallel parallel = new Parallel(m_Buffer, m_Safety);
|
||||
AtomicSafetyHandle.UseSecondaryVersion(ref parallel.m_Safety);
|
||||
#else
|
||||
Parallel parallel = new Parallel(m_Buffer);
|
||||
#endif
|
||||
return parallel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the underlying unmanaged memory has been created and not
|
||||
/// freed via a call to <see cref="Dispose"/>.
|
||||
///
|
||||
/// This operation has no access requirements.
|
||||
///
|
||||
/// This operation is O(1).
|
||||
/// </summary>
|
||||
///
|
||||
/// <value>
|
||||
/// Initially true when a non-default constructor is called but
|
||||
/// initially false when the default constructor is used. After
|
||||
/// <see cref="Dispose"/> is called, this becomes false. Note that
|
||||
/// calling <see cref="Dispose"/> on one copy of this object doesn't
|
||||
/// result in this becoming false for all copies if it was true before.
|
||||
/// This property should <i>not</i> be used to check whether the object
|
||||
/// is usable, only to check whether it was <i>ever</i> usable.
|
||||
/// </value>
|
||||
public bool IsCreated
|
||||
{
|
||||
get { return m_Buffer != null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Release the object's unmanaged memory. Do not use it after this. Do
|
||||
/// not call <see cref="Dispose"/> on copies of the object either.
|
||||
///
|
||||
/// This operation requires write access.
|
||||
///
|
||||
/// This complexity of this operation is O(1) plus the allocator's
|
||||
/// deallocation complexity.
|
||||
/// </summary>
|
||||
[WriteAccessRequired]
|
||||
public void Dispose()
|
||||
{
|
||||
RequireWriteAccess();
|
||||
|
||||
// Make sure we're not double-disposing
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
#if UNITY_2018_3_OR_NEWER
|
||||
DisposeSentinel.Dispose(ref m_Safety, ref m_DisposeSentinel);
|
||||
#else
|
||||
DisposeSentinel.Dispose(m_Safety, ref m_DisposeSentinel);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
UnsafeUtility.Free(m_Buffer, m_AllocatorLabel);
|
||||
m_Buffer = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set whether both read and write access should be allowed. This is
|
||||
/// used for automated testing purposes only.
|
||||
/// </summary>
|
||||
///
|
||||
/// <param name="allowReadOrWriteAccess">
|
||||
/// If both read and write access should be allowed
|
||||
/// </param>
|
||||
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
|
||||
public void TestUseOnlySetAllowReadAndWriteAccess(
|
||||
bool allowReadOrWriteAccess)
|
||||
{
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
AtomicSafetyHandle.SetAllowReadOrWriteAccess(
|
||||
m_Safety,
|
||||
allowReadOrWriteAccess);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throw an exception if the object isn't readable
|
||||
/// </summary>
|
||||
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
|
||||
private void RequireReadAccess()
|
||||
{
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throw an exception if the object isn't writable
|
||||
/// </summary>
|
||||
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
|
||||
private void RequireWriteAccess()
|
||||
{
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a debugger view of <see cref="NativeIntPtr"/>.
|
||||
/// </summary>
|
||||
internal sealed class NativeIntPtrDebugView
|
||||
{
|
||||
/// <summary>
|
||||
/// The object to provide a debugger view for
|
||||
/// </summary>
|
||||
private NativeIntPtr m_Ptr;
|
||||
|
||||
/// <summary>
|
||||
/// Create the debugger view
|
||||
/// </summary>
|
||||
///
|
||||
/// <param name="ptr">
|
||||
/// The object to provide a debugger view for
|
||||
/// </param>
|
||||
public NativeIntPtrDebugView(NativeIntPtr ptr)
|
||||
{
|
||||
m_Ptr = ptr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the viewed object's value
|
||||
/// </summary>
|
||||
///
|
||||
/// <value>
|
||||
/// The viewed object's value
|
||||
/// </value>
|
||||
public int Value
|
||||
{
|
||||
get { return m_Ptr.Value; }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84f8f9617c5a45b4ad9ca1cac1ed9b43
|
||||
timeCreated: 1693642959
|
@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "nadena.dev.modular-avatar.core",
|
||||
"references": [],
|
||||
"references": [
|
||||
"GUID:2665a8d13d1b3f18800f46e256720795"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"VRCSDKBase.dll",
|
||||
|
@ -35,11 +35,21 @@ Where necessary, PhysBone objects will have their targets updated, and ParentCon
|
||||
As of Modular Avatar 1.7.0, it is now possible to perform nested merges - that is, merge an armature A into B, then
|
||||
merge B into C. Modular Avatar will automatically determine the correct order to apply these merges.
|
||||
|
||||
## Locked mode
|
||||
## Position lock mode
|
||||
|
||||
If the locked option is enabled, the position and rotation of the merged bones will be locked to their parents in the editor. This is a two-way relationship; if you move the merged bone, the avatar's bone will move, and vice versa.
|
||||
Position locking allows the outfit to follow the movement of the base avatar, even in edit mode. This is useful for
|
||||
testing animations and poses, and for creating screenshots. There are three options for position lock mode:
|
||||
|
||||
This is intended for use when animating non-humanoid bones. For example, you could use this to build an animator which can animate cat-ear movements.
|
||||
* Not locked - the outfit will not follow the base avatar in edit mode
|
||||
* Base =======> Target (Unidirectional) - When the base avatar moves, the outfit will move too. However, if you move the
|
||||
outfit, the base avatar will not move. This mode will preserve any adjustments you've made to the outfit's fit, and is
|
||||
recommended for normal use.
|
||||
* Base <======> Target (Bidirectional) - When the base avatar moves, the outfit will move too. If you move the outfit,
|
||||
the base avatar will move too. This mode is useful for certain advanced use cases, such as creating a prefab which
|
||||
animates the base avatar's hair or animal ears.
|
||||
|
||||
When you set up an outfit with "setup outfit", the position lock mode will be set to "Base =======> Target
|
||||
(Unidirectional)". You can change this in the inspector if desired.
|
||||
|
||||
## Object references
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 58 KiB |
@ -38,12 +38,19 @@ Transform以外のコンポーネントが入っているボーンがある場
|
||||
Modular Avatar 1.7.0以降、連鎖的に統合することができます。つまり、AをBに統合して、BをCに統合することができます。
|
||||
統合参照に応じてModular Avatarが自動的に統合順序を計算します。
|
||||
|
||||
## 位置を固定
|
||||
## 位置追従モード
|
||||
|
||||
位置を固定を設定すると、統合先のボーンがエディタ上で元のボーンと同じ位置になります。これは総合的な関係で、どちらかのボーンが
|
||||
動けばもう片方が動きます。
|
||||
位置追従モードは、編集モードでベースアバターの動きに追従するようにする機能です。アニメーション制作やポーズの確認、スクリーンショットの
|
||||
撮影などに便利です。3つのモードがあります。
|
||||
|
||||
非・ヒューマノイドボーンのアニメーションを作る想定の機能です。たとえば、これでケモミミを動かすアニメーションを作ることができます。
|
||||
* 追従なし - 編集モードでベースアバターの動きに追従しません。
|
||||
* アバター =======> ターゲット(一方向) - ベースアバターが動くと衣装も動きます。衣装を動かしてもベースアバターは動きません。
|
||||
このモードは衣装のフィット調整を保持すので、通常の衣装導入なら推奨です。
|
||||
* アバター <======> ターゲット(双方向) - ベースアバターが動くと衣装も動きます。衣装を動かすとベースアバターも動きます。
|
||||
このモードは、ベースアバターの髪やケモミミを動かすアニメーションを作るなど、高度な用途向けです。
|
||||
|
||||
「Setup Outfit」で衣装を導入すると、位置追従モードは「アバター =======> ターゲット(一方向)」に自動設定されます。
|
||||
必要に応じてインスペクタで変更してください。
|
||||
|
||||
## オブジェクト引用
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 68 KiB |
Loading…
Reference in New Issue
Block a user