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…
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:
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'saffinitytable 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.paintadds 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
WheelColliderwheels do not read them yet. Grip per surface therefore comes from this package'ssample(), 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.
rollingis 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.
Scoped to this part · feeds back into the world's score.