Rigid-body physics for a Scene, backed by a
caller-injected Rapier 3D world. One body per
SceneObject, kept in step with the Scene
graph automatically.
The physics module attaches a Rapier rigid-body simulation to an
existing Scene. Each SceneObject maps
one-to-one to a Rapier RigidBody; every constituent
SceneMesh moves together as one rigid body
via a cached rest-pose matrix multiply per step.
Rapier is not bundled with the SDK — the caller supplies the
initialised Rapier module via ScenePhysicsParams.rapier. This
keeps the SDK Rapier-version-agnostic and lets demo / app code own
the WASM init step.
Shape
%%{init:{"theme":"dark"}}%%
classDiagram
direction TB
class getScenePhysics {
+(scene, params) ScenePhysics
}
class ScenePhysics {
+scene : Scene
+world : Rapier.World
+size : number
+setGravity(g)
+setBody(objectId, params)
+removeBody(objectId)
+getBody(objectId)
+applyImpulse(objectId, impulse)
+setLinvel(objectId, vel)
+step(dt?)
+destroy()
}
class ScenePhysicsParams {
+rapier : RapierAPI
+gravity? : Vec3
+autoCreateBodies? : boolean
}
class PhysicsBodyParams {
+type? : fixed | dynamic | kinematicPositionBased
+shape? : cuboid | ball
+density? : number
+friction? : number
+restitution? : number
}
class Scene {
<<scene>>
}
class SceneObject {
<<scene>>
}
class RapierWorld {
<<rapier>>
}
getScenePhysics ..> ScenePhysicsParams : reads
getScenePhysics ..> ScenePhysics : caches
ScenePhysics o-- Scene : observes
ScenePhysics o-- RapierWorld : drives
ScenePhysics "1" *-- "*" SceneObject : body per object
ScenePhysics ..> PhysicsBodyParams : reads on setBody
%%{init:{"theme":"default"}}%%
classDiagram
direction TB
class getScenePhysics {
+(scene, params) ScenePhysics
}
class ScenePhysics {
+scene : Scene
+world : Rapier.World
+size : number
+setGravity(g)
+setBody(objectId, params)
+removeBody(objectId)
+getBody(objectId)
+applyImpulse(objectId, impulse)
+setLinvel(objectId, vel)
+step(dt?)
+destroy()
}
class ScenePhysicsParams {
+rapier : RapierAPI
+gravity? : Vec3
+autoCreateBodies? : boolean
}
class PhysicsBodyParams {
+type? : fixed | dynamic | kinematicPositionBased
+shape? : cuboid | ball
+density? : number
+friction? : number
+restitution? : number
}
class Scene {
<<scene>>
}
class SceneObject {
<<scene>>
}
class RapierWorld {
<<rapier>>
}
getScenePhysics ..> ScenePhysicsParams : reads
getScenePhysics ..> ScenePhysics : caches
ScenePhysics o-- Scene : observes
ScenePhysics o-- RapierWorld : drives
ScenePhysics "1" *-- "*" SceneObject : body per object
ScenePhysics ..> PhysicsBodyParams : reads on setBody
classDiagram
direction TB
class getScenePhysics {
+(scene, params) ScenePhysics
}
class ScenePhysics {
+scene : Scene
+world : Rapier.World
+size : number
+setGravity(g)
+setBody(objectId, params)
+removeBody(objectId)
+getBody(objectId)
+applyImpulse(objectId, impulse)
+setLinvel(objectId, vel)
+step(dt?)
+destroy()
}
class ScenePhysicsParams {
+rapier : RapierAPI
+gravity? : Vec3
+autoCreateBodies? : boolean
}
class PhysicsBodyParams {
+type? : fixed | dynamic | kinematicPositionBased
+shape? : cuboid | ball
+density? : number
+friction? : number
+restitution? : number
}
class Scene {
<<scene>>
}
class SceneObject {
<<scene>>
}
class RapierWorld {
<<rapier>>
}
getScenePhysics ..> ScenePhysicsParams : reads
getScenePhysics ..> ScenePhysics : caches
ScenePhysics o-- Scene : observes
ScenePhysics o-- RapierWorld : drives
ScenePhysics "1" *-- "*" SceneObject : body per object
ScenePhysics ..> PhysicsBodyParams : reads on setBody
Lifecycle
One ScenePhysics per Scene, regardless
of how many panels / demos ask for it — getScenePhysics
caches by scene.id and auto-destroys on onSceneDestroyed.
The engine self-maintains: it subscribes to
onSceneObjectCreated / onSceneObjectDestroyed /
onSceneModelDestroyed so Rapier bodies stay in step with the
Scene graph without caller bookkeeping.
%%{init:{"theme":"dark"}}%%
flowchart TD
A[Scene] -- created --> B[getScenePhysics]
B --> C[ScenePhysics]
C -- onSceneObjectCreated --> D[queue default body]
C -- onSceneObjectDestroyed --> E[remove body]
C -- step --> F[drain pending<br/>advance world<br/>write mesh.matrix]
C -- onSceneDestroyed --> G[destroy]
%%{init:{"theme":"default"}}%%
flowchart TD
A[Scene] -- created --> B[getScenePhysics]
B --> C[ScenePhysics]
C -- onSceneObjectCreated --> D[queue default body]
C -- onSceneObjectDestroyed --> E[remove body]
C -- step --> F[drain pending<br/>advance world<br/>write mesh.matrix]
C -- onSceneDestroyed --> G[destroy]
flowchart TD
A[Scene] -- created --> B[getScenePhysics]
B --> C[ScenePhysics]
C -- onSceneObjectCreated --> D[queue default body]
C -- onSceneObjectDestroyed --> E[remove body]
C -- step --> F[drain pending<br/>advance world<br/>write mesh.matrix]
C -- onSceneDestroyed --> G[destroy]
Features
Caller-injected Rapier — the SDK never imports
@dimforge/rapier3d-compat; pass the initialised namespace via
params.rapier so the host owns version + WASM init.
Body per SceneObject — multi-mesh objects move rigidly
together via one matrix multiply per mesh per step. No
decompose / recompose cycle.
Auto-bodies — every existing and future
SceneObject gets a default
fixed-type cuboid body sized to its world AABB. Call
setBody to upgrade specific
objects to "dynamic" or "kinematicPositionBased".
Lazy body creation — bodies are queued on object-create
events and only materialised at the next step,
so a freshly-loaded city pays no physics cost until the
simulation actually runs.
Allocation-free step — temp matrices and the iteration loop
reuse pre-allocated scratch; the body-write pass is one matrix
multiply per mesh.
Collision-index friendly — step writes mesh.matrix
without firing onSceneMeshMoved, so the
SceneCollisionIndex BVH
is not invalidated by physics motion and ray picks stay cheap
during simulation.
Direct Rapier access — world
and getBody expose the raw
Rapier objects for advanced use (joints, sensors, sleeping,
queries) the surfaced API doesn't cover.
Rapier's compat build ships its WASM as a separate file that has
to be fetched and instantiated before the namespace is usable.
await RAPIER.init() does both. Run this once at app start.
awaitRAPIER.init();
3) Get the cached engine for a Scene
One engine per Scene — subsequent getScenePhysics(scene, …) calls
return the same instance regardless of params. The first caller
sets gravity and the auto-create policy; later callers' params
are ignored.
Call step() once per frame. The engine drains pending body
creations, advances Rapier, then writes the new world transform
onto every dynamic / kinematic mesh.
Pass dt for a fixed-rate sim independent of frame rate:
physics.step(1 / 60);
5) Upgrade a body to dynamic
Every SceneObject starts with a fixed cuboid body (so a loaded
BIM stays put). setBody replaces it
— pass "dynamic" + a shape and material params for the bodies
that should fall, roll, and collide.
Apply instantaneous impulses or set linear velocity directly. Both
no-op on fixed bodies; only dynamics respond.
physics.applyImpulse("ball-1", [0, 0, 5]); // kick up physics.setLinvel ("ball-1", [3, 0, 0]); // slide along +X
7) Disable auto-bodies for fine-grained control
When autoCreateBodies: false, the engine creates no bodies of
its own — the caller decides which objects participate by calling
setBody. Right for scenes where
most geometry is decorative and only a handful of objects need
simulation.
for (constidofdynamicObjectIds) { physics.setBody(id, { type:"dynamic", shape:"cuboid" }); }
8) Reach the raw Rapier API
physics.world is the Rapier World; physics.getBody(id)
returns the RigidBody. Use them for everything the surface API
doesn't cover — joints, character controllers, shape-casts,
sleeping, etc.
Teardown is automatic — the engine destroys itself when its Scene
fires onSceneDestroyed. To detach earlier, call
destroy directly; the next
getScenePhysics(scene, …) call will create a fresh engine.
xeokit Scene Physics
Rigid-body physics for a Scene, backed by a caller-injected Rapier 3D world. One body per SceneObject, kept in step with the Scene graph automatically.
The
physicsmodule attaches a Rapier rigid-body simulation to an existing Scene. Each SceneObject maps one-to-one to a RapierRigidBody; every constituent SceneMesh moves together as one rigid body via a cached rest-pose matrix multiply per step.Rapier is not bundled with the SDK — the caller supplies the initialised Rapier module via ScenePhysicsParams.rapier. This keeps the SDK Rapier-version-agnostic and lets demo / app code own the WASM init step.
Shape
Lifecycle
One ScenePhysics per Scene, regardless of how many panels / demos ask for it — getScenePhysics caches by
scene.idand auto-destroys ononSceneDestroyed.The engine self-maintains: it subscribes to
onSceneObjectCreated/onSceneObjectDestroyed/onSceneModelDestroyedso Rapier bodies stay in step with the Scene graph without caller bookkeeping.Features
@dimforge/rapier3d-compat; pass the initialised namespace viaparams.rapierso the host owns version + WASM init.fixed-type cuboid body sized to its world AABB. Call setBody to upgrade specific objects to"dynamic"or"kinematicPositionBased".stepwritesmesh.matrixwithout firingonSceneMeshMoved, so the SceneCollisionIndex BVH is not invalidated by physics motion and ray picks stay cheap during simulation.Installation
Quick Start
1) Import the entry point
2) Init the Rapier WASM module
Rapier's
compatbuild ships its WASM as a separate file that has to be fetched and instantiated before the namespace is usable.await RAPIER.init()does both. Run this once at app start.3) Get the cached engine for a Scene
One engine per Scene — subsequent
getScenePhysics(scene, …)calls return the same instance regardless ofparams. The first caller sets gravity and the auto-create policy; later callers'paramsare ignored.4) Drive the step loop
Call
step()once per frame. The engine drains pending body creations, advances Rapier, then writes the new world transform onto every dynamic / kinematic mesh.Pass
dtfor a fixed-rate sim independent of frame rate:5) Upgrade a body to dynamic
Every SceneObject starts with a
fixedcuboid body (so a loaded BIM stays put). setBody replaces it — pass"dynamic"+ a shape and material params for the bodies that should fall, roll, and collide.6) Push a body around
Apply instantaneous impulses or set linear velocity directly. Both no-op on
fixedbodies; only dynamics respond.7) Disable auto-bodies for fine-grained control
When
autoCreateBodies: false, the engine creates no bodies of its own — the caller decides which objects participate by calling setBody. Right for scenes where most geometry is decorative and only a handful of objects need simulation.8) Reach the raw Rapier API
physics.worldis the RapierWorld;physics.getBody(id)returns theRigidBody. Use them for everything the surface API doesn't cover — joints, character controllers, shape-casts, sleeping, etc.9) Tearing down
Teardown is automatic — the engine destroys itself when its Scene fires
onSceneDestroyed. To detach earlier, call destroy directly; the nextgetScenePhysics(scene, …)call will create a fresh engine.