mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 16:28:36 -04:00
// SkinnedModelRenderer - the only new entry point for speaking
SoundHandle SpeakSound( SoundEvent sound, GameObject mouth = null )
SoundHandle SpeakSound( SoundFile sound, float volume = 1, float pitch = 1, GameObject mouth = null )
// SoundFile - the authored lipsync track, read from the compiled vsnd
VisemeTrack Visemes { get; } // null = no track (or not loaded yet, see IsValidForPlayback)
// VisemeTrack - visemes over time, authored in the sound editor
IReadOnlyList<VisemeFrame> Frames { get; }
float Duration { get; }
bool Sample( float time, Span<float> weights )
// VisemeFrame - one viseme span (struct: Viseme, StartTime, EndTime)
// Visemes - the standard 15-viseme table, OVR order
static IReadOnlyList<string> MorphNames { get; }
static int Count { get; }
// MorphCollection - the one shared viseme blend (sounds, voice chat, editor preview)
void ApplyVisemes( ReadOnlySpan<float> visemeWeights, float scale = 1, float smoothTime = 0 )
// ModelMorphs - per-model viseme weight table, cached managed-side (no interop on lookups)
float GetVisemeMorph( int viseme, int morph )
float GetVisemeMorph( string viseme, int morph )
// ResourceCompileContext - inject managed data into natively-compiled resources
void WriteBlock( string blockName, ReadOnlySpan<byte> data ) // four-char block names, eg "LIPS"
void WriteBlockJson( string blockName, object obj )
JsonObject ReadMeta() // the asset's .meta, registers a compile dependency
T ReadMeta<T>( string key, T defaultValue = default )
// ResourceLoadContext - read blocks back when the resource loads (Resource.OnLoaded, engine-internal for now)
// public ref struct, only valid during the load callback
bool Exists( string blockName )
ReadOnlySpan<byte> ReadData( string blockName )
string ReadString( string blockName )
T ReadJson<T>( string blockName, T defaultValue = default )
148 lines
4.7 KiB
C#
148 lines
4.7 KiB
C#
using Sandbox.Utility;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace Sandbox;
|
|
|
|
[Expose]
|
|
public sealed class SceneAnimationSystem : GameObjectSystem<SceneAnimationSystem>
|
|
{
|
|
private HashSetEx<SkinnedModelRenderer> SkinnedRenderers { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Incremented once per frame when bones are updated. Lets per-frame bone caches - like
|
|
/// <see cref="SkinnedModelRenderer.BoneWorldTransforms"/> - know when they're stale without re-reading
|
|
/// the skeleton more than once per frame.
|
|
/// </summary>
|
|
public int BoneFrame { get; private set; }
|
|
|
|
internal void AddRenderer( SkinnedModelRenderer renderer )
|
|
{
|
|
SkinnedRenderers.Add( renderer );
|
|
}
|
|
|
|
internal void RemoveRenderer( SkinnedModelRenderer renderer )
|
|
{
|
|
SkinnedRenderers.Remove( renderer );
|
|
}
|
|
|
|
private ConcurrentQueue<GameTransform> ChangedTransforms { get; } = new();
|
|
|
|
// Reusable lists to avoid per-frame allocations from Parallel.ForEach with IEnumerable<T>.
|
|
// Parallel.ForEach on IEnumerable uses a dynamic partitioner that allocates KeyValuePair<long, T>[]
|
|
// chunks internally; passing an IList<T> uses the static range partitioner instead.
|
|
private readonly List<SkinnedModelRenderer> _rootRenderers = new();
|
|
private readonly List<SkinnedModelRenderer> _boneMergeRoots = new();
|
|
private readonly List<SkinnedModelRenderer> _physRenderers = new();
|
|
|
|
private static int _animThreadCount = Math.Max( 1, Environment.ProcessorCount - 1 );
|
|
|
|
private static ParallelOptions _animParallelOptions = new()
|
|
{
|
|
MaxDegreeOfParallelism = _animThreadCount
|
|
};
|
|
|
|
public SceneAnimationSystem( Scene scene ) : base( scene )
|
|
{
|
|
Listen( Stage.UpdateBones, 0, UpdateAnimation, "UpdateAnimation" );
|
|
Listen( Stage.FinishUpdate, 0, FinishUpdate, "FinishUpdate" );
|
|
Listen( Stage.PhysicsStep, 0, PhysicsStep, "PhysicsStep" );
|
|
}
|
|
|
|
void UpdateAnimation()
|
|
{
|
|
// Bump once per frame, before any FinishUpdate-stage consumer reads bone snapshots.
|
|
BoneFrame++;
|
|
|
|
using ( PerformanceStats.Timings.Animation.Scope() )
|
|
{
|
|
_rootRenderers.Clear();
|
|
_boneMergeRoots.Clear();
|
|
|
|
// Nothing animating - no bones to update, no decode caches worth maintaining
|
|
if ( SkinnedRenderers.Count == 0 )
|
|
return;
|
|
|
|
foreach ( var renderer in SkinnedRenderers.EnumerateLocked() )
|
|
{
|
|
// Drive viseme morphs from any speaking voice before bones/morphs update
|
|
if ( renderer.VoiceActive )
|
|
renderer.UpdateVoice();
|
|
|
|
if ( renderer.IsRootRenderer )
|
|
_rootRenderers.Add( renderer );
|
|
|
|
if ( !renderer.BoneMergeTarget.IsValid() && renderer.HasBoneMergeChildren )
|
|
_boneMergeRoots.Add( renderer );
|
|
}
|
|
|
|
// Skip out if we have a parent that is a skinned model, because we need to move relative to that
|
|
// and their bones haven't been worked out yet. They will get worked out after our parent is.
|
|
// Use a load-balanced partitioner: work per root is highly uneven (clothed characters have many
|
|
// bone-merged children), so static range partitioning would cause severe thread idle time.
|
|
System.Threading.Tasks.Parallel.ForEach( Partitioner.Create( _rootRenderers, loadBalance: true ), _animParallelOptions, ProcessRenderer );
|
|
|
|
// This is a good time to maintain decode caches
|
|
// Will copy local caches to the global cache and handle LRU eviction
|
|
// Can do this in a background task as nothing is touching these caches until next frame
|
|
Task.Run( g_pAnimationSystemUtils.MaintainDecodeCaches );
|
|
|
|
// Now merge any descendants without allocating per-merge delegates
|
|
System.Threading.Tasks.Parallel.ForEach( Partitioner.Create( _boneMergeRoots, loadBalance: true ), _animParallelOptions, renderer => renderer.MergeDescendants( ChangedTransforms ) );
|
|
|
|
while ( ChangedTransforms.TryDequeue( out var tx ) )
|
|
{
|
|
tx.TransformChanged( true );
|
|
}
|
|
|
|
//
|
|
// Run events in the main thread
|
|
//
|
|
foreach ( var x in SkinnedRenderers.EnumerateLocked() )
|
|
{
|
|
x.DispatchEvents();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProcessRenderer( SkinnedModelRenderer renderer )
|
|
{
|
|
if ( !renderer.IsValid() || !renderer.Enabled )
|
|
return;
|
|
|
|
if ( renderer.AnimationUpdate() )
|
|
{
|
|
ChangedTransforms.Enqueue( renderer.Transform );
|
|
}
|
|
|
|
foreach ( var child in renderer.SkinnedChildren )
|
|
{
|
|
ProcessRenderer( child );
|
|
}
|
|
}
|
|
|
|
void FinishUpdate()
|
|
{
|
|
using var _ = PerformanceStats.Timings.Animation.Scope();
|
|
|
|
foreach ( var renderer in SkinnedRenderers.EnumerateLocked() )
|
|
{
|
|
renderer.FinishUpdate();
|
|
}
|
|
}
|
|
|
|
void PhysicsStep()
|
|
{
|
|
using var _ = PerformanceStats.Timings.Animation.Scope();
|
|
|
|
_physRenderers.Clear();
|
|
|
|
foreach ( var renderer in SkinnedRenderers.EnumerateLocked() )
|
|
{
|
|
if ( renderer.Physics != null )
|
|
_physRenderers.Add( renderer );
|
|
}
|
|
|
|
System.Threading.Tasks.Parallel.ForEach( Partitioner.Create( _physRenderers, loadBalance: true ), _animParallelOptions, renderer => renderer.Physics.Step() );
|
|
}
|
|
}
|