Queries

Raycasting, shapecasting and collide-shape overlap tests, and who owns what.

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
Raycasterwhat does this ray hit
useMouseRaycasterwhat is under the pointer
Multicasterwhat do these many rays hit, in one call
AdvancedRaycastersame, with your own per-body / per-hit filtering
Shapecasterwhat does this shape hit as it sweeps along a direction
ShapeColliderwhat is this shape overlapping right now (Jolt's CollideShape)
Caution

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
positionworld-space hit point (THREE.Vector3)
start / endthe ray, as cast
distancefrom start to position
normal / directionthe ray's own normalised direction
impactNormalthe surface normal at the hit — this is the one you usually want
bodyHandlepass to bodySystem.getBody()
shapeIdValuesub-shape id within a compound
indexposition 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;
}
optiondefault
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
lengthcamera.farray 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.

Caution

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 / contactPointOn2world-space contact points
penetrationAxisthe raw Jolt axis (not normalised)
contactNormalpenetrationAxis, normalised
penetrationDepthhow deep the overlap is
bodyHandlethe other body
shapeMatrixthe transform the query was run with
subShapeId1 / subShapeId2raw 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:

classextendsadds
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>QueryBasethe 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().

hookfrees 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 with useEffect(() => () => caster.destroy(), [caster]).
  • destroy() is idempotent on every query type — see Shared base. ShapeCollider goes 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 / useShapeCollider hook 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.

Caution

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.