Appearance
Driving a Rapier body
@skidpad/rapier lets a Rapier rigid body be the car. Rapier owns collisions, joints and everything else in the scene; Skidpad computes what the tires and suspension do to the chassis and hands it back once per step. The split is the chassis proxy of ADR-0002.
The loop
Every host step, in this order:
beforeStep()copies the body's pose and velocities into the core and casts one ray per wheel, from the top of each strut down the body's vertical axis, to find the ground.world.step(dt)runs the core, which substeps against its own copy of the chassis.afterStep(dt)hands the step's net impulse to the body as a force and torque over the coming Rapier step.scene.step()integrates the body with everything else.
RapierVehicle.step(dt) does the first three for a world with one vehicle. With several vehicles in one Skidpad world, call beforeStep on each, step the world once, then afterStep on each.
Setting up
createChassisBody builds a dynamic body with the definition's mass and inertia and a massless box collider for the chassis. Wheels have no colliders: the rays are the wheels, so a car can drive over a kerb a box collider would stop at. Rapier is axis-agnostic; pass up: "y" (default, the three.js convention) or up: "z" and the adapter converts between the scene's frame and the core's ISO frame.
It takes a partial definition such as a preset, as long as the chassis sizes are given (every preset's are); for a definition that leaves them to the core, pass sp.completeDefinition(def), which fills in the core's defaults exactly as addVehicle does.
Step the scene once after creating its colliders. Rapier's query pipeline is empty until the first step, and the wheel rays would miss.
The sandbox's Rapier host (apps/sandbox/src/sim.ts) is the reference: a ground box, speed bumps, a ramp and a kerb, with the same obstacles rendered by three.js.
Moving platforms
The adapter reads the velocity of whatever body a ray hits at the hit point and passes it to the core, so a car parked on a kinematic platform rides it, and one with free wheels spins them like a dyno roller.
Surfaces
The core looks the ground under each wheel up in the world's surface table by an id the host attaches to each contact. Give the world a table, then pass a surfaceId callback that maps the collider a wheel ray hit to an id. A Rapier collider has no user data of its own (its body does), so a Map from collider handle to id is the plain way, filled when the scene is built:
ts
import { surfaceTable, surfaceId } from "@skidpad/presets";
import { surfaceIdsByHandle } from "@skidpad/rapier";
world.setSurfaces(surfaceTable());
const surfaceOf = new Map<number, number>();
surfaceOf.set(gravelCollider.handle, surfaceId("gravel"));
surfaceOf.set(iceCollider.handle, surfaceId("ice"));
const car = new RapierVehicle(RAPIER, world, vehicle, body, scene, {
surfaceId: surfaceIdsByHandle(surfaceOf), // or (collider) => surfaceOf.get(collider.handle) ?? 0
});A collider the map does not know reads as id 0, the reference surface, as does any id beyond the table. If you drive the core without the adapter, set surfaceId on the WheelContact you pass to writeWheelContact.
Determinism
The core is bit-exact for a given sequence of body states and contacts. Rapier is deterministic across platforms only in its enhanced-determinism build, @dimforge/rapier3d-deterministic-compat; the Skidpad determinism harness therefore runs the built-in host. The adapter types Rapier by the members it uses rather than by one package's declarations, so both builds plug in without a cast, and either satisfies its (optional) peer dependency. If you need replays under Rapier, use the deterministic build, build the scene in a fixed order from plain data, and record the inputs; re-running the scene then reproduces the run.