Documentation
    Preparing search index...

    A PhysicsSolverAdapter (§37) backed by Rapier 3D.

    const adapter = new Rapier3dAdapter();
    await adapter.initialize({ dimension: "3d", gravity: new Vector3(0, -9.81, 0) });
    const body = adapter.createBody({ type: "dynamic", position: new Vector3(0, 5, 0) });
    adapter.createCollider({ body, shape: { type: "sphere", radius: 0.5 } });
    adapter.step(1 / 60);
    const events = adapter.drainEvents();
    adapter.dispose();

    One instance owns exactly one Rapier World. initialize may be called once; dispose is idempotent and terminal.

    Implements

    Index
    capabilities: PhysicsCapabilities = RAPIER_3D_CAPABILITIES

    §37 capabilities. Readable before initialize, and frozen.

    name: string = ADAPTER_NAME

    §37 name.

    reportsJointReactions: false

    SolverJointAccess.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.

    • get version(): string

      §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.

      Returns string

    • 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.

      Parameters

      Returns PhysicsBodyHandle

    • 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.

      Parameters

      Returns PhysicsColliderHandle

    • 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:

      • a 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;
      • a 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.

      Parameters

      Returns PhysicsJointHandle

    • 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.

      Returns ArrayBuffer

    • 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.

      Parameters

      Returns void

    • 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.

      Parameters

      Returns void

    • 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.

      Parameters

      Returns void

    • 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).

      Parameters

      Returns Promise<void>

    • 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.

      Parameters

      Returns PointHit[]

    • 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.

      Parameters

      Returns RaycastHit[]

    • 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.

      Parameters

      • snapshot: ArrayBuffer

      Returns void

    • Replaces §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).

      Parameters

      Returns void

    • Re-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.

      Parameters

      Returns void

    • 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.

      Parameters

      Returns void

    • Reconfigures a revolute or prismatic joint's motor (§28 "motors").

      Rapier 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).

      Parameters

      Returns void

    • Sweeps 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).

      Parameters

      Returns ShapeCastHit[]

    • 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.

      Rapier 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.

      Parameters

      • delta: number

      Returns void

    • §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.

      Returns void