Physics

Spatial queries

Ask the physics world questions without stepping it - raycasts, shape casts, and overlap tests against the live world.

Three read-only queries let you interrogate the physics world between steps. None of them mutate the simulation, and all three run against the same spatial index the solver uses - one for static geometry, one for moving bodies - so they see exactly the world the simulation sees, and scale the same way.

Each takes an optional filter(entity, collider) => boolean, consulted before the expensive exact-shape test. Use it to skip the caller’s own body, allies, or whole layers.

Raycasts

raycast finds the nearest body a ray hits. The ray is origin + unit direction + tMax.

import { Ray3 }               from "@woosh/meep-engine/src/core/geom/3d/ray/Ray3.js";
import { PhysicsSurfacePoint } from "@woosh/meep-engine/src/engine/physics/queries/PhysicsSurfacePoint.js";

const ray = Ray3.from(0, 50, 0,  0, -1, 0);   // origin, unit direction, optional tMax
const hit = new PhysicsSurfacePoint();
if (physics.raycast(ray, hit)) {
    // hit.position, hit.normal, hit.t, hit.entity, hit.body_id
}

PhysicsSurfacePoint is an output parameter: pre-allocate one (or pull it from a frame pool) and pass it in. On a hit the query writes position (world-space point), normal, t (distance along the ray), entity and body_id - the packed index << 8 | generation, which goes stale once the body is unlinked, so keep entity for anything that crosses a frame. On a miss the fields are left untouched, so branch on the return value rather than on the contents.

Bullets-as-rays, line-of-sight checks, mouse picking, ground probes.

hit.position and hit.normal are three-element Float64Arrays. Read x, y and z as [0], [1], [2], for example hit.normal[1] for the upward component. They have no .x / .y / .z accessors or vector methods.

Rays pass through sensors. A trigger volume is not a solid surface, so a body or collider flagged IsSensor is skipped and the ray lands on the nearest solid surface behind it - you don’t need a filter for it. To find trigger volumes, use an overlap query instead; overlap and shapeCast still report sensors, and it’s the filter’s job to exclude them there if you don’t want them.

Normals are exact, with one caveat

Each candidate the broadphase turns up is refined against its true shape geometry, so t is the exact surface distance and normal the exact surface normal for every shape the engine defines: the convex primitives (sphere, box, capsule, cylinder, point), the convex hull, the concave mesh and heightmap, and the wrappers, which recurse into what they wrap. A leaf whose ray crosses the broadphase box but misses the true shape contributes nothing.

The one exception is a shape class defined outside the engine, which the refinement dispatch does not recognise. That leaf falls back to the shape’s tight world AABB hit plus an AABB-face normal. The fallback answers with the bounding box, so a ray crossing the box but missing the shape reports a phantom hit - it is a floor, not a contract. It also divides by the box extent per axis, so a shape whose AABB is flat on one is resolved through NaN comparisons rather than geometry.

A body with several colliders resolves its primary (first-attached) one: the broadphase leaf encodes only the body id.

Shape casts

shapeCast is a raycast with volume: it sweeps a convex shape along a ray and returns the first body it would hit. The shape starts at ray.origin oriented by rotation, translates along ray.direction for up to ray.tMax, and the result is the nearest time-of-impact.

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

const probe    = CapsuleShape3D.from(0.4, 1.2);
const identity = new Float64Array([0, 0, 0, 1]);   // poses are read as [x, y, z, w]
const ray      = Ray3.from(px, py, pz,  dx, dy, dz,  maxDistance);  // unit direction
const hit      = new PhysicsSurfacePoint();

if (physics.shapeCast(ray, probe, identity, hit, (entity) => entity !== self)) {
    // first contact within maxDistance - hit.position, hit.normal, hit.t
}

Internally it quickly gathers the bodies the sweep could reach, then computes the exact moment of contact for each and keeps the nearest, stopping early once nothing closer is possible. Unlike raycast this resolves against the real shapes throughout, so t is a true time of impact. This is the query behind character controllers (“how far can I step before I hit something?”), volumetric projectiles, and camera-collision that won’t clip through walls.

A shape already touching a target at t = 0 is not a hit. Contact depth at or below a shared tolerance (1e-4, the same CONTACT_EPSILON the narrowphase’s compute_penetration uses) counts as tangency rather than overlap - a kissing pair is not always numerically exact, and MPR reports a few microns of “overlap” for a sphere resting on a plane where a box-on-box kiss returns a clean zero. A tangency blocks only a sweep driving into it (more than about 0.006° off parallel); otherwise it steps aside so the cast can reach real blockers behind it. Without that, a character standing on the floor and casting sideways against a wall would fall through the floor.

Genuine overlap at t = 0 is still reported as a hit at t = 0: targets are solid and the sweep is blocked where it stands. Either way, one contact at the start of the sweep never blinds the cast to the rest of it.

Overlap queries

overlap is speculative: given a convex shape at a world pose, it reports every body that shape would touch - without moving anything. It’s the “would I collide if I stood here?” test, the foundation of kinematic and character controllers.

It writes the overlapping bodies’ packed body ids into a buffer you size, returning the count. Map an id back to its entity with physics.entityOf(id).

Both queries read the pose by index - position[0..2], rotation[0..3] - rather than as {x, y, z} objects, which is what lets a Transform64’s own translation and rotation views go straight in: physics.overlap(probe, t.translation, t.rotation, ids, 0) queries exactly the pose the entity is drawn at. A Vector3 works too, since it is a Float64Array; a plain {x, y, z} object does not.

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

const probe = BoxShape3D.from_size(2, 2, 2);
const ids   = new Uint32Array(64);            // caller sizes the buffer

const count = physics.overlap(
    probe,
    [0, 1, 0],                     // world position, read as [x, y, z]
    [0, 0, 0, 1],                  // world rotation, read as [x, y, z, w]
    ids, 0,                        // output buffer + start offset
    (entity) => entity !== self,   // skip ourselves
);

for (let i = 0; i < count; i++) {
    const entity = physics.entityOf(ids[i]);
    // react to the overlap…
}

Ids past the end of the buffer are dropped silently and the count caps at the space available - size the buffer for the most you care to handle. The query shape must be convex; concave shapes (heightmaps, meshes) throw, since they’re terrain, not probes.

Explosion-radius damage, build-placement validity, hand-placed trigger volumes, “is this spawn point clear?” checks.

Where to go next

  • Colliders & shapes - the shapes these queries take, and the layer/mask groups the filter complements.
  • Rigid bodies - driving a kinematic controller from shapeCast / overlap results.
  • Math & geometry - the closed-form ray primitives each candidate is refined against.
  • Spatial acceleration - the BVH both broadphase trees are built from.