Phaser AE

3D rigid bodies

Free packv3.0.0-43DPhysicsUpdated

Rigid-body physics for 3D games over a vendored Box3D core. Bind an object and it falls, stacks, topples, rolls, swings and drives, with joints, a character controller, triggers, raycasts and a blast radius, all in the engine's own vocabulary.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 14, 2026 · updated October 26, 2026

3D rigid bodies: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

physics3d

function physics3d(opts: Physics3dOptions = {}): Pack<{ physics3d: Physics3dApi }>

The physics-3d pack: rigid bodies for a 3D world, over a vendored Box3D core that loads as its own ~1 MB chunk. Requires a 3D world.

Helpers

createRigidWorld3d

async function createRigidWorld3d(options:Physics3dOptions={}):Promise<RigidWorld3d>

A headless rigid world over the same solver as game.physics3d, which you step and dispose yourself. Gravity defaults to −9.81 units/s² (the composed pack uses −20) and substeps to 4. Throws on gravity beyond ±1000 or substeps outside 1 to 32; its step throws on a dt outside 0 to 0.1 s.

Functions

continuous

function continuous(prev: number, next: number): number

next, shifted by whole turns so it is the SHORT way from prev.

The kernel interpolates an object's rotation linearly between fixed steps. A spinning body's yaw crosses from +π to −π every revolution, and a linear blend across that jump runs the whole way back round — one frame of a fast reverse spin, once per turn. Keeping the numbers continuous instead of wrapped costs nothing and makes that blend take the short path by construction.

eulerQuat

function eulerQuat(yaw: number, pitch: number, roll: number): Quat

Euler (the engine's yaw/pitch/roll) → quaternion, in the RENDERER'S OWN composition order.

render-3d's vertex stage computes rotY(−yaw) · rotX(pitch) · rotZ(roll), so the quaternion is qy · qx · qz with the yaw half-angle negated. Get this order wrong and a body simulates correctly while drawing at some other orientation — which reads as a physics bug and is not one.

quatConj

function quatConj(q: Quat): Quat

The inverse of a unit quaternion.

quatFromCols

function quatFromCols(cx: V3, cy: V3, cz: V3): Quat

A quaternion from an orthonormal basis, the columns being the frame's world axes.

quatFromTo

function quatFromTo(from: V3, to: V3): Quat

The rotation taking unit from onto to. Handles both poles, where the cross product is useless.

quatFromXTo

function quatFromXTo(axis: V3): Quat

The rotation aiming local +x along axis.

quatFromZTo

function quatFromZTo(axis: V3): Quat

The rotation aiming local +z along axis.

quatMul

function quatMul(a: Quat, b: Quat): Quat

Quaternion product.

quatRotate

function quatRotate(q: Quat, p: V3): V3

Rotate a point by a quaternion.

quatToEuler

function quatToEuler(x: number, y: number, z: number, w: number, out: EulerOut): void

Quaternion → the engine's yaw/pitch/roll, decomposed in the SAME rotY(−yaw)·rotX(pitch)·rotZ(roll) order the shader applies, so writing these onto an object reproduces the body's exact orientation on screen.

GIMBAL CAVEAT: at pitch = ±90° yaw and roll collapse onto the same axis, so the split between them is arbitrary there and this puts it all in yaw. A single frame's pose is still exact — every decomposition renders the right orientation — but a body tumbling THROUGH the pole can see yaw and roll swap values between frames. That is invisible on screen, because the composed rotation stays continuous; only code reading the raw numbers of a tumbling body would notice.

Classes

Body3d

class Body3d

A body bound to an object.

Dynamic bodies OWN their object's transform: steer them with impulse, force or velocity, never by writing x/y/z (except teleport). Kinematic bodies are the reverse — move the OBJECT.

  • readonly target: Body3dTarget — The object this body drives (dynamic) or follows (kinematic).
  • readonly type: 'dynamic' | 'static' | 'kinematic' — Which kind of body it is.
  • onHit: ((other: Body3d, speed: number) => void) | null — Contact above the impact threshold, with the approach speed. Assign at any time.
  • onTouch: ((other: Body3d) => void) | null — Contact began, any speed.
  • onRelease: ((other: Body3d) => void) | null — Contact ended.
  • onEnter: ((other: Body3d) => void) | null — A body entered this trigger.
  • onExit: ((other: Body3d) => void) | null — A body left this trigger.
  • _id: Id
  • _sim: Sim
  • _suspended: boolean
  • _dead: boolean
  • constructor(sim: Sim, id: Id, target: Body3dTarget, type: 'dynamic' | 'static' | 'kinematic')
  • get velocity(): V3 — Linear velocity, world units a second. Set the linear velocity, and wake the body — a velocity set on a sleeping body would otherwise do nothing.
  • set velocity(v: V3) — Linear velocity, world units a second. Set the linear velocity, and wake the body — a velocity set on a sleeping body would otherwise do nothing.
  • get spin(): V3 — Angular velocity, radians a second per axis. Set the angular velocity, and wake the body.
  • set spin(v: V3) — Angular velocity, radians a second per axis. Set the angular velocity, and wake the body.
  • get mass(): number — The body's mass. Static and kinematic bodies read 0.
  • get awake(): boolean — Is it awake? A sleeping body costs the solver nothing until something touches it. Wake it, or put it to sleep.
  • set awake(v: boolean) — Is it awake? A sleeping body costs the solver nothing until something touches it. Wake it, or put it to sleep.
  • impulse(v: Partial<V3>, at?: V3): void — An instant kick, MASS-SCALED so impulse({ y: 6 }) pops a crate about as high as you would guess whatever it weighs. Pass at — a world point — to spin it too: a hit off centre tumbles.
  • force(v: Partial<V3>): void — A continuous push that accumulates until the next step — thrusters, wind. Mass-scaled like impulse.
  • torque(v: Partial<V3>): void — A twist about the body's centre, in torque units.
  • teleport(position: V3, rotation?: { yaw?: number; pitch?: number; roll?: number }): void — Move the body and its object instantly, clearing velocity — a respawn. rotation is Euler radians in the engine's own yaw/pitch/roll; omitted means upright. It also SNAPS the object, so the frame that follows does not interpolate the jump: without that, a respawn smears the object across the whole map for one frame.
  • destroy(): void — Take this body out of the world; the object stays where it is. Safe to call twice — the second call does nothing, which matters because the solver RECYCLES body slots and an unguarded double-destroy could kill an unrelated new body.

Character3d

class Character3d extends Body3d

A capsule character controller: a rotation-locked dynamic body with velocity-driven movement. It pushes crates, rides platforms, climbs shallow steps by capsule shape, and reports grounded for jump gating.

  • constructor(sim: Sim, id: Id, target: Body3dTarget, height: number)
  • move(dir: { x: number; z: number }, speed: number): void — Steer: set horizontal velocity toward dir (any length) at speed, keeping vertical motion.
  • get grounded(): boolean — Standing on something, within a small tolerance below the feet?
  • jump(v: number): boolean — Jump at takeoff speed v. Only fires while grounded; returns whether it did.

Constants

IDENTITY

const IDENTITY: Quat

The identity rotation.

Types

BindOptions

interface BindOptions

Options for bind.

  • type?: 'dynamic' | 'static' | 'kinematic' — 'dynamic' (default) — fully simulated. 'static' — immovable level geometry. 'kinematic' — YOU move the object and it pushes dynamic bodies physically (moving platforms, doors on rails).
  • shape?: 'auto' | 'box' | 'sphere' | 'capsule' | 'hull' — The collision shape: 'auto' (default — a box of the object's own size), 'sphere', 'capsule' (upright), or 'hull' (a convex wrap of the points you supply — build them with hull-3d's hullOfVerts). Match it to what you can see. A hull is CONVEX: a donut's hull is a solid disc.
  • points?: Float32Array — The 'hull' shape's point cloud: flat xyz triples in the object's LOCAL unit space, spanning ±0.5 per axis, which the wrapper scales by sx/sy/sz exactly as it derives box half-extents. Required for shape: 'hull'; missing or degenerate points fall back to the box.
  • friction?: number — Surface grip, 0..1+. Default 0.5.
  • bounce?: number — Bounciness, 0..1. Default 0.
  • rollingResistance?: number — Rolling resistance, 0..~0.1 — rolling bodies slow down. Default 0, which rolls forever.
  • mass?: number — Total mass, dynamic bodies. Default: about 1 per cubic unit.
  • group?: string — Collision group name. Default 'default'.
  • hits?: string[] — The group names this body collides with. Default: everything.
  • trigger?: boolean — Sensor mode: overlaps fire onEnter/onExit and apply NO contact forces — pickup zones, pressure plates.
  • onEnter?: (other: Body3d) => void — A body entered this trigger.
  • onExit?: (other: Body3d) => void — A body left this trigger.
  • onHit?: (other: Body3d, speed: number) => void — Contact started, above the solver's impact threshold, with the approach speed — gate impact sounds on it.
  • onTouch?: (other: Body3d) => void — Contact BEGAN at any speed — standing-on bookkeeping, "what am I touching".
  • onRelease?: (other: Body3d) => void — Contact ENDED — the pair separated.
  • fast?: boolean — Continuous collision for fast movers, so a bullet does not tunnel through a thin wall.
  • damping?: number — Linear damping — air drag. Default 0.
  • angularDamping?: number — Angular damping. Default 0.05.
  • gravityScale?: number — Gravity multiplier for this body; 0 floats. Default 1.
  • lockRotation?: boolean — Freeze all rotation — top-down pucks, upright props. Default false.
  • fastSpin?: boolean — Allow very fast rotation. The solver clamps angular speed by default, which rev-limits a WHEEL. Default false.
  • balance?: V3 — Centre-of-mass offset in the object's LOCAL space. A car with balance: { y: -0.4 } carries its weight low like a real one — planted, hard to flip. Default: the geometric centre of the shape.
  • conveyor?: V3 — Conveyor belt: the surface drags whatever rests on it at this velocity while the body itself stays put — usually a static belt. The vector is in the shape's LOCAL space, so rotating the object turns the belt with it. More friction is more grip on the cargo.
  • family?: string — Family name: bodies sharing a family NEVER collide with each other while still colliding with the rest of the world. This is the ragdoll knob — limbs must overlap at their joints without fighting. Independent of group/hits.

Body3dTarget

interface Body3dTarget

What bind() accepts: anything with the pose fields a 3D object has. A kernel Handle from world3d.spawn() satisfies it, and so does a plain object in a headless test.

sx, sy and sz are the FULL sizes along each axis — the engine's primitives are unit-sized, so an object with sx = 2 really is two units wide. rot is yaw; pitch and roll are the other two, applied in the renderer's own order.

  • readonly alive?: boolean — Roster handles expose this; false lets physics release a body whose rendered object was killed.
  • readonly frozen?: boolean — Scene suspension removes this target from simulation until it resumes.
  • x: number — Centre x, world units.
  • y: number — Centre y, world units; y is up.
  • z: number — Centre z, world units.
  • sx: number — Full width along x.
  • sy: number — Full height along y.
  • sz: number — Full depth along z.
  • rot: number — Yaw about y, radians.
  • pitch: number — Pitch about x, radians.
  • roll: number — Roll about z, radians.

Character3dOptions

interface Character3dOptions

Options for character.

  • radius?: number — Capsule radius. Default 0.4.
  • height?: number — Total capsule height. Default 1.7.
  • friction?: number — Ground friction while standing still. Default 0.1 — low, because movement is velocity-driven.
  • group?: string — Collision group. Default 'characters'.

EulerOut

interface EulerOut

What quatToEuler writes into.

  • yaw: number — Rotation about the vertical axis in radians, with the engine's yaw sign (the renderer applies rotY(-yaw)).
  • pitch: number — Rotation about the x axis in radians, from -π/2 to π/2.
  • roll: number — Rotation about the z axis in radians; the full turn is applied in the renderer's yaw, pitch, roll composition order.

GrabOptions

interface GrabOptions

Options for grab — hold and drive a body relative to another.

  • force?: number — Max holding force: how heavy a thing it can carry against gravity and knocks. Default 200.
  • torque?: number — Max holding torque: how firmly it resists spinning away. Default 100.
  • stiffness?: number — Hold stiffness in Hz. Default 5, which is snappy; lower is a laggy tractor beam.
  • damping?: number — Hold damping 0..1. Default 0.8.

HingeOptions

interface HingeOptions

Options for hinge.

  • at: V3 — The world anchor the hinge turns around.
  • axis?: V3 — The world hinge axis. Default { x: 0, y: 1, z: 0 } — a door.
  • limits?: [number, number] — Swing limits in radians [min, max]. Omit for free spin.
  • motor?: { speed: number; torque?: number } — Drive it: radians/second, and a torque budget.

Joint3d

interface Joint3d

A joint. Keep the handle to drive its motor or destroy() it.

  • motor(speed: number, strength?: number): void — Retune the motor — a hinge in rad/s, a slider in units/s, a wheel in spin rad/s. No-op on springs and welds.
  • steer?(angle: number): void — Aim a wheel joint, radians. No-op on the others.
  • drive?(linear: V3, angular?: V3): void — Grab joints: drive the held body at this relative velocity.
  • destroy(): void — Remove it; the bodies separate.

Physics3dApi

interface Physics3dApi

game.physics3d.

  • readonly ready: Promise<void> — Resolves once the WASM core has loaded and the world exists. bind and every query throw before it, so a scene that uses physics awaits this first: await game.physics3d.ready; game.physics3d.bind(ground, { type: 'static' });
  • readonly live: boolean — Has the core landed yet?
  • gravity(g: number | V3): void — Set gravity: a number is straight down, or pass a full vector. Wakes every body.
  • bind(target: Body3dTarget, opts?: BindOptions): Body3d — Give an object a body. The shape comes from its own size and the pose from where it is NOW.
  • character(target: Body3dTarget, opts?: Character3dOptions): Character3d — A capsule character controller.
  • bodyOf(target: Body3dTarget): Body3d | null — The body bound to an object, or null.
  • raycast(origin: V3, dir: V3, maxDistance?: number, ignore?: Body3d, opts?: { hits?: string[] }): RayHit3d | null — The nearest hit along a ray. ignore skips one body — the shooter.
  • raycastAll(origin: V3, dir: V3, maxDistance?: number, opts?: { hits?: string[] }): RayHit3d[] — Every hit along a ray, nearest first.
  • overlap(center: V3, radius: number, opts?: { hits?: string[] }): Body3d[] — Every body overlapping a sphere.
  • overlapBox(center: V3, size: V3, opts?: { hits?: string[] }): Body3d[] — Every body overlapping an axis-aligned box.
  • explosion(at: V3, radius: number, strength: number): void — A radial blast: a mass-scaled outward kick, with lift, on every dynamic body in range.
  • hinge(a: Body3d, b: Body3d, opts: HingeOptions): Joint3d — A hinge — doors, windmills, seesaws.
  • socket(a: Body3d, b: Body3d, opts: SocketOptions): Joint3d — A ball-and-socket — shoulders, hips, chain links.
  • slider(a: Body3d, b: Body3d, opts: SliderOptions): Joint3d — A slider — elevators, pistons, sliding doors.
  • wheel(chassis: Body3d, wheel: Body3d, opts: WheelOptions): Joint3d — A vehicle wheel: suspension, drive and steering in one.
  • spring(a: Body3d, b: Body3d, opts?: SpringOptions): Joint3d — A springy rope — pendulums, cranes, bungees.
  • grab(a: Body3d, b: Body3d, opts?: GrabOptions): Joint3d — A motorised grip — tractor beams, claws, carrying a crate.
  • weld(a: Body3d, b: Body3d, at?: V3): Joint3d — Lock two bodies rigidly together.
  • clear(): void — Remove every body. The objects stay where they are.
  • readonly bodies: number — How many bodies exist.
  • readonly invalidBodies: number — Diagnostic: body handles found stale during a step. It should stay 0.
  • enabled: boolean — Turn the simulation off without tearing it down.

Physics3dOptions

interface Physics3dOptions

Manifest options.

  • gravity?: number | V3 — Gravity: a number is world units/s² straight down, or a full vector. Default −20 (arcade-weighty) for the composed pack; createRigidWorld3d defaults to −9.81.
  • substeps?: number — Solver substeps per fixed step. Default 4; more for taller stacks.
  • module?: string — Override the core module URL in a development build, on the game's own origin; another origin throws. Rarely needed — the default finds the shipped chunk.

Quat

interface Quat

Box3D's quaternion: a vector part and a scalar.

  • v: V3 — The vector part (x, y, z) of the quaternion, each between -1 and 1 for a unit rotation.
  • s: number — The scalar part (w), between -1 and 1 for a unit rotation; 1 with a zero vector part is no rotation.

RayHit3d

interface RayHit3d

What a ray found.

  • body: Body3d — The body it struck.
  • point: V3 — Where, in world units.
  • normal: V3 — The surface normal there.
  • distance: number — How far along the ray, world units.

RigidWorld3d

interface RigidWorld3d

A renderer-independent rigid world for rule packs and physical replay validation.

  • bind(target:Body3dTarget,options?:BindOptions):Body3d — Bind a plain owned pose to a real rigid body.
  • hinge(a:Body3d,b:Body3d,options:HingeOptions):Joint3d — Pin two bodies around a physical rotation axis.
  • step(dt:number):void — Advance at an explicitly supplied bounded time step.
  • clear():void — Remove every body and its constraints.
  • reset():void — Recreate the solver, clearing bodies, contacts and allocation history for deterministic replay.
  • dispose():void — Release the owned world; repeated disposal is safe.

SliderOptions

interface SliderOptions

Options for slider.

  • at: V3 — The world anchor on the slide axis.
  • axis?: V3 — The world slide axis. Default { x: 0, y: 1, z: 0 } — an elevator.
  • limits?: [number, number] — Travel limits along the axis, world units.
  • motor?: { speed: number; force?: number } — Drive it: units/second, and a force budget.

SocketOptions

interface SocketOptions

Options for socket — a ball-and-socket joint.

  • at: V3 — The world anchor: the ball's centre — a shoulder, a hip, a chain link.
  • axis?: V3 — The rest-pose axis the swing cone opens around; point it along the CHILD limb. Default straight down.
  • swing?: number — Max swing away from axis, radians. Omit for a free ball joint.
  • twist?: [number, number] — Twist limits about axis, radians. Omit for free twist.
  • stiffness?: number — A soft spring pulling back to the rest pose, in Hz. Adds life to a ragdoll. Default off.

SpringOptions

interface SpringOptions

Options for spring.

  • length?: number — Rest length. Default: the current distance between the anchors.
  • hertz?: number — Stiffness in Hz — oscillations a second. Default 3.
  • damping?: number — Damping 0..1; 1 is no wobble. Default 0.35.
  • atA?: V3 — World anchor on body A. Default A's origin.
  • atB?: V3 — World anchor on body B. Default B's origin.

V3

interface V3

A 3D vector, in world units.

  • x: number — Component along the x axis, in world units.
  • y: number — Component along the vertical y axis, in world units.
  • z: number — Component along the z axis, in world units.

WheelOptions

interface WheelOptions

Options for wheel — suspension, drive and steering in one joint.

  • at: V3 — The world wheel centre at rest.
  • axle?: V3 — The world axle (spin) axis. Default { x: 1, y: 0, z: 0 } — a car facing ±z.
  • hertz?: number — Suspension stiffness in Hz. Default 3.5.
  • damping?: number — Suspension damping 0..1; 1 is no bounce. Default 0.7.
  • travel?: [number, number] — Suspension travel [down, up] around the rest pose. Default [-0.25, 0.15].
  • drive?: boolean | { torque?: number } — This wheel is powered — joint.motor(speed) drives it in rad/s.
  • steer?: boolean | { limit?: number; torque?: number } — This wheel steers — joint.steer(angle) aims it. limit caps the lock. Default lock 0.65.