A scene Component representing a pre-race camera marker. It stores references to an end target and optional orbit target, computes interpolated camera position/rotation along a linear or orbital arc, and draws gizmos showing the path in the editor.
namespace Machines.Components;
/// <summary>
/// Pre-race camera viewpoint; interpolates from this transform to EndTarget while facing OrbitTarget.
/// </summary>
public sealed class CameraMarker : Component
{
/// <summary>
/// All active camera markers in the current scene.
/// </summary>
public static IReadOnlyList<CameraMarker> All => _all;
private static readonly List<CameraMarker> _all = new();
/// <summary>
/// The end position/rotation the camera interpolates toward over the dwell time.
/// </summary>
[Property]
public GameObject EndTarget { get; set; }
/// <summary>
/// Optional look-at target during dwell; if null, rotation interpolates between start and end.
/// </summary>
[Property]
public GameObject OrbitTarget { get; set; }
/// <summary>
/// Arc blend around OrbitTarget: 0 = straight line, 1 = full orbital arc.
/// </summary>
[Property, Range( 0f, 1f )]
public float ArcAmount { get; set; } = 1f;
/// <summary>
/// Extra degrees added to the arc angle; forces a wider swing.
/// </summary>
[Property]
public float ArcAngleOffset { get; set; } = 0f;
/// <summary>
/// When true, the arc goes the long way around the orbit target.
/// </summary>
[Property]
public bool InvertArc { get; set; } = false;
protected override void OnEnabled()
{
_all.Add( this );
}
protected override void OnDisabled()
{
_all.Remove( this );
}
/// <summary>
/// Evaluate position and rotation at normalized time t (0-1).
/// </summary>
public void Evaluate( float t, out Vector3 position, out Rotation rotation )
{
var startPos = WorldPosition;
var startRot = WorldRotation;
var endPos = EndTarget.IsValid() ? EndTarget.WorldPosition : startPos;
var endRot = EndTarget.IsValid() ? EndTarget.WorldRotation : startRot;
if ( OrbitTarget.IsValid() )
{
var pivot = OrbitTarget.WorldPosition;
var startOffset = startPos - pivot;
var endOffset = endPos - pivot;
// Arc angle: shortest path + offset, optionally inverted
var startAngle = MathF.Atan2( startOffset.y, startOffset.x );
var endAngle = MathF.Atan2( endOffset.y, endOffset.x );
var delta = endAngle - startAngle;
if ( delta > MathF.PI ) delta -= MathF.PI * 2f;
if ( delta < -MathF.PI ) delta += MathF.PI * 2f;
// Invert: take the long way around
if ( InvertArc )
{
if ( delta > 0 ) delta -= MathF.PI * 2f;
else delta += MathF.PI * 2f;
}
delta += MathX.DegreeToRadian( ArcAngleOffset );
var angle = startAngle + delta * t;
var radius = MathX.Lerp( startOffset.WithZ( 0 ).Length, endOffset.WithZ( 0 ).Length, t );
var height = MathX.Lerp( startOffset.z, endOffset.z, t );
var arcPos = pivot + new Vector3( MathF.Cos( angle ) * radius, MathF.Sin( angle ) * radius, height );
// Blend between linear and arc
var linearPos = Vector3.Lerp( startPos, endPos, t );
position = Vector3.Lerp( linearPos, arcPos, ArcAmount );
var dir = (pivot - position).Normal;
rotation = Rotation.LookAt( dir, Vector3.Up );
}
else
{
position = Vector3.Lerp( startPos, endPos, t );
rotation = Rotation.Slerp( startRot, endRot, t );
}
}
protected override void DrawGizmos()
{
Gizmo.Draw.Color = Color.Cyan.WithAlpha( 0.6f );
Gizmo.Draw.LineSphere( Vector3.Zero, 16f );
if ( !EndTarget.IsValid() )
return;
var steps = 20;
var prev = Vector3.Zero;
Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.8f );
for ( int i = 1; i <= steps; i++ )
{
var t = (float)i / steps;
Evaluate( t, out var worldPoint, out _ );
var localPoint = Transform.World.PointToLocal( worldPoint );
Gizmo.Draw.Line( prev, localPoint );
prev = localPoint;
}
Gizmo.Draw.Color = Color.Red.WithAlpha( 0.6f );
Gizmo.Draw.LineSphere( prev, 8f );
if ( OrbitTarget.IsValid() )
{
var targetLocal = Transform.World.PointToLocal( OrbitTarget.WorldPosition );
Gizmo.Draw.Color = Color.Green.WithAlpha( 0.4f );
Gizmo.Draw.Line( Vector3.Zero, targetLocal );
}
}
}