Queries
Four query objects, all created from the physics system and all following the same shape: set up
a caster, cast() it, read hits.
| what it answers | |
|---|---|
Raycaster | what does this ray hit |
useMouseRaycaster | what is under the pointer |
Multicaster | what do these many rays hit, in one call |
AdvancedRaycaster | same, with your own per-body / per-hit filtering |
Shapecaster | what does this shape hit as it sweeps along a direction |
ShapeCollider | what is this shape overlapping right now (Jolt's CollideShape) |
Every query object owns Jolt (WASM) memory and must be destroyed. The useRaycaster /
useMulticaster hooks do it for you on unmount. Anything you get from physicsSystem.get*()
yourself is yours to destroy() — see Lifetime.
Raycasting
import { Raycaster, RaycastHit, useRaycaster } from '@react-three/jolt';
import { useEffect } from 'react';
export function Laser() {
const raycaster: Raycaster = useRaycaster([0, 5, 0], [0, -10, 0]);
useEffect(() => {
raycaster.cast(
(hit: RaycastHit) => {
console.log(hit.position, hit.distance, hit.impactNormal, hit.bodyHandle);
},
() => console.log('no hit')
);
}, [raycaster]);
return null;
}
useRaycaster(origin?, direction?, type?) creates a caster, applies the arguments, and destroys
it when the component unmounts. direction is not normalised — its length is the length of
the ray, exactly like THREE.Raycaster's far.
Casting
| call | |
|---|---|
cast(onHit?, onMiss?) | cast with the current origin/direction |
castFrom(origin, onHit?, onMiss?) | move the origin, keep the direction |
castTo(destination, onHit?, onMiss?) | keep the origin, aim at a point |
castBetween(origin, destination, onHit?, onMiss?) | both |
set(origin, direction) | just update, don't cast |
cast() also returns the hit (or the array of hits in 'all' mode), so the handlers are
optional.
Collectors
setCollector(type), or the third argument to useRaycaster, chooses what Jolt collects:
| type | |
|---|---|
'closest' | the nearest hit (default) |
'any' | the first hit found — cheapest, use for line-of-sight tests |
'all' | every hit along the ray; hits is the full array |
cullBackFaces (default true) skips triangles facing away from the ray.
RaycastHit
| member | |
|---|---|
position | world-space hit point (THREE.Vector3) |
start / end | the ray, as cast |
distance | from start to position |
normal / direction | the ray's own normalised direction |
impactNormal | the surface normal at the hit — this is the one you usually want |
bodyHandle | pass to bodySystem.getBody() |
shapeIdValue | sub-shape id within a compound |
index | position in the hit list |
useMouseRaycaster
The physics answer to THREE.Raycaster.setFromCamera(): it builds a world-space ray from the
pointer and the camera and casts it against Jolt bodies instead of the three.js scene graph.
import { useMouseRaycaster } from '@react-three/jolt';
import { useFrame } from '@react-three/fiber';
export function Picker() {
const { hit, raycaster } = useMouseRaycaster({
onHit: (h) => {
if (!Array.isArray(h) && h) console.log(h.bodyHandle);
}
});
useFrame(() => {
const current = hit.current;
if (current && !Array.isArray(current)) highlight(current.bodyHandle);
});
raycaster.isDebugging = false;
return null;
}
| option | default | |
|---|---|---|
mode | 'frame' | rebuild the ray every rendered frame, so it stays right when only the camera moves. 'pointermove' rebuilds it only on real pointer moves |
type | 'closest' | collector, as above |
length | camera.far | ray length |
onHit | — | called with the latest hit (or undefined on a miss) every cast |
filter | — | swap in your own bpFilter / objectFilter / bodyFilter / shapeFilter |
It returns { raycaster, hit }, where hit is a mutable ref updated in place — read
hit.current from useFrame, an event handler or onHit. It is deliberately not React state: a
fast-moving pointer would otherwise re-render every frame.
Passing a filter transfers ownership of it to the raycaster this hook creates: it replaces
(and frees) the default filter, and will free yours on the next filter change or on unmount.
Don't share one filter instance across several of these. filter participates in the hook's
dependencies by identity, so memoise it.
Unlike its siblings, this hook destroys its raycaster when type or filter change as well
as on unmount — see Lifetime.
Multicaster
One raycaster, many origins or many origin/destination pairs — sweeps, foot probes, spread patterns:
import { useMulticaster } from '@react-three/jolt';
import { useEffect } from 'react';
import * as THREE from 'three';
export function Sweep() {
const multicaster = useMulticaster();
useEffect(() => {
multicaster.positions = [new THREE.Vector3(-5, 5, 0), new THREE.Vector3(5, 5, 0)];
multicaster.direction = new THREE.Vector3(0, -10, 0);
multicaster.cast();
}, [multicaster]);
return null;
}
cast() walks positions with the shared direction; castRays() walks rays
({ origin, destination }[]). Both fill hits (flat) and results (grouped per ray), and both
clear them first.
AdvancedRaycaster
useAdvancedRaycaster() exposes Jolt's collector callbacks so you can filter or bail out during
the cast:
import type Jolt from 'jolt-physics';
const caster = useAdvancedRaycaster();
caster.onBody((body: Jolt.Body) => {
/* inspect each body the ray reaches */
});
caster.addHit((result: Jolt.RayCastResult) => {
/* return truthy to shorten the cast to this hit */
});
caster.onReset(() => {
/* also resets the collector's early-out fraction */
});
The callbacks hand you raw Jolt objects the collector owns. Read what you need and return —
do not keep them, and never destroy() them. They are declared loosely, so annotate the
parameters yourself as above.
cast() resets the collector before casting (resetting after would wipe the hits it just
collected). In 'all' mode the success handler is called once per hit.
Shapecasting
A shapecast sweeps a whole shape along a direction — "will this capsule fit through there", a thick ground probe, a camera collision test. There is no hook yet; take one from the physics system and destroy it yourself.
import { Shapecaster, ShapecastHit, useJolt } from '@react-three/jolt';
import { useEffect } from 'react';
import * as THREE from 'three';
export function GroundProbe() {
const { physicsSystem } = useJolt();
const shapecaster: Shapecaster = physicsSystem.getShapecaster();
useEffect(() => () => shapecaster.destroy(), [shapecaster]);
useEffect(() => {
shapecaster.origin = new THREE.Vector3(0, 5, 0);
shapecaster.direction = new THREE.Vector3(0, -6, 0);
shapecaster.cast((hit: ShapecastHit) => {
console.log(hit.position, hit.distance, hit.bodyHandle);
});
}, [shapecaster]);
return null;
}
It casts a 0.5-radius sphere by default. shape takes any Jolt.Shape (build one with
createShapeFromSettings); origin, rotation, scale
and direction accept three.js values and rebuild the underlying cast for you.
ignoreBackfaceTriangles and ignoreBackfaceConvex (both true) control back-face handling,
and setCollector('closest' | 'any' | 'all') works as it does for rays.
ShapecastHit carries position, start, end, distance, normal/direction,
impactNormal, bodyHandle, shapeIdValue and index — the same surface as RaycastHit.
ShapeCollider
CollideShape asks a different question: not "what will I hit if I move", but "what am I
overlapping right now, and by how much". This is what you want for triggers, spawn checks and
push-out resolution.
import { ShapeCollider, useJolt } from '@react-three/jolt';
import { useEffect } from 'react';
import * as THREE from 'three';
export function Overlap() {
const { physicsSystem } = useJolt();
const collider: ShapeCollider = physicsSystem.getShapeCollider();
useEffect(() => () => collider.destroy(), [collider]);
useEffect(() => {
collider.position = new THREE.Vector3(0, 1, 0);
collider.setCollector('all');
collider.cast((hits: typeof collider.hits) => {
hits.forEach((hit) => console.log(hit.penetrationDepth, hit.contactNormal));
});
}, [collider]);
return null;
}
Set position, rotation or matrix and the world transform is rebuilt in place — no
allocation per frame, which is what makes it safe to drive from useFrame (the camera rig does).
cast() returns the single hit when there is exactly one, the hits array when there are more,
and false on a miss.
Shape ownership
collider.shape is reference counted, not owned outright:
const shape = generateShape({ type: 'box', size: [1, 1, 1] });
collider.shape = shape; // the collider takes its own reference (AddRef)
releaseShape(shape); // you can drop yours immediately; the collider still holds one
Assigning a new shape AddRefs it and Releases the previous one; destroy() releases the
current one rather than hard-destroying it. So a caller that also holds — and later frees — a
reference to the same shape never ends up with a dangling pointer. Assigning the shape it already
has is a no-op.
Shapecaster.shape does not do this: it writes the shape straight into the cast, so keep your
own reference alive for as long as the shapecaster uses it.
CollisionResult
| member | |
|---|---|
contactPointOn1 / contactPointOn2 | world-space contact points |
penetrationAxis | the raw Jolt axis (not normalised) |
contactNormal | penetrationAxis, normalised |
penetrationDepth | how deep the overlap is |
bodyHandle | the other body |
shapeMatrix | the transform the query was run with |
subShapeId1 / subShapeId2 | raw Jolt.SubShapeID objects owned by the collector — read .GetValue(), never destroy |
Debugging
Casters draw themselves when you ask — they deliberately ignore <Physics debug>, because a
caster can fire thousands of times a second.
raycaster.initDebugging(scene); // adds a debug object to the scene
raycaster.drawPoints = true; // start/hit/end points
raycaster.drawMarkers = true; // markers at each hit
raycaster.lineColor = '#68D8D6';
raycaster.stopDebugging();
Raycasters, shapecasters and the multicaster's inner raycaster all share this API.
Markers
A Raycaster's markers are a small ring plus a normal-indicator line, oriented along the hit
surface normal (Quaternion.setFromUnitVectors((0, 1, 0), hit.impactNormal)) rather than
axis-aligned to the world. A degenerate normal falls back to world up.
The debug objects are pooled per caster: one shared material per drawing type, geometry
reused through setFromPoints, and one marker group per hit index, so repeated drawMarker() /
cast() calls update an existing group's transform instead of allocating. Markers past the
current hit count are hidden rather than destroyed. destroy() and clearDebugging() dispose the
pools.
Shapecaster behaves the same way: it has the same pooled drawing and the same normal-oriented
markers, and its destroy() / clearDebugging() dispose the pools too.
Shared base
Raycaster, Shapecaster, ShapeCollider and Multicaster used to each duplicate the same
filter setup, destroy bookkeeping and (for the ray-like casters) debug-drawing code (issue #217).
They now all extend a small hierarchy instead — useful mainly if you're building a fifth query
type, or want to know exactly what destroy() covers:
| class | extends | adds |
|---|---|---|
QueryBase | — | the joltPhysicsSystem/joltInterface/bodyInterface wiring, the four filters (bpFilter, objectFilter, bodyFilter, shapeFilter) that make a query cast as if a dynamic object, and an idempotent destroy() template |
CastQueryBase<THit, TCollector> | QueryBase | the collector lifecycle, the cast()/castFrom()/castTo()/castBetween() family, and the pooled debug-drawing machinery described above |
HitBase | — | the shape every single-cast result shares: a start/end/position triple, a bodyHandle/shapeIdValue pair, and the impactNormal getter that turns those back into a live surface normal |
Raycaster and Shapecaster extend CastQueryBase (and AdvancedRaycaster extends
Raycaster); ShapeCollider and Multicaster extend QueryBase directly since neither uses the
ray-cast collector family. RaycastHit and ShapecastHit both extend HitBase, which is why
their fields line up exactly (see RaycastHit above).
QueryBase implements destroy() once, as a template: it flips an internal destroyed flag and
calls the abstract releaseResources() every subclass implements for whatever else it allocated.
This means destroy() is idempotent on every query type now — Raycaster, Shapecaster,
ShapeCollider and Multicaster alike, not just ShapeCollider as before #217. Calling it twice
is safe and the second call does nothing, because Raw.module.destroy() on an
already-freed object does not throw — it silently double-frees.
successHandler/failHandler on cast()/castFrom()/castTo()/castBetween() stay untyped
(any) rather than a (hit?: THit | THit[]) => void alias: TypeScript checks a plain
function-type parameter's parameter list contravariantly, so a caller passing a narrower callback
— (hit: RaycastHit) => void, the shape every example above uses — would stop compiling even
though cast() never actually calls it with undefined or an array unless the hit shape says so.
Annotate your own handler's parameter as shown in the examples on this page.
Lifetime
Every caster allocates Jolt filters, a collector and settings objects on the WASM heap. They are
freed only by destroy().
| hook | frees its caster |
|---|---|
useMouseRaycaster() | on unmount and whenever type / filter change |
useRaycaster() | unmount only — a change to origin / direction / type builds a new one and leaks the old |
useMulticaster() | unmount only, and it frees the inner Raycaster rather than calling Multicaster.destroy() |
useAdvancedRaycaster() | never — call destroy() yourself |
useRaycaster's origin and direction are compared by identity, so passing an inline array or
Vector3 re-creates the caster on every render. Hoist them, or use raycaster.set(...).
- Anything from
physicsSystem.getShapecaster()/getShapeCollider()/getRaycaster()is yours: pair it withuseEffect(() => () => caster.destroy(), [caster]). destroy()is idempotent on every query type — see Shared base.ShapeCollidergoes one step further and also nulls out every field it freed, so a stray call afterwards fails loudly instead of touching freed memory; the others just no-op on a second call.- There is no
useShapecaster/useShapeColliderhook yet.
Objects Jolt hands you
Some query surfaces give you real Jolt objects: AdvancedRaycaster's collector callbacks,
CollisionResult.subShapeId1/2, BodyState.body, anything reached through useJolt().jolt.
Never destroy() an object you did not create. Jolt's Emscripten binding returns
by-value results as a pointer to one shared static temporary per function — freeing it hands
memory the binder still owns back to the allocator, which reuses it immediately, and the crash
surfaces somewhere else entirely. The same goes for references into a collector's storage.
Copy what you need out (vec3.three(v), id.GetValue()) and let go.
Full rules in Memory & lifecycle.