Enable WebXR Hit Test for Floor Detection in Three.js
This article provides a step-by-step guide on how to request and enable the WebXR Hit-Test API within a Three.js augmented reality (AR) session to detect physical floors and surfaces. You will learn how to configure the WebXR manager, request the required hit-test features, set up a hit-test source, and use the resulting matrix data to place virtual 3D objects accurately on real-world surfaces.
Step 1: Initialize the AR Button with Hit-Test Features
To use the hit-test feature, you must explicitly request it when
initiating the WebXR AR session. In Three.js, this is done by passing
requiredFeatures or optionalFeatures to the
ARButton.
import { ARButton } from 'three/addons/webxr/ARButton.js';
// Create the renderer and enable WebXR
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
// Request the AR session with the 'hit-test' feature enabled
const arButton = ARButton.createButton(renderer, {
requiredFeatures: ['hit-test']
});
document.body.appendChild(arButton);Step 2: Request the Hit-Test Source
Once the AR session starts, you need to obtain a hit-test source.
This source uses the device’s camera and sensors to calculate collisions
with physical surfaces. You will need to request a reference space
(usually 'viewer') to project the hit-test ray from the
camera’s perspective.
let hitTestSource = null;
let localSpace = null;
renderer.xr.addEventListener('sessionstart', async () => {
const session = renderer.xr.getSession();
// Retrieve the viewer reference space to cast rays from the camera
const viewerSpace = await session.requestReferenceSpace('viewer');
// Retrieve the local reference space for absolute positioning in the physical world
localSpace = await session.requestReferenceSpace('local');
// Request the hit test source using the viewer space
session.requestHitTestSource({ space: viewerSpace }).then((source) => {
hitTestSource = source;
});
});
renderer.xr.addEventListener('sessionend', () => {
hitTestSource = null;
localSpace = null;
});Step 3: Perform Hit-Testing in the Render Loop
Inside the Three.js animation/render loop, query the frame for hit-test results. If a physical surface like a floor is detected, the WebXR frame will return an array of hit results. You can extract the transformation matrix from the first result and apply it to a 3D object (such as a ring-shaped reticle) to visualize where the floor is.
// Create a reticle to visual represent the detected floor
const reticleGeometry = new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2);
const reticleMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const reticle = new THREE.Mesh(reticleGeometry, reticleMaterial);
reticle.matrixAutoUpdate = false; // We will update the matrix manually
reticle.visible = false;
scene.add(reticle);
// Animation/Render loop
renderer.setAnimationLoop((timestamp, frame) => {
if (frame && hitTestSource) {
// Get hit test results relative to the local physical space
const hitTestResults = frame.getHitTestResults(hitTestSource);
if (hitTestResults.length > 0) {
const hit = hitTestResults[0];
const pose = hit.getPose(localSpace);
// Make the reticle visible and position it on the detected floor
reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix);
} else {
// Hide the reticle if no surface is detected
reticle.visible = false;
}
}
renderer.render(scene, camera);
});Step 4: Spawning Objects on the Detected Floor
To place a 3D object permanently at the detected floor position, listen for a controller selection event. When the user taps the screen, check if the reticle is visible, and copy its matrix position to spawn your 3D mesh.
const controller = renderer.xr.getController(0);
scene.add(controller);
controller.addEventListener('select', () => {
if (reticle.visible) {
// Create the 3D object to place on the floor
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
// Position the mesh exactly where the reticle is on the physical floor
mesh.position.setFromMatrixPosition(reticle.matrix);
scene.add(mesh);
}
});