WebXR Hit Testing in Three.js AR Tutorial
This article provides a step-by-step guide on how to implement WebXR hit testing in Three.js to place 3D objects onto real-world surfaces. You will learn how to set up an AR session, request hit-test features, detect physical environments using a device camera, and position virtual objects accurately using a visual reticle indicator.
Step 1: Enable the WebXR Hit-Test Feature
To perform hit testing, you must request the hit-test
feature when initializing your AR session. Three.js provides the
ARButton utility to simplify this process.
import { ARButton } from 'three/addons/webxr/ARButton.js';
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.xr.enabled = true;
document.body.appendChild(ARButton.createButton(renderer, {
requiredFeatures: ['hit-test']
}));Step 2: Create a Visual Reticle (Target Marker)
A reticle is a 3D marker that lies flat on detected real-world surfaces, showing users where their object will be placed. Create a ring mesh and set its matrix auto-updates to false, as WebXR will control its transform matrix directly.
const reticle = new THREE.Mesh(
new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2),
new THREE.MeshBasicMaterial({ color: 0x00ff00 })
);
reticle.matrixAutoUpdate = false;
reticle.visible = false;
scene.add(reticle);Step 3: Request the Hit-Test Source
Once the AR session starts, you need to request a
XRHitTestSource. This source continuously calculates
intersection points between the user’s camera view and physical
surfaces.
let hitTestSource = null;
let hitTestSourceRequested = false;
function requestHitTestSource(session) {
session.requestReferenceSpace('viewer').then((referenceSpace) => {
session.requestHitTestSource({ space: referenceSpace }).then((source) => {
hitTestSource = source;
});
});
session.addEventListener('end', () => {
hitTestSourceRequested = false;
hitTestSource = null;
});
hitTestSourceRequested = true;
}Step 4: Perform the Hit Test in the Animation Loop
In your Three.js render loop, retrieve the hit test results using the current XR frame. If a surface is detected, extract its pose matrix and apply it to the reticle.
renderer.setAnimationLoop((timestamp, frame) => {
if (frame) {
const session = renderer.xr.getSession();
if (!hitTestSourceRequested) {
requestHitTestSource(session);
}
if (hitTestSource) {
const referenceSpace = renderer.xr.getReferenceSpace();
const hitTestResults = frame.getHitTestResults(hitTestSource);
if (hitTestResults.length > 0) {
const hit = hitTestResults[0];
const pose = hit.getPose(referenceSpace);
reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix);
} else {
reticle.visible = false;
}
}
}
renderer.render(scene, camera);
});Step 5: Place the 3D Object on Tap
To place an object, listen for the WebXR controller
select event. When the user taps their screen and the
reticle is visible, spawn a 3D mesh at the reticle’s current position
and rotation.
const controller = renderer.xr.getController(0);
controller.addEventListener('select', onSelect);
scene.add(controller);
function onSelect() {
if (reticle.visible) {
// Create the 3D object to place (e.g., a box)
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshPhongMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
// Position and orient the object matching the reticle matrix
mesh.position.setFromMatrixPosition(reticle.matrix);
mesh.quaternion.setFromRotationMatrix(reticle.matrix);
scene.add(mesh);
}
}