mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-02 19:38:24 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
524 lines
12 KiB
C#
524 lines
12 KiB
C#
|
|
namespace Sandbox;
|
|
|
|
/// <summary>
|
|
/// Renders a skinned model in the world. A skinned model is any model with bones/animations.
|
|
/// </summary>
|
|
[Title( "Model Renderer (skinned)" )]
|
|
[Category( "Rendering" )]
|
|
[Icon( "sports_martial_arts" )]
|
|
[Alias( "AnimatedModelComponent" )]
|
|
public sealed partial class SkinnedModelRenderer : ModelRenderer, Component.ExecuteInEditor
|
|
{
|
|
bool _createBones = false;
|
|
|
|
[Property, Group( "Bones", StartFolded = true )]
|
|
public bool CreateBoneObjects
|
|
{
|
|
get => _createBones;
|
|
set
|
|
{
|
|
if ( _createBones == value ) return;
|
|
_createBones = value;
|
|
|
|
UpdateObject();
|
|
}
|
|
}
|
|
|
|
SkinnedModelRenderer _boneMergeTarget;
|
|
|
|
[Property, Group( "Bones" )]
|
|
public SkinnedModelRenderer BoneMergeTarget
|
|
{
|
|
get => _boneMergeTarget;
|
|
set
|
|
{
|
|
if ( value == this ) return;
|
|
if ( _boneMergeTarget == value ) return;
|
|
|
|
_boneMergeTarget?.SetBoneMerge( this, false );
|
|
|
|
_boneMergeTarget = value;
|
|
|
|
_boneMergeTarget?.SetBoneMerge( this, true );
|
|
}
|
|
}
|
|
|
|
bool _useAnimGraph = true;
|
|
|
|
/// <summary>
|
|
/// Usually used for turning off animation on ragdolls.
|
|
/// </summary>
|
|
[Property, Group( "Animation" ), Title( "Use Animation Graph" )]
|
|
public bool UseAnimGraph
|
|
{
|
|
get => _useAnimGraph;
|
|
set
|
|
{
|
|
if ( _useAnimGraph == value ) return;
|
|
|
|
_useAnimGraph = value;
|
|
|
|
if ( SceneModel.IsValid() )
|
|
{
|
|
SceneModel.UseAnimGraph = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
AnimationGraph _animationGraph;
|
|
|
|
/// <summary>
|
|
/// Override animgraph, otherwise uses animgraph of the model.
|
|
/// </summary>
|
|
[Property, Group( "Animation" ), ShowIf( nameof( UseAnimGraph ), true )]
|
|
public AnimationGraph AnimationGraph
|
|
{
|
|
get => _animationGraph;
|
|
set
|
|
{
|
|
if ( _animationGraph == value ) return;
|
|
|
|
_animationGraph = value;
|
|
|
|
if ( SceneModel.IsValid() )
|
|
{
|
|
SceneModel.AnimationGraph = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows playback of sequences directly, rather than using an animation graph.
|
|
/// Requires <see cref="UseAnimGraph"/> disabled if the scene model has one.
|
|
/// </summary>
|
|
[Property, Group( "Animation" ), ShowIf( nameof( ShouldShowSequenceEditor ), true ), InlineEditor( Label = false )]
|
|
public SequenceAccessor Sequence
|
|
{
|
|
get
|
|
{
|
|
_sequence ??= new SequenceAccessor( this );
|
|
return _sequence;
|
|
}
|
|
}
|
|
|
|
float _playbackRate = 1.0f;
|
|
|
|
/// <summary>
|
|
/// Control playback rate of animgraph or current sequence.
|
|
/// </summary>
|
|
[Property, Range( 0.0f, 4.0f ), Group( "Animation" )]
|
|
public float PlaybackRate
|
|
{
|
|
get => _playbackRate;
|
|
set
|
|
{
|
|
if ( _playbackRate == value ) return;
|
|
|
|
_playbackRate = value;
|
|
|
|
if ( SceneModel.IsValid() )
|
|
{
|
|
SceneModel.PlaybackRate = _playbackRate;
|
|
}
|
|
}
|
|
}
|
|
|
|
public SceneModel SceneModel => _sceneObject as SceneModel;
|
|
|
|
public Transform RootMotion => SceneModel.IsValid() ? SceneModel.RootMotion : default;
|
|
|
|
readonly HashSet<SkinnedModelRenderer> mergeChildren = new();
|
|
|
|
/// <summary>
|
|
/// Does our model have collision and joints.
|
|
/// </summary>
|
|
bool HasBonePhysics()
|
|
{
|
|
return Model.IsValid() && Model.Physics is { Parts.Count: > 0, Joints.Count: > 0 };
|
|
}
|
|
|
|
private void SetBoneMerge( SkinnedModelRenderer newChild, bool enabled )
|
|
{
|
|
ArgumentNullException.ThrowIfNull( newChild );
|
|
|
|
if ( enabled )
|
|
{
|
|
mergeChildren.Add( newChild );
|
|
|
|
// Merge immediately if we can. This prevents a problem where components
|
|
// are added after the animation has been worked out, so you get a one frame
|
|
// flicker of the default pose.
|
|
if ( SceneModel is not null && newChild.SceneModel is not null )
|
|
{
|
|
newChild.SceneModel.Transform = SceneModel.Transform;
|
|
newChild.SceneModel.MergeBones( SceneModel );
|
|
|
|
// Updated bones, transform is no longer dirty.
|
|
newChild._transformDirty = false;
|
|
|
|
// Create bone physics on child if they exist.
|
|
newChild.Physics?.Destroy();
|
|
newChild.Physics = newChild.HasBonePhysics() ? new BonePhysics( newChild, this ) : null;
|
|
|
|
if ( !newChild.UpdateGameObjectsFromBones() )
|
|
return;
|
|
|
|
if ( ThreadSafe.IsMainThread )
|
|
{
|
|
newChild.Transform.TransformChanged();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mergeChildren.Remove( newChild );
|
|
|
|
newChild.Physics?.Destroy();
|
|
newChild.Physics = null;
|
|
}
|
|
}
|
|
|
|
protected override void OnEnabled()
|
|
{
|
|
Assert.True( _sceneObject == null, "_sceneObject should be null - disable wasn't called" );
|
|
Assert.NotNull( Scene, "Scene should not be null" );
|
|
|
|
var model = Model ?? Model.Load( "models/dev/box.vmdl" );
|
|
|
|
var so = new SceneModel( Scene.SceneWorld, model, WorldTransform );
|
|
_sceneObject = so;
|
|
|
|
if ( AnimationGraph is not null )
|
|
{
|
|
so.AnimationGraph = AnimationGraph;
|
|
}
|
|
|
|
if ( so.UseAnimGraph != UseAnimGraph )
|
|
{
|
|
so.UseAnimGraph = UseAnimGraph;
|
|
}
|
|
|
|
so.PlaybackRate = PlaybackRate;
|
|
|
|
OnSceneObjectCreated( _sceneObject );
|
|
|
|
Transform.OnTransformChanged += OnTransformChanged;
|
|
}
|
|
|
|
internal override void OnSceneObjectCreated( SceneObject o )
|
|
{
|
|
base.OnSceneObjectCreated( o );
|
|
|
|
ApplyStoredAnimParameters();
|
|
|
|
Morphs.Apply();
|
|
Sequence.Apply();
|
|
}
|
|
|
|
protected override void UpdateObject()
|
|
{
|
|
BuildBoneHierarchy();
|
|
|
|
base.UpdateObject();
|
|
|
|
if ( !SceneModel.IsValid() )
|
|
return;
|
|
|
|
SceneModel.OnFootstepEvent = InternalOnFootstep;
|
|
SceneModel.OnSoundEvent = InternalOnSoundEvent;
|
|
SceneModel.OnGenericEvent = InternalOnGenericEvent;
|
|
SceneModel.OnAnimTagEvent = InternalOnAnimTagEvent;
|
|
|
|
//
|
|
// If we have a bone merge target then just set up the bone merge
|
|
// which will read the bones and set the game object positions.
|
|
//
|
|
// If we're not bone merge, then do a first frame update to set
|
|
// the bone positions before anything else happens.
|
|
//
|
|
if ( _boneMergeTarget is not null )
|
|
{
|
|
_boneMergeTarget.SetBoneMerge( this, true );
|
|
}
|
|
else
|
|
{
|
|
if ( Scene.IsEditor && !CanUpdateInEditor() )
|
|
{
|
|
SceneModel.UpdateToBindPose( ReadBonesFromGameObjects );
|
|
}
|
|
else
|
|
{
|
|
UpdateTransform( WorldTransform );
|
|
}
|
|
|
|
// Updated bones, transform is no longer dirty.
|
|
_transformDirty = false;
|
|
|
|
UpdateGameObjectsFromBones();
|
|
}
|
|
}
|
|
|
|
internal override void OnDisabledInternal()
|
|
{
|
|
try
|
|
{
|
|
ClearBoneProxies();
|
|
}
|
|
finally
|
|
{
|
|
Transform.OnTransformChanged -= OnTransformChanged;
|
|
|
|
base.OnDisabledInternal();
|
|
}
|
|
|
|
Physics?.Destroy();
|
|
}
|
|
|
|
public void PostAnimationUpdate()
|
|
{
|
|
ThreadSafe.AssertIsMainThread();
|
|
|
|
if ( !SceneModel.IsValid() )
|
|
return;
|
|
|
|
SceneModel.RunPendingEvents();
|
|
SceneModel.DispatchTagEvents();
|
|
|
|
// Skip if we're bone merged, the target will handle the merge.
|
|
if ( _boneMergeTarget.IsValid() )
|
|
return;
|
|
|
|
// Bone merge all children in hierarchy in order.
|
|
MergeDescendants();
|
|
}
|
|
|
|
/// <summary>
|
|
/// If true then animations will play while in an editor scene.
|
|
/// </summary>
|
|
public bool PlayAnimationsInEditorScene { get; set; }
|
|
|
|
internal bool CanUpdateInEditor()
|
|
{
|
|
if ( PlayAnimationsInEditorScene ) return true;
|
|
|
|
// Do we have any modified animgraph parameters?
|
|
if ( parameters.Count > 0 )
|
|
return true;
|
|
|
|
// If we're not using animgraph, do we have a sequence selected?
|
|
if ( !UseAnimGraph && !string.IsNullOrWhiteSpace( Sequence.Name ) )
|
|
return true;
|
|
|
|
// Have we procedurally moved any bones?
|
|
return SceneModel.IsValid() && SceneModel.HasBoneOverrides();
|
|
}
|
|
|
|
internal bool AnimationUpdate()
|
|
{
|
|
if ( !SceneModel.IsValid() )
|
|
return false;
|
|
|
|
SceneModel.Transform = WorldTransform;
|
|
|
|
lock ( this )
|
|
{
|
|
// Update physics bones if they exist.
|
|
Physics?.Update();
|
|
|
|
if ( Scene.IsEditor && !CanUpdateInEditor() )
|
|
{
|
|
SceneModel.UpdateToBindPose( ReadBonesFromGameObjects );
|
|
}
|
|
else
|
|
{
|
|
SceneModel.Update( Time.Delta, ReadBonesFromGameObjects );
|
|
}
|
|
}
|
|
|
|
// Updated bones, transform is no longer dirty.
|
|
_transformDirty = false;
|
|
|
|
// Skip if we're bone merged, the target will handle the merge.
|
|
return !_boneMergeTarget.IsValid() && UpdateGameObjectsFromBones();
|
|
}
|
|
|
|
bool _transformDirty;
|
|
|
|
void OnTransformChanged()
|
|
{
|
|
// Check transform because we could get a false positive.
|
|
_transformDirty = SceneModel.IsValid() && SceneModel.Transform != WorldTransform;
|
|
}
|
|
|
|
internal void FinishUpdate()
|
|
{
|
|
// Debug draw physics world if it exists.
|
|
Physics?.DebugDraw();
|
|
|
|
if ( !_transformDirty )
|
|
return;
|
|
|
|
// Skip if we're bone merged, the target will handle the merge.
|
|
if ( _boneMergeTarget.IsValid() )
|
|
return;
|
|
|
|
// Transform changed, make sure bones are updated.
|
|
UpdateTransform( WorldTransform );
|
|
|
|
// Updated bones, transform is no longer dirty.
|
|
_transformDirty = false;
|
|
|
|
// Update all bone merge children to new transform.
|
|
MergeDescendants();
|
|
}
|
|
|
|
void UpdateTransform( Transform transform )
|
|
{
|
|
if ( !SceneModel.IsValid() )
|
|
return;
|
|
|
|
SceneModel.Transform = transform;
|
|
ReadBonesFromGameObjects();
|
|
SceneModel.FinishBoneUpdate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// For Procedural Bones, copy the current value to the animation bone
|
|
/// </summary>
|
|
void ReadBonesFromGameObjects()
|
|
{
|
|
foreach ( var entry in boneToGameObject )
|
|
{
|
|
if ( !entry.Value.Flags.Contains( GameObjectFlags.ProceduralBone ) )
|
|
continue;
|
|
|
|
// Ignore absolute bones, they're probably physics bones
|
|
if ( entry.Value.Flags.Contains( GameObjectFlags.Absolute ) )
|
|
continue;
|
|
|
|
var localTransform = entry.Value.LocalTransform;
|
|
if ( localTransform.IsValid )
|
|
{
|
|
SceneModel.SetParentSpaceBone( entry.Key.Index, localTransform );
|
|
}
|
|
}
|
|
}
|
|
|
|
private SkinnedModelRenderer RootBoneMergeTarget => BoneMergeTarget.IsValid() ? BoneMergeTarget.RootBoneMergeTarget : this;
|
|
|
|
/// <summary>
|
|
/// For non procedural bones, copy the "parent space" bone from to the GameObject transform. Will
|
|
/// return true if any transforms have changed.
|
|
/// </summary>
|
|
bool UpdateGameObjectsFromBones()
|
|
{
|
|
bool transformsChanged = false;
|
|
|
|
var mergeTarget = RootBoneMergeTarget;
|
|
|
|
// The offset between our transform and root target.
|
|
Transform? mergeOffset = mergeTarget.IsValid() ? WorldTransform.ToLocal( mergeTarget.WorldTransform ) : default;
|
|
|
|
foreach ( var entry in boneToGameObject )
|
|
{
|
|
// Ignore procedural bones, local transform is set manually.
|
|
if ( entry.Value.Flags.Contains( GameObjectFlags.ProceduralBone ) )
|
|
continue;
|
|
|
|
// Ignore absolute bones, they're probably physics bones.
|
|
if ( entry.Value.Flags.Contains( GameObjectFlags.Absolute ) )
|
|
continue;
|
|
|
|
var transform = SceneModel.GetParentSpaceBone( entry.Key.Index );
|
|
if ( !transform.IsValid )
|
|
continue;
|
|
|
|
// Offset root bones to move us to the root target.
|
|
if ( mergeOffset.HasValue && entry.Key.Parent is null )
|
|
{
|
|
transform = mergeOffset.Value.ToWorld( transform );
|
|
}
|
|
|
|
transformsChanged |= entry.Value.Transform.SetLocalTransformFast( transform );
|
|
}
|
|
|
|
foreach ( var entry in attachmentToGameObject )
|
|
{
|
|
var transform = SceneModel.GetAttachment( entry.Key.Name, false );
|
|
if ( !transform.HasValue )
|
|
continue;
|
|
|
|
// Offset root attachments to move us to the root target.
|
|
if ( mergeOffset.HasValue && entry.Key.Bone is null )
|
|
{
|
|
transform = mergeOffset.Value.ToWorld( transform.Value );
|
|
}
|
|
|
|
transformsChanged |= entry.Value.Transform.SetLocalTransformFast( transform.Value );
|
|
}
|
|
|
|
return transformsChanged;
|
|
}
|
|
|
|
private IEnumerable<SkinnedModelRenderer> GetMergeDescendants()
|
|
{
|
|
foreach ( var child in mergeChildren )
|
|
{
|
|
if ( !child.IsValid() )
|
|
continue;
|
|
|
|
yield return child;
|
|
|
|
foreach ( var descendant in child.GetMergeDescendants() )
|
|
{
|
|
yield return descendant;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void MergeDescendants()
|
|
{
|
|
if ( mergeChildren.Count == 0 )
|
|
return;
|
|
|
|
var descendants = GetMergeDescendants();
|
|
foreach ( var descendant in descendants )
|
|
{
|
|
if ( !descendant.IsValid() )
|
|
continue;
|
|
|
|
var so = descendant.SceneModel;
|
|
if ( !so.IsValid() )
|
|
continue;
|
|
|
|
var target = descendant.BoneMergeTarget;
|
|
if ( !target.IsValid() )
|
|
continue;
|
|
|
|
var parent = target.SceneModel;
|
|
if ( !parent.IsValid() )
|
|
continue;
|
|
|
|
so.Transform = parent.Transform;
|
|
so.MergeBones( parent );
|
|
|
|
// Updated bones, transform is no longer dirty.
|
|
descendant._transformDirty = false;
|
|
|
|
if ( !descendant.UpdateGameObjectsFromBones() )
|
|
continue;
|
|
|
|
if ( ThreadSafe.IsMainThread )
|
|
{
|
|
descendant.Transform.TransformChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public Transform? GetAttachment( string name, bool worldSpace = true )
|
|
{
|
|
return SceneModel?.GetAttachment( name, worldSpace );
|
|
}
|
|
}
|