# roadPieces
Road and track pieces built as real geometry, in three styles that one setting switches:
| style | deck | markings | roadside |
|---|---|---|---|
| `country` | 6.7 m tarmac, two lanes | faded dashed white centre line, edge lines | 2.5 m mown verges, white marker posts every 25 m |
| `highway` | 17.2 m tarmac, two lanes each way, shoulders | double yellow centre, dashed lane lines, edge lines | concrete (New Jersey) barriers on the shoulders, 3 m verges |
| `dirt` | 4.4 m track | none | dirt wheel ruts, gravel shoulders, a grass crown down the middle, verges |
Pieces: `straight`, `curve` (radius, angle; positive turns right), `tJunction`, `xJunction` (filleted kerb corners, stop lines on the side roads), `roundabout` (kerbed grass island, 1 to 4 arms clipped exactly to the ring, give-way lines), `ramp` (slope, length), `kicker` (lip height and angle, gap, table-top landing with an 18° run-out), `bankedTurn` (radius, angle, bank eased in and out, red/white kerb on the inside, concrete wall on the outside), `guardrail` (W-beam on posts), `wall` (concrete barrier), `tyreWall` (two staggered rows of tyre stacks), and `spline` (a continuous road through a list of points that follows their heights).
The asphalt and its markings are one procedural shader (`roadSurface`, on driveSurfaces' pavement library): aggregate, repair patches, cracks, polished wheel tracks per lane and worn paint, placed by world position so consecutive pieces join without a seam. Every piece's deck, verges, kerbs and walls collide (static exact mesh colliders) and carry a driveSurfaces `Surface`: deck tarmac (dirt on the dirt style, gravel shoulders, grass crown), verges grass, kerbs rumble, walls and ramp sides concrete.
## First example
Paste this into the console (`execute`) of a fresh world that has roadPieces (with driveSurfaces and groundMaterials) installed. It builds a country road with a bend and a roundabout, then switches the bend to a highway with one setting:
```luau
local Roads = require("roadPieces.roads")
-- every piece starts at its origin and runs along its local -Z
local straight = Roads.spawn("straight", { style = "country", length = 30, position = { 0, 0, 0 }, synced = true })
local bend = Roads.spawn("curve", { style = "country", radius = 25, angle = 90, position = { 0, 0, -30 }, synced = true })
local rb = Roads.spawn("roundabout", { style = "country", radius = 8, length = 12, position = { 60, 0, -40 }, synced = true })
-- one setting restyles a piece in place
bend.component.get("roadPieces.RoadPiece").style = "highway"
-- the centreline in world space, for an AI driver (aiDrivers: AiDriver:setPath(points, loop))
local points, loop = Roads.pathOf(rb)
print(#points, loop) --> 49 true (the ring, counter-clockwise from above)
```
A spline road through your own points (world positions with heights):
```luau
local hill = Roads.spline({
vector.create(0, 0, 0), vector.create(40, 3, -30), vector.create(10, 6, -80), vector.create(-30, 2, -110),
}, { style = "dirt", synced = true })
```
To see everything, load the gallery (edit mode): `zero scene load roadPieces.roadGallery`. Rows are the three styles (country at z 0, highway at z 100, dirt at z 200), columns the twelve kinds, each labelled.
## API (`require("roadPieces.roads")`)
| Call | What it does |
|---|---|
| `Roads.spawn(kind, opts?)` | spawns an entity with a `RoadPiece`; `opts` takes any RoadPiece field plus `position`, `yaw` (degrees), `parent`, `name`, `synced` |
| `Roads.spline(points, opts?)` | a `spline` piece through `points` (vectors or `{x,y,z}`, in the entity's local space = world space at the origin); `opts.loop` closes it |
| `Roads.pathOf(road, offset?)` | `(points, loop)`: the centreline on the deck surface in world space; `offset` metres shifts it to the right of travel (lane centres). Junctions answer their through road, a roundabout its ring. Accepts the piece or any of its children |
| `Roads.build(spec)` / `Roads.realize(root, built, style)` | the pure geometry builder and the step that stands it up as temporary children |
| `Roads.specOf(component)`, `Roads.encodePoints`, `Roads.decodePoints` | field <-> spec helpers |
| `Roads.gallery(parent?)` | every piece in every style, labelled |
| `Roads.STYLES`, `Roads.KINDS`, `Roads.DECK` (deck height, 5 cm), `Roads.BASE_DEPTH` | the tables behind it |
## The `RoadPiece` component
Fields (all synced): `kind`, `style`, `length` (20), `radius` (25), `angle` (90), `bank` (25), `slope` (12), `height` (1.2), `gap` (8), `width` (0 = the style's), `arms` (4), `verge` (true), `rails` (`auto` | `none` | `posts` | `guardrail` | `concrete` | `tyres`), `points` (spline points as `"x,y,z;x,y,z"`), `loop`. Changing any field rebuilds the piece. Methods: `:path(offset?)`, `:rebuild()`.
Geometry is built locally on every peer from those fields as temporary, internal child entities, so a scene stores only the RoadPiece record and nothing has to replicate but the fields. Spline points live in one string field rather than a synced list (synced lists with gaps, #21242) and nothing holds a synced entity reference (#21241).
## Placing pieces
- Every piece lies in its own local space: it starts at the origin and runs along local -Z, right is +X, up +Y. The deck top is 5 cm above the piece's ground level (local y = 0); verges and base plates lie at y = 0 and reach 0.7 m down, so pieces sit into terrain without gaps.
- A curve of `radius` R and `angle` A ends at `(R(1-cos A), 0, -R sin A)` (mirrored for negative angles) heading along the turned direction: place the next piece there with `yaw = -A`.
- Junction arms and roundabout arms end flush with straights of the same style, so a network is pieces end to end.
- With `verge = true`, ramps, kickers, banked turns and barriers stand on a grass base plate; set it false to drop them on your own ground.
## Known limits
- **#1404:** the engine's built-in roads are in progress, which is why this package builds its own road meshes. When they land, the styles and the Surface wiring here can sit on top of them.
- The engine's z-fighting diagnostic flags ground laid near other ground from a distance (it uses a tolerance that grows with distance: about 10 cm at 20 m). Within a piece no asphalt lies over grass (junction and roundabout plates wrap the decks instead of sitting under them), but a deck 5 cm above its verge, or a piece on terrain, still flags from far off. The gallery stands each piece on a 0.6 m plinth above its meadow for that reason.
- Pieces do not bend to terrain: lay them on flat ground or use a `spline` whose points follow the terrain.
## Files
`roads.module`, `RoadPiece.component`, `shaders/roadSurface.shader`, `materials/` (roadCountry, roadHighway, barrierConcrete, kerb, kerbConcrete, steel, rubber, postWhite), `roadGallery.scene`.
# driveSurfaces
The ground a tyre stands on. Fourteen surfaces in a fixed order, each with what it does to a tyre (grip, slide grip, stiffness, rolling resistance, bogging drag, roughness, dust colour, skid sound, tyre class) and what it looks like; one call that answers which surface a wheel is on; a `Surface` component that paints any entity with a surface; and a `SurfacePatch` component that lays a puddle, an oil spill or a rumble strip over whatever ground is under it.
Paved looks are procedural and texture-free (`drivePavement` shader): aged asphalt with aggregate stones, repair patches and hairline cracks; wet asphalt with standing water; sawn concrete slabs; granite setts in running bond; red/white ribbed kerbs; still puddle water with faint ripples; black oil with a thin-film rainbow sheen. They show at real scale on any box, plane or mesh. Natural grounds wear the installed **groundMaterials** (Terra Firma) surfaces.
## First example
Paste this into the console (`execute`) of a fresh world that has driveSurfaces and groundMaterials installed. It lays four 8 m pads and an oil spill in front of the spawn, then asks which surface a wheel would read on each:
```luau
local Surfaces = require("driveSurfaces.surfaces")
local pads = {}
for i, kind in { "tarmac", "gravel", "grass", "ice" } do
-- an 8 x 8 m pad whose top is at y = 0.05: material, BoxCollider and Surface component
pads[kind] = Surfaces.pad(kind, { (i - 2.5) * 9, 0.05, -12 }, { synced = true })
end
-- a 3 x 5 m oil spill lying on the tarmac pad (a zone: it wins over the pad under it)
Surfaces.patch("oil", { -13.5, 0.05, -12 }, { size = { 3, 5 }, yaw = 30, synced = true })
-- what a wheel resting at a point reads (normally: the WheelCollider's hit entity and point)
print(Surfaces.sample(pads.gravel.id, vector.create(-4.5, 0.05, -12)).name) --> gravel
print(Surfaces.sample(pads.tarmac.id, vector.create(-13.5, 0.05, -12)).name) --> oil
print(Surfaces.sample(nil, nil).name) --> tarmac
```
To remove it: `for _, e in pads do entity.despawn(e.id) end` and despawn `patch_oil`. To see every surface at once, load the gallery: `zero scene load driveSurfaces.surfacesGallery` in the shell (edit mode; it bakes on its first load).
## API (`require("driveSurfaces.surfaces")`)
| Call | Answers |
|---|---|
| `Surfaces.names()` | surface names in their fixed order: the `car.surfaceId` order of the driving contract |
| `Surfaces.list()` | every definition, in order |
| `Surfaces.get(name)` / `Surfaces.byId(id)` | one definition, or nil |
| `Surfaces.default()` | tarmac |
| `Surfaces.sample(hitEntityId, position)` | the surface at a wheel contact: a patch zone at `position` first, else the `Surface` on the hit entity or its nearest ancestor that has one, else tarmac. Takes an id string or entity proxy (or nil), and a vector, a position proxy or `{x, y, z}` |
| `Surfaces.ofEntity(entity)` | the entity's own / nearest ancestor's surface, else tarmac |
| `Surfaces.patchAt(position)` | the patch zone surface at a point, or nil |
| `Surfaces.material(name)` | the material a surface wears (an AssetRef for `Model.material`) |
| `Surfaces.paint(entity, kind)` | gives an entity the surface's material (adding a cube Model if it has none) and a `Surface` component |
| `Surfaces.pad(kind, position, opts?)` | spawns a drivable pad (`opts.size` {x,y,z}, default 8 x 0.2 x 8, top at `position.y`; `parent`, `name`, `synced`, `yaw`) |
| `Surfaces.patch(kind, position, opts?)` | spawns a patch zone lying on the ground at `position` (`opts.size` {x, z}, default 4 x 4) |
| `Surfaces.gallery(parent?)` | lays out every surface as a labelled pad |
| `Surfaces.invalidate()`, `registerPatch`, `unregisterPatch`, `patchCount()` | what the components call; `patchCount` for debugging |
A definition is `{ name, label, id, class, grip, slide, stiffness, rolling, drag, bump, dust: vector, skid, material, patchOnly }`:
- `class`: `paved` | `wet` | `loose` | `soft` | `snow` | `ice`, the keys of a tyre's `affinity` table in torqueCars.
- `grip`: peak grip multiplier (tarmac 1). `slide`: multiplier on the tyre's sliding grip. `stiffness`: multiplier on tyre stiffness.
- `rolling`: rolling resistance coefficient. `drag`: extra speed drag in s^-1 (bogging). `bump`: 0..1 roughness felt through the suspension.
- `dust`: colour of the dust or spray a sliding tyre throws. `skid`: `tarmac` | `gravel` | `soft` | `none` (which skid sound).
| id | name | class | grip | slide | stiff | rolling | drag | bump | skid | look |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | tarmac | paved | 1.00 | 1.00 | 1.00 | 0.012 | 0 | 0.03 | tarmac | aged asphalt (drivePavement) |
| 2 | wetTarmac | wet | 0.70 | 0.62 | 0.90 | 0.014 | 0 | 0.03 | tarmac | wet asphalt, standing water |
| 3 | concrete | paved | 0.95 | 0.95 | 1.00 | 0.013 | 0 | 0.06 | tarmac | 4 m sawn slabs |
| 4 | cobbles | paved | 0.80 | 0.80 | 0.95 | 0.018 | 0 | 0.35 | tarmac | granite setts |
| 5 | gravel | loose | 0.60 | 0.75 | 0.60 | 0.030 | 0 | 0.30 | gravel | groundMaterials `gravel` |
| 6 | dirt | loose | 0.65 | 0.70 | 0.70 | 0.025 | 0 | 0.20 | gravel | groundMaterials `dirt` |
| 7 | mud | soft | 0.40 | 0.45 | 0.50 | 0.060 | 0.20 | 0.25 | soft | groundMaterials `mud` |
| 8 | grass | soft | 0.50 | 0.55 | 0.60 | 0.035 | 0 | 0.15 | soft | groundMaterials `shortGrass` |
| 9 | sand | soft | 0.45 | 0.55 | 0.45 | 0.080 | 0.25 | 0.10 | soft | groundMaterials `drySand` |
| 10 | snow | snow | 0.35 | 0.40 | 0.50 | 0.030 | 0.05 | 0.08 | soft | groundMaterials `packedSnow` |
| 11 | ice | ice | 0.10 | 0.12 | 0.90 | 0.010 | 0 | 0.01 | none | groundMaterials `ice` |
| 12 | rumble | paved | 0.90 | 0.90 | 1.00 | 0.020 | 0 | 0.85 | tarmac | red/white ribbed kerb |
| 13 | puddle | wet | 0.45 | 0.40 | 0.80 | 0.020 | 0 | 0.02 | none | still dark water |
| 14 | oil | wet | 0.15 | 0.15 | 0.90 | 0.012 | 0 | 0 | none | black film, rainbow sheen |
The order is fixed: new surfaces are appended, never inserted, because `car.surfaceId` is an index into it.
## Components
- **`Surface`** (`kind`, synced): this entity, and every descendant without a Surface of its own, is that ground. `Surfaces.paint` adds it.
- **`SurfacePatch`** (`kind`, `showLook`, synced): a zone of one surface over other ground. The zone is the entity's unit box times its world scale, turned with the entity; the entity's origin sits ON the ground, and the zone reaches half its Y scale up and down plus 0.35 m of slack. It registers itself into the module's flat patch list on awake (and moves with its entity, checked every 0.25 s) and unregisters on destroy. Its look sits flush on the ground at the origin: water (puddle) and oil are 1.5–2 cm films cut to an irregular blob, rumble is a 3 cm red/white slab whose stripes run along local Z, any other kind a 2 cm slab of its material.
## How a car uses it
torqueCars calls `Surfaces.sample(hit.entityId, hit.point)` for each wheel's ground contact, multiplies the tyre's own grip fields by `grip` / `slide` / `stiffness` and its `affinity[class]`, adds `rolling` and `drag` to its resistance, shakes the suspension by `bump`, and publishes the result's `id` as `car.surfaceId`. `sample` is two table lookups for an entity it has seen (cached per entity id, invalidated when a Surface appears, changes or goes) plus one oriented-box test per patch zone, about 0.25 µs per call.
## Known limits
- **#20569:** the engine's named terrain surfaces exist, but `WheelCollider` wheels do not read them yet. Grip per surface therefore comes from this package's `sample()`, which the car calls per wheel and applies to its WheelCollider grip fields. When the engine reads surfaces itself, the same table can drive it.
- **#20610:** the engine's rolling resistance is in progress. `rolling` is a coefficient the car applies itself (as a resisting torque/force) until the WheelCollider takes one.
- **Thin films:** puddle and oil looks are planes 1.5–2 cm above the ground they lie on (a decal projector stamps base colour only, so it cannot carry glossy water). They read cleanly in lit frames, and the engine's z-fighting diagnostic flags them from far away by its distance-relative tolerance.
- **Patch zones are boxes:** a puddle's look is an irregular blob, but its zone is the whole box.
## Multiplayer
`Surface` and `SurfacePatch` fields are synced, so a patch or paint applied to a synced entity reaches every player; every peer registers its own patch zones from its own entities, so `sample()` answers the same everywhere. Script-made pads in edit mode are local unless spawned `synced`.
## Files
`surfaces.module` (the table and `sample`), `Surface.component`, `SurfacePatch.component`, `shaders/pavementLib.shaderModule` (procedural asphalt, concrete, setts, kerb, blob, thin-film, bump), `shaders/drivePavement.shader`, `materials/` (tarmac, wetTarmac, concrete, cobbles, rumble, puddle, oil), `surfacesGallery.scene`.
# roads (roadPieces.roads)
Procedural road and track pieces with real geometry: straight, curve, T and X junctions (filleted corners, stop lines), roundabout (kerbed island, arms clipped to the ring, give-way lines), ramp, kicker jump with table-top landing, banked turn (inside kerb, outside wall), guardrail, concrete wall, tyre wall, and a Catmull-Rom spline road through any list of points that follows their heights. Three styles switched by one name: country (narrow two-lane tarmac, faded dashed centre line, grass verges, marker posts), highway (four lanes, edge lines, double centre line, concrete barriers) and dirt (rutted dirt track with gravel shoulders and a grass crown, no markings). `build(spec)` makes the mesh data, `realize(root, built, style)` stands it up as temporary children with static exact MeshColliders and driveSurfaces Surfaces, `spawn(kind, opts)` and `spline(points, opts)` place a RoadPiece, `pathOf(road, offset?)` returns its centreline in world space for AI drivers, and `gallery(parent)` lays out every piece in every style.
# Surface
Says which driving surface an entity is (`kind`: tarmac, wetTarmac, concrete, cobbles, gravel, dirt, mud, grass, sand, snow, ice, rumble, puddle or oil; synced). Every descendant without a Surface of its own reads the same, so a road's deck, a car park or a field needs it once on its root. `driveSurfaces.surfaces.sample` reads it for a wheel's hit entity; `Surfaces.paint(entity, kind)` adds it together with the surface's material. Needs no sibling component; a collider somewhere in the hierarchy is what wheels actually hit.
# RoadPiece
Turns its entity into a road or track piece: `kind` (straight, curve, tJunction, xJunction, roundabout, ramp, kicker, bankedTurn, guardrail, wall, tyreWall, spline), `style` (country, highway, dirt) and size fields (`length`, `radius`, `angle`, `bank`, `slope`, `height`, `gap`, `width`, `arms`, `verge`, `rails`, `points`, `loop`), all synced. It builds the geometry locally on every peer as temporary child entities (one per material, each with a static exact MeshCollider, verges/kerbs/walls with their own driveSurfaces Surface) and rebuilds whenever a field changes, so one setting restyles the piece. It also gives the entity a Surface matching the style (tarmac or dirt). Inside a scene build it records itself and builds right after, so scenes store only the fields. `:path(offset?)` returns the centreline in world space.