---
title: "Physics"
description: "Physics in Zero has two surfaces, reached in two different ways:"
section: "Topics"
slug: "topics-physics"
canonical: "https://origozero.ai/docs/topics-physics"
updated: "2026-09-04T19:42:52.070930647+00:00"
tags: ["documentation", "guide"]
---

# Physics

- **`Physics`** — a Luau global you call from inside scripts and components: forces, velocity, raycasts, overlaps, sleep control, gravity, joints, wheels, collision groups, and the observation reads that report what the solver holds for a body and why it is not moving it. This is the code surface.
- **`phys`** — a toolbox, not a Luau global. Its verbs compose an entity + model + body + collider in one call, so you can stand up physical objects without writing a script. Drive it from any tool surface: `zero phys` in the shell lists the verbs and `zero phys <verb> --help` gives one verb's arguments, `search_tools` → `use_tool` does the same over MCP, and `tools.use("phys", "<verb>", ...)` does it from Luau. The bare name is not a global — calling `phys.spawnStatic(...)` in a script fails with "attempt to index nil".

Physics simulates during play; in edit mode the world is posed, not running (the engine guide covers modes).

## Making something physical (in code)

A body is `static` (immovable — floors, walls), `dynamic` (moved by forces and gravity), or `kinematic` (moved by your code, pushes dynamics). Build one from the component primitives: give an entity a collider for its shape and a `Physics` component for its body.

```lua
local crate = entity.spawn("crate")
crate.position = { 0, 8, 0 }
crate.component.add("BoxCollider", {})                   -- sized by the entity's mesh and scale
crate.component.add("Physics", { kind = "dynamic" })     -- static | dynamic | kinematic
```

A collider alone (no `Physics`) is a static collider fixed in world space — the right thing for ground and walls:

```lua
local ground = entity.spawn("ground")
ground.localScale = { 40, 1, 40 }
ground.component.add("BoxCollider", {})
```

A dynamic body's mass comes from its collider volume by default; set `Physics.mass` to a value greater than zero to pin the exact total mass instead (a value of zero or below keeps the volume-derived mass).

## Choosing a collider

The shape is the component. Each one carries only the geometry that shape has, so there is nothing to set that means nothing.

| Component | Geometry | Reach for it when |
|-----------|----------|-------------------|
| `BoxCollider` | `half` | Crates, walls, ground, most props |
| `SphereCollider` | `radius` | Balls, blast radii, proximity triggers |
| `CapsuleCollider` | `radius`, `height` | Characters — a rounded bottom slides over small steps |
| `MeshCollider` | `mesh`, `fit` | The shape has to follow the model |
| `CompoundCollider` | `children` | Many boxes sharing one material — a voxel chunk |
| `HeightfieldCollider` | `rows`, `cols`, `extent`, `heights` | Terrain |

Every one also takes `isTrigger`, `friction` and `restitution`.

**Leave a dimension unset and it comes from the entity's mesh.** A bare `BoxCollider` fits what is rendered, at any scale. Set the dimension when you want something other than the mesh:

```lua
entity.find("crate").component.add("BoxCollider", {})                        -- fits the mesh
entity.find("wall").component.add("BoxCollider", { half = { 2, 4, 0.2 } })   -- authored
```

### Mesh colliders and the two fits

`MeshCollider` collides against mesh geometry, and `fit` chooses how that geometry is approximated:

- `"hull"` (the default) wraps the mesh in a convex hull. Cheap, and valid on a dynamic body because a hull has an interior and therefore mass. Concavities fill in: a doorway becomes a slab.
- `"exact"` uses the mesh's own triangles. It follows concavities, and it is for static bodies — a triangle mesh is a surface with no interior, so a dynamic body built from one has no mass and other bodies pass through it. Asking for `"exact"` on a dynamic body raises an error naming `"hull"`.

`mesh` names the geometry. Left unset the collider tracks whatever the entity renders; set it to collide against a cheaper proxy than the one on screen:

```lua
entity.find("rock").component.add("MeshCollider", {})                          -- what you see
entity.find("level").component.add("MeshCollider", { fit = "exact" })          -- concave, static
entity.find("rock").component.add("MeshCollider", { mesh = "rock_collision" }) -- cheap proxy
```

Identical geometry is built once and shared, so a field of a hundred of the same rock pays for one hull.

### Asking whether something has a collider

The shape is the component, so there is no one component name to check for. Ask for the job instead — every collider component declares the `collider` role, including the superseded `Collider`, so this answers correctly for content authored against either surface:

```lua
if e.component.hasRole("collider") then ... end

local c = e.component.byRole("collider")          -- the collider, or nil
for _, c in ipairs(e.component.allByRole("collider")) do ... end
```

`Physics.colliderOn(id)` answers the same question with the component's name, and `Physics.removeCollider(id)` removes whichever collider an entity carries.

### Looking at what the simulation collides with

The simulation collides with colliders, not with what is drawn, and the two part company silently — a collider several times its mesh, one missing from a single tile in a row, a trigger sized as though its numbers were already world-space. None of that shows in a normal render, and reading the numbers off each entity finds only the mistakes you already suspect.

`capture(pass = "physics")` draws the collider set as solid shaded geometry on black, so the collision becomes a picture. Hue is what each collider takes part in — **blue static, orange dynamic, green kinematic, magenta and translucent for a sensor** — and lightness separates the individual objects inside a family, so two neighbouring crates never read as one shape.

```lua
capture { pass = "physics" }          -- the colliders alone
capture { pass = "physics_context" }  -- the colliders over the scene's meshes in dim grey
```

Take `physics` when the question is what the collision actually is: a gap in an otherwise continuous run of blue is the tile something falls through, and two colliders drawn deeply inside each other are bodies the solver will fire apart on the first frame of play. Take `physics_context` when the question is whether a collider sits where its model does — a collider entirely inside a mesh reads as hidden there, which is itself the answer.

`Physics.colliderManifest()` is the same reading as data: every collider in the world with its entity, shape type, role, and whether its geometry is the shape's true surface. `Physics.colliderGeometry(opts)` returns those colliders as drawable triangles for a view of your own. For the state behind the picture — mass, sleep, contacts, and why a body is not moving — see *Asking what the solver holds* below.

### Composing several shapes

Colliders compose. A collider on a child entity attaches to the nearest ancestor carrying a `Physics` component, and each keeps its own material and trigger flag:

```lua
local body = entity.spawn("player")
body.component.add("Physics", { kind = "dynamic" })
body.component.add("CapsuleCollider", { radius = 0.3, height = 1.6 })

local head = entity.spawn("head", { parent = body.id })
head.localPosition = { 0, 1, 0 }
head.component.add("SphereCollider", { radius = 0.2, isTrigger = true })
```

Queries report whichever shape they hit, so the head stays distinguishable from the torso.

Reach for `CompoundCollider` instead when a body carries many children that share one material. Separate components cost one broadphase entry each; a compound is one entry for the whole set, with contacts still resolved per child. That crossover is what makes a compound right for a voxel chunk and wrong for a character's head.

## Driving and querying (in code)

```lua
Physics.applyImpulse(id, { 0, 10, 0 })   -- one-shot push
Physics.setVelocity(id, 0, 5, 0)         -- direct velocity (numeric args), or setVelocity(id, {0,5,0})
Physics.setGravity({ 0, -9.81, 0 })      -- world gravity

local hit = Physics.raycast({ 0, 5, 0 }, { 0, -1, 0 }, 100)   -- origin, direction, maxDist
if hit then print(hit.entityId, hit.distance) end

local near = Physics.overlapSphere({ 0, 0, 0 }, 10)           -- entity ids whose colliders overlap
```

### A ray answers from colliders, and what to ask when the surface has none

Every physics query — `raycast`, `raycastAll`, `raycastScreen`, `boxCast`,
`sphereCast`, `capsuleCast`, `overlapSphere`, `hasLineOfSight` — answers from
the physics world, which holds colliders. A mesh that renders and carries no
collider is not in it, so a ray fired through a ground mesh, a terrain, or
anything a generator cut reports the same `nil` a ray through open air does.

`renderer.raycast` answers the same ray against the geometry the renderer
DRAWS — the surface itself, triangle by triangle, whether or not anything gave
it a body. That is the query for the height of the ground under a camera
station, a prop, a sound source or a scatter.

```lua
local hit = renderer.raycast({ x, 100, z }, { 0, -1, 0 }, 200)
if hit then eye.position = { x, hit.point.y + 1.7, z } end
```

A `nil` from a physics ray means the ray met no COLLIDER.
`Physics.colliderCount()` separates that from a world holding none for it to
meet, in one read off the collider set:

```lua
local hit = Physics.raycast(eye, down, 200)
if hit == nil and Physics.colliderCount() == 0 then
    hit = renderer.raycast(eye, down, 200)   -- nothing here is solid; ask what is drawn
end
```

A body at rest sleeps to save simulation cost. `applyImpulse`, `setVelocity`, and `setAngularVelocity` wake it for you; call `Physics.wakeUp(id)` to wake one yourself, and `Physics.isSleeping(id)` to check.

### Moving a kinematic body

A kinematic body goes where you put it and carries the dynamic bodies it meets. Drive it either way — write its transform each frame, or give it a velocity:

```lua
paddle.position = { 0, 1, z }            -- pose it per frame
Physics.setVelocity(paddle.id, 0, 0, 0.4)  -- or hand it a velocity and let it run
```

While play is running, both are motion: the body travels from where it was to where you put it, and whatever stands in that span is pushed along at the speed the move implies. So a per-frame nudge carries a stack of crates the way a real platform would, and a single write across the level sweeps that whole distance in one step — position a kinematic body before starting play, or in small steps, when you want it somewhere without shoving what is in between.

`Physics.getVelocity(id)` reads back the speed the body is actually travelling at, which is what neighbouring bodies feel.

A velocity is a **world-space** vector at both ends: the body travels along the world axes the vector names — whatever its own rotation, and whatever its parent's — and `getVelocity` answers in the same frame, so `setVelocity(id, v)` followed by `getVelocity(id)` reads back `v`. To drive a body along its own facing, read that facing as a world direction first (`entity(id).transform.forward`) and scale it by the speed you want.

## Asking what the solver holds, and why something is not moving

The `Physics` component carries the numbers content wrote. The simulation carries the numbers it kept, and the two part company whenever a write is refused, clamped, or aimed at a body that was never built — a mass set on a static body, a collider that resolved to nothing, a body type flipped behind the component's back. `Physics.observe` reads the second set.

```lua
local o = Physics.observe()                 -- every body in the world
for _, body in o.bodies do                  -- an array; each entry names its own entity
    print(body.entity, body.bodyType, body.sleeping, body.stillness)
end

local b = Physics.bodyState(id)             -- one body
print(b.bodyType, b.mass, b.gravityScale, b.sleeping, b.inIsland)
```

A body state carries what the solver resolved: body type, mass and inverse mass, centre of mass, principal inertia, gravity scale, linear and angular damping, all six lock flags, CCD, each collider with its shape and collision-group masks, its sleep timers, its velocities, the force and torque queued for the next step, its joints and their motors, and its transform constraints. `Physics.setMass(id, -50)` followed by `Physics.bodyState(id).mass` is how you find out what the solver did with the number.

### Why is it not moving

```lua
local why, detail = Physics.whyStill(id)
-- "resting", "held by 4 touching contact(s), the deepest against ground at 0.0012m of penetration"
```

`whyStill` answers `nil` when the solver IS advancing the body, and otherwise names the nearest cause from a closed set — the thing to change, rather than a consequence of it:

| reason | what it means |
|---|---|
| `noBody` | the entity carries no rigid body; nothing was built for it |
| `simulationNotStepping` | the engine is not running the gameplay simulation — it is paused, or the world is still bootstrapping |
| `disabled` | the body is disabled in the solver |
| `static` | its body type is static, which the solver never integrates |
| `kinematic` | its body type is kinematic and nothing is driving it |
| `infiniteMass` | its inverse mass is zero, so no force accelerates it |
| `translationLocked` | every axis the acceleration reaching it works along is locked — all three, or just the one gravity pulls along |
| `gravityDisabled` | world gravity times its gravity scale is zero, and nothing is pushing it |
| `asleep` | the solver put it to sleep, and none of the three causes above is what settled it |
| `outsideIsland` | awake, but not in the solver's active island set on the last step |
| `resting` | the colliders it touches are holding it up |
| `aboutToMove` | nothing holds it and a net force reaches it — the next step accelerates it |

The three configured causes — `infiniteMass`, `translationLocked`, `gravityDisabled` — are read ahead of `asleep`, because each of them is what puts a body to sleep in the first place and each stays true once it is asleep. So a body that has been sitting still for minutes still answers with the thing to change rather than with the sleep that followed from it.

`Physics.stillnessReasons()` returns that list in the order the engine considers it. A body reading `aboutToMove` on sample after sample is having its velocity or pose written from outside the solver every frame.

### Contacts

```lua
for _, c in Physics.contacts(id) do
    print(c.other, c.touching, c.deepestPenetration, c.totalImpulse)
end

local touching, depth = Physics.touching(crate, ground)
```

`contacts` reports every pair one body's colliders are in, each with the entity on the other side, the normal, how deeply the two interpenetrate, the impulse the last step applied, and the individual contact points. That is the read behind "is the character grounded", "did these two interpenetrate", and "how hard did that land".

### The world, and what a step costs

```lua
local w = Physics.worldState()
print(w.bodies.awake .. "/" .. w.bodies.total, w.contacts.touchingPairs, w.joints.impulse)

local c = Physics.stepCost()
if c then print(c.stepMs, c.narrowPhaseMs, c.solverResolutionMs) end
```

`worldState` counts what the simulation holds — bodies by type, how many are asleep, colliders, joints, contact pairs and points, and the bodies the last step integrated — so a body that failed to build is missing here while its component still exists. `stepCost` breaks the last step into its stages — the same figures `worldState().step` carries. Each is that one step, not a running window, and it is `nil` on a frame where nothing stepped. Consecutive steps over the same resting scene vary by tens of percent, so average several samples before quoting what a step costs.

Two counts read oddly until you know which pool they cover. `bodies.awake` and `bodies.sleeping` split *every* body between them, static ones included — the solver never sleeps a static body, so a scene that has completely settled reads `awake` equal to its static count rather than zero. And `colliders.total` counts more than `bodies.total` whenever the scene holds a collider with no `Physics` component: those are `colliders.standalone`, the static colliders fixed in world space.

Both answer in edit mode as well as play mode, and neither needs a screenshot, a recording, or a probe entity.

## Joints, wheels, groups

Joint motors, wheel colliders, and collision-group filtering live on `Physics` (`Physics.setJointMotor`, `Physics.addWheelCollider`, `Physics.setCollisionGroups`). The `phys` toolbox composes the common constraints (`fixed`, `revolute`, `prismatic`, `spherical`, `spring`, `rope`) from the tool surface.

Rope and chain behavior is a joint, not a script: `kind = "rope"` with `maxDistance` gives a hard maximum-distance limit solved inside the physics step — slack is free, taut resists extension, and an orbiting body on a taut rope keeps its speed. A constraint solved per-frame from Luau reads state one step late and writes one step later; for a steadily rotating configuration that lag feeds energy in every solve, so a hand-rolled rope pumps an orbiting body faster and faster. Reel a rope in or out by writing `maxDistance` on the live joint.

## Breakable joints

A joint given `breakForce` or `breakTorque` releases the moment the reaction it is carrying exceeds either. The reaction is what the constraint solver actually applied over the step that broke it, in real units: a fixed joint holding a hanging 10 kg mass reads 98.1 N, a 20 kg mass 196.2 N, a 40 kg mass 392.4 N.

The two thresholds are independent, and they read two different things. `breakForce` is the joint's linear reaction — the force the constraint applies at its anchor points to hold them together. That is where weight lands, including weight hung off to one side: a 100 kg mass on a 10 m arm reads 981 N of linear reaction and an angular reaction of 0, because the force at the anchor is already what holds the body up and holds its orientation. Its bending moment is that force times the arm, so size a cantilever with `breakForce`.

`breakTorque` is the angular row of the same reaction — the couple the constraint resolves when something twists the two frames apart, which is what a motor or `Physics.applyTorque` drives into a joint. Measured against an applied couple the reading comes back proportional to it rather than equal to it, so calibrate `breakTorque` against a `jointReaction` reading taken under the twist you intend the joint to survive rather than from the torque you apply. A joint given only one threshold can only break that way.

```lua
phys.addJoint("crate", { target = "wall", kind = "fixed", breakForce = 1200, breakTorque = 800 })

Physics.onJointBreak(function(e)
    -- e.entityId, e.connectedEntityId, e.kind
    -- e.force / e.torque — what broke it
    -- e.impulse / e.angularImpulse — the reaction over the breaking step
    -- e.position — where the joint's anchor was
    spawnDebris(e.position, e.force)
end)
```

A joint reports once: the constraint is already released by the time the record arrives, and it stays released until something re-arms it. The `Joint` component carries the same event per joint (`joint:onBreak(fn)`, `joint:isBroken()`, `joint:breakEvent()`) and `joint:reattach()` puts a broken one back — at the anchors it was authored with, so move the bodies back to where those anchors coincide before reattaching. A joint given neither threshold is never measured.

Breaks reach a listener from the tick of an enabled `Joint` component. A joint written straight into the ECS as `ecs.PhysicsJoint` has no such tick behind it: poll `Physics.jointBreaks()` or call `Physics.pumpJointBreaks()` each frame to deliver its breaks.

`Physics.jointBreaks()` is the polling form for a script that would rather read than register; it returns everything that has broken since its own last call.

Size a threshold by measuring rather than guessing: `Physics.jointReaction(id)` reads the load a live joint is carrying right now, against the same quantities the thresholds are compared to. Build the assembly, read the joint at rest, and set the threshold above that — then whatever you drop on it decides. A sleeping body reports the last step that solved it and carries nothing new; `Physics.wakeUp(id)` puts it back under load.

One thing will surprise a threshold set by eye. A joint whose anchors do not line up at the moment it is created is *violated*, and the solver pulls the two bodies together with whatever it takes — a corrective load orders of magnitude above the resting one, which breaks any sane threshold on the first step. Place `localAnchor` and `remoteAnchor` so the two joint frames coincide where the bodies already stand.

Under multiplayer, every peer runs its own simulation, so each peer breaks the joint from the load it computed and no break message crosses the wire. The `Joint` component's `breakForce` and `breakTorque` are synced fields, so peers agree on the threshold; whether they agree on the instant depends on their simulations having stayed in step.

## The full surface

`lsp.methods("Physics")` lists every `Physics` method with its signature; `zero phys` (or `search_tools`) surfaces the `phys` toolbox verbs. The model to hold: `phys` composes from the tool surface, `Physics` controls from code, and simulation happens in play.
