Components/TireMarkStrip.cs

A component that renders a persistent tire-mark quad strip along a series of control points. It evaluates a Catmull-Rom spline over provided Points, builds a quad-strip vertex buffer in a SceneCustomObject, and draws it each frame with configurable appearance (material, color, width, subdivisions, fade).

Native InteropFile Access
using System;

namespace Machines;

/// <summary>
/// Renders a persistent quad-strip tire mark through a series of control points.
/// </summary>
[Title( "Tire Mark Strip" )]
[Category( "Track" )]
[Icon( "tire_repair" )]
public sealed class TireMarkStrip : Component, Component.ExecuteInEditor
{
	[Property, Category( "Appearance" )]
	public Material Material { get; set; }

	[Property, Category( "Appearance" )]
	public Color MarkColor { get; set; } = Color.Black.WithAlpha( 0.85f );

	[Property, Category( "Appearance" ), Range( 1f, 40f )]
	public float Width { get; set; } = 6f;

	[Property, Category( "Appearance" ), Range( 0f, 5f )]
	public float GroundOffset { get; set; } = 0.5f;

	[Property, Category( "Shape" )]
	public int Subdivisions { get; set; } = 4;

	[Property, Category( "Shape" )]
	public int FadePoints { get; set; } = 2;

	[Property]
	public List<Vector3> Points { get; set; } = new();

	private TireMarkSceneObject _sceneObject;

	protected override void OnEnabled()
	{
		_sceneObject = new TireMarkSceneObject( this, Scene.SceneWorld );
	}

	protected override void OnDisabled()
	{
		_sceneObject?.Delete();
		_sceneObject = null;
	}

	/// <summary>
	/// Call after modifying Points to force a bounds update.
	/// </summary>
	public void Rebuild()
	{
		// Scene object re-renders each frame; just force a bounds update.
		if ( _sceneObject != null )
			_sceneObject.Bounds = BBox.FromPositionAndSize( Vector3.Zero, float.MaxValue );
	}

	/// <summary>
	/// Catmull-Rom spline evaluation through four control points.
	/// </summary>
	private static Vector3 CatmullRom( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t )
	{
		var t2 = t * t;
		var t3 = t2 * t;
		return 0.5f * (
			(2f * p1) +
			(-p0 + p2) * t +
			(2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
			(-p0 + 3f * p1 - 3f * p2 + p3) * t3
		);
	}

	/// <summary>
	/// Interpolated positions from control points via Catmull-Rom.
	/// </summary>
	internal List<Vector3> GetInterpolatedPoints()
	{
		var result = new List<Vector3>();
		if ( Points.Count < 2 )
		{
			result.AddRange( Points );
			return result;
		}

		int subdivs = Math.Max( 1, Subdivisions );

		for ( int i = 0; i < Points.Count - 1; i++ )
		{
			var p0 = Points[Math.Max( 0, i - 1 )];
			var p1 = Points[i];
			var p2 = Points[i + 1];
			var p3 = Points[Math.Min( Points.Count - 1, i + 2 )];

			for ( int s = 0; s < subdivs; s++ )
			{
				float t = (float)s / subdivs;
				result.Add( CatmullRom( p0, p1, p2, p3, t ) );
			}
		}

		// Final point.
		result.Add( Points[^1] );
		return result;
	}

	/// <summary>
	/// Scene object that renders the strip as a quad-strip mesh.
	/// </summary>
	private class TireMarkSceneObject : SceneCustomObject
	{
		private readonly TireMarkStrip _owner;

		public TireMarkSceneObject( TireMarkStrip owner, SceneWorld world ) : base( world )
		{
			_owner = owner;
			Bounds = BBox.FromPositionAndSize( Vector3.Zero, float.MaxValue );
			Flags.CastShadows = false;
		}

		public override void RenderSceneObject()
		{
			if ( !_owner.IsValid() || _owner.Material == null )
				return;

			var points = _owner.GetInterpolatedPoints();
			if ( points.Count < 2 )
				return;

			float halfWidth = _owner.Width * 0.5f;
			var baseColor = _owner.MarkColor;
			int fadePoints = _owner.FadePoints;

			var vb = new VertexBuffer();
			vb.Init( true );

			for ( int i = 0; i < points.Count; i++ )
			{
				var pos = points[i];

				// Forward direction from neighboring points.
				Vector3 forward;
				if ( i == 0 )
					forward = (points[1] - points[0]).Normal;
				else if ( i == points.Count - 1 )
					forward = (points[^1] - points[^2]).Normal;
				else
					forward = (points[i + 1] - points[i - 1]).Normal;

				var right = Vector3.Cross( forward, Vector3.Up ).Normal;

				if ( right.LengthSquared < 0.01f )
					right = Vector3.Cross( forward, Vector3.Right ).Normal;

				float alpha = 1f;
				if ( i < fadePoints )
					alpha = (float)(i + 1) / (fadePoints + 1);
				else if ( i >= points.Count - fadePoints )
					alpha = (float)(points.Count - i) / (fadePoints + 1);

				var color = (Color32)(baseColor.WithAlphaMultiplied( alpha ));
				var leftEdge = pos - right * halfWidth;
				var rightEdge = pos + right * halfWidth;

				float v = (float)i / (points.Count - 1);

				vb.Default.Normal = Vector3.Up;
				vb.Default.Tangent = new Vector4( right.x, right.y, right.z, 1f );
				vb.Default.Color = color;

				vb.Default.TexCoord0 = new Vector4( 0f, v, 0f, 0f );
				vb.Default.Position = leftEdge;
				vb.Add( vb.Default );

				vb.Default.TexCoord0 = new Vector4( 1f, v, 0f, 0f );
				vb.Default.Position = rightEdge;
				vb.Add( vb.Default );

				if ( i > 0 )
				{
					int baseIdx = (i - 1) * 2;
					vb.AddRawIndex( baseIdx );
					vb.AddRawIndex( baseIdx + 1 );
					vb.AddRawIndex( baseIdx + 2 );

					vb.AddRawIndex( baseIdx + 1 );
					vb.AddRawIndex( baseIdx + 3 );
					vb.AddRawIndex( baseIdx + 2 );
				}
			}

			vb.Draw( _owner.Material );
		}
	}
}