feat: improvements to armature tracking (#665)

* opti(armature-lock): parallelize burst jobs for armature lock processing

* feat: continue armature tracking when the MAMA GameObject is disabled

Closes: #500

* feat: add global toggle for armature locking

Closes: #484
This commit is contained in:
bd_ 2024-02-17 19:20:08 +09:00 committed by GitHub
parent 27f0557367
commit 648c9a9608
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 192 additions and 41 deletions

View File

@ -24,6 +24,7 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using nadena.dev.modular_avatar.ui;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -45,7 +46,7 @@ namespace nadena.dev.modular_avatar.core.editor
ApplyToCurrentAvatar(); ApplyToCurrentAvatar();
} }
[MenuItem("Tools/Modular Avatar/Manual bake avatar", true)] [MenuItem(UnityMenuItems.TopMenu_ManualBakeAvatar, true, UnityMenuItems.TopMenu_ManualBakeAvatarOrder)]
private static bool ValidateApplyToCurrentAvatar() private static bool ValidateApplyToCurrentAvatar()
{ {
return ndmf.AvatarProcessor.CanProcessObject(Selection.activeGameObject); return ndmf.AvatarProcessor.CanProcessObject(Selection.activeGameObject);

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using nadena.dev.modular_avatar.ui;
using nadena.dev.ndmf.localization; using nadena.dev.ndmf.localization;
using nadena.dev.ndmf.ui; using nadena.dev.ndmf.ui;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -87,7 +88,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
[MenuItem("Tools/Modular Avatar/Reload localizations")] [MenuItem(UnityMenuItems.TopMenu_ReloadLocalizations, false, UnityMenuItems.TopMenu_ReloadLocalizationsOrder)]
public static void Reload() public static void Reload()
{ {
Localizer.ReloadLocalizations(); Localizer.ReloadLocalizations();

View File

@ -1,13 +1,65 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using nadena.dev.modular_avatar.ui;
using UnityEngine; using UnityEngine;
#if UNITY_EDITOR #if UNITY_EDITOR
using UnityEditor; using UnityEditor;
#endif #endif
namespace nadena.dev.modular_avatar.core.armature_lock namespace nadena.dev.modular_avatar.core.armature_lock
{ {
internal class ArmatureLockConfig
#if UNITY_EDITOR
: UnityEditor.ScriptableSingleton<ArmatureLockConfig>
#endif
{
#if !UNITY_EDITOR
internal static ArmatureLockConfig instance { get; } = new ArmatureLockConfig();
#endif
[SerializeField]
private bool _globalEnable = true;
internal bool GlobalEnable
{
get => _globalEnable;
set
{
if (value == _globalEnable) return;
#if UNITY_EDITOR
Undo.RecordObject(this, "Toggle Edit Mode Bone Sync");
Menu.SetChecked(UnityMenuItems.TopMenu_EditModeBoneSync, value);
#endif
_globalEnable = value;
if (!value)
{
// Run prepare one last time to dispose of lock structures
UpdateLoopController.InvokeArmatureLockPrepare();
}
}
}
#if UNITY_EDITOR
[InitializeOnLoadMethod]
static void Init()
{
EditorApplication.delayCall += () => {
Menu.SetChecked(UnityMenuItems.TopMenu_EditModeBoneSync, instance._globalEnable);
};
}
[MenuItem(UnityMenuItems.TopMenu_EditModeBoneSync, false, UnityMenuItems.TopMenu_EditModeBoneSyncOrder)]
static void ToggleBoneSync()
{
instance.GlobalEnable = !instance.GlobalEnable;
}
#endif
}
internal class ArmatureLockController : IDisposable internal class ArmatureLockController : IDisposable
{ {
private static long lastMovedFrame = 0; private static long lastMovedFrame = 0;
@ -24,6 +76,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock
private readonly GetTransformsDelegate _getTransforms; private readonly GetTransformsDelegate _getTransforms;
private IArmatureLock _lock; private IArmatureLock _lock;
private bool GlobalEnable => ArmatureLockConfig.instance.GlobalEnable;
private bool _updateActive; private bool _updateActive;
private bool UpdateActive private bool UpdateActive
@ -35,11 +88,13 @@ namespace nadena.dev.modular_avatar.core.armature_lock
#if UNITY_EDITOR #if UNITY_EDITOR
if (value) if (value)
{ {
UpdateLoopController.OnArmatureLockUpdate += VoidUpdate; UpdateLoopController.OnArmatureLockPrepare += UpdateLoopPrepare;
UpdateLoopController.OnArmatureLockUpdate += UpdateLoopFinish;
} }
else else
{ {
UpdateLoopController.OnArmatureLockUpdate -= VoidUpdate; UpdateLoopController.OnArmatureLockPrepare -= UpdateLoopPrepare;
UpdateLoopController.OnArmatureLockUpdate -= UpdateLoopFinish;
} }
_updateActive = value; _updateActive = value;
@ -70,6 +125,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock
set set
{ {
if (Enabled == value) return; if (Enabled == value) return;
_enabled = value; _enabled = value;
if (_enabled) UpdateActive = true; if (_enabled) UpdateActive = true;
} }
@ -105,24 +161,73 @@ namespace nadena.dev.modular_avatar.core.armature_lock
return RebuildLock() && (_lock?.IsStable() ?? false); return RebuildLock() && (_lock?.IsStable() ?? false);
} }
private void VoidUpdate() private void VoidPrepare()
{ {
Update(); UpdateLoopPrepare();
}
private void UpdateLoopFinish()
{
DoFinish();
} }
internal bool Update() internal bool Update()
{ {
LockResult result; UpdateLoopPrepare();
return DoFinish();
}
private bool IsPrepared = false;
private void UpdateLoopPrepare()
{
if (_mama == null || !_mama.gameObject.scene.IsValid())
{
UpdateActive = false;
return;
}
if (!Enabled) if (!Enabled)
{ {
UpdateActive = false; UpdateActive = false;
_lock?.Dispose();
_lock = null;
return;
}
if (!GlobalEnable)
{
_lock?.Dispose();
_lock = null;
return;
}
if (_curMode == _mode)
{
_lock?.Prepare();
IsPrepared = _lock != null;
}
}
private bool DoFinish()
{
LockResult result;
if (!GlobalEnable)
{
_lock?.Dispose(); _lock?.Dispose();
_lock = null; _lock = null;
return true; return true;
} }
var wasPrepared = IsPrepared;
IsPrepared = false;
if (!Enabled) return true;
if (_curMode == _mode) if (_curMode == _mode)
{ {
if (!wasPrepared) _lock?.Prepare();
result = _lock?.Execute() ?? LockResult.Failed; result = _lock?.Execute() ?? LockResult.Failed;
if (result == LockResult.Success) if (result == LockResult.Success)
{ {
@ -134,6 +239,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock
if (!RebuildLock()) return false; if (!RebuildLock()) return false;
_lock?.Prepare();
result = (_lock?.Execute() ?? LockResult.Failed); result = (_lock?.Execute() ?? LockResult.Failed);
return result != LockResult.Failed; return result != LockResult.Failed;

View File

@ -20,6 +20,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock
private NativeIntPtr WroteAny; private NativeIntPtr WroteAny;
private JobHandle LastOp; private JobHandle LastOp;
private JobHandle LastPrepare;
public BidirectionalArmatureLock(IReadOnlyList<(Transform, Transform)> bones) public BidirectionalArmatureLock(IReadOnlyList<(Transform, Transform)> bones)
{ {
@ -142,21 +143,20 @@ namespace nadena.dev.modular_avatar.core.armature_lock
_disposed = true; _disposed = true;
} }
private bool DoCompute(out JobHandle handle) public void Prepare()
{ {
handle = default; if (_disposed) return;
if (_disposed) return false;
WroteAny.Value = 0;
LastOp.Complete(); LastOp.Complete();
WroteAny.Value = 0;
var readBase = new ReadBone() var readBase = new ReadBone()
{ {
_state = BaseBones, _state = BaseBones,
}.Schedule(_baseBoneAccess); }.Schedule(_baseBoneAccess);
LastOp = handle = new Compute()
LastOp = LastPrepare = new Compute()
{ {
BaseBones = BaseBones, BaseBones = BaseBones,
MergeBones = MergeBones, MergeBones = MergeBones,
@ -165,7 +165,12 @@ namespace nadena.dev.modular_avatar.core.armature_lock
ShouldWriteMerge = ShouldWriteMerge, ShouldWriteMerge = ShouldWriteMerge,
WroteAny = WroteAny.GetParallel(), WroteAny = WroteAny.GetParallel(),
}.Schedule(_mergeBoneAccess, readBase); }.Schedule(_mergeBoneAccess, readBase);
}
private bool CheckConsistency()
{
if (_disposed) return false;
// Check parents haven't changed // Check parents haven't changed
for (int i = 0; i < _baseBones.Length; i++) for (int i = 0; i < _baseBones.Length; i++)
{ {
@ -186,27 +191,27 @@ namespace nadena.dev.modular_avatar.core.armature_lock
public bool IsStable() public bool IsStable()
{ {
if (!DoCompute(out var compute)) return false; Prepare();
if (!CheckConsistency()) return false;
compute.Complete(); LastPrepare.Complete();
return WroteAny.Value == 0; return WroteAny.Value == 0;
} }
public LockResult Execute() public LockResult Execute()
{ {
if (!DoCompute(out var compute)) return LockResult.Failed; if (!CheckConsistency()) return LockResult.Failed;
var commitBase = new Commit() var commitBase = new Commit()
{ {
BoneState = BaseBones, BoneState = BaseBones,
ShouldWrite = ShouldWriteBase, ShouldWrite = ShouldWriteBase,
}.Schedule(_baseBoneAccess, compute); }.Schedule(_baseBoneAccess, LastPrepare);
var commitMerge = new Commit() var commitMerge = new Commit()
{ {
BoneState = MergeBones, BoneState = MergeBones,
ShouldWrite = ShouldWriteMerge, ShouldWrite = ShouldWriteMerge,
}.Schedule(_mergeBoneAccess, compute); }.Schedule(_mergeBoneAccess, LastPrepare);
commitBase.Complete(); commitBase.Complete();
commitMerge.Complete(); commitMerge.Complete();

View File

@ -4,6 +4,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock
{ {
internal interface IArmatureLock : IDisposable internal interface IArmatureLock : IDisposable
{ {
void Prepare();
LockResult Execute(); LockResult Execute();
bool IsStable(); bool IsStable();
} }

View File

@ -4,11 +4,11 @@ using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections;
using Unity.Burst; using Unity.Burst;
using Unity.Collections; using Unity.Collections;
using Unity.Jobs; using Unity.Jobs;
using UnityEngine;
using UnityEngine.Jobs;
#if UNITY_EDITOR #if UNITY_EDITOR
using UnityEditor; using UnityEditor;
#endif #endif
using UnityEngine;
using UnityEngine.Jobs;
namespace nadena.dev.modular_avatar.core.armature_lock namespace nadena.dev.modular_avatar.core.armature_lock
{ {
@ -29,7 +29,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock
private TransformAccessArray _baseBonesAccessor, _mergeBonesAccessor; private TransformAccessArray _baseBonesAccessor, _mergeBonesAccessor;
private bool _disposed; private bool _disposed;
private JobHandle LastOp; private JobHandle LastOp, LastPrepare;
[BurstCompile] [BurstCompile]
struct WriteBone : IJobParallelForTransform struct WriteBone : IJobParallelForTransform
@ -197,13 +197,13 @@ namespace nadena.dev.modular_avatar.core.armature_lock
return (scale.x < epsilon || scale.y < epsilon || scale.z < epsilon); return (scale.x < epsilon || scale.y < epsilon || scale.z < epsilon);
} }
private bool DoCompute(out JobHandle handle) public void Prepare()
{ {
handle = default; if (_disposed) return;
if (_disposed) return false;
LastOp.Complete(); LastOp.Complete();
_fault.Value = 0; _fault.Value = 0;
_wroteAny.Value = 0; _wroteAny.Value = 0;
@ -216,7 +216,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock
_state = _mergeState _state = _mergeState
}.Schedule(_mergeBonesAccessor); }.Schedule(_mergeBonesAccessor);
var readAll = JobHandle.CombineDependencies(jobReadBase, jobReadMerged); var readAll = JobHandle.CombineDependencies(jobReadBase, jobReadMerged);
LastOp = handle = new ComputePosition LastOp = LastPrepare = new ComputePosition
{ {
_boneStatic = _boneStaticData, _boneStatic = _boneStaticData,
_mergeState = _mergeState, _mergeState = _mergeState,
@ -225,6 +225,11 @@ namespace nadena.dev.modular_avatar.core.armature_lock
_fault = _fault.GetParallel(), _fault = _fault.GetParallel(),
_wroteAny = _wroteAny.GetParallel(), _wroteAny = _wroteAny.GetParallel(),
}.Schedule(_baseBones.Length, 32, readAll); }.Schedule(_baseBones.Length, 32, readAll);
}
private bool CheckConsistency()
{
if (_disposed) return false;
// Validate parents while that job is running // Validate parents while that job is running
for (int i = 0; i < _baseBones.Length; i++) for (int i = 0; i < _baseBones.Length; i++)
@ -246,9 +251,10 @@ namespace nadena.dev.modular_avatar.core.armature_lock
public bool IsStable() public bool IsStable()
{ {
if (!DoCompute(out var compute)) return false; Prepare();
if (!CheckConsistency()) return false;
compute.Complete(); LastPrepare.Complete();
return _fault.Value == 0 && _wroteAny.Value == 0; return _fault.Value == 0 && _wroteAny.Value == 0;
} }
@ -259,14 +265,14 @@ namespace nadena.dev.modular_avatar.core.armature_lock
/// <returns>True if successful, false if cached data was invalidated and needs recreating</returns> /// <returns>True if successful, false if cached data was invalidated and needs recreating</returns>
public LockResult Execute() public LockResult Execute()
{ {
if (!DoCompute(out var compute)) return LockResult.Failed; if (!CheckConsistency()) return LockResult.Failed;
var commit = new WriteBone() var commit = new WriteBone()
{ {
_fault = _fault, _fault = _fault,
_values = _mergeSavedState, _values = _mergeSavedState,
_shouldWrite = _wroteAny _shouldWrite = _wroteAny
}.Schedule(_mergeBonesAccessor, compute); }.Schedule(_mergeBonesAccessor, LastPrepare);
commit.Complete(); commit.Complete();

View File

@ -4,6 +4,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock
{ {
internal static class UpdateLoopController internal static class UpdateLoopController
{ {
internal static event Action OnArmatureLockPrepare;
internal static event Action OnArmatureLockUpdate; internal static event Action OnArmatureLockUpdate;
internal static event Action OnMoveIndependentlyUpdate; internal static event Action OnMoveIndependentlyUpdate;
@ -11,14 +12,22 @@ namespace nadena.dev.modular_avatar.core.armature_lock
[UnityEditor.InitializeOnLoadMethod] [UnityEditor.InitializeOnLoadMethod]
private static void Init() private static void Init()
{ {
#if UNITY_EDITOR
UnityEditor.EditorApplication.update += () => UnityEditor.EditorApplication.update += () =>
{ {
OnArmatureLockUpdate?.Invoke(); if (ArmatureLockConfig.instance.GlobalEnable)
{
OnArmatureLockPrepare?.Invoke();
OnArmatureLockUpdate?.Invoke();
}
OnMoveIndependentlyUpdate?.Invoke(); OnMoveIndependentlyUpdate?.Invoke();
}; };
#endif
} }
#endif #endif
internal static void InvokeArmatureLockPrepare()
{
OnArmatureLockPrepare?.Invoke();
}
} }
} }

View File

@ -74,6 +74,7 @@ namespace nadena.dev.modular_avatar.core
base.OnValidate(); base.OnValidate();
MigrateLockConfig(); MigrateLockConfig();
RuntimeUtil.delayCall(SetLockMode); RuntimeUtil.delayCall(SetLockMode);
Debug.Log("$$$ OnValidate");
} }
internal void ResetArmatureLock() internal void ResetArmatureLock()
@ -106,7 +107,7 @@ namespace nadena.dev.modular_avatar.core
} }
} }
_lockController.Enabled = isActiveAndEnabled; _lockController.Enabled = enabled;
} }
private void MigrateLockConfig() private void MigrateLockConfig()
@ -126,7 +127,8 @@ namespace nadena.dev.modular_avatar.core
private void OnDisable() private void OnDisable()
{ {
_lockController.Enabled = false; // we use enabled instead of activeAndEnabled to ensure we track even when the GameObject is disabled
_lockController.Enabled = enabled;
} }
protected override void OnDestroy() protected override void OnDestroy()

3
Runtime/UI.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e36cbbfd8a704421becea83cb2d1e72f
timeCreated: 1707812798

View File

@ -0,0 +1,14 @@
namespace nadena.dev.modular_avatar.ui
{
internal class UnityMenuItems
{
internal const string TopMenu_EditModeBoneSync = "Tools/Modular Avatar/Sync Bones in Edit Mode";
internal const int TopMenu_EditModeBoneSyncOrder = 100;
internal const string TopMenu_ManualBakeAvatar = "Tools/Modular Avatar/Manual Bake Avatar";
internal const int TopMenu_ManualBakeAvatarOrder = 1000;
internal const string TopMenu_ReloadLocalizations = "Tools/Modular Avatar/Reload Localizations";
internal const int TopMenu_ReloadLocalizationsOrder = 1001;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c5b3fcacdeb74934b0fb8554490d5d9c
timeCreated: 1707812807