TrackSurface
<TrackSurface> draws the ground a track rides: a lane, its shoulders, a wall each side and an apron beyond, extruded along the centerline. A stretch the track tags as ice or dirt draws in its own material. A sled run, a kart circuit and an endless runner each draw their ground this way.
Its props are in TrackSurfaceProps.
The cross-section
Section titled “The cross-section”The shape is data: a list of points, from the left of travel to the right, in track coordinates. Each point takes the following fields:
across: metres across, positive to the right of travel.from: whereacrossis measured from,"center"by default, or the track’s"left"or"right"edge. A shoulder measured from an edge keeps its width as the lane widens.height: metres above the track, along its up.spans: how many strips the surface to the next point is cut into. More strips follow aliftmore closely.wall: the surface to the next point is a wall face, which shades flat from its crease instead of blending into the ground beside it.
A cross-section is laid every spacing metres, half a metre by default. Where a side past the track’s edge reaches over another leg of the track, as at a hairpin, it stops at the line between the two legs, so an apron never covers the other lane. A spot belongs to another leg when the stretch of track nearest it lies more than 20 m further along or back.
A track point may carry a zone, such as "ice", that blends to the next point. track.zoneWeight(distance, zone) gives how much of the zone the track is at a distance, from 0 to 1, so a game can make an icy stretch fast as well as shiny. materials names a material for each zone. A strip draws in the zone that weighs more than half there, and in the first material when none does.
Lift and paint
Section titled “Lift and paint”Two optional functions of the point’s track coordinates shape the surface further:
lift(across, along, point)returns metres to raise the surface straight up: loose snow, wind drifts or a hillside. It also receives the world point before the raise, for a height that comes from the map rather than the track.paint(across, along)returns a vertex colour as linear red, green and blue. Turn onvertexColorsin the materials to see it.
The normals follow the lifted surface, so a drift a few centimetres deep still catches the light. The uvs run in metres across and along, over tile metres per texture repeat.
Terrain to the horizon
Section titled “Terrain to the horizon”Pass terrain to stand the surface in land that reaches the horizon, so the sky never shows below it. The land draws in the same mesh, as a last zone named terrain, in the material materials.terrain names. It has the following shape:
- Under the surface, the land lies up to 8 m below it, so its wider triangles never show through the lane. It rises to the surface’s height at the rims and over the last 4 m of each end.
- Past the surface, the land leaves the nearest vertex of the surface’s outline at that vertex’s height and slope, so the two meet without a step.
- Further out, mountains rise out of a ridged noise, over the first 90 m and up to
heightmetres, 340 by default.
The land is one disc of rings round the track’s middle, reaching radius metres from it, 1200 by default. Keep the radius inside the camera’s far plane, and keep terrain out of the render, or the land is built again on every render. Its normals are smooth, its uvs are world x and z over tile, and its vertex colours are white, so a material can shade it by how steep it is.
{} takes every default. A height of 0 stands no mountains: the land carries the outline’s own slope on and eases it level over the next few tens of metres.
Unreal’s Landscape Splines deform a landscape to a road, and Unity’s terrain tools stamp a spline into a heightmap: in both, the game places a terrain and a path and joins them. Godot, Roblox and PlayCanvas leave the join to the game. TrackSurface builds the land from the track, so an agent gets a world with no seam from one option.
Standing things on it
Section titled “Standing things on it”buildTrackSurface(options) builds the same geometry without drawing it. Build it once, stand trees or props on it with a TrackScatter, on the land as well when it has some, and pass it to <TrackSurface geometry> with the materials. The game keeps and disposes a geometry it built. A surface the component builds itself is disposed on unmount.
The surface has no collider. A TrackMover rides the track, not the mesh.
Unity’s SplineExtrude, Unreal’s SplineMeshComponent and Godot’s CSGPolygon3D in path mode each sweep a 2D shape along a path. Roblox and PlayCanvas leave it to the game. TrackSurface sweeps a shape too, but measures it in track coordinates and from either edge, so an agent writes a course’s cross-section as a short list of numbers.
Sample
Section titled “Sample”A lane with shoulders and low walls, icy in the middle:
import { Track, TrackSurface, type TrackSectionPoint,} from "@daniel-zarinski/engine";
const track = new Track([ { x: 0, y: 0, z: 0, width: 12 }, { x: 8, y: -4, z: -20, width: 14, zone: "ice" }, { x: -8, y: -8, z: -45, width: 12 },]);
const section: TrackSectionPoint[] = [ { from: "left", across: -1.5, height: 1.5, wall: true }, { from: "left", across: 0, spans: 3 }, { from: "left", across: 3, spans: 8 }, { from: "right", across: -3, spans: 3 }, { from: "right", across: 0, wall: true }, { from: "right", across: 1.5, height: 1.5 },];
export function Course() { return ( <TrackSurface track={track} section={section} materials={{ snow: <meshStandardMaterial color="#f4f7fb" roughness={0.95} />, ice: <meshStandardMaterial color="#9fd3f0" roughness={0.3} />, }} /> );}