using System; using System.Collections.Generic; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using UnityEngine; using UnityEngine.Jobs; #if UNITY_EDITOR using UnityEditor; #endif namespace nadena.dev.modular_avatar.core.armature_lock { internal class MaMoveIndependentlyManager { internal static MaMoveIndependentlyManager Instance { get; } = new(); private MaMoveIndependentlyManager() { _nativeMemoryManager = new NativeMemoryManager(); _vpState = _nativeMemoryManager.CreateArray(); _tpState = _nativeMemoryManager.CreateArray(); _targetState = _nativeMemoryManager.CreateArray(); _mappingStates = _nativeMemoryManager.CreateArray(); _errorFlags = _nativeMemoryManager.CreateArray(); _enabled = _nativeMemoryManager.CreateArray(); _sceneRootParent = _nativeMemoryManager.CreateArray(); _falseArray = _nativeMemoryManager.CreateArray(); _anyError = new NativeArray(1, Allocator.Persistent); _anyDirty = new NativeArray(1, Allocator.Persistent); _nativeMemoryManager.OnSegmentMove += MoveTransforms; #if UNITY_EDITOR AssemblyReloadEvents.beforeAssemblyReload += OnDomainUnload; #endif } private void OnDomainUnload() { Dispose(); } private void Dispose() { _lastJob.Complete(); #if UNITY_EDITOR AssemblyReloadEvents.beforeAssemblyReload -= OnDomainUnload; #endif if (_virtualParents.isCreated) DeferDestroy.DeferDestroyObj(_virtualParents); if (_trueParents.isCreated) DeferDestroy.DeferDestroyObj(_trueParents); if (_targets.isCreated) DeferDestroy.DeferDestroyObj(_targets); _nativeMemoryManager.Dispose(); if (_anyError.IsCreated) _anyError.Dispose(); if (_anyDirty.IsCreated) _anyDirty.Dispose(); } private const float PosEpsilon = 0.0000001f; private const float RotEpsilon = 0.0000001f; private const float ScaleEpsilon = 0.0000001f; // Our basic strategy is to identify all children of MoveIndependently objects, and to find the first parent // that is not a member of the same MoveIndependently group. We then compute the local transform of the child // relative to that parent, and keep it constant (unless the true local transform of the child changes). // // If an active MAMoveIndep is a child of another MAMoveIndep, we consider it to be ungrouped (even if it's // named in the parent). private readonly NativeMemoryManager _nativeMemoryManager; private Transform[] _virtualParentsT; private Transform[] _trueParentsT; private Transform[] _targetsT; private TransformAccessArray _virtualParents; private TransformAccessArray _trueParents; private TransformAccessArray _targets; private bool _transformAccessDirty; private readonly NativeArrayRef _vpState; private readonly NativeArrayRef _tpState; private readonly NativeArrayRef _targetState; private readonly NativeArrayRef _mappingStates; private readonly NativeArrayRef _errorFlags; private NativeArray _anyError, _anyDirty; private readonly NativeArrayRef _enabled; private readonly NativeArrayRef _sceneRootParent, _falseArray; private readonly Dictionary _slotToState = new(); private struct MappingState { // Our last observed local transform, relative to our actual parent transform public TransformState TrueLocal; // Our last observed local transform, relative to our virtual parent transform public Matrix4x4 VirtualLocal; // The position of our parent relative to our virtual parent public Matrix4x4 TrueLocalToVirtualLocal; public bool RequestWriteback, CacheValid; } private class State { public MAMoveIndependently MoveIndep; public ISegment Segment; } private readonly Dictionary _moveIndeps = new(); private JobHandle _lastJob; private bool _isRegistered; private int _maxComputeDepth; private bool UpdateRegistered { get => _isRegistered; set { if (value == _isRegistered) return; if (value) { UpdateLoopController.OnMoveIndependentlyUpdate += Update; } else { UpdateLoopController.OnMoveIndependentlyUpdate -= Update; } _isRegistered = value; } } private void EnsureTransformCapacity(int targetLength) { if (_virtualParentsT == null) { _virtualParentsT = new Transform[targetLength]; _trueParentsT = new Transform[targetLength]; _targetsT = new Transform[targetLength]; return; } if (targetLength <= _virtualParentsT.Length) return; var newCapacity = Mathf.Max(_virtualParentsT.Length * 2, targetLength); Array.Resize(ref _virtualParentsT, newCapacity); Array.Resize(ref _trueParentsT, newCapacity); Array.Resize(ref _targetsT, newCapacity); } private void MoveTransforms(int oldoffset, int newoffset, int length) { Array.Copy(_virtualParentsT, oldoffset, _virtualParentsT, newoffset, length); Array.Copy(_trueParentsT, oldoffset, _trueParentsT, newoffset, length); Array.Copy(_targetsT, oldoffset, _targetsT, newoffset, length); _transformAccessDirty = true; } private void UpdateTransformAccess() { if (!_transformAccessDirty) return; UpdateTransformAccess(ref _virtualParents, _virtualParentsT); UpdateTransformAccess(ref _trueParents, _trueParentsT); UpdateTransformAccess(ref _targets, _targetsT); _transformAccessDirty = false; } private void UpdateTransformAccess(ref TransformAccessArray arr, Transform[] t) { if (!arr.isCreated || arr.length != t.Length) { if (arr.isCreated) arr.Dispose(); arr = new TransformAccessArray(t); } else { arr.SetTransforms(t); } } private void Update() { _lastJob.Complete(); UpdateTransformAccess(); _anyError[0] = false; _anyDirty[0] = false; var clearErrors = new JClearErrorFlags { ErrorFlags = _errorFlags }; var clearErrorsHandle = clearErrors.Schedule(_errorFlags.Length, 16); var readVp = new JReadTransforms { States = _vpState, Enabled = _enabled, ErrorFlags = _errorFlags, SceneRootParent = _sceneRootParent }; var readTp = new JReadTransforms { States = _tpState, Enabled = _enabled, ErrorFlags = _errorFlags, SceneRootParent = _falseArray }; var readTarget = new JReadTransforms { States = _targetState, Enabled = _enabled, ErrorFlags = _errorFlags, SceneRootParent = _falseArray }; var readVpHandle = readVp.Schedule(_virtualParents, clearErrorsHandle); var clearVpHandle = new JClearRootTransforms { States = _vpState, SceneRootParent = _sceneRootParent }.Schedule(_vpState.Length, 16, readVpHandle); var readTpHandle = readTp.Schedule(_trueParents, clearErrorsHandle); var readTargetHandle = readTarget.Schedule(_targets, clearErrorsHandle); var readHandle = JobHandle.CombineDependencies(clearVpHandle, readTpHandle, readTargetHandle); var compute = new JCompute { VpState = _vpState, TpState = _tpState, TargetState = _targetState, States = _mappingStates, AnyDirty = _anyDirty, AnyError = _anyError, ErrorFlags = _errorFlags, Enabled = _enabled }; var computeHandle = compute.Schedule(_mappingStates.Length, 16, readHandle); _lastJob = computeHandle; computeHandle.Complete(); List prefabRecord = null; if (_anyDirty[0]) { prefabRecord = new List(); for (var i = 0; i < _mappingStates.Length; i++) { if (_mappingStates[i].RequestWriteback) { #if UNITY_EDITOR Undo.RecordObject(_targets[i], "Move Independently"); #endif prefabRecord.Add(_targets[i]); } } } var writeback = new JWriteback { States = _mappingStates, Errors = _errorFlags, Enabled = _enabled, AnyError = _anyError }; var writebackHandle = writeback.Schedule(_targets, computeHandle); _lastJob = writebackHandle; writebackHandle.Complete(); if (prefabRecord != null) { foreach (var transform in prefabRecord) { #if UNITY_EDITOR PrefabUtility.RecordPrefabInstancePropertyModifications(transform); #endif } } if (_anyError[0]) { List reactivate = new(); for (var i = 0; i < _mappingStates.Length; i++) { if (_errorFlags[i] && _slotToState.TryGetValue(i, out var state)) { Deactivate(state); reactivate.Add(state.MoveIndep); } } foreach (var moveIndep in reactivate) { if (moveIndep != null) Activate(moveIndep); } } } internal void Activate(MAMoveIndependently moveIndep) { if (!_anyDirty.IsCreated) return; // domain reload timing issues if (_moveIndeps.TryGetValue(moveIndep, out var state)) Deactivate(state); HashSet groupedTransforms = new(); groupedTransforms.Add(moveIndep.transform); RegisterGroupedTransforms(moveIndep, groupedTransforms); List toReregister = new(); foreach (var t in groupedTransforms) { // If we have a direct child MAMI, we need it to change its virtual parent, so trigger a reregister // on it. if (t.TryGetComponent(out var mami) && mami != moveIndep) { toReregister.Add(mami); } } var ptr = moveIndep.transform.parent; while (ptr != null) { var parentMoveIndep = ptr.GetComponentInParent(); if (parentMoveIndep == null) break; RegisterGroupedTransforms(parentMoveIndep, groupedTransforms); ptr = parentMoveIndep.transform.parent; } // Compute leaf transforms List leafTransforms = new(); Walk(moveIndep.transform); var segment = _nativeMemoryManager.Allocate(leafTransforms.Count); EnsureTransformCapacity(segment.Offset + segment.Length); _transformAccessDirty = true; var virtualParent = moveIndep.transform.parent; while (virtualParent != null && groupedTransforms.Contains(virtualParent)) virtualParent = virtualParent.parent; for (var i = 0; i < leafTransforms.Count; i++) { var j = i + segment.Offset; _mappingStates[j] = new MappingState { CacheValid = false }; _virtualParentsT[j] = virtualParent; _trueParentsT[j] = leafTransforms[i].parent; _targetsT[j] = leafTransforms[i]; _enabled[j] = true; _sceneRootParent[j] = virtualParent == null; _slotToState[j] = state; } _moveIndeps[moveIndep] = new State { MoveIndep = moveIndep, Segment = segment }; UpdateRegistered = true; foreach (var mami in toReregister) { if (mami != null) Activate(mami); } void Walk(Transform t) { foreach (Transform child in t) { if (groupedTransforms.Contains(child)) { Walk(child); continue; } leafTransforms.Add(child); } } } private void RegisterGroupedTransforms(MAMoveIndependently moveIndep, HashSet groupedTransforms) { var candidates = new HashSet(moveIndep.GroupedBones); candidates.Add(moveIndep.gameObject); Walk(moveIndep.transform); void Walk(Transform t) { if (!candidates.Contains(t.gameObject)) return; groupedTransforms.Add(t); foreach (Transform child in t) { if (child.TryGetComponent(out _)) continue; Walk(child); } } } internal void Deactivate(MAMoveIndependently moveIndep) { if (_moveIndeps.TryGetValue(moveIndep, out var state)) Deactivate(state); } private void Deactivate(State state) { if (!_anyDirty.IsCreated) return; // domain reload timing issues for (var i = 0; i < state.Segment.Length; i++) { var j = i + state.Segment.Offset; _enabled[j] = false; _virtualParents[j] = null; _trueParents[j] = null; _targets[j] = null; _slotToState.Remove(j); } _nativeMemoryManager.Free(state.Segment); _moveIndeps.Remove(state.MoveIndep); if (_moveIndeps.Count == 0) UpdateRegistered = false; } [BurstCompile] private static bool MatDiffers(Matrix4x4 a, Matrix4x4 b) { var aPos = a.GetColumn(3); var bPos = b.GetColumn(3); if ((aPos - bPos).sqrMagnitude > PosEpsilon) return true; var aRot = a.rotation; var bRot = b.rotation; if (Quaternion.Angle(aRot, bRot) > RotEpsilon) return true; var aScale = a.lossyScale; var bScale = b.lossyScale; return (aScale - bScale).sqrMagnitude > ScaleEpsilon; } private struct JClearErrorFlags : IJobParallelFor { [WriteOnly] public NativeArray ErrorFlags; public void Execute(int index) { ErrorFlags[index] = false; } } // For some reason checking SceneRootParent in JReadTransforms was ignored...? // Maybe IJobParallelForTransform doesn't execute on null transforms. private struct JClearRootTransforms : IJobParallelFor { [WriteOnly] public NativeArray States; [ReadOnly] public NativeArray SceneRootParent; public void Execute(int index) { if (SceneRootParent[index]) { States[index] = new TransformState { localToWorldMatrix = Matrix4x4.identity, localRotation = Quaternion.identity, localScale = Vector3.one, localPosition = Vector3.zero }; } } } private struct JReadTransforms : IJobParallelForTransform { [WriteOnly] public NativeArray States; [ReadOnly] public NativeArray Enabled; [ReadOnly] public NativeArray SceneRootParent; [NativeDisableContainerSafetyRestriction] [WriteOnly] public NativeArray ErrorFlags; [BurstCompile] public void Execute(int index, TransformAccess transform) { if (!Enabled[index]) return; if (SceneRootParent[index]) return; if (!transform.isValid) { ErrorFlags[index] = true; return; } States[index] = new TransformState { localToWorldMatrix = transform.localToWorldMatrix, localRotation = transform.localRotation, localScale = transform.localScale, localPosition = transform.localPosition }; } } private struct JCompute : IJobParallelFor { [ReadOnly] public NativeArray VpState, TpState, TargetState; [WriteOnly] [NativeDisableContainerSafetyRestriction] public NativeArray AnyDirty; [WriteOnly] [NativeDisableContainerSafetyRestriction] public NativeArray AnyError; public NativeArray States; public NativeArray ErrorFlags; [ReadOnly] public NativeArray Enabled; [BurstCompile] public void Execute(int index) { if (!Enabled[index]) return; var state = States[index]; var vp = VpState[index]; var tp = TpState[index]; var target = TargetState[index]; if (ErrorFlags[index]) { AnyError[0] = true; return; } // First, compute the virtual parent transform - we'll need it in any case. var trueLocalToVirtualLocal = vp.worldToLocalMatrix * tp.localToWorldMatrix; state.RequestWriteback = false; if (TransformState.Differs(target, state.TrueLocal) || !state.CacheValid) { // Our local position changed, so don't try to make any corrections; just remember the new values. state.CacheValid = true; state.TrueLocal = target; state.TrueLocalToVirtualLocal = trueLocalToVirtualLocal; state.VirtualLocal = trueLocalToVirtualLocal * Matrix4x4.TRS( state.TrueLocal.localPosition, state.TrueLocal.localRotation, state.TrueLocal.localScale ); } else if (MatDiffers(trueLocalToVirtualLocal, state.TrueLocalToVirtualLocal)) { // Our local position didn't change, but our virtual parent did, so we need to correct. // To do this, we take our _old_ virtual local transform, and use it to transform our old true local // position into virtual local space; we then go from _current_ virtual local space to true local. var virtualLocalToTrueLocal = trueLocalToVirtualLocal.inverse; var trueLocal = virtualLocalToTrueLocal * state.VirtualLocal; state.TrueLocal = new TransformState { localPosition = trueLocal.GetColumn(3), localRotation = trueLocal.rotation, localScale = trueLocal.lossyScale }; state.TrueLocalToVirtualLocal = trueLocalToVirtualLocal; state.RequestWriteback = true; AnyDirty[0] = true; } States[index] = state; } } private struct JWriteback : IJobParallelForTransform { [ReadOnly] public NativeArray States; [ReadOnly] public NativeArray Errors; [ReadOnly] public NativeArray Enabled; [NativeDisableContainerSafetyRestriction] [WriteOnly] public NativeArray AnyError; [BurstCompile] public void Execute(int index, TransformAccess transform) { var state = States[index]; if (!Enabled[index] || Errors[index] || !state.RequestWriteback) return; if (!transform.isValid) { Errors[index] = true; AnyError[0] = true; return; } var pos = state.TrueLocal.localPosition; var rot = state.TrueLocal.localRotation; var scale = state.TrueLocal.localScale; transform.localPosition = pos; transform.localRotation = rot; transform.localScale = scale; } } } }