mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-05-29 09:15:27 -04:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
353 lines
10 KiB
C#
353 lines
10 KiB
C#
using Sandbox.MovieMaker;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace Editor.MovieMaker;
|
|
|
|
#nullable enable
|
|
|
|
public enum KeyframeInterpolation
|
|
{
|
|
Unknown = -1,
|
|
|
|
Step = 0,
|
|
Linear,
|
|
Quadratic,
|
|
Cubic
|
|
}
|
|
|
|
public interface IKeyframe
|
|
{
|
|
MovieTime Time { get; }
|
|
object? Value { get; }
|
|
KeyframeInterpolation Interpolation { get; }
|
|
}
|
|
|
|
public readonly record struct Keyframe( MovieTime Time, object? Value, KeyframeInterpolation Interpolation ) : IKeyframe, IComparable<Keyframe>
|
|
{
|
|
public int CompareTo( Keyframe other ) => Time.CompareTo( other.Time );
|
|
|
|
public static InterpolationMode GetInterpolationMode( KeyframeInterpolation prev, KeyframeInterpolation next ) => (prev, next) switch
|
|
{
|
|
(KeyframeInterpolation.Step, _) => InterpolationMode.None,
|
|
(<= KeyframeInterpolation.Linear, <= KeyframeInterpolation.Linear) => InterpolationMode.Linear,
|
|
(_, KeyframeInterpolation.Linear) => InterpolationMode.QuadraticIn,
|
|
(KeyframeInterpolation.Linear, _) => InterpolationMode.QuadraticOut,
|
|
_ => InterpolationMode.QuadraticInOut
|
|
};
|
|
}
|
|
|
|
partial record PropertySignal
|
|
{
|
|
private ImmutableArray<Keyframe>? _keyframes;
|
|
|
|
[JsonIgnore]
|
|
public IReadOnlyList<Keyframe> Keyframes => _keyframes ??= [..OnGetKeyframes().Order()];
|
|
|
|
[JsonIgnore]
|
|
public bool HasKeyframes => Keyframes.Count > 0;
|
|
|
|
public IEnumerable<Keyframe> GetKeyframes( MovieTimeRange timeRange ) => Keyframes
|
|
.SkipWhile( x => x.Time < timeRange.Start )
|
|
.TakeWhile( x => x.Time <= timeRange.End );
|
|
|
|
protected virtual IEnumerable<Keyframe> OnGetKeyframes() => [];
|
|
|
|
private static MethodInfo FromKeyframesCoreMethod { get; } = typeof(PropertySignal)
|
|
.GetMethod( nameof(FromKeyframesCore), BindingFlags.Static | BindingFlags.NonPublic )!;
|
|
|
|
public static PropertySignal FromKeyframes( Type propertyType, IEnumerable<Keyframe> keyframes, PropertySignal? baseSignal = null )
|
|
{
|
|
var method = FromKeyframesCoreMethod.MakeGenericMethod( propertyType );
|
|
|
|
return (PropertySignal)method.Invoke( null, [keyframes, baseSignal] )!;
|
|
}
|
|
|
|
private static PropertySignal FromKeyframesCore<T>( IEnumerable<Keyframe> keyframes, PropertySignal<T>? baseSignal = null )
|
|
{
|
|
var keyframeSignal = new KeyframeSignal<T>( [..keyframes.Select( x => (Keyframe<T>)x )] );
|
|
|
|
return baseSignal is not null
|
|
? baseSignal + keyframeSignal
|
|
: keyframeSignal;
|
|
}
|
|
}
|
|
|
|
partial record PropertySignal<T>
|
|
{
|
|
/// <summary>
|
|
/// Replace the keyframes in this signal with the given sequence.
|
|
/// </summary>
|
|
public PropertySignal<T> WithKeyframes( IReadOnlyList<Keyframe<T>> keyframes )
|
|
{
|
|
if ( !AreOrdered( keyframes ) )
|
|
{
|
|
throw new ArgumentException( "Keyframes must be in ascending time order.", nameof( keyframes ) );
|
|
}
|
|
|
|
return OnWithKeyframes( keyframes );
|
|
}
|
|
|
|
protected virtual PropertySignal<T> OnWithKeyframes( IReadOnlyList<Keyframe<T>> keyframes )
|
|
{
|
|
if ( keyframes.Count == 0 ) return this;
|
|
|
|
if ( Transformer.GetDefault<T>() is not { } transformer )
|
|
{
|
|
// If we can't do additive blending, replace this signal with the new keyframe signal.
|
|
|
|
return new KeyframeSignal<T>( [..keyframes] );
|
|
}
|
|
|
|
return this + new KeyframeSignal<T>( [..keyframes.Select( x =>
|
|
x with { Value = transformer.Difference( GetValue( x.Time ), x.Value ) } )] );
|
|
}
|
|
|
|
private static bool AreOrdered( IReadOnlyList<Keyframe<T>> keyframes )
|
|
{
|
|
if ( keyframes.Count < 2 ) return true;
|
|
|
|
var prev = keyframes[0];
|
|
|
|
foreach ( var next in keyframes.Skip( 1 ) )
|
|
{
|
|
if ( prev.Time > next.Time ) return false;
|
|
|
|
prev = next;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public readonly record struct Keyframe<T>(
|
|
MovieTime Time,
|
|
T Value,
|
|
KeyframeInterpolation Interpolation ) : IKeyframe, IComparable<Keyframe<T>>
|
|
{
|
|
public static implicit operator Keyframe( Keyframe<T> keyframe ) =>
|
|
new ( keyframe.Time, keyframe.Value, keyframe.Interpolation );
|
|
public static explicit operator Keyframe<T>( Keyframe keyframe ) =>
|
|
new( keyframe.Time, (T)keyframe.Value!, keyframe.Interpolation );
|
|
|
|
public int CompareTo( Keyframe<T> other ) => Time.CompareTo( other.Time );
|
|
|
|
object? IKeyframe.Value => Value;
|
|
}
|
|
|
|
public interface IKeyframeSignal : IPropertySignal;
|
|
|
|
[JsonDiscriminator( "Keyframes" )]
|
|
file sealed record KeyframeSignal<T>( ImmutableArray<Keyframe<T>> Keyframes ) : PropertySignal<T>, IKeyframeSignal
|
|
{
|
|
private readonly ImmutableArray<Keyframe<T>> _keyframes = ValidateKeyframes( Keyframes );
|
|
|
|
public new ImmutableArray<Keyframe<T>> Keyframes
|
|
{
|
|
get => _keyframes;
|
|
private init => _keyframes = ValidateKeyframes( value );
|
|
}
|
|
|
|
[JsonIgnore]
|
|
public MovieTimeRange TimeRange => (Keyframes[0].Time, Keyframes[^1].Time);
|
|
|
|
protected override IEnumerable<Keyframe> OnGetKeyframes() => Keyframes.Select( x => (Keyframe)x );
|
|
|
|
public override T GetValue( MovieTime time )
|
|
{
|
|
if ( time <= Keyframes[0].Time )
|
|
{
|
|
return Keyframes[0].Value;
|
|
}
|
|
|
|
if ( time >= Keyframes[^1].Time )
|
|
{
|
|
return Keyframes[^1].Value;
|
|
}
|
|
|
|
var index = FindIndex( time );
|
|
|
|
if ( _interpolator is not { } interpolator )
|
|
{
|
|
return Keyframes[index].Value;
|
|
}
|
|
|
|
var p0 = Keyframes[index];
|
|
var p1 = Keyframes[index + 1];
|
|
|
|
var timeRange = new MovieTimeRange( p0.Time, p1.Time );
|
|
var fraction = timeRange.GetFraction( time );
|
|
|
|
if ( fraction <= 0f ) return p0.Value;
|
|
if ( fraction >= 1f ) return p1.Value;
|
|
|
|
if ( p0.Interpolation == KeyframeInterpolation.Step )
|
|
{
|
|
p1 = p1 with { Interpolation = KeyframeInterpolation.Step };
|
|
}
|
|
|
|
if ( _transformer is not { } transformer || p0.Interpolation <= KeyframeInterpolation.Quadratic && p1.Interpolation <= KeyframeInterpolation.Quadratic )
|
|
{
|
|
var mode = Keyframe.GetInterpolationMode( p0.Interpolation, p1.Interpolation );
|
|
return interpolator.Interpolate( p0.Value, p1.Value, mode.Apply( fraction ) );
|
|
}
|
|
|
|
var pPrev = Keyframes[Math.Max( 0, index - 1 )];
|
|
var pNext = Keyframes[Math.Min( Keyframes.Length - 1, index + 2 )];
|
|
|
|
var dtPrev = p0.Time > pPrev.Time ? (float)(timeRange.Duration.TotalSeconds / (p0.Time - pPrev.Time).TotalSeconds) : 1f;
|
|
var dtNext = pNext.Time > p1.Time ? (float)(timeRange.Duration.TotalSeconds / (pNext.Time - p1.Time).TotalSeconds) : 1f;
|
|
|
|
var dxPrev = transformer.Difference( pPrev.Value, p0.Value );
|
|
var dxCurr = transformer.Difference( p0.Value, p1.Value );
|
|
var dxNext = transformer.Difference( p1.Value, pNext.Value );
|
|
|
|
// Bias tangents based on intervals between keyframes, so faster keyframes have more influence over tangents
|
|
|
|
var tangent0 = interpolator.Interpolate( dxCurr, dxPrev, dtPrev / (dtPrev + 1f / dtPrev) );
|
|
var tangent1 = interpolator.Interpolate( dxCurr, dxNext, dtNext / (dtNext + 1f / dtNext) );
|
|
|
|
// We're scaling the tangent velocities on both sides, so sqrt to meet in the middle.
|
|
// Divide by 3 to get a constant velocity when points and tangents are all aligned.
|
|
|
|
var tangentScalePrev = MathF.Sqrt( dtPrev ) / 3f;
|
|
var tangentScaleNext = MathF.Sqrt( dtNext ) / 3f;
|
|
|
|
var control0 = p0.Interpolation <= KeyframeInterpolation.Quadratic ? p0.Value : interpolator.Interpolate( p0.Value, transformer.Apply( p0.Value, tangent0 ), tangentScalePrev );
|
|
var control1 = p1.Interpolation <= KeyframeInterpolation.Quadratic ? p1.Value : interpolator.Interpolate( p1.Value, transformer.Apply( p1.Value, transformer.Invert( tangent1 ) ), tangentScaleNext );
|
|
|
|
// De Casteljau's algorithm
|
|
|
|
var a0 = interpolator.Interpolate( p0.Value, control0, fraction );
|
|
var a1 = interpolator.Interpolate( control0, control1, fraction );
|
|
var a2 = interpolator.Interpolate( control1, p1.Value, fraction );
|
|
|
|
var b0 = p0.Interpolation <= KeyframeInterpolation.Linear ? p0.Value : interpolator.Interpolate( a0, a1, fraction );
|
|
var b1 = p1.Interpolation <= KeyframeInterpolation.Linear ? p1.Value : interpolator.Interpolate( a1, a2, fraction );
|
|
|
|
return interpolator.Interpolate( b0, b1, fraction );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the current keyframe index for the given time.
|
|
/// </summary>
|
|
private int FindIndex( MovieTime time )
|
|
{
|
|
var index = Keyframes.BinarySearch( new Keyframe<T>( time, default!, default ) );
|
|
|
|
// exact match
|
|
|
|
if ( index >= 0 ) return index;
|
|
|
|
// ~index is next keyframe after time, we want previous keyframe
|
|
|
|
return Math.Clamp( ~index - 1, 0, Keyframes.Length - 1 );
|
|
}
|
|
|
|
private int? FindIndexExact( MovieTime time )
|
|
{
|
|
var index = Keyframes.BinarySearch( new Keyframe<T>( time, default!, default ) );
|
|
|
|
return index >= 0 ? index : null;
|
|
}
|
|
|
|
protected override PropertySignal<T> OnWithKeyframes( IReadOnlyList<Keyframe<T>> keyframes )
|
|
{
|
|
if ( keyframes.Count == 0 )
|
|
{
|
|
if ( _transformer is { } transformer )
|
|
{
|
|
return transformer.Identity.AsSignal();
|
|
}
|
|
|
|
// TODO: is this a sensible default?
|
|
|
|
return Keyframes[0].Value.AsSignal();
|
|
}
|
|
|
|
return this with { Keyframes = [..keyframes] };
|
|
}
|
|
|
|
protected override PropertySignal<T> OnReduce( MovieTime? start, MovieTime? end )
|
|
{
|
|
var i = 0;
|
|
var j = Keyframes.Length - 1;
|
|
|
|
if ( start is { } s )
|
|
{
|
|
i = FindIndex( s );
|
|
}
|
|
|
|
if ( end is { } e )
|
|
{
|
|
j = Math.Min( FindIndex( e ) + 1, Keyframes.Length - 1);
|
|
}
|
|
|
|
// Cubic needs to know about previous / next keyframe
|
|
|
|
if ( i > 0 && Keyframes[i].Interpolation is KeyframeInterpolation.Cubic )
|
|
{
|
|
i -= 1;
|
|
}
|
|
|
|
if ( j < Keyframes.Length - 1 && Keyframes[j].Interpolation is KeyframeInterpolation.Cubic )
|
|
{
|
|
j += 1;
|
|
}
|
|
|
|
if ( i == 0 && j == Keyframes.Length - 1 ) return this;
|
|
|
|
return this with { Keyframes = Keyframes[i..(j + 1)] };
|
|
}
|
|
|
|
protected override PropertySignal<T> OnTransform( MovieTransform value ) =>
|
|
new KeyframeSignal<T>( [..Keyframes.Select( x => x with { Time = value * x.Time } )] );
|
|
|
|
public override IEnumerable<MovieTimeRange> GetPaintHints( MovieTimeRange timeRange )
|
|
{
|
|
var prev = Keyframes[0];
|
|
|
|
// TODO: non-interpolated
|
|
|
|
foreach ( var next in Keyframes.Skip( 1 ) )
|
|
{
|
|
if ( timeRange.Intersect( (prev.Time, next.Time) ) is { } intersection )
|
|
{
|
|
yield return intersection;
|
|
}
|
|
|
|
prev = next;
|
|
}
|
|
}
|
|
|
|
private static ImmutableArray<Keyframe<T>> ValidateKeyframes( ImmutableArray<Keyframe<T>> keyframes )
|
|
{
|
|
if ( keyframes.IsDefaultOrEmpty )
|
|
{
|
|
throw new ArgumentException( "Expected at least one keyframe.", nameof(keyframes) );
|
|
}
|
|
|
|
var prevTime = keyframes[0].Time;
|
|
|
|
foreach ( var keyframe in keyframes.Skip( 1 ) )
|
|
{
|
|
if ( keyframe.Time < prevTime )
|
|
{
|
|
throw new ArgumentException( "Keyframes must be sorted by ascending time.", nameof(keyframes) );
|
|
}
|
|
|
|
prevTime = keyframe.Time;
|
|
}
|
|
|
|
return keyframes;
|
|
}
|
|
|
|
[SkipHotload]
|
|
private static readonly IInterpolator<T>? _interpolator = Interpolator.GetDefault<T>();
|
|
|
|
[SkipHotload]
|
|
private static readonly ITransformer<T>? _transformer = Transformer.GetDefault<T>();
|
|
}
|