feat: Unidirectional armature sync mode (#416)

This commit is contained in:
bd_ 2023-09-24 16:59:43 +09:00 committed by GitHub
parent f38eb55010
commit f9e319bf49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1380 additions and 156 deletions

View File

@ -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)

View File

@ -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))

View File

@ -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.",

View File

@ -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": "統合されるアニメーター",

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 077f9b1712f9416a9bb0143c3ad5e934
timeCreated: 1693673568

View File

@ -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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 323d19fa721747ecbc3bc9c71805f630
timeCreated: 1693713247

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9e5ed2de355448b0934da7691cd6b584
timeCreated: 1693712306

View File

@ -0,0 +1,10 @@
using System;
namespace nadena.dev.modular_avatar.core.armature_lock
{
internal interface IArmatureLock : IDisposable
{
LockResult Execute();
bool IsStable();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7b4b88c94c2144128ffbe7f271b28ba2
timeCreated: 1693712261

View File

@ -0,0 +1,9 @@
namespace nadena.dev.modular_avatar.core.armature_lock
{
internal enum LockResult
{
Failed,
Success,
NoOp,
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 361faa0a05e34f7b8fbd1b2ae73d27bf
timeCreated: 1693713933

View File

@ -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
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 869a3bad24fd44d8964b6a7dca81bd26
timeCreated: 1693640255

View File

@ -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
};
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: df590c12d16249608a9d8a8204b154bf
timeCreated: 1693712551

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 47655dad667d40afa5b75d67913c069c
timeCreated: 1693712494

View File

@ -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()

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6599bbb2aea142b1896d8db008b3759e
timeCreated: 1693642951

View 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; }
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84f8f9617c5a45b4ad9ca1cac1ed9b43
timeCreated: 1693642959

View File

@ -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",

View File

@ -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

View File

@ -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