|
1 | 1 | ---
|
2 | 2 | title: Hit Test
|
3 |
| -description: How to add hit testing capabilities to your AR experiences? |
| 3 | +description: How to add hit testing capabilities to your AR experiences |
4 | 4 | nav: 19
|
5 | 5 | ---
|
6 | 6 |
|
7 |
| -Hit testing allows to check intersections with real-world geometry in AR experiences. `@react-three/xr` provides various hooks and components for setting up hit testing. |
8 |
| -The following example shows how to set up a hit test inside the right hand using the `XRHitTest` component, how to get the first hit test result using the `onResults` callback, and how to get the world position of that result into a vector. |
| 7 | +Hit testing is a technique that allows developers to check for intersections with real-world surfaces in AR experiences. `@react-three/xr` provides hooks and components for setting up hit testing. This tutorial covers all the hit testing hooks available in React Three XR and demonstrates how to use them effectively. |
| 8 | + |
| 9 | +## Overview of Hit Testing Components |
| 10 | + |
| 11 | +React Three XR provides three hooks for hit testing: |
| 12 | + |
| 13 | +- **`useXRHitTest`** - Provides continuous hit testing with automatic frame updates |
| 14 | +- **`useXRHitTestSource`** - Lower-level hook for creating and managing hit test sources |
| 15 | +- **`useXRRequestHitTest`** - One-time hit test requests on demand |
| 16 | + |
| 17 | +Additionally, React Three XR provides the `XRHitTest` component, which is a convenience wrapper for using the `useXRHitTest` hook to perform continuous hit testing. |
| 18 | + |
| 19 | +All rays cast by these components originate from the source's position and are cast in the direction that the source object is oriented (quaternion; typically -z). |
| 20 | + |
| 21 | +## useXRHitTest Hook |
| 22 | + |
| 23 | +The `useXRHitTest` hook is the most commonly used hook for hit testing in the library. It automatically performs hit tests every frame and calls your callback function with the results. |
| 24 | + |
| 25 | +**What it does:** |
| 26 | + |
| 27 | +Sets up continuous hit testing that runs every frame, providing real-time intersection data with the real world. |
| 28 | + |
| 29 | +**When to use it:** |
| 30 | + |
| 31 | +Use this when you need continuous tracking of where a ray intersects with real-world surfaces, such as for cursor positioning, object placement previews, or interactive AR elements. |
| 32 | + |
| 33 | +**Parameters:** |
| 34 | + |
| 35 | +- `fn` - Callback function that receives hit test results and a function to retrieve the world matrix |
| 36 | +- `relativeTo` - The object, XR space, or reference space to cast rays from. This reference must be static in your scene. |
| 37 | +- `trackableType` - Optional parameter specifying what types of surfaces to hit test against |
9 | 38 |
|
10 | 39 | ```tsx
|
11 | 40 | const matrixHelper = new Matrix4()
|
12 | 41 | const hitTestPosition = new Vector3()
|
13 | 42 |
|
14 |
| -const store = createXRStore({ |
15 |
| - hand: { |
16 |
| - right: () => { |
17 |
| - const state = useXRHandState() |
18 |
| - return ( |
19 |
| - <> |
20 |
| - <XRHandModel /> |
21 |
| - <XRHitTest |
22 |
| - space={state.inputSource.targetRaySpace} |
23 |
| - onResults={(results, getWorldMatrix) => { |
24 |
| - if (results.length === 0) { |
25 |
| - return |
26 |
| - } |
27 |
| - getWorldMatrix(matrixHelper, results[0]) |
28 |
| - hitTestPosition.setFromMatrixPosition(matrixHelper) |
29 |
| - }} |
30 |
| - /> |
31 |
| - </> |
32 |
| - ) |
| 43 | +function ContinuousHitTest() { |
| 44 | + const previewRef = useRef<Mesh>(null) |
| 45 | + |
| 46 | + useXRHitTest( |
| 47 | + (results, getWorldMatrix) => { |
| 48 | + if (results.length === 0) return |
| 49 | + |
| 50 | + getWorldMatrix(matrixHelper, results[0]) |
| 51 | + hitTestPosition.setFromMatrixPosition(matrixHelper) |
33 | 52 | },
|
| 53 | + 'viewer', // Cast rays from the viewer reference space. This will typically be either the camera or where the user is looking |
| 54 | + 'plane' // Only hit test against detected planes |
| 55 | + ) |
| 56 | + |
| 57 | + useFrame(() => { |
| 58 | + if (hitTestPosition && previewRef.current) { |
| 59 | + previewRef.current.position.copy(hitTestPosition) |
| 60 | + } |
| 61 | + }) |
| 62 | + |
| 63 | + return ( |
| 64 | + {/* Renders a sphere where the hit test intersects with the plane */} |
| 65 | + <mesh ref={previewRef} position={hitPosition}> |
| 66 | + <sphereGeometry args={[0.05]} /> |
| 67 | + <meshBasicMaterial color="red" /> |
| 68 | + </mesh> |
| 69 | + ) |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +## XRHitTest |
| 74 | + |
| 75 | +`XRHitTest` is a component that wraps the `useXRHitTest` hook. This makes it easier to add hit testing anywhere within your component tree. |
| 76 | + |
| 77 | +```tsx |
| 78 | +const matrixHelper = new Matrix4() |
| 79 | +const hitTestPosition = new Vector3() |
| 80 | + |
| 81 | +const store = createXRStore({ |
| 82 | + hand: () => { |
| 83 | + const inputSourceState = useXRInputSourceStateContext() |
| 84 | + |
| 85 | + return ( |
| 86 | + <> |
| 87 | + <DefaultXRHand /> |
| 88 | + <XRHitTest |
| 89 | + space={inputSourceState.inputSource.targetRaySpace} |
| 90 | + onResults={(results, getWorldMatrix) => { |
| 91 | + if (results.length === 0) return |
| 92 | + getWorldMatrix(matrixHelper, results[0]) |
| 93 | + hitTestPosition.setFromMatrixPosition(matrixHelper) |
| 94 | + }} |
| 95 | + /> |
| 96 | + </> |
| 97 | + ) |
34 | 98 | },
|
35 | 99 | })
|
36 | 100 | ```
|
37 | 101 |
|
38 |
| -With the `hitTestPosition` containing the world position of the last hit test, we can use it to create a 3d object and sync it to the object's position on every frame. |
| 102 | +`XRHitTest` has all of the same functionality as the `useXRHitTest` hook, just that it's built as a component. |
| 103 | + |
| 104 | +## useXRHitTestSource Hook |
| 105 | + |
| 106 | +The `useXRHitTestSource` hook provides lower-level access to hit test sources, giving you more control over when and how hit tests are performed. It is the same as the `useXRHitTest` hook, the only difference being that you have to manually check for hit test results; typically every frame, or every few frames. |
| 107 | + |
| 108 | +**What it does:** |
| 109 | + |
| 110 | +Does the same thing as the `useXRHitTest` hook, but does not automatically hit test every frame. |
| 111 | + |
| 112 | +**When to use it:** |
| 113 | + |
| 114 | + In most cases you should use either `useXRHitTest` or `useXRRequestHitTest`, but you can use this hook when you have a static hit test source that you only want to occasionally perform constant hit tests from. Or if you want to recreate the `useXRHitTest` behavior manually. |
| 115 | + |
| 116 | +**Parameters:** |
| 117 | + |
| 118 | +- `relativeTo` - The object, XR space, or reference space to cast rays from |
| 119 | +- `trackableType` - Optional parameter specifying what types of surfaces to hit test against |
| 120 | + |
| 121 | +**Returns:** |
| 122 | + |
| 123 | + A hit test source object that you can use with `frame.getHitTestResults()` |
39 | 124 |
|
40 | 125 | ```tsx
|
41 |
| -function Point() { |
42 |
| - const ref = useRef<Mesh>(null) |
43 |
| - useFrame(() => ref.current?.position.copy(hitTestPosition)) |
| 126 | +function ManualHitTest() { |
| 127 | + const meshRef = useRef<Mesh>(null) |
| 128 | + const hitTestSource = useXRHitTestSource(meshRef) |
| 129 | + const [someCondition, setSomeCondition] = useState(false) |
| 130 | + const [hitResults, setHitResults] = useState<XRHitTestResult[]>([]) |
| 131 | + |
| 132 | + useFrame((_, __, frame: XRFrame | undefined) => { |
| 133 | + // Only perform hit testing when certain conditions are met |
| 134 | + if (frame && hitTestSource && someCondition) { |
| 135 | + const results = frame.getHitTestResults(hitTestSource.source) |
| 136 | + setHitResults(results) |
| 137 | + } |
| 138 | + }) |
| 139 | + |
44 | 140 | return (
|
45 |
| - <mesh scale={0.05} ref={ref}> |
46 |
| - <sphereGeometry /> |
47 |
| - <meshBasicMaterial /> |
| 141 | + <mesh ref={meshRef}> |
| 142 | + {/* Render hit test results. This will put spheres everywhere the hit test succeeds. In a real app don't use index as the key */} |
| 143 | + {hitResults.map((result, index) => { |
| 144 | + const matrix = new Matrix4() |
| 145 | + hitTestSource?.getWorldMatrix(matrix, result) |
| 146 | + const position = new Vector3().setFromMatrixPosition(matrix) |
| 147 | + return ( |
| 148 | + <mesh key={index} position={position}> |
| 149 | + <sphereGeometry args={[0.05]} /> |
| 150 | + <meshBasicMaterial color="red" /> |
| 151 | + </mesh> |
| 152 | + ) |
| 153 | + })} |
48 | 154 | </mesh>
|
49 | 155 | )
|
50 | 156 | }
|
51 | 157 | ```
|
52 | 158 |
|
53 |
| -Alternatively, for devices that provide mesh detection, we can also add normal pointer events listeners to the XR Mesh to achieve the same behavior. Check out [this tutorial](./object-detection.md) for more information about mesh detection. |
| 159 | +## useXRRequestHitTest Hook |
| 160 | + |
| 161 | +The `useXRRequestHitTest` hook provides a function for one-time hit test requests. Useful for event-driven hit testing. Cannot be called in the `useFrame` hook. |
| 162 | + |
| 163 | +**What it does:** |
| 164 | + |
| 165 | +Returns a function that can perform a single hit test request when called. |
| 166 | + |
| 167 | +**When to use it:** |
| 168 | + |
| 169 | +Use this for event-driven hit testing, such as when a user taps the screen, clicks a button, or performs a gesture. It's ideal for placing objects or checking intersections at specific moments. |
| 170 | + |
| 171 | +**Returns:** |
| 172 | + |
| 173 | +A function that takes the same parameters as other hit test hooks and returns a promise with hit test results |
| 174 | + |
| 175 | +```tsx |
| 176 | +const matrixHelper = new Matrix4() |
| 177 | +function EventDrivenHitTest() { |
| 178 | + const requestHitTest = useXRRequestHitTest() |
| 179 | + const [placedObjects, setPlacedObjects] = useState<Vector3[]>([]) |
| 180 | + |
| 181 | + const handleTap = async () => { |
| 182 | + const hitTestResult = await requestHitTest('viewer', ['plane', 'mesh']) |
| 183 | + const { results, getWorldMatrix } = hitTestResult |
| 184 | + if (results?.length > 0) { |
| 185 | + getWorldMatrix(matrixHelper, results[0]) |
| 186 | + const position = new Vector3().setFromMatrixPosition(matrixHelper) |
| 187 | + setPlacedObjects((prev) => [...prev, position]) |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + return ( |
| 192 | + <> |
| 193 | + <IfInSessionMode allow={'immersive-ar'}> |
| 194 | + <XRDomOverlay> |
| 195 | + <button onClick={handleTap}>Place Object</button> |
| 196 | + </XRDomOverlay> |
| 197 | + </IfInSessionMode> |
| 198 | + |
| 199 | + {/* Render placed objects */} |
| 200 | + {placedObjects.map((position, index) => ( |
| 201 | + <mesh key={index} position={position}> |
| 202 | + <sphereGeometry args={[0.1]} /> |
| 203 | + <meshBasicMaterial color="blue" /> |
| 204 | + </mesh> |
| 205 | + ))} |
| 206 | + </> |
| 207 | + ) |
| 208 | +} |
| 209 | +``` |
| 210 | + |
| 211 | +## Trackable Types |
| 212 | + |
| 213 | +All hit testing hooks support specifying trackable types to control what surfaces the hit tests should target: |
| 214 | + |
| 215 | +- `'plane'` - Hit test against detected planes (floors, walls, tables) |
| 216 | +- `'point'` - Hit test against feature points in the environment |
| 217 | +- `'mesh'` - Hit test against detected meshes (requires mesh detection support) |
| 218 | + |
| 219 | +You can specify a single type or an array of types: |
| 220 | + |
| 221 | +```tsx |
| 222 | +// Single type |
| 223 | +useXRHitTest(callback, spaceRef, 'plane') |
| 224 | + |
| 225 | +// Multiple types |
| 226 | +useXRHitTest(callback, spaceRef, ['plane', 'mesh']) |
| 227 | +``` |
| 228 | + |
| 229 | +## Practical Example: Object Placement |
| 230 | + |
| 231 | +Here's a complete example combining multiple hooks for a robust object placement system: |
| 232 | + |
| 233 | +```tsx |
| 234 | +const matrixHelper = new Matrix4() |
| 235 | +const hitTestPositionHelper = new Vector3() |
| 236 | + |
| 237 | +function ObjectPlacement() { |
| 238 | + const [placedObjects, setPlacedObjects] = useState<Vector3[]>([]) |
| 239 | + const [previewPosition, setPreviewPosition] = useState<Vector3 | null>(null) |
| 240 | + const controllerRef = useRef<Group>(null) |
| 241 | + |
| 242 | + // Continuous hit testing for preview |
| 243 | + useXRHitTest( |
| 244 | + (results, getWorldMatrix) => { |
| 245 | + if (results.length > 0) { |
| 246 | + getWorldMatrix(matrixHelper, results[0]) |
| 247 | + const position = hitTestPositionHelper.setFromMatrixPosition(matrixHelper) |
| 248 | + setPreviewPosition(position) |
| 249 | + } else { |
| 250 | + setPreviewPosition(null) |
| 251 | + } |
| 252 | + }, |
| 253 | + 'viewer', // Use viewer space for screen-based hit testing |
| 254 | + ) |
| 255 | + |
| 256 | + const placeObject = async () => { |
| 257 | + if (previewPosition) { |
| 258 | + setPlacedObjects((prev) => [...prev, previewPosition.clone()]) |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + return ( |
| 263 | + <> |
| 264 | + {/* Preview object at hit test position */} |
| 265 | + {previewPosition && ( |
| 266 | + <mesh position={previewPosition}> |
| 267 | + <sphereGeometry args={[0.05]} /> |
| 268 | + <meshBasicMaterial color="yellow" transparent opacity={0.7} /> |
| 269 | + </mesh> |
| 270 | + )} |
| 271 | + |
| 272 | + {/* Placed objects */} |
| 273 | + {placedObjects.map((position, index) => ( |
| 274 | + <mesh key={index} position={position}> |
| 275 | + <sphereGeometry args={[0.05]} /> |
| 276 | + <meshBasicMaterial color="green" /> |
| 277 | + </mesh> |
| 278 | + ))} |
| 279 | + |
| 280 | + {/* Placement trigger */} |
| 281 | + <IfInSessionMode allow={'immersive-ar'}> |
| 282 | + <XRDomOverlay> |
| 283 | + <button onClick={placeObject}>Place Object</button> |
| 284 | + </XRDomOverlay> |
| 285 | + </IfInSessionMode> |
| 286 | + </> |
| 287 | + ) |
| 288 | +} |
| 289 | +``` |
| 290 | + |
| 291 | +Alternatively, for devices that provide mesh detection -- such as newer Meta Quest devices -- you can also add normal pointer event listeners to an XR Mesh to achieve the same behavior. Check out [this tutorial](./object-detection.md) for more information about mesh detection. |
0 commit comments