Skip to content

Commit 6736a09

Browse files
authored
Bone tracks, IK controls, character anim tools (#3021)
Documentation: https://sbox.game/dev/doc/systems/movie-maker/skeletal-animation/
1 parent 333e999 commit 6736a09

44 files changed

Lines changed: 2461 additions & 193 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

engine/Sandbox.Engine/Systems/Movies/Binder/Binder.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,36 @@ private ITrackTarget CreateTarget( ITrack track, ITrackTarget? parent = null )
8282
};
8383
}
8484

85+
/// <summary>
86+
/// Get all reference targets for tracks in the given <paramref name="clip"/>.
87+
/// </summary>
88+
/// <typeparam name="T">Reference type, for example <see cref="GameObject"/> or a <see cref="Component"/> type.</typeparam>
89+
/// <param name="clip">Movie clip to find track bindings for.</param>
90+
public IEnumerable<ITrackReference<T>> GetReferences<T>( IMovieClip clip )
91+
where T : Component
92+
{
93+
return clip.Tracks
94+
.OfType<IReferenceTrack<T>>()
95+
.Select( Get );
96+
}
97+
98+
/// <summary>
99+
/// Get all property targets for tracks in the given <paramref name="clip"/>.
100+
/// </summary>
101+
/// <typeparam name="T">Property value type.</typeparam>
102+
/// <param name="clip">Movie clip to find track bindings for.</param>
103+
public IEnumerable<ITrackProperty<T>> GetProperties<T>( IMovieClip clip )
104+
{
105+
return clip.Tracks
106+
.OfType<IPropertyTrack<T>>()
107+
.Select( Get );
108+
}
109+
110+
/// <summary>
111+
/// Get all bound component references for tracks in the given <paramref name="clip"/>.
112+
/// </summary>
113+
/// <typeparam name="T">Component type.</typeparam>
114+
/// <param name="clip">Movie clip to find track bindings for.</param>
85115
public IEnumerable<T> GetComponents<T>( IMovieClip clip )
86116
where T : Component
87117
{

engine/Sandbox.Engine/Systems/Movies/Binder/Properties/AnimParam.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ public T Value
3333
[Expose]
3434
file sealed class AnimParamPropertyFactory : ITrackPropertyFactory<ITrackProperty<ParameterAccessor?>>
3535
{
36-
string ITrackPropertyFactory.CategoryName => "Anim Graph";
37-
3836
public IEnumerable<string> GetPropertyNames( ITrackProperty<ParameterAccessor?> parent )
3937
{
4038
var graph = parent is { IsBound: true } ? parent.Value?.Graph : null;
@@ -47,6 +45,8 @@ public IEnumerable<string> GetPropertyNames( ITrackProperty<ParameterAccessor?>
4745
}
4846
}
4947

48+
public string GetCategoryName( ITrackProperty<ParameterAccessor?> parent, string name ) => "Anim Graph";
49+
5050
/// <summary>
5151
/// Any property in a <see cref="ParameterAccessor"/> is an anim graph parameter, but we
5252
/// can only determine the type if it actually exists.
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace Sandbox.MovieMaker.Properties;
4+
5+
#nullable enable
6+
7+
/// <summary>
8+
/// Pseudo-property on a <see cref="SkinnedModelRenderer"/> that has a sub-property for each bone.
9+
/// Stores movie-driven transforms for each bone during playback, and applies them when
10+
/// <see cref="MovieBoneAnimatorSystem"/> performs <see cref="GameObjectSystem.Stage.UpdateBones"/>.
11+
/// </summary>
12+
[Expose]
13+
public sealed class BoneAccessor
14+
{
15+
private readonly Dictionary<int, Transform> _parentSpaceOverrides = new();
16+
private readonly Dictionary<int, Transform> _localSpaceOverrides = new();
17+
private readonly SkinnedModelRenderer _renderer;
18+
19+
/// <summary>
20+
/// Renderer this accessor was created for.
21+
/// </summary>
22+
public SkinnedModelRenderer Renderer => _renderer;
23+
24+
public BoneAccessor( SkinnedModelRenderer renderer )
25+
{
26+
_renderer = renderer;
27+
}
28+
29+
/// <summary>
30+
/// Helper to see if the renderer's model has a bone with the given <paramref name="name"/>.
31+
/// </summary>
32+
public bool HasBone( string name ) => _renderer.Model?.Bones.HasBone( name ) ?? false;
33+
34+
/// <summary>
35+
/// Gets the current movie-driven parent-space transform of the given bone. If the bone
36+
/// isn't controlled by a movie, just returns the current parent-space transform.
37+
/// </summary>
38+
public Transform GetParentSpace( int index )
39+
{
40+
return _parentSpaceOverrides.TryGetValue( index, out var transform )
41+
? transform
42+
: Renderer.SceneModel?.GetParentSpaceBone( index ) ?? Transform.Zero;
43+
}
44+
45+
/// <summary>
46+
/// Sets the current movie-driven parent-space transform of the given bone.
47+
/// </summary>
48+
public void SetParentSpace( int index, Transform value )
49+
{
50+
_parentSpaceOverrides[index] = value;
51+
}
52+
53+
/// <summary>
54+
/// Clears any movie-driven bone transforms for this renderer.
55+
/// </summary>
56+
public void ClearOverrides()
57+
{
58+
_parentSpaceOverrides.Clear();
59+
}
60+
61+
/// <summary>
62+
/// Applies any movie-driven bone transforms. Called during <see cref="GameObjectSystem.Stage.UpdateBones"/>.
63+
/// </summary>
64+
public void ApplyOverrides()
65+
{
66+
_renderer.ClearPhysicsBones();
67+
68+
if ( _renderer.Model is not { } model ) return;
69+
if ( _renderer.SceneModel is not { } sceneModel ) return;
70+
if ( _parentSpaceOverrides.Count == 0 ) return;
71+
72+
// TODO: I'm assuming parent bones are always listed before child bones
73+
74+
_localSpaceOverrides.Clear();
75+
76+
foreach ( var bone in model.Bones.AllBones )
77+
{
78+
if ( !_parentSpaceOverrides.TryGetValue( bone.Index, out var parentLocalTransform ) )
79+
{
80+
// Even if this bone doesn't have an override, one of its ancestors
81+
// might have so we need to update its local space transform
82+
83+
parentLocalTransform = bone.Parent is { } parent
84+
? parent.LocalTransform.ToLocal( bone.LocalTransform )
85+
: bone.LocalTransform;
86+
}
87+
88+
{
89+
var parentTransform = bone.Parent is { } parent
90+
? _localSpaceOverrides.TryGetValue( parent.Index, out var parentLocalOverride )
91+
? parentLocalOverride
92+
: sceneModel.GetBoneLocalTransform( parent.Index )
93+
: Transform.Zero;
94+
95+
var localTransform = parentTransform.ToWorld( parentLocalTransform );
96+
97+
_localSpaceOverrides[bone.Index] = localTransform;
98+
99+
sceneModel.SetBoneOverride( bone.Index, localTransform );
100+
}
101+
}
102+
}
103+
}
104+
105+
/// <summary>
106+
/// Reads / writes a bone transform on a <see cref="SkinnedModelRenderer"/>.
107+
/// </summary>
108+
file sealed record BoneProperty( ITrackProperty<BoneAccessor?> Parent, string Name )
109+
: ITrackProperty<Transform>
110+
{
111+
private (SkinnedModelRenderer? Renderer, int? Index)? _cached;
112+
113+
public bool IsBound => Parent.Value?.HasBone( Name ) ?? false;
114+
115+
public Transform Value
116+
{
117+
get => GetInfo().Index is not { } index ? Transform.Zero : Parent.Value?.GetParentSpace( index ) ?? Transform.Zero;
118+
set
119+
{
120+
if ( GetInfo().Index is { } index )
121+
{
122+
Parent.Value?.SetParentSpace( index, value );
123+
}
124+
}
125+
}
126+
127+
ITrackTarget ITrackProperty.Parent => Parent;
128+
129+
private (SkinnedModelRenderer? Renderer, int? Index) GetInfo()
130+
{
131+
if ( _cached is { } cached && cached.Renderer == Parent.Value?.Renderer )
132+
{
133+
return cached;
134+
}
135+
136+
var renderer = Parent.Value?.Renderer;
137+
var index = renderer?.Model?.Bones.GetBone( Name )?.Index;
138+
139+
_cached = cached = (renderer, index);
140+
141+
return cached;
142+
}
143+
}
144+
145+
[Expose]
146+
file sealed class BonePropertyFactory : ITrackPropertyFactory<ITrackProperty<BoneAccessor?>, Transform>
147+
{
148+
/// <summary>
149+
/// Any property inside a <see cref="BoneAccessor"/> is a bone.
150+
/// </summary>
151+
public bool PropertyExists( ITrackProperty<BoneAccessor?> parent, string name ) => true;
152+
153+
public ITrackProperty<Transform> CreateProperty( ITrackProperty<BoneAccessor?> parent, string name ) => new BoneProperty( parent, name );
154+
155+
public IEnumerable<string> GetPropertyNames( ITrackProperty<BoneAccessor?> parent )
156+
{
157+
return parent is { IsBound: true, Value.Renderer.Model: { } model }
158+
? model.Bones.AllBones.Select( x => x.Name )
159+
: [];
160+
}
161+
162+
public string GetCategoryName( ITrackProperty<BoneAccessor?> parent, string name )
163+
{
164+
var firstSep = name.IndexOf( '_' );
165+
var lastSep = name.LastIndexOf( '_' );
166+
167+
if ( firstSep == lastSep ) return "Bones";
168+
169+
return $"Bones/{name[..firstSep].ToTitleCase()}";
170+
}
171+
}
172+
173+
file sealed record BoneAccessorProperty( ITrackReference<SkinnedModelRenderer> Parent )
174+
: ITrackProperty<BoneAccessor?>
175+
{
176+
public const string PropertyName = "Bones";
177+
178+
public string Name => PropertyName;
179+
180+
public BoneAccessor? Value
181+
{
182+
get => Parent.Value is { } renderer
183+
? MovieBoneAnimatorSystem.Current?.GetBoneAccessor( renderer )
184+
: null;
185+
186+
set
187+
{
188+
// Can't write (CanWrite = false)
189+
}
190+
}
191+
192+
bool ITrackProperty.CanWrite => false;
193+
194+
ITrackTarget ITrackProperty.Parent => Parent;
195+
}
196+
197+
[Expose]
198+
file sealed class BoneAccessorPropertyFactory : ITrackPropertyFactory<ITrackReference<SkinnedModelRenderer>, BoneAccessor?>
199+
{
200+
public string GetCategoryName( ITrackReference<SkinnedModelRenderer> parent, string name ) => "Members";
201+
202+
public IEnumerable<string> GetPropertyNames( ITrackReference<SkinnedModelRenderer> parent ) =>
203+
[BoneAccessorProperty.PropertyName];
204+
205+
public bool PropertyExists( ITrackReference<SkinnedModelRenderer> parent, string name ) =>
206+
name == BoneAccessorProperty.PropertyName;
207+
208+
public ITrackProperty<BoneAccessor?> CreateProperty( ITrackReference<SkinnedModelRenderer> parent, string name ) =>
209+
new BoneAccessorProperty( parent );
210+
}
211+
212+
/// <summary>
213+
/// Coordinates playing bone animations from <see cref="MoviePlayer"/>s. Holds a <see cref="BoneAccessor"/>
214+
/// for <see cref="SkinnedModelRenderer"/>s in the scene, which store any movie-controlled bone transforms.
215+
/// </summary>
216+
[Expose]
217+
public sealed class MovieBoneAnimatorSystem : GameObjectSystem<MovieBoneAnimatorSystem>
218+
{
219+
private readonly ConditionalWeakTable<SkinnedModelRenderer, BoneAccessor> _accessors = new();
220+
221+
public MovieBoneAnimatorSystem( Scene scene ) : base( scene )
222+
{
223+
Listen( Stage.UpdateBones, -1_000, UpdateBones, "UpdateBones" );
224+
}
225+
226+
/// <summary>
227+
/// Applies any active movie-driven bone transformations.
228+
/// </summary>
229+
public void UpdateBones()
230+
{
231+
foreach ( var (_, accessor) in _accessors )
232+
{
233+
accessor.ApplyOverrides();
234+
}
235+
}
236+
237+
/// <summary>
238+
/// Clears all movie-driven bone transformations for the given <paramref name="renderer"/>.
239+
/// </summary>
240+
public void ClearBones( SkinnedModelRenderer renderer )
241+
{
242+
if ( _accessors.TryGetValue( renderer, out var accessor ) )
243+
{
244+
accessor.ClearOverrides();
245+
}
246+
}
247+
248+
/// <summary>
249+
/// Gets the current movie-driven parent-space transform for the given bone. If this
250+
/// bone isn't currently being controlled by a movie, returns its current transform.
251+
/// </summary>
252+
public Transform GetParentSpaceBone( SkinnedModelRenderer renderer, int index )
253+
{
254+
return GetBoneAccessor( renderer ).GetParentSpace( index );
255+
}
256+
257+
/// <summary>
258+
/// Sets the current movie-driven parent-space transform for the given bone.
259+
/// </summary>
260+
public void SetParentSpaceBone( SkinnedModelRenderer renderer, int index, Transform transform )
261+
{
262+
GetBoneAccessor( renderer ).SetParentSpace( index, transform );
263+
}
264+
265+
internal BoneAccessor GetBoneAccessor( SkinnedModelRenderer renderer )
266+
{
267+
if ( _accessors.TryGetValue( renderer, out var existing ) ) return existing;
268+
269+
existing = new BoneAccessor( renderer );
270+
_accessors.Add( renderer, existing );
271+
272+
return existing;
273+
}
274+
}

engine/Sandbox.Engine/Systems/Movies/Binder/Properties/Member.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ private bool IsBoneTransformProperty( [NotNullWhen( true )] out GameObject? bone
9595
[Expose]
9696
file sealed class MemberPropertyFactory : ITrackPropertyFactory
9797
{
98-
string ITrackPropertyFactory.CategoryName => "Members";
99-
10098
int ITrackPropertyFactory.Order => 0x4000_0000;
10199

102100
IEnumerable<string> ITrackPropertyFactory.GetPropertyNames( ITrackTarget parent )
@@ -119,6 +117,8 @@ IEnumerable<string> ITrackPropertyFactory.GetPropertyNames( ITrackTarget parent
119117
.FirstOrDefault( CanMakeTrackFromMember );
120118
}
121119

120+
public string GetCategoryName( ITrackTarget parent, string name ) => "Members";
121+
122122
public Type? GetTargetType( ITrackTarget parent, string name )
123123
{
124124
return GetMember( parent, name ) switch

engine/Sandbox.Engine/Systems/Movies/Binder/Properties/Morph.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ public float Value
2323
[Expose]
2424
file sealed class MorphPropertyFactory : ITrackPropertyFactory<ITrackProperty<MorphAccessor?>, float>
2525
{
26-
string ITrackPropertyFactory.CategoryName => "Morphs";
27-
2826
/// <summary>
2927
/// Any property inside a <see cref="MorphAccessor"/> is a morph.
3028
/// </summary>
@@ -37,4 +35,6 @@ public IEnumerable<string> GetPropertyNames( ITrackProperty<MorphAccessor?> pare
3735
? accessor.Names
3836
: Enumerable.Empty<string>();
3937
}
38+
39+
public string GetCategoryName( ITrackProperty<MorphAccessor?> parent, string name ) => "Morphs";
4040
}

0 commit comments

Comments
 (0)