Skip to content

Commit e796aa3

Browse files
committed
docs: handheld-ar, hit test, anchor examples
1 parent c9cd905 commit e796aa3

File tree

14 files changed

+489
-1
lines changed

14 files changed

+489
-1
lines changed

examples/handheld-ar/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

examples/handheld-ar/app.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Canvas, createPortal, useThree } from '@react-three/fiber'
2+
import { createXRStore, XR, XRDomOverlay, XROrigin } from '@react-three/xr'
3+
import { useEffect, useState } from 'react'
4+
import {} from '@react-three/drei'
5+
6+
const store = createXRStore({})
7+
8+
export function App() {
9+
const [bool, setBool] = useState(false)
10+
return (
11+
<>
12+
<button onClick={() => store.enterVR()}>Enter VR</button>
13+
<button onClick={() => store.enterAR()}>Enter AR</button>
14+
<Canvas style={{ width: '100%', flexGrow: 1 }}>
15+
<XR store={store}>
16+
<XROrigin />
17+
<ambientLight />
18+
19+
<ViewerBox />
20+
</XR>
21+
</Canvas>
22+
</>
23+
)
24+
}
25+
26+
function ViewerBox() {
27+
const camera = useThree((s) => s.camera)
28+
const scene = useThree((s) => s.scene)
29+
const [red, setRed] = useState(false)
30+
return (
31+
<>
32+
<primitive object={camera} />
33+
{createPortal(
34+
<mesh onClick={() => setRed((r) => !r)} position-z={-5}>
35+
<boxGeometry />
36+
<meshBasicMaterial color={red ? 'red' : 'green'} />
37+
</mesh>,
38+
camera,
39+
)}
40+
</>
41+
)
42+
}

examples/handheld-ar/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
<script async type="module" src="./index.tsx"></script>
8+
</head>
9+
<body style="touch-action: none; margin: 0; position: relative; width: 100dvw; height: 100dvh; overflow: hidden;">
10+
<div id="root" style="position: absolute; inset: 0; display: flex; flex-direction: column;"></div>
11+
</body>
12+
</html>

examples/handheld-ar/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createRoot } from 'react-dom/client'
2+
import { App } from './app.js'
3+
import { StrictMode } from 'react'
4+
5+
createRoot(document.getElementById('root')!).render(
6+
<StrictMode>
7+
<App />
8+
</StrictMode>,
9+
)

examples/handheld-ar/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"dependencies": {
3+
"@react-three/drei": "^9.109.2",
4+
"@react-three/xr": "workspace:^"
5+
},
6+
"scripts": {
7+
"dev": "vite --host"
8+
}
9+
}

examples/handheld-ar/vite.config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineConfig } from 'vite'
2+
import path from 'path'
3+
import react from '@vitejs/plugin-react'
4+
import basicSsl from '@vitejs/plugin-basic-ssl'
5+
6+
// https://vitejs.dev/config/
7+
export default defineConfig({
8+
plugins: [react(), basicSsl()],
9+
resolve: {
10+
alias: [{ find: '@react-three/xr', replacement: path.resolve(__dirname, '../../packages/react/xr/src/index.ts') }],
11+
dedupe: ['@react-three/fiber', 'three'],
12+
},
13+
})

examples/hit-test-anchor/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

examples/hit-test-anchor/app.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { Canvas } from '@react-three/fiber'
2+
import {
3+
XRSpace,
4+
createXRStore,
5+
XR,
6+
XRHitTest,
7+
useXRControllerState,
8+
XRControllerModel,
9+
XRHandModel,
10+
useXRHandState,
11+
GetWorldMatrixFromXRHitTest,
12+
useXRScreenInputState,
13+
useXRInputSourceEvent,
14+
useXRRequestHitTest,
15+
useXRAnchor,
16+
} from '@react-three/xr'
17+
import { useEffect, useRef } from 'react'
18+
import { Matrix4, Mesh, Vector3 } from 'three'
19+
import { create } from 'zustand'
20+
21+
const matrixHelper = new Matrix4()
22+
23+
function onResults(handedness: XRHandedness, results: XRHitTestResult[], getWorldMatrix: GetWorldMatrixFromXRHitTest) {
24+
;(handedness === 'left' ? useLeftPoints : useRightPoints).setState(
25+
results
26+
.map((result) => {
27+
if (!getWorldMatrix(matrixHelper, result)) {
28+
return undefined
29+
}
30+
return new Vector3().setFromMatrixPosition(matrixHelper)
31+
})
32+
.filter((vector) => vector != null),
33+
)
34+
}
35+
36+
const store = createXRStore({
37+
hand: () => {
38+
// eslint-disable-next-line react-hooks/rules-of-hooks
39+
const state = useXRHandState()
40+
return (
41+
<>
42+
<XRHandModel />
43+
<XRSpace space={state.inputSource.targetRaySpace}>
44+
<XRHitTest onResults={onResults.bind(null, state.inputSource.handedness)} />
45+
</XRSpace>
46+
</>
47+
)
48+
},
49+
controller: () => {
50+
// eslint-disable-next-line react-hooks/rules-of-hooks
51+
const state = useXRHandState()
52+
return (
53+
<>
54+
<XRControllerModel />
55+
<XRSpace space={state.inputSource.targetRaySpace}>
56+
<XRHitTest onResults={onResults.bind(null, state.inputSource.handedness)} />
57+
</XRSpace>
58+
</>
59+
)
60+
},
61+
screenInput: () => {
62+
// eslint-disable-next-line react-hooks/rules-of-hooks
63+
const state = useXRScreenInputState()
64+
return (
65+
<>
66+
<XRSpace space={state.inputSource.targetRaySpace}>
67+
<XRHitTest onResults={onResults.bind(null, state.inputSource.handedness)} />
68+
</XRSpace>
69+
</>
70+
)
71+
},
72+
})
73+
74+
const useLeftPoints = create<Array<Vector3>>(() => [])
75+
const useRightPoints = create<Array<Vector3>>(() => [])
76+
77+
export function App() {
78+
const leftLength = useLeftPoints((s) => s.length)
79+
const rightLength = useRightPoints((s) => s.length)
80+
return (
81+
<>
82+
<button onClick={() => store.enterAR()}>Enter AR</button>
83+
<Canvas style={{ width: '100%', flexGrow: 1 }}>
84+
<XR store={store}>
85+
<ambientLight />
86+
{new Array(leftLength).fill(undefined).map((_, i) => (
87+
<Point left key={i} index={i} />
88+
))}
89+
{new Array(rightLength).fill(undefined).map((_, i) => (
90+
<Point key={i} index={i} />
91+
))}
92+
<Anchors />
93+
</XR>
94+
</Canvas>
95+
</>
96+
)
97+
}
98+
99+
function Anchors() {
100+
const [anchor, requestAnchor] = useXRAnchor()
101+
const requestHitTest = useXRRequestHitTest()
102+
const controllerState = useXRControllerState('right')
103+
const handState = useXRHandState('right')
104+
const inputSource = controllerState?.inputSource ?? handState?.inputSource
105+
useXRInputSourceEvent(
106+
inputSource,
107+
'select',
108+
async () => {
109+
if (inputSource == null) {
110+
return
111+
}
112+
const result = await requestHitTest(inputSource.targetRaySpace)
113+
if (result == null || result.results.length === 0) {
114+
return
115+
}
116+
requestAnchor({ relativeTo: 'hit-test-result', hitTestResult: result.results[0] })
117+
},
118+
[requestHitTest, requestAnchor, inputSource],
119+
)
120+
if (anchor == null) {
121+
return null
122+
}
123+
return (
124+
<XRSpace space={anchor.anchorSpace}>
125+
<mesh scale={0.1}>
126+
<boxGeometry />
127+
</mesh>
128+
</XRSpace>
129+
)
130+
}
131+
132+
function Point({ index, left }: { left?: boolean; index: number }) {
133+
const ref = useRef<Mesh>(null)
134+
useEffect(
135+
() =>
136+
(left ? useLeftPoints : useRightPoints).subscribe((state) => {
137+
ref.current!.position.copy(state[index])
138+
}),
139+
[index, left],
140+
)
141+
return (
142+
<mesh scale={0.05} ref={ref}>
143+
<sphereGeometry />
144+
<meshBasicMaterial />
145+
</mesh>
146+
)
147+
}

examples/hit-test-anchor/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
<script async type="module" src="./index.tsx"></script>
8+
</head>
9+
<body style="touch-action: none; margin: 0; position: relative; width: 100dvw; height: 100dvh; overflow: hidden;">
10+
<div id="root" style="position: absolute; inset: 0; display: flex; flex-direction: column;"></div>
11+
</body>
12+
</html>

examples/hit-test-anchor/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createRoot } from 'react-dom/client'
2+
import { App } from './app.js'
3+
import { StrictMode } from 'react'
4+
5+
createRoot(document.getElementById('root')!).render(
6+
<StrictMode>
7+
<App />
8+
</StrictMode>,
9+
)

0 commit comments

Comments
 (0)