Physics

Colliders & shapes

The Collider component — collision shapes, surface materials, layer/mask filtering, sensors, contact events, and raycasts.

A Collider pairs an immutable shape with a surface material (friction, restitution). Its world pose comes from the entity’s Transform.

import { Collider }  from "@woosh/meep-engine/src/engine/physics/ecs/Collider.js";
import { BoxShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/BoxShape3D.js";

const collider = new Collider();
collider.shape       = BoxShape3D.from_size(2, 1, 2);
collider.friction    = 0.6;
collider.restitution = 0.2;

Add it to the same entity as the body. The ColliderObserverSystem is what attaches a collider to its body, so register it alongside the PhysicsSystem — and register the PhysicsSystem first, so the body’s slot exists before the collider attaches to it.

Shapes

Shapes are shared, immutable values — build one and reuse it across every collider of that size.

ShapeConstructNotes
SphereSphereShape3D.from(radius)The simplest and fastest shape.
BoxBoxShape3D.from_size(w, h, d)Also BoxShape3D.from(hx, hy, hz) for half-extents.
CapsuleCapsuleShape3D.from(radius, height)Y-aligned; height is the cylinder section only (the rounded caps add radius at each end).
CylinderCylinderShape3D.from(radius, height)Y-aligned; height is the full height.
HeightmapHeightMapShape3D.from(sampler, sizeX, sizeY, sizeZ)A terrain surface read from a Sampler2D of heights, spanning sizeX × sizeZ with heights up to sizeY. It’s a non-convex surface — put it on a Static body for ground.

Sphere, box, and capsule pairs are handled by fast, exact routines written for those specific shapes — quicker and more robust than the general-purpose path on the common, smooth cases.

Size lives on the shape, not the Transform. The simulation reads position and rotation only — Transform.scale is ignored. Bake dimensions into the shape (from_size, from, the capsule radius/height), and match your render mesh to it.

Surface material

  • friction — Coulomb coefficient. Combined with the other surface via a geometric-mean rule.
  • restitution — bounciness, 0 (inelastic) to 1 (elastic).
  • density — used by inertia helpers if you opt in; informational otherwise.

Layers and masks

Each body carries a layer bitmask (its category) and a mask (the layers it collides with). A pair (A, B) collides only when both sides agree:

(A.layer & B.mask) !== 0  &&  (B.layer & A.mask) !== 0

Defaults are layer = 1, mask = 0xFFFFFFFF — everything collides with everything. Carve groups out by assigning bits (e.g. put debris on its own layer and clear that bit from the player’s mask so debris never blocks the player).

Sensors and contact events

A sensor detects overlaps and fires events but applies no impulse — the basis for triggers and volumes. Flag the whole body (RigidBodyFlags.IsSensor) or a single collider (ColliderFlags.IsSensor).

The system emits contact events both as engine-level signals and as entity events:

import { PhysicsEvents } from "@woosh/meep-engine/src/engine/physics/ecs/PhysicsEvents.js";

physics.onContactBegin.add((contact) => { /* new pair this step */ });
physics.onContactStay.add((contact)  => { /* persisting pair */ });
physics.onContactEnd.add((contact)   => { /* pair separated */ });

The payload is reused per dispatch — copy any field you need to keep past the listener.

For broad-strokes filtering, install a contact filter; return false to discard a pair before the full collision test (the cheap layer/mask test runs first):

physics.setContactFilter((entityA, entityB, colliderA, colliderB) => {
    return true; // keep, or false to ignore this pair
});

Querying without colliding

These same shapes drive the read-only spatial queriesraycast, shapeCast, and overlap — which ask the physics world questions (“what’s under the cursor?”, “would I fit here?”) without adding a body or stepping the simulation.

Where to go next