Documentation
    Preparing search index...

    Class RigidBody

    A simulated body attached to a node (§23), created from a RigidBodyDescriptor.

    const body = node.addComponent(
    new RigidBody({ type: "dynamic", mass: 2, linearDamping: 0.1 }),
    );
    node.addComponent(new Collider({ shape: { type: "sphere", radius: 0.5 } }));

    body.on("collisionstart", (event) => {
    console.log(event.totalImpulse.length());
    });

    body.applyImpulse(new Vector3(0, 5, 0)); // applied at the next fixed step

    One per node (§6a). See the module header for the pose, command, event, and dimension contracts.

    Hierarchy (View Summary)

    Implements

    Index
    • Builds a body from descriptor, which is validated against §22, §23, §31, and §85 (dimension-independently — see the module header).

      Vectors and the inertia tensor are copied, so the caller keeps ownership of whatever it passes in.

      Parameters

      Returns RigidBody

    angularVelocity: Vector3

    Angular velocity in radians per second (§23), as an axis-angle rate vector; in a "2d" world only z is meaningful (§21).

    centerOfMass: Vector3

    Centre of mass in the body's local frame (§23). Owned by the component and never replaced — write into it. The origin by default.

    §23 makes this always present, which is not the same as always authored: a body that never mentioned a centre of mass is asking the solver to derive one from its colliders (§23, §25), and a body that placed one is overriding that derivation. RigidBody.centerOfMassAuthored is that distinction and RigidBody.toDescriptor emits the field only when it holds.

    derivedMass?: number

    The last mass the solver derived for this body, or undefined before one was read (§23, §25).

    Read-only mirror of solver state, written by PhysicsWorld.addBody's mass refresh. It exists so that a derived mass can be reported — RigidBody.mass falls back to it, so inverseMass is a number rather than NaN for a density-derived body — without being authored: only an authored mass is re-emitted by RigidBody.toDescriptor. See RigidBody.massAuthored.

    host: ComponentHost | null

    The node this component is attached to, or null. Written by the ComponentRegistry alone (§6a); never assign it.

    inertiaTensor?: Matrix3

    Rotational inertia about the centre of mass (§23), or undefined to let the solver derive it from the colliders and the mass.

    §23 declares this non-optional, but §23 also derives mass (and with it inertia) from collider density when it is not authored — and "derive it" and "the identity tensor" are different instructions. The component keeps them distinguishable: undefined is this field's authored flag, which is why it needs no companion to RigidBody.centerOfMassAuthored. In a "2d" world only the Z diagonal entry is used (§23).

    For the same reason a future syncSolverToScene must not publish a solver-derived tensor back into this field (WP-5.2-fix1): doing so would turn a derivation into an authored mass distribution, which is exactly the defect RigidBody.centerOfMassAuthored exists to prevent. A derived tensor belongs in a separate read-only field.

    initialPosition?: Vector3

    The initial world position a descriptor carried, or undefined (§37).

    The live pose is the node's Transform (§7, §42); this is only what the descriptor asked for, kept so a descriptor round-trips through the component unchanged. A Vector2 has already been widened to z = 0.

    initialRotation?: Quaternion

    The initial world rotation a descriptor carried, or undefined. A scalar angle has already been resolved to its quaternion about +Z (§7a).

    linearVelocity: Vector3

    Linear velocity in metres per second (§23). Authored state going in, solved state coming back — see the module header.

    liveSolverWriteWorldCount: number

    How many of those worlds can carry a §37 property change into their solver — the subset of RigidBody.registeredWorldCount whose adapter implements SolverBodyTuningAccess (PH-1 stage 2, 2026-08-07).

    Read-only state, written by the same addBody / removeBody pair. It is what decides whether a setter in the module header's table warns or merely marks the property dirty: a write is on the record as unreachable exactly when some world holding this body cannot deliver it, so the two counts differ. Registering a body with a tuning adapter and a plain one leaves the warning on, which is the truth — one of the two simulations will not see the change.

    pendingSolverWrites: number

    Which §37 property writes are waiting for the next fixed step, as a bit set of the RIGID_BODY_*_DIRTY flags (PH-1 stage 2, 2026-08-07).

    Read-only state like RigidBody.sleeping: the setters in the module header's table set bits, and PhysicsWorld clears them at the top of step after pushing each one through SolverBodyTuningAccess. 0 — the value for a body nobody wrote to — is what the world tests to decide that this body needs no solver call at all.

    Bits are set whether or not any world can carry them, because the cost is one |= and an unregistered body that is later added rebuilds itself from toDescriptor() anyway: a stale bit is at worst one redundant solver call with the value the solver already has, never a wrong one.

    registeredWorldCount: number

    How many PhysicsWorlds currently hold this component (§37), normally 0 or 1.

    Read-only state like RigidBody.sleeping, written by PhysicsWorld.addBody and removeBody. A count rather than a flag because nothing forbids registering one component with two worlds, and because the body stops being registered only when the last of them lets go. It is what turns this class's "that write reaches no solver" warnings on — see the module header's table.

    sleeping: boolean

    Whether the solver has put this body to sleep (§23, §32).

    Read-only state, exactly as §23 requires: RigidBody.wake and RigidBody.sleep are the commands, and the flag changes only when the solver reports the transition after a step — at which point the body also emits "sleep" or "wake" (WP-5.3).

    space: SpaceMode

    The §8 space this body is solved in (PH-12, 2026-08-09). DEFAULT_SPACE_MODE ("world") unless the descriptor said otherwise, so every body written before PH-12 means exactly what it meant.

    §8 gives physics two legal frames — "physics normally operates in world or local-plane space" — and forbids a third case outright: "screen-space UI should not automatically participate in physical simulation unless explicitly mapped to a simulation plane". PhysicsWorld.addBody therefore accepts "world" and "local-plane" and refuses the rest:

    • "screen", "viewport", "camera", "billboard" — refused because §8 says so. The sentence's escape hatch is a mapping the author performs: simulate a body on a node in the simulated frame and drive the presentation node from it.
    • "local-plane" — legal under §8 and accepted by PhysicsWorld.addBody: the body's 2D pose lives in PhysicsWorld.localPlane's frame and feed/publish maps through that basis (§21). Presentation frames stay refused.

    Plain and mutable, with no change hook and no validation: PhysicsWorld is the only reader and it reads at registration, so writing it on an already-registered body does nothing until the body is registered again. @fourjs/core's isSimulationSpaceMode answers §8's own question (world and local-plane pass; presentation frames fail).

    typeName: "rigid-body"

    Component key (plan D2) and §79 serialization name.

    • get angularDamping(): number

      Angular damping coefficient, >= 0 (§23). See RigidBody.linearDamping.

      Returns number

    • set angularDamping(value: number): void

      Parameters

      • value: number

      Returns void

    • get animationWeight(): number

      How much of a "blended" node's final pose comes from the animated target pose — the PoseTarget on the node (§19, P7-1) — relative to RigidBody.physicsWeight. See that property for the whole contract.

      Defaults to DEFAULT_ANIMATION_WEIGHT: a body nobody has blended is fully physical.

      Returns number

    • set animationWeight(value: number): void

      Parameters

      • value: number

      Returns void

    • get ccdMode(): CCDMode

      Which §31 method sweeps this body, "disabled" when it is not swept.

      This is the authoritative field; RigidBody.continuousCollisionDetection is §23's on/off view of it.

      Live on a world whose adapter implements SolverBodyTuningAccess: an assignment takes effect at the next fixed step, carrying RigidBody.ccdPredictionDistance with it for "speculative". On an adapter without that seam it warns once, like the rest of the module header's table (PH-1 stage 2, 2026-08-07 — before which this setter did not even warn).

      Returns CCDMode

    • set ccdMode(value: CCDMode): void

      Parameters

      Returns void

    • get ccdPredictionDistance(): number | undefined

      §31's "speculative" prediction distance, in world units, when authored — undefined means the adapter's documented default (1 m on both Rapier adapters). Carried by the component so world.addBody's rebuilt descriptor delivers it to the solver (2026-08-05 review fix: the constructor validated and accepted the field, then toDescriptor() silently dropped it, so the authored distance never reached the adapter on the component path).

      Read-only, like the shape of a collider: the distance is applied at createBody and Rapier offers no post-creation re-tune through the seam.

      Returns number | undefined

    • get centerOfMassAuthored(): boolean

      Whether RigidBody.centerOfMass is an authored mass distribution rather than the always-present default §23 gives every body (WP-5.2-fix1).

      §23 derives a body's mass from collider density times volume whenever mass is not authored, and a solver derives the centre of mass and the rotational inertia from the same geometry in the same breath — mass, centre, and inertia are one triple. Naming any part of that triple means "do not derive it", so a component that reported its default origin as an authored centre would make the density-derived mass unreachable: an adapter asked for a distribution with no mass to distribute has nothing to do but refuse.

      A centre of mass counts as authored when either holds:

      1. the constructor descriptor carried centerOfMass (whatever its value, including the origin), or RigidBody.markCenterOfMassAuthored has been called — the sticky half;
      2. RigidBody.centerOfMass is not at the origin right now — the live half, which catches body.centerOfMass.set(…) after construction without asking the caller to announce it.

      The union is what makes the omission safe. Rule 1 keeps an explicitly authored origin — the one value rule 2 cannot see — and rule 2 keeps every post-construction edit. The only state that is omitted is a centre that is at the origin and was never mentioned, which is precisely the state that carries no instruction. -0 reads as the origin (-0 !== 0 is false), and a non-finite component reads as authored, so §85 validation rejects it rather than the descriptor quietly losing it.

      The one case rule 2 alone would get wrong is "pin the centre to the origin even though the colliders sit off it", which is a real instruction that looks identical to the default. Authoring centerOfMass in the descriptor states it; RigidBody.markCenterOfMassAuthored states it after the fact.

      Returns boolean

    • get continuousCollisionDetection(): boolean

      §23's switch, expressed over RigidBody.ccdMode so the two can never disagree: reading it asks whether a method is selected, and body.continuousCollisionDetection = true (§31's own example) selects DEFAULT_ENABLED_CCD_MODE unless a method is already set. Assigning false clears the method.

      Returns boolean

    • set continuousCollisionDetection(value: boolean): void

      Parameters

      • value: boolean

      Returns void

    • get disposed(): boolean

      Whether RigidBody.dispose has run. Disposal is terminal (§83): a disposed component refuses its §26/§32 commands and world registration with INVALID_APPLICATION_STATE (§89) rather than silently working.

      Returns boolean

    • get gravityScale(): number

      Multiplier on world gravity for this body (§23). 1 leaves gravity as-is, 0 makes the body weightless, a negative value inverts it.

      Applied at createBody; see RigidBody.linearDamping for what an assignment to a registered body does and does not do.

      Returns number

    • set gravityScale(value: number): void

      Parameters

      • value: number

      Returns void

    • get inverseMass(): number

      Derived reciprocal mass (§23), read-only:

      • 0 for "static" and both kinematic types — they do not respond to forces (§22), which is what an infinite effective mass means;
      • 1 / mass for a dynamic body with an authored mass;
      • NaN for a dynamic body whose mass has not been authored and has not yet been derived from collider density (§23). NaN and not 0: an unknown mass is not an infinite one, and a value that silently reads as "immovable" would turn a missing collider into a body that quietly refuses to move.

      Returns number

    • get linearDamping(): number

      Linear damping coefficient, >= 0 (§23).

      Applied to the solver body at createBody and, on a world whose adapter implements SolverBodyTuningAccess, at the next fixed step after every assignment (PH-1 stage 2). Assigning to a registered dynamic body held by an adapter without that seam warns once instead — see the module header.

      Returns number

    • set linearDamping(value: number): void

      Parameters

      • value: number

      Returns void

    • get mass(): number | undefined

      Mass in kilograms (§23): the authored mass when there is one, otherwise the last mass the solver derived from collider density times volume (§23, §24, §25), or undefined when neither exists.

      Authoritative when authored and re-validated on assignment: a dynamic body's mass must be positive, and non-simulated mass is expressed through the body type, never mass = 0 (§23, §85). Assigning here authors the mass — see RigidBody.massAuthored — and queues §23's whole triple (mass, centre of mass, rotational inertia) for the next fixed step, which a world whose adapter implements SolverBodyTuningAccess pushes into the solver (PH-1 stage 2). See the module header's table for what happens when it does not, and for the one assignment that still reaches nothing: mass = undefined.

      Returns number | undefined

    • set mass(value: number | undefined): void

      Parameters

      • value: number | undefined

      Returns void

    • get massAuthored(): boolean

      Whether RigidBody.mass is an authored mass rather than one the solver derived (§23, §25).

      PhysicsWorld.addBody reads the solver's mass back onto the component so that a density-derived body stops reporting inverseMass === NaN. Before 2026-08-06 that read went through the mass setter, which made the derived number indistinguishable from an authored one — so RigidBody.toDescriptor re-emitted it as mass, and re-registering the body (or serializing and reloading it) turned "derive my mass from my colliders" into "my mass is 3.0141594". Scaling a collider afterwards then changed nothing, silently.

      The two masses are therefore kept apart: #mass for what the author said, RigidBody.derivedMass for what the solver answered, and toDescriptor emits the field only when this holds. It is the same rule RigidBody.centerOfMassAuthored states for the centre, with a simpler test: no default value competes with undefined here, so an authored mass is exactly a defined #mass — which is what this getter derives rather than mirroring in a second field.

      Returns boolean

    • get physicsWeight(): number

      How much of a "blended" node's final pose comes from the solver (§19), relative to RigidBody.animationWeight.

      §19's sketch verbatim:

      body.transformAuthority = "blended";
      body.physicsWeight = 0.35;
      body.animationWeight = 0.65;

      The two weights are independent settables, normalized at use — they are not required to sum to 1 and setting one never rewrites the other, so 0.35/0.65 and 35/65 describe the same blend and a fade can drive either slider alone. RigidBody.normalizedWeights is where the ratio becomes a pair of fractions.

      Defaults to DEFAULT_PHYSICS_WEIGHT; must be finite and >= 0 (§85), checked on assignment. Inert under every authority except "blended" (§42).

      Returns number

    • set physicsWeight(value: number): void

      Parameters

      • value: number

      Returns void

    • get type(): BodyType

      Which §22 simulation model this body follows (§23). Settable; the §23 mass rule is re-checked against the new type, so promoting a zero-mass body to "dynamic" fails here rather than in the solver.

      This setter changes the component. Once the body is registered with a PhysicsWorld, the solver is running a body of the type it was created with, and writing here alone would leave the two disagreeing about what is being simulated — the fixed-step pipeline would keep feeding the old model.

      world.setBodyControlMode(node, type, options?) is the call that changes both: it re-types the solver body in place (the id, the checksum order, the colliders, and the pose all survive), moves this property with it, and can seed the new velocities from an animated PoseTarget — §19's animated → kinematic → dynamic transitions. WP-5.3's rule that a re-typed body had to be removed and added again is superseded by it.

      Assigning here while the body is registered is not silently accepted: the setter warns once per body (2026-08-06) naming setBodyControlMode, which is the call that moves the solver too. The assignment still happens — the component is the caller's — but the divergence is on the record instead of showing up later as a body that refuses to fall.

      Returns BodyType

    • set type(value: BodyType): void

      Parameters

      Returns void

    • Accumulates an angular impulse in newton-metre-seconds (§26). A number is the scalar about +Z (plan P5-3).

      Parameters

      Returns void

    • Accumulates a force in newtons, applied at the centre of mass (§26).

      Acts for one fixed step; call it every step to push continuously. Applying a force does not implicitly wake a sleeping body — call RigidBody.wake as well, so that what wakes a body is always something the caller asked for (decision, WP-5.2).

      Per-step path: the components are widened but not range-checked, matching §85's allowance that per-step validation may be skipped. A non-finite force is caught by the solver, not here.

      Parameters

      Returns void

    • Accumulates a force in newtons applied at worldPoint, a world-space position (§26).

      Kept as a point load rather than being decomposed into a force and a torque at the call site: the lever arm is measured from the world-space centre of mass, which the component does not know — §23's centerOfMass is in the body's local frame and the pose belongs to the node and the solver. The adapter, which has both, does the decomposition.

      Parameters

      Returns void

    • Accumulates a linear impulse in newton-seconds, applied at the centre of mass (§26).

      One-shot: the world applies it at the next fixed step and clears the buffer, so a single call changes momentum exactly once.

      Parameters

      Returns void

    • Accumulates a torque in newton-metres (§26). A number is the scalar about +Z (plan P5-3). Acts for one fixed step, like RigidBody.applyForce.

      Parameters

      Returns void

    • Drops every listener and every queued command (§6a, §83).

      Detaching a component does not dispose it (§6a), so this runs only from an explicit dispose() or ComponentRegistry.disposeAll. Removing the body from its world is the world's business, not the component's.

      Returns void

    • Number of live listener registrations across every event type (§83).

      Used by teardown diagnostics — a detached node that still holds listeners pins its subtree until those listeners are removed.

      Returns number

    • Declares RigidBody.centerOfMass an authored mass distribution from now on, so RigidBody.toDescriptor emits it even at the origin.

      The escape hatch for the one case the live half of RigidBody.centerOfMassAuthored cannot infer: a centre deliberately pinned to the body origin while the colliders sit elsewhere. One-way by design — nothing un-authors a distribution, because "forget what I asked for" is the silent data loss this whole mechanism exists to avoid. To go back to a derived centre, build a body whose descriptor omits centerOfMass.

      Remember that an authored distribution needs an authored RigidBody.mass as well: solvers set mass, centre, and inertia as one triple (§23), and an adapter is entitled to reject half of it.

      Returns void

    • Queues §23's mass triple for the next fixed step after RigidBody.centerOfMass or RigidBody.inertiaTensor was edited in place (PH-1 stage 2, 2026-08-07).

      centerOfMass is a readonly vector mutated with set/copy and inertiaTensor is a plain field, so neither edit passes through a setter and nothing can observe it. The alternatives were to compare both against a shadow copy on every body on every step — real per-step cost for a value that changes approximately never — or to say so and give the caller one call. This is that call, and it is the same bargain RigidBody.markCenterOfMassAuthored strikes for the same field.

      The whole triple, because a solver sets the three together (§23) — so it also picks up an authored RigidBody.mass that has not changed. Calling it on a body whose mass was never authored queues nothing and warns once: §23 derives centre and inertia from geometry precisely when no mass was named, and an adapter asked for a distribution with no mass to distribute has nothing to do but refuse (the rule RigidBody.centerOfMassAuthored states at length). Author a mass first.

      Free to call on an unregistered body, and free to call twice: the bit is already set.

      Returns void

    • The two §19 weights as fractions that sum to 1, written into out and returned.

      const split = body.normalizedWeights(scratch); // per fixed step
      pose.position
      .copy(solverPosition)
      .scale(split.physics)
      .add(targetScaled);

      Pass an out on a per-step path and the call allocates nothing (§7b, plan D7); omit it and one record is allocated per call.

      A body with physicsWeight === 0 and animationWeight === 0 has asked for a pose made of nothing, which has no answer. Rather than produce NaN — or a zero pose, which would teleport the node to the origin — this falls back to fully physical ({ physics: 1, animation: 0 }, the constructed default) and warns once per body via console.warn, the same development semantics warnAuthorityConflict uses in @fourjs/scene: the first occurrence names the mistake and every repeat is suppressed, because a misconfigured body would otherwise print once per fixed step forever. The suppression is sticky for the life of the body — fixing the weights and zeroing them again warns no further.

      It is a warning and not a validation error because the state is reachable one legal assignment at a time (physicsWeight = 0 while animationWeight is still 0 is the first half of every "fade to fully animated"), so rejecting it at the setter would reject the sequence rather than the mistake.

      Finiteness is guaranteed by the setters, so the only other arithmetic edge is two weights whose sum overflows to infinity; that case is rescaled by the larger weight so the ratio survives instead of collapsing to 0/0.

      Parameters

      Returns BlendWeights

    • Removes every listener, or every listener of one type. Used by teardown paths that must not leave detached nodes retaining listeners (§83).

      Type Parameters

      Parameters

      • Optionaltype: K

      Returns void

    • Requests that the solver put this body to sleep (§23, §32). See RigidBody.wake.

      Returns void

    • The descriptor an adapter would be handed for this body (§37 createBody).

      A fresh record whose vectors and tensor are references to this component's live state, not copies: the adapter reads them during createBody and must not retain them. mass, centerOfMass, inertiaTensor, position, and rotation are present only when authored, so a descriptor that omitted them round-trips unchanged.

      centerOfMass is the subtle one: §23 keeps it always present on the component, so emitting it unconditionally would tell every adapter that every body carries an authored mass distribution and make §23's density-derived mass unreachable (WP-5.2-fix1). It is emitted exactly when RigidBody.centerOfMassAuthored holds, which that property documents.

      mass follows the same rule against the same defect (2026-08-06): only an authored mass is emitted, so a body whose mass a solver derived at registration still asks the next solver to derive it rather than presenting the first solver's answer as an instruction. See RigidBody.massAuthored.

      §19's RigidBody.physicsWeight and RigidBody.animationWeight are deliberately absent: a solver simulates a body, it does not blend one, and the blend happens entirely above the adapter (§37) after the solve (§19 step 5). Putting a weight in the descriptor would tell every adapter it had a say in something it never sees.

      Returns RigidBodyDescriptor

    • Re-validates this body against a world of dimension (§21, §85).

      Called when the body is registered with a PhysicsWorld (WP-5.3), which is the first moment the dimension is known: a "2d" world rejects an out-of-plane centre of mass, initial position, or angular velocity here rather than projecting it away. Not a per-step path — it builds a descriptor.

      Parameters

      Returns void

    • Requests that the solver wake this body at the next fixed step (§23, §32).

      Queued, not immediate: RigidBody.sleeping changes when the solver reports the transition. Calling wake() then sleep() in the same step leaves the later command standing.

      Returns void