Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: animation API (work in progress) #467

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
458425e
feat: animation API (work in progress)
bdunderscore Oct 28, 2024
7df9bea
Initial implementation of VirtualState
bdunderscore Nov 9, 2024
675671a
Add VirtualState test
bdunderscore Nov 9, 2024
64040dc
Add VirtualTransition
bdunderscore Nov 9, 2024
05d8e2e
Rename CloneCache -> CloneContext.cs
bdunderscore Nov 9, 2024
bbc874e
AnimatorStateTransition -> State graph cloning
bdunderscore Nov 9, 2024
715f58b
VirtualStateMachine (tests are incomplete)
bdunderscore Nov 9, 2024
ca4fb60
Adjust transition class hierarchy
bdunderscore Nov 9, 2024
fb53d3b
VirtualLayer
bdunderscore Nov 9, 2024
b579b81
VirtualAnimatorController
bdunderscore Nov 10, 2024
44918b2
Initial end-to-end testing
bdunderscore Nov 10, 2024
bc040c8
Minor fixes and optimizations
bdunderscore Nov 10, 2024
67abab6
Add VirtualBlendTreeTest
bdunderscore Nov 10, 2024
691f2f1
Add cache invalidation hooks
bdunderscore Nov 10, 2024
8daee36
WIP: animation index
bdunderscore Nov 10, 2024
da40655
WIP: ObjectPathRemapper
bdunderscore Nov 10, 2024
93e9c03
AnimationIndex tests
bdunderscore Nov 10, 2024
94eb7ff
fix: test failures
bdunderscore Nov 10, 2024
fa9fa87
Add support for AnimatorOverrideControllers
bdunderscore Nov 12, 2024
e0f55ab
Add support for synced layer overrides
bdunderscore Nov 12, 2024
968fef6
chore: remove obsolete comment
bdunderscore Nov 12, 2024
1831027
Fix non-VRChat platform builds
bdunderscore Nov 12, 2024
046de13
wip
bdunderscore Nov 17, 2024
23e8d33
Implement animator state machine transitions
bdunderscore Nov 17, 2024
2537eaa
Enable nullable warnings for the animation API
bdunderscore Nov 17, 2024
01b5dfe
Minor API adjustments
bdunderscore Nov 17, 2024
02da23e
Add support for correcting VRC animator layer control references
bdunderscore Nov 18, 2024
ed4f850
Add tests for ObjectPathRemapper
bdunderscore Nov 18, 2024
34a855f
Remove some temporary hacks
bdunderscore Nov 18, 2024
06c588f
Fix standalone build
bdunderscore Nov 18, 2024
4ebb754
Fix standalone builds (again)
bdunderscore Nov 19, 2024
a22c778
rename AnimatorServicesContext -> VirtualControllerContext
bdunderscore Nov 19, 2024
deeef2b
Nailing down the VirtualControllerContext API
bdunderscore Nov 19, 2024
f39f8ef
Create the AnimatorServicesContext
bdunderscore Nov 19, 2024
3b6e6a0
chore: add CHANGELOG entry
bdunderscore Nov 19, 2024
83192c8
chore: add some convenience APIs
bdunderscore Nov 25, 2024
abebecf
wip
bdunderscore Dec 15, 2024
ba3106d
wip
bdunderscore Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- [#486] Add Simplified Chinese support
- [#467] Added the `AnimatorServicesContext` and lots of supporting APIs for working with animator controllers.
- [#472] [#474] Added the `DependsOnContext` attribute, for declaring dependencies between extension contexts.
- [#473] Added `BuildContext.SetEnableUVDistributionRecalculation` to allow opting out from the automatic call to
`Mesh.RecalculateUVDistributionMetrics` on generated meshes.

### Fixed
- [#487] Fixed a performance issue where all assets would potentially be loaded on reimport, taking a lot of time and
memory in the process
Expand Down
3 changes: 3 additions & 0 deletions Editor/API/AnimatorServices.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

230 changes: 230 additions & 0 deletions Editor/API/AnimatorServices/AnimationIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;

namespace nadena.dev.ndmf.animator
{
public sealed class AnimationIndex
{
private readonly Func<IEnumerable<VirtualNode>> _getRoots;
private readonly Func<long> _getInvalidationToken;

private long _lastInvalidationToken;

private readonly Action _invalidateAction;
private bool _isValid;

private bool IsValid => _isValid && _lastInvalidationToken == _getInvalidationToken();

private readonly Dictionary<string, HashSet<VirtualClip>> _objectPathToClip = new();
private readonly Dictionary<EditorCurveBinding, HashSet<VirtualClip>> _bindingToClip = new();
private readonly Dictionary<VirtualClip, HashSet<EditorCurveBinding>> _lastBindings = new();

internal AnimationIndex(
Func<IEnumerable<VirtualAnimatorController>> getRoots,
Func<long> getInvalidationToken)
{
_getRoots = getRoots;
_getInvalidationToken = getInvalidationToken;
_invalidateAction = () => _isValid = false;
}

public AnimationIndex(IEnumerable<VirtualNode> controllers)
{
_invalidateAction = () => _isValid = false;
var controllerList = new List<VirtualNode>(controllers);
_getRoots = () => controllerList;
_getInvalidationToken = () => _lastInvalidationToken;
}

public IEnumerable<VirtualClip> GetClipsForObjectPath(string objectPath)
{
if (!IsValid) RebuildCache();

if (_objectPathToClip.TryGetValue(objectPath, out var clips))
{
return clips;
}

return Enumerable.Empty<VirtualClip>();
}

public IEnumerable<VirtualClip> GetClipsForBinding(EditorCurveBinding binding)
{
if (!IsValid) RebuildCache();

if (_bindingToClip.TryGetValue(binding, out var clips))
{
return clips;
}

return Enumerable.Empty<VirtualClip>();
}

public void RewritePaths(Func<string, string?> rewriteRules)
{
if (!IsValid) RebuildCache();

var rewriteSet = _objectPathToClip.Values.SelectMany(s => s).Distinct();

RewritePaths(rewriteSet, rewriteRules);
}

public void RewritePaths(Dictionary<string, string?> rewriteRules)
{
if (!IsValid) RebuildCache();

HashSet<VirtualClip> rewriteSet = new();

foreach (var key in rewriteRules.Keys)
{
if (!_objectPathToClip.TryGetValue(key, out var clips)) continue;
rewriteSet.UnionWith(clips);
}

Func<string, string?> rewriteFunc = k =>
{
// Note: We don't use GetValueOrDefault here as we want to distinguish between null and missing keys
// ReSharper disable once CanSimplifyDictionaryTryGetValueWithGetValueOrDefault
if (rewriteRules.TryGetValue(k, out var v)) return v;
return k;
};

RewritePaths(rewriteSet, rewriteFunc);
}

private void RewritePaths(IEnumerable<VirtualClip> rewriteSet, Func<string, string?> rewriteFunc)
{
List<VirtualClip> recacheNeeded = new();

foreach (var clip in rewriteSet)
{
clip.EditPaths(rewriteFunc);
if (!_isValid)
{
recacheNeeded.Add(clip);
}

_isValid = true;
}

foreach (var clip in recacheNeeded)
{
CacheClip(clip);
}
}

public void EditClipsByBinding(IEnumerable<EditorCurveBinding> binding, Action<VirtualClip> processClip)
{
if (!IsValid) RebuildCache();

var clips = binding.SelectMany(GetClipsForBinding).ToHashSet();
var toRecache = new List<VirtualClip>();
foreach (var clip in clips)
{
processClip(clip);
if (!_isValid)
{
toRecache.Add(clip);
}

_isValid = true;
}

foreach (var clip in toRecache)
{
CacheClip(clip);
}
}

private void RebuildCache()
{
_objectPathToClip.Clear();
_bindingToClip.Clear();
_lastBindings.Clear();

foreach (var clip in EnumerateClips())
{
CacheClip(clip);
}

_isValid = true;
}

private void CacheClip(VirtualClip clip)
{
if (_lastBindings.TryGetValue(clip, out var lastBindings))
{
foreach (var binding in lastBindings)
{
_bindingToClip[binding].Remove(clip);
_objectPathToClip[binding.path].Remove(clip);
}
}
else
{
lastBindings = new HashSet<EditorCurveBinding>();
_lastBindings[clip] = lastBindings;
}

lastBindings.Clear();
lastBindings.UnionWith(clip.GetObjectCurveBindings());
lastBindings.UnionWith(clip.GetFloatCurveBindings());

foreach (var binding in lastBindings)
{
if (!_bindingToClip.TryGetValue(binding, out var clips))
{
clips = new HashSet<VirtualClip>();
_bindingToClip[binding] = clips;
}

clips.Add(clip);

if (!_objectPathToClip.TryGetValue(binding.path, out var pathClips))
{
pathClips = new HashSet<VirtualClip>();
_objectPathToClip[binding.path] = pathClips;
}

pathClips.Add(clip);
}
}

private IEnumerable<VirtualClip> EnumerateClips()
{
HashSet<object> visited = new();
Queue<VirtualNode> queue = new();

_lastInvalidationToken = _getInvalidationToken();
foreach (var controller in _getRoots())
{
queue.Enqueue(controller);
}

while (queue.Count > 0)
{
var node = queue.Dequeue();
node.RegisterCacheObserver(_invalidateAction);

if (!visited.Add(node))
{
continue;
}

foreach (var child in node.EnumerateChildren())
{
if (!visited.Contains(child)) queue.Enqueue(child);
}

if (node is VirtualClip clip)
{
yield return clip;
}
}
}
}
}
3 changes: 3 additions & 0 deletions Editor/API/AnimatorServices/AnimationIndex.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions Editor/API/AnimatorServices/AnimatorServicesContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#nullable enable

using System;
using JetBrains.Annotations;

namespace nadena.dev.ndmf.animator
{
/// <summary>
/// Provides a number of NDMF services based on virtualizing animator controllers.
/// While this context is active, NDMF will automatically track object renames, and apply them to all known
/// animators. It will also keep animators cached in the VirtualControllerContext (which is also, as a convenience,
/// available through this class).
/// Note that any new objects created should be registered in the ObjectPathRemapper if they'll be used in animations;
/// this ensures that subsequent movements will be tracked properly. Likewise, use ObjectPathRemapper to obtain
/// (virtual) object paths for newly created objects.
/// </summary>
[DependsOnContext(typeof(VirtualControllerContext))]
[PublicAPI]
public sealed class AnimatorServicesContext : IExtensionContext
{
private VirtualControllerContext? _controllerContext;

public VirtualControllerContext ControllerContext => _controllerContext ??
throw new InvalidOperationException(
"ControllerContext is not available outside of the AnimatorServicesContext");

private AnimationIndex? _animationIndex;

public AnimationIndex AnimationIndex => _animationIndex ??
throw new InvalidOperationException(
"AnimationIndex is not available outside of the AnimatorServicesContext");

private ObjectPathRemapper? _objectPathRemapper;

public ObjectPathRemapper ObjectPathRemapper => _objectPathRemapper ??
throw new InvalidOperationException(
"ObjectPathRemapper is not available outside of the AnimatorServicesContext");

public void OnActivate(BuildContext context)
{
_controllerContext = context.Extension<VirtualControllerContext>();
_animationIndex = new AnimationIndex(
() => ControllerContext.GetAllControllers(),
() => ControllerContext.CacheInvalidationToken
);
_objectPathRemapper = new ObjectPathRemapper(context.AvatarRootTransform);
}

public void OnDeactivate(BuildContext context)
{
AnimationIndex.RewritePaths(ObjectPathRemapper.GetVirtualToRealPathMap());

_objectPathRemapper = null;
_animationIndex = null;
_controllerContext = null;
}
}
}
3 changes: 3 additions & 0 deletions Editor/API/AnimatorServices/AnimatorServicesContext.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading