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).
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 );
}
}
}