Skip to content

Commit 109d897

Browse files
authored
Merge pull request #458 from taeuscherpferd/Update-hit-testing-tutorial
docs: Update hit testing tutorial and example
2 parents 24a878f + 7032a8c commit 109d897

File tree

15 files changed

+712
-84
lines changed

15 files changed

+712
-84
lines changed

docs/tutorials/hit-test.md

Lines changed: 268 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,291 @@
11
---
22
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
44
nav: 19
55
---
66

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
938

1039
```tsx
1140
const matrixHelper = new Matrix4()
1241
const hitTestPosition = new Vector3()
1342

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)
3352
},
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+
)
3498
},
3599
})
36100
```
37101

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()`
39124

40125
```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+
44140
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+
})}
48154
</mesh>
49155
)
50156
}
51157
```
52158

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.

examples/hit-testing/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { StrictMode } from 'react'
22
import { createRoot } from 'react-dom/client'
33
import { App } from './src/app.js'
4+
import './style.css'
45

56
createRoot(document.getElementById('root')!).render(
67
<StrictMode>

examples/hit-testing/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
22
"name": "hit-testing",
33
"dependencies": {
4-
"@react-three/xr": "workspace:~"
4+
"@react-three/xr": "workspace:~",
5+
"@react-three/uikit": "^0.8.21"
56
},
67
"scripts": {
78
"dev": "vite --host",
89
"build": "vite build",
910
"check:eslint": "eslint \"src/**/*.{ts,tsx}\"",
1011
"fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix"
1112
}
12-
}
13+
}

0 commit comments

Comments
 (0)