# Physics

> Rigid bodies on cannon-es behind a typed API (Physics, RigidBody, StaticBody, LevelBodies, TriggerVolume, Joint, PhysicsWorld), the player's capsule, collisions and rays, the drivable Vehicle, and GPU Debris for thousands of small pieces.

The physics module (`src/engine/physics/`, exported from `@/engine`) makes the world react: crates that fall and stack, balls that bounce, doors on hinges, pickups through trigger volumes, impacts that thud. It has two tiers behind one typed vocabulary:

- **Rigid bodies** for the few objects that matter, on cannon-es (pure JavaScript, no WebAssembly), stepped at a fixed 60 Hz from the frame loop.
- **GPU debris** for the many small things (crash pieces, coins, glass, hail, confetti): thousands of pieces simulated in a TypeGPU compute shader, drawn in one call. They bounce off the world, not off each other.

Units are metres, kilograms and seconds; +Y is up. Gameplay rules (scores, lives) stay in your code. Physics is what the world does when it is hit. The player's character keeps the character controller for movement ([Character and cameras](https://docs.moonspell.app/engine/character-and-cameras.md)); its capsule shoves dynamic bodies and is never pushed.

## Smallest working example

A floor and a crate that falls onto it, with a thud on impact:

```tsx
import React from 'react';
import { EngineCanvas, Environment, Physics, RigidBody, StaticBody, impactStrength, useCollisions } from '@/engine';

const Thuds = () => {
  useCollisions((event) => {
    const strength = impactStrength(event, 1, 10); // 0..1
    if (strength > 0.1) console.log('thud', strength.toFixed(2));
  });
  return null;
};

export const CrateScene = () => (
  <EngineCanvas camera={{ position: [0, 4, 8] }}>
    <Environment preset="day" />
    <Physics>
      <Thuds />
      <StaticBody shape={{ kind: 'box', w: 20, h: 1, d: 20 }} position={[0, -0.5, 0]}>
        <mesh receiveShadow>
          <boxGeometry args={[20, 1, 20]} />
          <meshStandardMaterial color="#5A6A5A" />
        </mesh>
      </StaticBody>
      <RigidBody shape={{ kind: 'box', w: 0.8, h: 0.8, d: 0.8 }} mass={4} material="wood" position={[0, 4, 0]} rotation={[0.3, 0.4, 0]}>
        <mesh castShadow>
          <boxGeometry args={[0.8, 0.8, 0.8]} />
          <meshStandardMaterial color="#A86F3A" />
        </mesh>
      </RigidBody>
    </Physics>
  </EngineCanvas>
);
```

Put the meshes at the body's origin inside `RigidBody`: the body moves the group. In a level made with the character foundation, add `<LevelBodies level={level} />` inside `<Physics>` and the level's boxes become solid.

## Components

### `Physics`

Owns one `PhysicsWorld` and steps it every frame before anything reads positions (priority `-10`), then dispatches the frame's collision and trigger events in order.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `gravity` | `Vec3` | `{ x: 0, y: -9.81, z: 0 }` | m/s² |
| `fixedStep` | `number` | `1/60` | Seconds per physics step |
| `maxSubsteps` | `number` | `4` | Steps one frame may run to catch up before time is dropped |
| `paused` | `boolean` | `false` | Freeze the simulation (a pause menu) |
| `timeScale` | `number` | `1` | Simulation speed |
| `world` | `PhysicsWorld` | its own | A world you made yourself (tests, gameplay code that shares it) |

Frame deltas are capped at 0.1 s before stepping.

### Bodies

| Component | What it is |
| --- | --- |
| `RigidBody` | A dynamic body: falls, rolls, gets knocked about |
| `StaticBody` | Never moves: walls, floors. May use any shape, trimeshes included |
| `TriggerVolume` | An invisible static zone that reports bodies entering and leaving; collides with nothing |
| `LevelBodies` | Makes a character-foundation `Level` solid: a static box per level box and a ground slab under the floor. Moving platforms are skipped |

Props shared by `RigidBody` and `StaticBody`:

| Prop | Type | What it does |
| --- | --- | --- |
| `shape` | a `Shape` (below) | Required. `RigidBody` takes any shape except `trimesh` |
| `position` | `[x, y, z]` or `Vec3` | Where it starts |
| `rotation` | `[x, y, z]` | Euler radians |
| `material` | `'default'`, `'wood'`, `'metal'`, `'rubber'`, `'ice'` or `{ friction?, restitution? }` | Surface. Default friction 0.4, restitution 0.1 |
| `shapeOffset` | `Vec3` | Where the shape sits relative to the body origin |
| `name` | `string` | For debugging and events |
| `isTrigger` | `boolean` | Report overlaps instead of colliding |
| `onBody` | `(id: BodyId) => void` | The body's id, for `world.positionOf(id)` and joints |
| `onCollide` | `(event, other) => void` | This body started touching another |
| `children` | `React.ReactNode` | What to draw |

Extra `RigidBody` props: `mass` (kg, default 1), `velocity`, `angularVelocity`, `damping` (default 0.01 per second), `allowSleep` (default true), `ignoredByWheels` (vehicle wheels roll through it instead of climbing it: cones, bottles).

`TriggerVolume` props: `shape` (not a trimesh), `position`, `rotation`, `onEnter(other)`, `onExit(other)`, `onBody`, `children`.

```tsx
<TriggerVolume shape={{ kind: 'sphere', radius: 0.7 }} position={[3, 0.7, 2]} onEnter={(other) => { if (other === playerBody.current) collect(); }}>
  <mesh>
    <sphereGeometry args={[0.3]} />
    <meshStandardMaterial color="#FFD23F" />
  </mesh>
</TriggerVolume>
```

### Shapes

| `kind` | Fields | Notes |
| --- | --- | --- |
| `'box'` | `w`, `h`, `d` | Full size along x, y, z |
| `'sphere'` | `radius` | |
| `'capsule'` | `radius`, `height` | Upright; `height` includes the caps |
| `'cylinder'` | `radius`, `height` | Upright |
| `'convex'` | `points: Vec3[]` | The convex hull of the points, relative to the body origin |
| `'trimesh'` | `vertices: number[]`, `indices: number[]` | Exact triangles; static bodies only |

Helpers: `convexFromPoints(points, maxVertices = 64)`, `meshPoints(object3d)` (the points of a mesh, for a hull) and `shapeHalfExtents(shape)`. `MATERIALS` holds the named materials. When two bodies touch, friction is multiplied and bounce takes the larger value.

### `Joint`

A joint between two bodies, by id (from `onBody`), removed on unmount. It is only added once both bodies exist.

| `kind` | Fields | What it is |
| --- | --- | --- |
| `'hinge'` | `pivot`, `axis`, `pivotB?`, `limits?: { min, max }`, `motor?: { speed, maxTorque? }` | A door, a wheel, a pendulum. Limits are radians from the rest pose |
| `'point'` | `pivot`, `pivotB` | Ball and socket |
| `'distance'` | `distance?` | A rope or rod (default: the current distance) |
| `'lock'` | | A weld |

All joints take `a`, `b`, `collide?` (let the two bodies hit each other, default false) and `onJoint?(id)`.

### The player's capsule: `useCharacterBody`

```tsx
const state = useRef(createCharacterState(spawn));
const playerBody = useCharacterBody(state, params); // a ref to the capsule's BodyId
```

Inside `<Physics>`, call it in the component that steps the player. A kinematic capsule mirrors the controller's state right before each step, with its velocity, so crates the player walks into are shoved. Options: `{ material?, name? }`. A kinematic body pushes any dynamic body whatever its mass, so things that must stop the player are static bodies or level boxes. `attachCharacter(world, params, options)` is the same thing without React.

### Hooks

| Hook | What it does |
| --- | --- |
| `usePhysics()` | The nearest `<Physics>` world; throws outside one |
| `usePhysicsOptional()` | The same, or `null` outside one |
| `useCollisions(fn)` | Every `CollisionEvent` in the world, after each step |
| `useTriggers(fn)` | Every `TriggerEvent` (enter and exit) |
| `impactStrength(event, soft = 1, hard = 10)` | How hard a hit was, 0 to 1, from the closing speed in m/s |

A `CollisionEvent` is `{ kind: 'collision', a, b, impulse, speed, point, normal }`, reported once per contact (a landing is one event), not per contact point. A `TriggerEvent` is `{ kind: 'trigger', trigger, other, phase: 'enter' | 'exit' }`.

## `PhysicsWorld`

The world behind `<Physics>`, usable headless (tests, gameplay reads and pokes). `new PhysicsWorld({ gravity?, fixedStep?, maxSubsteps? })`.

| Method | What it does |
| --- | --- |
| `addBody(def)`, `removeBody(id)`, `has(id)`, `kindOf(id)` | Bodies by `BodyDef` (`kind: 'static'`, `'dynamic'` or `'kinematic'`) |
| `positionOf(id)`, `velocityOf(id)`, `quaternionOf(id)`, `isSleeping(id)` | Exact state after the last step |
| `transformOf(id)` | Interpolated for rendering between the last two steps |
| `setTransform(id, position, quaternion?)` | Teleport; wakes it |
| `moveKinematic(id, position, velocity, quaternion?)` | Move a kinematic body for this frame, before the step |
| `applyImpulse(id, impulse, worldPoint?)` | An instant kick in N·s |
| `applyForce(id, force, worldPoint?)` | A force in N for this step; call every frame while it acts |
| `setVelocity(id, velocity, angular?)` | |
| `raycast(from, to, options?)` | Nearest hit `{ body, point, normal, distance }` or `null` |
| `raycastAll(from, to, options?)` | Every hit, nearest first |
| `addJoint(def)`, `removeJoint(id)`, `hasJoint(id)`, `hingeAngle(id)` | Joints |
| `onCollision(fn)`, `onTrigger(fn)` | Subscribe; returns the unsubscribe |
| `step(dt)` | Advance by real seconds in fixed steps (`<Physics>` calls it) |
| `stepFixed(n)` | Exactly `n` fixed steps (tests) |
| `drainEvents()`, `dispatchEvents()` | The event queue (`<Physics>` dispatches once per frame) |
| `stats` | `{ bodies, awake, joints, stepMs, substeps }` |
| `dispose()` | Release everything |

Ray options: `filter(body)` to skip bodies, `skipTriggers` (default true). `fromLevel(world, level)` is what `LevelBodies` calls; it returns the ids it added.

## `Vehicle`

A drivable car: a chassis on four sprung raycast wheels that drives, slides, drifts with the handbrake, flies off ramps, lands and rights itself after a while on its roof. It hits every body in the world. Use it for driving games that are not lap races; the racing module keeps its own arcade car.

```tsx
import React, { useMemo, useRef } from 'react';
import { Physics, Vehicle, impactStrength, shake, type VehicleBody, type VehicleInput } from '@/engine';

export const Buggy = () => {
  const input = useMemo<VehicleInput>(() => ({ throttle: 0, steer: 0 }), []); // your controls write it
  const car = useRef<VehicleBody | null>(null);
  return (
    <Physics>
      <Vehicle
        position={[0, 1, 0]}
        input={input}
        color="#FF5C3A"
        onVehicle={(v) => (car.current = v)}
        onLand={(airTime) => shake(Math.min(0.6, airTime * 0.4))}
        onCollide={(event) => {
          if (impactStrength(event) > 0.3) shake(0.3);
        }}
      />
    </Physics>
  );
};
```

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `position` | `[x, y, z]` or `Vec3` | required | Where it starts; later changes are ignored (call `reset`) |
| `yaw` | `number` | `0` | Heading in radians, 0 faces +z |
| `input` | `VehicleInput` or `(state, dt) => VehicleInput` | required | `throttle` -1 to 1, `steer` -1 to 1 (positive toward +x), `brake?` 0 to 1, `handbrake?` |
| `children` | `React.ReactNode` | a body and cabin | The chassis look, in chassis space (nose toward +z) |
| `color`, `wheelColor` | `string` | `'#E8ECF4'`, `'#15161C'` | Default look |
| `showWheels` | `boolean` | `true` | Hide them when your model has its own |
| `onVehicle` | `(vehicle: VehicleBody) => void` | none | The vehicle: `state`, `rideHeight`, `wheelPoses()`, `reset(position, yaw?)` |
| `onCollide` | `(event, other) => void` | none | The chassis hit something |
| `onLand` | `(airTime: number) => void` | none | Landed after `airTime` seconds in the air |
| `chassis` | `{ w?, h?, d?, mass? }` | 1.8 by 0.5 by 3.6, 150 kg | |
| `wheel` | `{ radius?, halfTrack?, halfBase?, suspensionRest?, stiffness?, friction?, rollInfluence?, mount? }` | | `friction` is grip, about the g the tyres hold |
| `engine` | `{ force?, brake?, coast?, maxSteer?, maxSpeed? }` | | Drive force per rear wheel, brake force, engine braking, steering in radians, top speed in m/s |
| `centerOfMassDrop` | `number` | | Ballast: higher stops tipping |
| `downforce` | `number` | `0` | Grip at speed |
| `recoverAfter` | `number` | `1.2` | Seconds on its roof before it rights itself; 0 turns it off |

`VehicleBody.state` has `speed`, `yaw`, `steer`, `spin`, `wheelsDown`, `airborne`, `airTime`, `landed`, `flipped`, `recoveries`. Headless: `createVehicle(world, def)`, then `drive(input, dt)` before each step.

## `Debris`

Many small pieces on the GPU that bounce off the world's static boxes and a few moving bodies. They settle, sleep and shrink away at the end of their life. Bursts reuse the oldest pieces when `count` is full.

```tsx
import React, { useRef } from 'react';
import { Debris, type DebrisHandle } from '@/engine';

export const Coins = () => {
  const coins = useRef<DebrisHandle>(null);
  // later, from gameplay: coins.current?.burst({ x: 0, y: 1, z: 0 }, { x: 0, y: 1, z: 0 }, { count: 20 });
  return <Debris ref={coins} preset="coins" count={200} />;
};
```

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `preset` | `'crash'`, `'coins'`, `'shells'`, `'glass'`, `'hail'`, `'confetti'` | `'crash'` | The look and behaviour |
| `count` | `number` | `300` | Pieces alive at once |
| `proxies` | body ids or refs to them | `[]` | Moving bodies the pieces bounce off (the car, the player's capsule) |
| `groundY` | `number` | `0` | The ground when there is no `<Physics>` world to read boxes from |
| `timeScale` | `number` | `1` | |
| `source` | `{ at, direction, rate, area? }` | none | A continuous source: `rate` pieces per second (hail from a cloud) |
| any `DebrisConfig` field | | from preset | `burst`, `speed`, `spread`, `size`, `sizeJitter`, `life`, `fade`, `gravity`, `restitution`, `friction`, `drag`, `spin`, `spinDamping`, `sleepSpeed`, `color`, `mesh` (`'chunk'`, `'coin'`, `'shard'`, `'ball'`) |

The handle has one method: `burst(at, direction, { count?, speed?, spread? })`. `DEBRIS_PRESETS` holds the presets. Colliders are fixed-size GPU arrays: at most 96 static boxes and 16 moving spheres.

## `PhysicsDebug`

A development-only line (bodies awake and total, joints, step time, substeps) drawn outside the canvas: `<PhysicsDebug world={world} extra="..." />`. With `<EngineCanvas debug>`, `<Physics>` also adds a `physics` line to the debug overlay. Neither shows in production builds.

## Gotchas

- Rigid bodies run in JavaScript, and Hermes on the phones has no JIT, so they are expensive. The template's physics skill measured 20 bodies at about 0.5 ms a step, while 200 toppling boxes cost 80 to 100 ms (the game drops to about 12 fps) and even 200 sleeping bodies cost 10 ms. Keep a level under about 60 bodies with 30 awake at once, let things sleep, and remove what left the level.
- Quantity goes into `Debris`: thousands of pieces cost one draw call per `<Debris>`.
- Trimeshes only on static bodies, and only when a box or a hull will not do.
- Dynamic bodies should be direct children of the scene, not nested under moving groups.
- A hinged gate swings only if no level box overlaps its leaf and it hangs a few centimetres above the floor (a leaf resting on the ground is pinned by friction).
- `<Model collides="box">`, `"sphere"` or `"hull"` gives a generated model a body ([Models and scenery](https://docs.moonspell.app/engine/models-and-scenery.md)).
- The world costs nothing when `<Physics>` is not rendered. Games without moving props do not need it.
- For trigger areas that do not need bodies at all (checkpoints, "near the shop"), zones are cheaper ([Utilities](https://docs.moonspell.app/engine/utilities.md)).
