Skip to content

Physics

Physics manipulation on nodes via the Jolt backend. All methods are static on the Physics class. Methods operate on nodes that have physics bodies; they are no-ops if the node has no body.

Physics bodies are created automatically when a .blend is instantiated (if the Blender object has a Rigid Body) — see Resource.loadBlend. Bodies can also be created explicitly with the body builders below — box, sphere, capsule, cylinder, convex hull, triangle mesh, and heightfield. Static bodies are colliders (floors, terrain); dynamic bodies are simulated and take a mass. A body’s surface material (bounce, friction, damping) is tunable at runtime — see Material properties. Configure collision layers and the collision matrix in game.toml.

Physics.addStaticBox(scene, node, hx, hy, hz)

Section titled “Physics.addStaticBox(scene, node, hx, hy, hz)”

Create a static box body (floor, wall, platform). The body position and rotation come from the node.

Parameters:

  • scene (Scene) — The scene whose physics world receives this body.
  • node (Node) — The node to bind the body to.
  • hx, hy, hz (Num) — Box half-extents on each axis.
Physics.addStaticBox(_scene, _floorNode, 10, 0.1, 10)

Physics.addDynamicBox(scene, node, hx, hy, hz, mass)

Section titled “Physics.addDynamicBox(scene, node, hx, hy, hz, mass)”

Create a dynamic box body (falling object, throwable prop).

Parameters:

  • scene (Scene) — The scene whose physics world receives this body.
  • node (Node) — The node to bind the body to.
  • hx, hy, hz (Num) — Box half-extents.
  • mass (Num) — Mass in kg.
Physics.addDynamicBox(_scene, _cubeNode, 0.5, 0.5, 0.5, 1.0)

Physics.addStaticSphere(scene, node, radius) / Physics.addDynamicSphere(scene, node, radius, mass)

Section titled “Physics.addStaticSphere(scene, node, radius) / Physics.addDynamicSphere(scene, node, radius, mass)”

Create a sphere body — the natural shape for a ball that rolls and bounces.

Parameters:

  • scene (Scene), node (Node) — as above.
  • radius (Num) — Sphere radius.
  • mass (Num, dynamic only) — Mass in kg.
Physics.addDynamicSphere(_scene, _ballNode, 0.3, 0.05)

Physics.addStaticCapsule(scene, node, halfHeight, radius) / Physics.addDynamicCapsule(scene, node, halfHeight, radius, mass)

Section titled “Physics.addStaticCapsule(scene, node, halfHeight, radius) / Physics.addDynamicCapsule(scene, node, halfHeight, radius, mass)”

Create a capsule body (a cylinder with hemispherical caps, standing along Y).

Parameters:

  • scene (Scene), node (Node) — as above.
  • halfHeight (Num) — Half the cylindrical section; the total height is 2*halfHeight + 2*radius.
  • radius (Num) — Cap/cylinder radius.
  • mass (Num, dynamic only) — Mass in kg.
Physics.addDynamicCapsule(_scene, _charNode, 0.5, 0.3, 1.0)

Physics.addStaticCylinder(scene, node, halfHeight, radius) / Physics.addDynamicCylinder(scene, node, halfHeight, radius, mass)

Section titled “Physics.addStaticCylinder(scene, node, halfHeight, radius) / Physics.addDynamicCylinder(scene, node, halfHeight, radius, mass)”

Create a cylinder body (standing along Y).

Parameters:

  • scene (Scene), node (Node) — as above.
  • halfHeight (Num) — Half the total height.
  • radius (Num) — Radius.
  • mass (Num, dynamic only) — Mass in kg.
Physics.addDynamicCylinder(_scene, _drumNode, 0.5, 0.4, 2.0)

Physics.addStaticConvexHull(scene, node, vertices) / Physics.addDynamicConvexHull(scene, node, vertices, mass)

Section titled “Physics.addStaticConvexHull(scene, node, vertices) / Physics.addDynamicConvexHull(scene, node, vertices, mass)”

Create a convex hull body from a point cloud — a solid convex volume wrapping the points.

Parameters:

  • scene (Scene), node (Node) — as above.
  • vertices (List) — A flat list of point coordinates [x, y, z, x, y, z, …], at least 4 points.
  • mass (Num, dynamic only) — Mass in kg.

Malformed vertex data (not a list of numbers, a length that is not a multiple of 3, or fewer than 4 points) aborts the fiber with a diagnostic.

Physics.addDynamicConvexHull(_scene, _gemNode, [-0.5, 0, -0.5, 0.5, 0, -0.5, 0, 0, 0.5, 0, 1, 0], 1.0)

Physics.addStaticMesh(scene, node, vertices, indices)

Section titled “Physics.addStaticMesh(scene, node, vertices, indices)”

Create a static concave triangle-mesh collider for arbitrary terrain — curved, U-shaped, S-shaped, or sloped courses that a box can’t approximate. Static only (Jolt mesh shapes cannot be dynamic).

Parameters:

  • scene (Scene), node (Node) — as above.
  • vertices (List) — A flat list of vertex coordinates [x, y, z, …].
  • indices (List) — A flat list of triangle indices into vertices (three per triangle).

Winding matters: Jolt mesh shapes are one-sided — they collide from the triangle-normal side only. Wind triangles so their normals face the side objects collide from (e.g. counter-clockwise seen from above, for ground). A back-face-only mesh silently lets bodies pass through.

Malformed geometry (non-number lists, a vertex length not a multiple of 3, an index count not a whole number of triangles, or any index >= vertexCount) aborts the fiber with a diagnostic.

// A flat quad floor at y=0, wound so its normals point up.
Physics.addStaticMesh(_scene, _terrainNode,
[-5, 0, -5, 5, 0, -5, 5, 0, 5, -5, 0, 5],
[0, 2, 1, 0, 3, 2])

Physics.addStaticHeightfield(scene, node, heights, sampleCount, cellSize)

Section titled “Physics.addStaticHeightfield(scene, node, heights, sampleCount, cellSize)”

Create a static heightfield terrain collider from a regular grid of heights — the efficient path for large terrain (vs addStaticMesh). Static only.

Parameters:

  • scene (Scene), node (Node) — as above.
  • heights (List) — A flat, row-major list of sampleCount * sampleCount heights: heights[z*sampleCount + x].
  • sampleCount (Num) — The grid is square sampleCount × sampleCount; sampleCount must be an even integer ≥ 4 (a Jolt block-size constraint).
  • cellSize (Num) — Horizontal spacing between samples.

Heights are absolute world-Y. The grid’s (x=0, z=0) sample sits at the node origin and extends +X/+Z by (sampleCount-1) * cellSize; the node transform positions the whole field. Rectangular, holed, or overhanging terrain must use addStaticMesh instead.

Malformed input (odd or < 4 sampleCount, cellSize <= 0, or a heights length != sampleCount²) aborts the fiber with a diagnostic.

// An 8x8 bowl: low in the centre, rising toward the edges.
var n = 8
var heights = []
for (z in 0...n) {
for (x in 0...n) {
var dx = x - 3.5
var dz = z - 3.5
heights.add((dx * dx + dz * dz) * 0.2)
}
}
Physics.addStaticHeightfield(_scene, _terrainNode, heights, n, 1)

A body’s surface material is tunable at runtime. Defaults (unchanged when unset): friction 0.5, restitution 0.0 (no bounce), linear/angular damping 0.05.

Set the body’s restitution (bounce), 0..1. 0 = no bounce (default); 0.9 = very bouncy.

Physics.setRestitution(_ballNode, 0.6) // a lively golf ball

Set the body’s Coulomb friction coefficient (>= 0, default 0.5). Higher = grips / slides less.

Physics.setFriction(_ballNode, 0.4)

Set the body’s linear and angular velocity damping (>= 0, default 0.05). Angular damping approximates rolling resistance — Jolt has no dedicated rolling-friction scalar, so raise angular damping to shorten a ball’s roll-out.

Physics.setDamping(_ballNode, 0.05, 0.4) // rolls, but slows to a stop

Forces are applied continuously (call each frame). Impulses are instantaneous.

Apply a force at the center of mass. Call every frame for continuous acceleration.

Physics.addForceAtPoint(node, fx, fy, fz, px, py, pz)

Section titled “Physics.addForceAtPoint(node, fx, fy, fz, px, py, pz)”

Apply a force at a world-space point. Off-center forces create torque.

Apply an instantaneous impulse at the center of mass (single frame, no need to call each frame).

Physics.addImpulseAtPoint(node, ix, iy, iz, px, py, pz)

Section titled “Physics.addImpulseAtPoint(node, ix, iy, iz, px, py, pz)”

Apply an instantaneous impulse at a world-space point.

Apply angular force (torque) to the body.

// Kick a cube upward on Space
if (Input.keyJustPressed("space")) {
Physics.addImpulse(_cubeNode, 0, 10, 0)
}
// Continuous upward fan force
Physics.addForce(_fanNode, 0, 50, 0)

Physics.setPosition(node, x, y, z) / Physics.setRotation(node, pitch, yaw, roll)

Section titled “Physics.setPosition(node, x, y, z) / Physics.setRotation(node, pitch, yaw, roll)”

Teleport a body to a new position or rotation. Updates both the physics body and the node. Use for resets or warp-to-point.

// Reset cube on R
Physics.setPosition(_cubeNode, 0, 5, 0)
Physics.setLinearVelocity(_cubeNode, 0, 0, 0)
Physics.setAngularVelocity(_cubeNode, 0, 0, 0)
Physics.activate(_cubeNode)

Physics.setLinearVelocity(node, vx, vy, vz) / Physics.getLinearVelocity(node)

Section titled “Physics.setLinearVelocity(node, vx, vy, vz) / Physics.getLinearVelocity(node)”

Set or get the linear (translation) velocity. getLinearVelocity returns [vx, vy, vz].

Physics.setAngularVelocity(node, wx, wy, wz) / Physics.getAngularVelocity(node)

Section titled “Physics.setAngularVelocity(node, wx, wy, wz) / Physics.getAngularVelocity(node)”

Set or get the angular (rotation) velocity. getAngularVelocity returns [wx, wy, wz].

Physics.setGravity(scene, gx, gy, gz) / Physics.getGravity(scene)

Section titled “Physics.setGravity(scene, gx, gy, gz) / Physics.getGravity(scene)”

Set or get the gravity vector for the scene’s physics world. Default is (0, -9.81, 0). getGravity returns [gx, gy, gz].

Physics.setGravity(_scene, 0, -20, 0) // Stronger gravity
Physics.setGravity(_scene, 0, 0, 0) // Zero-G

Physics.setSimulationPaused(scene, paused) / Physics.isSimulationPaused(scene)

Section titled “Physics.setSimulationPaused(scene, paused) / Physics.isSimulationPaused(scene)”

Pause or resume the physics step for a scene. While paused, no bodies move but transforms still sync.

if (Input.keyJustPressed("p")) {
var paused = Physics.isSimulationPaused(_scene)
Physics.setSimulationPaused(_scene, !paused)
}

Physics.step(scene, dt) / Physics.stepN(scene, dt, n)

Section titled “Physics.step(scene, dt) / Physics.stepN(scene, dt, n)”

Advance a scene’s physics manually, decoupled from the game loop: step runs exactly one step of dt and then syncs transforms to nodes; stepN runs n such steps (identical to n separate step calls). One step is precisely what one iteration of the loop’s fixed-step accumulator does.

This is what makes headless physics possible. A plume3d --test run never calls the game loop, so nothing auto-steps physics — Physics.step is the only way to advance a simulation there, which lets a test resolve an outcome and assert it deterministically instead of capturing a windowed screenshot.

Parameters:

  • scene (Scene) — Scene to advance. Its auto-step must be paused.
  • dt (Num) — Step size in seconds. Must be finite, > 0, and <= 0.1 (the engine’s stability bound — the accumulator never hands Jolt a larger step). To advance further, use stepN rather than a bigger dt.
  • n (Num) — Step count for stepN. A whole number >= 0; 0 is a legal no-op.

Malformed input (an unpaused scene, an out-of-range dt, a fractional n) aborts the fiber with a diagnostic rather than failing quietly.

Contact callbacks do not fire during a manual step. onContactBegin/onContactEnd/onContactStay invoke Wren, which is not permitted from inside a foreign method; events raised during manual steps stay queued for the next auto-stepped frame if the scene is later unpaused, up to the queue cap — a long enough manual run drops the excess. Poll state instead — Physics.isActive(node) and node positions — which is what “step to rest” needs anyway.

Determinism. A fixed dt step is the same operation the accumulator already performs, and Jolt runs on a single-threaded job system here, so an identical initial state stepped by an identical sequence lands on a bitwise-identical result on the same build and machine. That is what a headless gate can assert. Reproducibility across platforms or builds is not claimed — floating-point differences make that a separate problem.

// Resolve a shot by stepping to true rest, not to a wall-clock guess.
Physics.setSimulationPaused(_scene, true)
Physics.setLinearVelocity(_ball, 5, 1.5, 2)
var steps = 0
while (Physics.isActive(_ball) && steps < 3000) {
Physics.step(_scene, Physics.fixedDeltaTime)
steps = steps + 1
}
var restingPosition = _ball.getPosition()

Returns: Num — The game loop’s fixed timestep (1/60).

Use this as dt to step exactly as the loop would, rather than hardcoding 1/60 — a hardcoded value would silently diverge from the engine if the fixed step were ever retuned.

Returns: Num — The total number of physics bodies in the scene’s world — static and dynamic, awake and asleep.

This is a census, not an activity signal: a world that has fully settled reports the same count as one mid-collision, and a floor collider counts the same as a tumbling crate. To ask whether anything is still moving, poll Physics.isActive per body — which is what the step-to-rest loop above does. (Jolt tracks an active-body count internally, but it is not exposed to Wren.)

Physics.activate(node) / Physics.deactivate(node) / Physics.isActive(node)

Section titled “Physics.activate(node) / Physics.deactivate(node) / Physics.isActive(node)”

Wake up or put to sleep a physics body. Dynamic bodies that come to rest are automatically deactivated by Jolt; use activate after a teleport or reset to ensure they simulate.

These use the physics world (tests against Jolt broadphase, hits on physics-body nodes only). For raycasts against scene mesh geometry, see Raycast.

Physics.raycast(scene, ox, oy, oz, dx, dy, dz)

Section titled “Physics.raycast(scene, ox, oy, oz, dx, dy, dz)”

Returns: RaycastHit or null — Closest physics body hit.

Parameters:

  • scene (Scene) — Scene whose physics world to test.
  • ox, oy, oz (Num) — Ray origin.
  • dx, dy, dz (Num) — Ray direction. Normalise this — the magnitude is not ignored, it scales the ray (see the caution below).

Physics.raycastMaxDist(scene, ox, oy, oz, dx, dy, dz, maxDist)

Section titled “Physics.raycastMaxDist(scene, ox, oy, oz, dx, dy, dz, maxDist)”

Same as raycast but limited to maxDist units.

var hit = Physics.raycast(_scene, 0, 10, 0, 0, -1, 0)
if (hit != null) {
Logger.info("Hit: %(hit.getNodeName()) at %(hit.getDistance())")
}

A gameplay collision layer is a bitmask you tag a body with to filter queries — “which enemies are near me”, “cast line-of-sight against walls only”. It is separate from physical collision (it does not change what bodies bump into): it only affects overlapSphere and masked raycasts. A query passes a mask; a body matches when bodyLayer & mask != 0. A mask of 0 (or 0xFFFFFFFF) means any layer. This is the sensory primitive behind enemy AI (perception, aggro), gameplay pings, and area effects.

Physics.setCollisionLayer(node, mask) / Physics.getCollisionLayer(node)

Section titled “Physics.setCollisionLayer(node, mask) / Physics.getCollisionLayer(node)”

Set or read a body’s layer bitmask. Assign your own bit conventions, e.g. 1 = players, 2 = enemies, 4 = walls.

Physics.setCollisionLayer(_enemyNode, 2) // this body is on the "enemies" layer (bit 1)

Physics.overlapSphere(scene, x, y, z, radius, mask)

Section titled “Physics.overlapSphere(scene, x, y, z, radius, mask)”

Returns: List of nodes whose body center is within radius of (x, y, z) and whose layer intersects mask.

The “who is near me” query. Center-distance semantics (cheap: broadphase candidates + exact refine). Bodies with no bound scene node are omitted.

// Every enemy (layer bit 1 → mask 2) within 8 units of the player.
for (enemy in Physics.overlapSphere(_scene, px, py, pz, 8, 2)) {
// ... react to enemy ...
}

Physics.raycast with a layer mask — Raycast.fromPointMasked(scene, ox, oy, oz, dx, dy, dz, maxDist, mask)

Section titled “Physics.raycast with a layer mask — Raycast.fromPointMasked(scene, ox, oy, oz, dx, dy, dz, maxDist, mask)”

Returns: RaycastHit or null — the closest hit whose body’s layer intersects mask.

The layer-filtered line-of-sight primitive: cast against just the obstruction layer so other enemies don’t block sight. (Physics.raycast / Physics.raycastMaxDist above are the unmasked forms.)

// Line of sight from eye to target, blocked only by walls (layer bit 2 → mask 4):
var dx = tx - ex; var dy = ty - ey; var dz = tz - ez
var blocked = Raycast.fromPointMasked(_scene, ex, ey, ez, dx, dy, dz, 1000, 4)
var canSee = (blocked == null)

Swept / at-pose / piercing narrow-phase queries over the scene’s rigid bodies (PLM-196, ADR 0066) — the same core the combat hitbox resolver uses, exposed for scripted attacks. Each takes a Shape and returns a List of RaycastHit (near→far), mapping each hit body back to its Node. mask 0/0xFFFFFFFF = any gameplay layer.

Physics.castShape(scene, shape, fx, fy, fz, tx, ty, tz, mask)

Section titled “Physics.castShape(scene, shape, fx, fy, fz, tx, ty, tz, mask)”

Returns: List of RaycastHit — every body a shape sweep from (fx,fy,fz) to (tx,ty,tz) crosses. The anti-tunnel primitive for a fast attack.

Physics.overlapShape(scene, shape, x, y, z, qx, qy, qz, qw, mask)

Section titled “Physics.overlapShape(scene, shape, x, y, z, qx, qy, qz, qw, mask)”

Returns: List of RaycastHit — every body a shape overlaps at a pose (position + rotation quaternion x,y,z,w). Catches a large body whose center is outside the shape, unlike overlapSphere.

Physics.raycastAll(scene, ox, oy, oz, dx, dy, dz, maxDist, mask)

Section titled “Physics.raycastAll(scene, ox, oy, oz, dx, dy, dz, maxDist, mask)”

Returns: List of RaycastHit — every body a ray from (ox,oy,oz) along (dx,dy,dz) up to maxDist pierces. The piercing-hitscan primitive.

Physics.onContactBegin(scene, callback) / Physics.onContactEnd(scene, callback)

Section titled “Physics.onContactBegin(scene, callback) / Physics.onContactEnd(scene, callback)”

Register a callback fired when two physics bodies begin or end contact. Pass null to clear.

Callback signature: Fn.new { |contact| ... } — the callback takes exactly one argument, a PhysicsContact carrying both bodies and the manifold. The count is checked when you register: a callback with any other number of parameters is rejected with a logged error and never fired, so a stale multi-argument callback fails loudly rather than silently receiving garbage.

Physics.onContactBegin(_scene, Fn.new { |contact|
if (contact.getNodeA() == "Player" || contact.getNodeB() == "Player") {
Logger.info("Player touched %(contact.getNodeA())/%(contact.getNodeB())")
}
})

onContactEnd fires with hasManifold() == false and a zeroed point/normal — an ending contact has no manifold — so test hasManifold() before trusting the point.

The single object handed to every contact callback. All accessors are methods:

MethodReturnsMeaning
getNodeA() / getNodeB()StringName of each node ("" if the body has no bound node). Names are not unique.
getNodeAId() / getNodeBId()NumNumeric node id, unique per node; 0 when the body has no bound node.
getPointX/Y/Z()NumContact point in world space (zeroed when hasManifold() is false).
getNormalX/Y/Z()NumContact normal (zeroed when hasManifold() is false).
hasManifold()Booltrue for begin/stay, false for end. Distinguishes “no manifold” from a real contact at the origin.

The contact reports names and a numeric id, not a Node object. There is no built-in lookup from either back to a live node — Scene.findNodeById matches a node’s blend node_id string, a different field from both the name and the numeric id, and it is empty for nodes created in script. So if you need the real node — to move it, query it, despawn it — keep your own map as you create the bodies. Prefer keying it by the numeric id, which is unique, over the name, which is not:

_bodies = {} // node id -> Node
var crate = _scene.addNode("Crate_01")
_bodies[crate.id] = crate
Physics.addDynamicBox(_scene, crate, 0.5, 0.5, 0.5, 1.0)
Physics.onContactBegin(_scene, Fn.new { |contact|
var node = _bodies[contact.getNodeAId()] // null if the contact is not one of ours
if (node != null) {
Physics.addImpulse(node, 0, 2, 0)
}
})

Register a callback fired for each pair still touching, once per physics step, for as long as the contact lasts. Pass null to clear. Same one-argument PhysicsContact callback as onContactBegin, with the live contact point and normal (hasManifold() is true).

Use it for ongoing-contact state — standing on a surface, a sustained push, continuous damage — where onContactBegin/onContactEnd only tell you about the edges.

This is a firehose. It fires every step for every touching pair: 50 resting contacts at 60 fps is 3,000 callbacks a second, each one a Wren call. Prefer the edge callbacks and track state yourself unless you genuinely need per-step updates. The queue backing this callback only runs while a callback is registered, so an app that never registers one pays nothing for it.

// Track whether the player is standing on something this frame.
Physics.onContactStay(_scene, Fn.new { |contact|
// A contact normal pointing up means it is underfoot.
if (contact.getNodeA() == "Player" && contact.getNormalY() > 0.7) {
_playerGrounded = true
}
})

Sleeping bodies generate no contacts, so a body at rest long enough to fall asleep stops firing this callback until something wakes it.

Contact events are queued as physics runs and handed to your callbacks each frame. Each queue holds at most 4096 events; beyond that, further events are dropped rather than growing memory without bound. Dropping only happens when events are produced faster than they are delivered — in normal frame-by-frame play the queues drain every frame and never come close to the cap.

The case that can reach it is a long manual-stepping loop (see Manual stepping): contacts raised across thousands of manual steps queue up with no frame in between to drain them, so a long step-to-rest loop over colliding bodies can exceed the cap and drop the excess. If you need every event from a long manual run, unpause periodically and let a frame drain the queue.

A callback registered late does not receive a backlog. The queues are emptied every frame whether or not anything is listening, so registering onContactBegin partway through a game delivers contacts from that point on — not everything that happened while no callback was set. Register in init() if you need a contact from the very first frame.


See Scene for createCharacterController and createParticleEmitter. See Configuration for collision layer and collision matrix setup. See Raycast for mesh-level raycasts.