Readonlycapabilities§37 capabilities. Readable before initialize, and frozen.
Readonlyname§37 name.
ReadonlyreportsSolverJointAccess.reportsJointReactions — false for Rapier 0.19.3
(plan P6-2, finding 1 in the module header).
Not a shortcut and not a to-do: the 3D build exposes no joint impulse, no
joint reaction, and no joint force anywhere in its typed surface or its raw
wasm surface (both prototypes enumerated at runtime on 2026-08-01). A world
reads this flag and refuses to register a joint carrying breakForce or
breakTorque rather than accepting a threshold this adapter could never
enforce.
§37 version — Rapier's own version string, once it is known.
Empty until Rapier3dAdapter.initialize resolves, because the value
lives inside the wasm module: calling version() before init() throws
a TypeError from the bindings (verified at 3D 0.19.3). This adapter would
rather report "not known yet" than invent a number that §34 will bake into
a snapshot's validity key.
§26 body.applyAngularImpulse(angularImpulse) about +Z.
§26 body.applyForce(force) — newtons, at the centre of mass.
§26 body.applyForceAtPoint(force, worldPoint).
§26 body.applyImpulse(impulse) — newton-seconds at the centre of mass.
§26 body.applyImpulseAtPoint(impulse, worldPoint).
§26 body.applyTorque(torque) — newton-metres about +Z.
Number of contact points across collider manifolds in the current solver world.
Creates a body (§37, §22, §23).
§22's four types map to Rapier's Fixed, Dynamic,
KinematicPositionBased, and KinematicVelocityBased. Position, rotation,
both velocities, both damping coefficients, and gravityScale are applied
from the descriptor; §31's CCD mode is resolved from the §23 boolean and the
§31 mode together, exactly as RigidBodyDescriptor.ccdMode documents.
Rapier composes a body's mass additively, so an explicit mass cannot
simply be handed to RigidBodyDesc: additional mass is added to whatever
the colliders contribute. §23 says mass is authoritative, so the adapter
resolves one of three MassModes here and every createCollider on
this body honours it. In particular, an explicit mass with no inertia
tensor is carried by the body's first collider through
ColliderDesc.setMass, which gives the requested mass and a
geometry-derived inertia tensor — measured: a unit cube at setMass(7)
reports mass 7 and principal inertia (1.1667, 1.1667, 1.1667).
The 3D-only wrinkle is the inertia tensor. Rapier wants three principal
moments plus the frame they are expressed in, where §23 offers a full
Matrix3; toPrincipalInertia3d accepts a diagonal tensor (exact, with
the identity frame) and rejects anything else rather than guessing. See its
documentation for why an eigendecomposition was not the right answer for
this phase.
A centerOfMass or inertiaTensor without a mass is rejected:
Rapier can only set the whole quadruple, and silently dropping the one the
caller did supply is the kind of quiet substitution §85 exists to prevent.
Creates a collider on desc.body (§37, §24, §25).
The full §24 3D shape list — sphere, box, capsule, cylinder, cone, convex
hull, triangle mesh, height field — maps to ColliderDesc.ball,
.cuboid, .capsule, .cylinder, .cone, .convexHull, .trimesh,
and .heightfield; see conversions3d.ts for the capsule/cylinder/cone
axis, the hull's degeneracy verdict, and the height field's
samples-versus-subdivisions conversion. §24's compound is several
colliders on one body, which needs nothing here (PH-22a).
Friction and restitution come from the explicit fields, then the material,
then @fourjs/physics's defaults. Their combine rules are set to
Appendix A's — friction Average, restitution Max — because Rapier's own
default for restitution is Average, which would quietly contradict §25 for
every contact in the world. §25's rollingFriction and spinningFriction
have no Rapier 3D binding at 0.19.3 and cannot be applied here — declared
in capabilities.tuning, so a PhysicsWorld registering a collider that
carries one warns rather than dropping it in silence (2026-08-06).
collisionGroups/collisionMask are packed into Rapier's single
InteractionGroups word; sensor sets the sensor flag and widens
ActiveCollisionTypes to ALL, because Rapier's default excludes
kinematic-vs-fixed and fixed-vs-fixed pairs — a static trigger volume would
otherwise never notice a kinematic character walking through it (verified in
3D, and a §29 requirement rather than a preference). Solid colliders keep
Rapier's default, where those pairs are genuinely uninteresting.
ActiveEvents.COLLISION_EVENTS is enabled on every collider: §29's five
event names are part of the engine's contract, not an opt-in.
CONTACT_FORCE_EVENTS is deliberately not enabled — the contact
manifolds read in step carry per-contact impulses, which is strictly more
information than a contact-force event's summed force and needs no
threshold to be configured.
Creates a joint (§37, §28; plan P6-1, WP-6.3).
Anchors and axes in desc are body-local, which is what Rapier's
JointData.* factories want, so nothing is converted here beyond widening
a Vector2 — PhysicsWorld.addJoint already did the world-to-local work
once, at registration.
The type mapping and every measurement behind it are in the joint section of the module header. Three of its consequences show up as code here:
fixed joint's second frame is derived from the two bodies' current
orientations, so the weld preserves the relative pose §28 promises rather
than snapping the bodies together;collisionEnabled is applied explicitly on every joint, because Rapier's
default is true (measured) and §28's is false;spherical descriptor carrying limits is refused: Rapier 0.19.3
cannot express a swing cone, and per-axis angular limits measurably fail
to bound a diagonal swing.Limits and motors on revolute and prismatic joints are applied immediately
after creation through the same SolverJointAccess entry points a world
uses for a live change, so a joint authored with a motor and a joint
reconfigured into one end up in the same solver state.
Serializes the world (§34).
Rapier's World.takeSnapshot() captures the solver — bodies, colliders,
contacts, joints, integration parameters, gravity — and preserves its
own handles across World.restoreSnapshot (verified in 3D, joints
included: limits and motors both survive and 60 further steps reproduce the
original continuation exactly). It does not know about this adapter's
monotonic ids, mass modes, collision-group values, or which §28 type each
joint handle was, all of which §33's checksum, §30's filters, and §28's
live reconfiguration depend on. The buffer this method returns is therefore
an envelope, byte-compatible in layout with the 2D adapter's and
distinguished from it by the magic word:
offset 0 u32 magic 0x33523446 ("F4R3", little-endian)
offset 4 u32 format version 2 (joint registry added, WP-6.3)
offset 8 u32 metadata length in bytes
offset 12 u32 Rapier length in bytes
offset 16 … metadata UTF-8 JSON (SnapshotMeta)
offset 16+m … Rapier snapshot World.takeSnapshot() bytes, verbatim
The envelope is opaque adapter data in §37's sense: valid only for this adapter, this Rapier version, and this world configuration.
Destroys a body and everything attached to it (§37).
Rapier removes the body's colliders and its joints with it (both measured), so their records are dropped here too, along with any contact pair they were part of. The monotonic id is not reused, which is what keeps Rapier3dAdapter.forEachBody's order stable under destruction (§33).
@fourjs/physics destroys a joint before either of its bodies (§83), so this
path is reached only by a caller driving the adapter directly; it exists so
that such a caller is left with a registry that matches the solver rather
than with joint handles pointing at a constraint Rapier has already freed.
Destroys a collider (§37). Any contact pair it was part of is forgotten.
Identical to Rapier2dAdapter.destroyCollider, and fixing the same silent
pair of defects: the id leaves BodyRecord.colliderIds (so a
replacement collider on a "first-collider" body is created carrying the
authored mass instead of density: 0), a surviving "first-collider" body
hands its authored mass to the surviving collider with the lowest monotonic
id (§33-stable creation order), and the body's mass properties are
recomputed so getBodyMass stays truthful before the next step.
Re-applying rather than refusing: §23's authored mass belongs to the body, not to whichever collider was holding it. A body left with no collider at all keeps nothing — there is nowhere to hold it — and gets the mass back from the next collider created on it.
Not called when the body itself is going away (2026-08-07):
PhysicsWorld tears a registration down with a single destroyBody, which
§37 defines as taking everything attached with it, so none of this runs for
a body one line from destruction — see that method's note.
Destroys a joint (§37, §28). The handle is invalid afterwards.
The two bodies survive and are woken, which is what a constraint disappearing under load means physically. Removing a body already removes its joints in Rapier (measured), and Rapier3dAdapter.destroyBody forgets their records for the same reason.
Releases the Rapier world and event queue (§37, §83). Idempotent and terminal: every handle this adapter minted is invalid afterwards, and every other method throws.
Returns and clears the events accumulated during the preceding step
(§37).
Order must be deterministic for a given step. Returning an empty array is normal. The physics package normalizes these into node-facing events (§101) and dispatches them after the step (§6b, §29) — an adapter never calls user code itself.
Visits every live body in creation order, which is ascending RapierBodyAccess.getBodyId because ids are monotonic and never reused. This is the iteration §33 requires: destroying a body removes it from the sequence and creating one appends, so no destruction can permute the order a checksum is taken in.
Visits every live collider in creation order.
Visits every live joint in creation order, which is ascending SolverJointAccess.getJointId — the iteration §33 permits, for the same reason SolverBodyAccess.forEachBody exists.
The §31 CCD mode the solver is actually running for this body — read back from Rapier rather than remembered, so it reports what the solver does and not what the descriptor asked for.
World-space centre of mass into out (§25); out.z is 0 in this planar
build. Served by Rapier's RigidBody.worldCom().
The body's mass in kilograms, as the solver computed it (§23, §25).
Reads a body's world transform (§42's "physics" authority reads this).
outRotation receives the pure Z quaternion for Rapier's scalar angle.
Reads linear (m/s) and angular (rad/s) velocity into out parameters.
The body a collider belongs to (§24, §37).
The collider's monotonic id, ordered like RapierBodyAccess.getBodyId.
The monotonic id this joint is registered and iterated under (§33).
Never available on this solver: throws NOT_IMPLEMENTED (plan P6-2,
WP-6.3, 2026-08-01).
SolverJointAccess promises this method is only ever called when
Rapier3dAdapter.reportsJointReactions is true, and it is false
here, so reaching this code means somebody bypassed the flag. It throws
rather than writing zeros, because zeros would read as "this joint is under
no load" and would silently keep every §28 break threshold from ever firing
— the exact failure plan P6-2 forbids faking.
Revisit when a Rapier release exposes joint impulses in its JavaScript bindings; nothing else in this adapter has to change when it does.
Creates the Rapier world (§37).
Loads the wasm module (once per process — see init.ts), then builds a
World with the §21-resolved gravity. Options are validated with
@fourjs/physics's own validators, so this adapter accepts exactly what the
engine accepts and no more.
SleepingConfig.enabled maps to RigidBodyDesc.setCanSleep, applied to
every body this adapter creates. linearThreshold, angularThreshold, and
timeThreshold do not map: IntegrationParameters in the 3D 0.19.3
build exposes no sleeping thresholds at all (verified by enumerating the
prototype — see the module header for the full member list). Rapier's
thresholds are compiled into the wasm. The adapter therefore honours the
on/off switch and leaves the three thresholds unimplemented rather than
pretending — and says so in capabilities.tuning.sleepThresholds, which is
what makes PhysicsWorld warn about an authored threshold instead of
dropping it silently (2026-08-06).
§32 sleep state. The transitions are also reported as drainEvents events.
Tests a static shape for overlaps (§30).
§30's named forms fall out of the shape union: an overlap with a
"sphere" is overlapSphere and one with a "box" is overlapBox, whose
rotation is a full orientation here rather than 2D's single angle. Hits
carry no distance, so sorted does nothing — OverlapHit has nothing to
sort by, which QueryOptions already says.
Tests a point (§30) — every collider that contains it.
Each hit's point and distance come from Rapier's projectPoint(point, solid: true), which returns the query point itself for a point inside a
plain shape. Since a point query only ever reports containing colliders,
distance is 0 on every hit; that is PointHit's documented meaning of
"the point is inside", not a field this adapter failed to fill.
Casts a ray (§30).
The direction is normalized before the cast, so Rapier's time of impact —
which is measured in units of the ray direction's length — equals the
distance in world units that RaycastHit.distance promises.
Filtering is done entirely by @fourjs/physics's passesQueryFilter
rather than by Rapier's own group words: one implementation of §30's filter
semantics means Rapier and every future adapter answer a query identically,
which is exactly the portability the §37 seam exists for.
mode: "first" uses Rapier's nearest-hit entry point; "all" enumerates
and then applies maxHits in traversal order and sorted afterwards.
Note the module header's warning: a collider created since the last step
is not yet in the broad phase and cannot be hit.
Clears the accumulated forces and torques (not impulses, which are applied immediately). Rapier keeps user forces across steps until reset.
Restores a world previously captured by Rapier3dAdapter.createSnapshot (§34).
The Rapier world is replaced wholesale — World.restoreSnapshot builds a
new one — and the old one is freed. The adapter's registry is then rebuilt
from the envelope's metadata, reusing the existing record objects wherever
an id survives: that is what makes handles minted before the snapshot keep
working afterwards, so "body identity and ordering survive the round trip"
is true in the sense §33's checksum needs. A record whose id is not in the
snapshot is marked dead, and its handle is rejected from then on.
The touching-pair set is rebuilt from the restored narrow phase, so
collisionstay and collisionend keep working for pairs that were already
in contact when the snapshot was taken — Rapier will not re-report those as
started, so without this they would go silent.
Selects §31's method on a live body — the 2D adapter's rule and its reversibility argument unchanged; see Rapier2dAdapter.setBodyCcdMode.
OptionalpredictionDistance: numberReplaces §23's two damping coefficients, both >= 0.
Replaces §23's gravity multiplier: 1 leaves gravity as-is, 0 is weightless.
Optionalwake: booleanReplaces §23's mass triple on a live body —
SolverBodyTuningAccess.setBodyMassProperties (PH-1 stage 2, 2026-08-07).
The rule, the mass-mode rewrite, and the trailing
recomputeMassPropertiesFromColliders() are the 2D adapter's, verbatim:
see Rapier2dAdapter.setBodyMassProperties. What differs is 3D's
shape of the distribution — a principal-inertia vector plus a frame,
which is why toPrincipalInertia3d refuses a tensor with off-diagonal
terms here and elements[8] suffices there (§23, §37).
Optionalwake: booleanTeleports a body (§37 "teleports"). Use RapierBodyAccess.setNextKinematicTransform for a kinematic body that should push dynamic bodies on the way.
Optionalwake: booleanRe-types a live body in place — SolverBodyAccess.setBodyType's contract
(a documented own summary rather than @inheritDoc, which cannot carry
additional paragraphs).
The 3D build's setBodyType(type, wakeUp) behaves exactly like the 2D
one's — same required wakeUp, same preserved handle, colliders, pose, and
mass (verified separately against the 3D wasm, WP-7.2). See
Rapier2dAdapter.setBodyType for why record.sleeping is left alone.
Optionalwake: booleanWrites both velocities (§23).
Optionalwake: booleanReplaces a collider's §24 participation — see
Rapier2dAdapter.setColliderFilter for the ActiveCollisionTypes
widening and for what happens to a pair that is already touching.
Replaces a collider's §25 surface properties — see
Rapier2dAdapter.setColliderMaterial for why density is optional
and what an undefined one protects.
Returns and clears the events collected during the preceding step (§37).
The array is handed over, not shared: the adapter starts a fresh buffer, so a caller may keep, sort, or splice the result. Contact vectors are freshly allocated per event and are never pooled, so they stay valid for as long as the caller holds them.
Replaces both body-local anchors, live (§28, PH-22f).
ImpulseJoint.setAnchor1 / setAnchor2 are on Rapier's base joint
class, so unlike setLimits this works for every §28 type this adapter
builds. The vectors are already body-local — the same space createJoint
received — and are not re-measured against any pose.
Turns contact generation between the two joined bodies on or off, live
(§28 collisionEnabled; PH-22f, 2026-08-08).
ImpulseJoint.setContactsEnabled is on Rapier's base joint class, so
unlike setLimits and the motor configuration it works for every §28 type
this adapter builds — which is why this is the one §28 property that could
stop being frozen. It is the same call createJoint makes, so a joint
created with contacts on and a joint switched to contacts on cannot drift
apart.
Reconfigures a revolute or prismatic joint's travel limits (§28), in radians or metres respectively.
Only those two types have limits in Rapier 0.19.3 — they are the only
UnitImpulseJoints — so any other type throws rather than accepting a
limit that would never be enforced.
Reconfigures a revolute or prismatic joint's motor (§28 "motors").
maxEffort becomes, and what it does notRapier 0.19.3's JavaScript bindings have no motor force limit:
configureMotorVelocity(targetVelocity, factor) reaches
jointConfigureMotor(handle, axis, targetPos, targetVel, stiffness, damping) and there is no maxForce parameter anywhere in the typed or raw
surface (verified 2026-08-01 — see finding 3 in the module header).
SolverJointMotor.maxEffort is therefore passed as Rapier's damping
gain: the effort the motor applies is roughly maxEffort × (targetVelocity − currentVelocity) under MotorModel.ForceBased, so a larger maxEffort
is a stronger motor, but the number is a gain and not the cap its name
promises. A motor commanded to 4 rad/s settles at 3.60 rad/s with a gain of
0.1 and reaches exactly 4 rad/s from a gain of 1 upwards (measured).
MotorModel.ForceBased is chosen over AccelerationBased because §28
states the cap in newton-metres and newtons: a force-based motor's effort
is what the units describe, where an acceleration-based one scales with the
body's inertia.
A motor that is disabled, or whose maxEffort is 0, is configured with
INERT_MOTOR_GAIN and a zero target instead of being removed —
Rapier cannot remove a motor, and a gain of exactly 0 is its rigid case
rather than its inert one. The inert gain reproduces an unmotorized joint
bit for bit (measured).
Sets a "kinematic-position" body's target pose for the next step (§22).
Rapier derives an artificial velocity from it so dynamic bodies react.
Optionalrotation: RotationInputSweeps a shape (§30).
Multiplicity limit, declared rather than hidden: Rapier 0.19.3's 3D
query surface exposes castShape, which returns the first impact and
nothing else — there is no "all shape-cast hits" entry point, in 3D any more
than in 2D. This method therefore returns at most one hit whatever mode,
maxHits, or sorted ask for. capabilities.queries.shapeCast stays
true because the query is implemented; the cap is a property of the
solver, and a caller that needs every crossing should step the sweep or use
overlap.
ShapeCastHit.point is Rapier's witness1, which is world-space on the hit
collider, and normal is normal1, pointing out of it (both verified in
3D: sweeping a 0.25 ball down onto a 0.5 ball centred at y = 3 gives
witness1 = (0, 3.5, 0) in world space and witness2 = (0, -0.25, 0) in
the cast shape's local frame, which is why witness2 is not used).
Advances the solver by exactly delta seconds (§37, §10) and collects the
step's events.
The events are gathered here rather than in drainEvents for two reasons,
both Rapier's: the EventQueue is auto-drained at the start of the next
step, and contact manifolds only carry the impulses the solver just
applied for as long as no further stepping has happened.
collisionstay, which Rapier does not haveRapier reports only started and stopped. §29 names three collision
phases, so somebody has to derive the middle one, and this adapter does it —
the same decision, for the same reason, as the 2D adapter: the contact
manifold a collisionstay payload needs is only readable here,
immediately after the solve.
The rule: a pair that Rapier reported as started is remembered; on every
later step where it has not been reported as stopped, a collisionstay
carrying a fresh manifold is emitted; when Rapier reports it stopped, a
collisionend is emitted and the pair is forgotten. Trigger (sensor) pairs
are tracked the same way but produce no stay, because §29 defines only
triggerenter and triggerexit.
Deterministic and documented: all collisionstart/triggerenter in
Rapier's queue order, then every collisionstay in pair-creation order,
then all collisionend/triggerexit in queue order, then every §32
sleep/wake in body-creation order.
§37's pre-step hook. A documented no-op for this adapter.
Rapier applies every write the moment it is made — there is no staging
buffer for this call to flush. Scene-authored state reaches the solver
through RapierBodyAccess, which @fourjs/physics calls from inside its own
syncSceneToSolver phase; see the module header.
§37's post-step hook. A documented no-op for this adapter, for the same reason as Rapier3dAdapter.syncSceneToSolver: solved transforms are read per body through Rapier3dAdapter.getBodyTransform, which is the only way an adapter that cannot see the scene graph could publish them.
A
PhysicsSolverAdapter(§37) backed by Rapier 3D.One instance owns exactly one Rapier
World.initializemay be called once;disposeis idempotent and terminal.