Skip to content

Commit b44cc10

Browse files
feat(Controller): environment map (#282)
1 parent 0b28034 commit b44cc10

13 files changed

+579
-221
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ Controllers can be added with `<Controllers />` for [motion-controllers](https:/
160160
/>
161161
```
162162

163+
### Environment map
164+
165+
You can set environment map and/or it's intensity on controller models via props on `<Controllers />`. See [ControllerEnvMap](./examples/src/demos/ControllersEnvMap.tsx) to find out how to do it.
166+
163167
### useController
164168

165169
`useController` references an `XRController` by handedness, exposing position and orientation info.
@@ -172,14 +176,15 @@ const gazeController = useController('none')
172176

173177
### XRController
174178

175-
`XRController` is an `Object3D` that represents an [`XRInputSource`](https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource) with the following properties:
179+
`XRController` is an long-living `Object3D` that represents an [`XRInputSource`](https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource) with the following properties:
176180

177181
```jsx
178182
index: number
179183
controller: THREE.XRTargetRaySpace
180184
grip: THREE.XRGripSpace
181185
hand: THREE.XRHandSpace
182-
inputSource: XRInputSource
186+
inputSource: XRInputSource | null
187+
xrControllerModel: XRControllerModel | null
183188
```
184189

185190
## Interactions
Binary file not shown.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Canvas, useThree } from '@react-three/fiber'
2+
import { XR, VRButton, Controllers } from '@react-three/xr'
3+
import { PMREMGenerator, Texture } from 'three'
4+
import { RGBELoader } from 'three-stdlib'
5+
import { useEffect, useState } from 'react'
6+
import EnvMap from '../assets/brown_photostudio_04_256.hdr'
7+
8+
function ControllersWithEnvMap() {
9+
const renderer = useThree(({ gl }) => gl)
10+
const [envMap, setEnvMap] = useState<Texture>()
11+
12+
useEffect(() => {
13+
const generateEnvMap = async () => {
14+
const rgbeLoader = new RGBELoader()
15+
const dataTexture = await rgbeLoader.loadAsync(EnvMap)
16+
const pmremGenerator = new PMREMGenerator(renderer)
17+
pmremGenerator.compileEquirectangularShader()
18+
const rt = pmremGenerator.fromEquirectangular(dataTexture)
19+
const radianceMap = rt.texture
20+
setEnvMap(radianceMap)
21+
pmremGenerator.dispose()
22+
}
23+
24+
generateEnvMap()
25+
}, [renderer])
26+
27+
return <Controllers envMap={envMap} envMapIntensity={1} />
28+
}
29+
30+
export default function () {
31+
return (
32+
<>
33+
<VRButton onError={(e) => console.error(e)} />
34+
<Canvas>
35+
<XR>
36+
<ControllersWithEnvMap />
37+
</XR>
38+
</Canvas>
39+
</>
40+
)
41+
}

examples/src/demos/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ const HitTest = { Component: lazy(() => import('./HitTest')) }
55
const Player = { Component: lazy(() => import('./Player')) }
66
const Text = { Component: lazy(() => import('./Text')) }
77
const Hands = { Component: lazy(() => import('./Hands')) }
8+
const ControllersEnvMap = { Component: lazy(() => import('./ControllersEnvMap')) }
89
const Teleport = { Component: lazy(() => import('./Teleport')) }
910
const CameraLinkedObject = { Component: lazy(() => import('./CameraLinkedObject')) }
1011

11-
export { Interactive, HitTest, Player, Text, Hands, Teleport, CameraLinkedObject }
12+
export { Interactive, HitTest, Player, Text, Hands, Teleport, CameraLinkedObject, ControllersEnvMap }

examples/src/global.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module '*.hdr' {
2+
const path: string
3+
export = path
4+
}

examples/yarn.lock

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -634,10 +634,10 @@
634634
"@jridgewell/resolve-uri" "3.1.0"
635635
"@jridgewell/sourcemap-codec" "1.4.14"
636636

637-
"@mediapipe/[email protected].2-rc2":
638-
version "0.10.2-rc2"
639-
resolved "https://registry.yarnpkg.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.2-rc2.tgz#e3fa5d84d58b9031a0e975d1e5ef8eb8e4a6fc11"
640-
integrity sha512-b9ar6TEUo8I07n/jXSuKDu5HgzkDah9pe4H8BYpcubhCEahlfDD5ixE+9SQyJM4HXHXdF9nN/wRQT7rEnLz7Gg==
637+
"@mediapipe/tasks-vision@^0.10.0":
638+
version "0.10.1"
639+
resolved "https://registry.yarnpkg.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.1.tgz#68047459352019cc141dc9c1d15c05b8ab689423"
640+
integrity sha512-/zIKjOAIABx+KVfqe8hA6X2pxBGsBYlEtvD7/gpXecvzKefo/JQO6XaggmJul7+noaqiPYM0CVGZxmFJ2oTdSQ==
641641

642642
"@nicolo-ribaudo/semver-v6@^6.3.3":
643643
version "6.3.3"
@@ -718,9 +718,9 @@
718718
zustand "^3.5.13"
719719

720720
"@react-three/fiber@^8.10.0":
721-
version "8.13.4"
722-
resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-8.13.4.tgz#27cf964bd1d353884fb9555e21b0460736d173b5"
723-
integrity sha512-OmyRKt9JU2i/Rc3uw4A+zERXKkFdu8slJjWQZfacoFNHIzGP9QVQ9XxlJWgTbgTLIOD39cUgnmH3RZZGWJqAoQ==
721+
version "8.13.7"
722+
resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-8.13.7.tgz#809f63c85effc7dddd3001ee10c2256c53a82b16"
723+
integrity sha512-fH1wYi8+A2YZX8uYd9N4hfbAV+kHE565s7f62+SMNmpeynaUsN8NzXACmmJ6BpVKAKdxfvOde6dBGwG1BrWOKQ==
724724
dependencies:
725725
"@babel/runtime" "^7.17.8"
726726
"@types/react-reconciler" "^0.26.7"
@@ -930,10 +930,10 @@ cac@^6.7.14:
930930
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
931931
integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
932932

933-
camera-controls@^2.4.2:
934-
version "2.7.0"
935-
resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-2.7.0.tgz#13e2895375fbd8fb3353baeada6c8bc267a60d09"
936-
integrity sha512-HONMoMYHieOCQOoweS639bdWHP/P/fvVGR08imnECGVUp04mqGfsX/zp1ZufLeiAA5hA6i1JhP6SrnOwh01C0w==
933+
camera-controls@^2.3.1:
934+
version "2.4.2"
935+
resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-2.4.2.tgz#815aa5d7c4c43054fc55fb8b6cc685a56540fea2"
936+
integrity sha512-blYDPECYFT/4egDMNWqKc2lBrpOfIAjPPRUNVswQELPi8naGBXUvZM3sDJSNuIRaHqid+JKPtlcoZk+Cb+X5qg==
937937

938938
caniuse-lite@^1.0.30001503:
939939
version "1.0.30001512"
@@ -1035,10 +1035,10 @@ deepmerge@^4.2.2:
10351035
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
10361036
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
10371037

1038-
detect-gpu@^5.0.28:
1039-
version "5.0.31"
1040-
resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.31.tgz#5749bea3aa56bc2ec41383b585f6cbd965619fee"
1041-
integrity sha512-+ZZr/deA5OvuBxod6kKFUvpZA9YR2r4fRYlAJGL7N5aUSLrY3Xgi+K4U5NHmeuk2mNC044n1YJwsq2Aw6hPmUw==
1038+
detect-gpu@^5.0.14:
1039+
version "5.0.27"
1040+
resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.27.tgz#821d9331c87e32568c483d85e12a9adee43d7bb2"
1041+
integrity sha512-IDjjqTkS+f0xm/ntbD21IPYiF0srzpePC/hhUMmctEsoklZwJwStJiMi/KN0pnH0LjSsgjwbP+QwW7y+Qf4/SQ==
10421042
dependencies:
10431043
webgl-constants "^1.1.1"
10441044

@@ -1251,10 +1251,10 @@ lru-cache@^5.1.1:
12511251
dependencies:
12521252
yallist "^3.0.2"
12531253

1254-
maath@^0.6.0:
1255-
version "0.6.0"
1256-
resolved "https://registry.yarnpkg.com/maath/-/maath-0.6.0.tgz#7841d0fb95bbb37d19b08b7c5458ef70190950d2"
1257-
integrity sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==
1254+
maath@^0.5.2:
1255+
version "0.5.3"
1256+
resolved "https://registry.yarnpkg.com/maath/-/maath-0.5.3.tgz#777a1f9b8463c6ffb199ea43406874a357c0cd58"
1257+
integrity sha512-ut63A4zTd9abtpi+sOHW1fPWPtAFrjK0E17eAthx1k93W/T2cWLKV5oaswyotJVDvvW1EXSdokAqhK5KOu0Qdw==
12581258

12591259
magic-string@^0.27.0:
12601260
version "0.27.0"
@@ -1532,20 +1532,25 @@ supports-color@^7.1.0:
15321532
dependencies:
15331533
has-flag "^4.0.0"
15341534

1535+
suspend-react@^0.0.8:
1536+
version "0.0.8"
1537+
resolved "https://registry.yarnpkg.com/suspend-react/-/suspend-react-0.0.8.tgz#b0740c1386b4eb652f17affe4339915ee268bd31"
1538+
integrity sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg==
1539+
15351540
suspend-react@^0.1.3:
15361541
version "0.1.3"
15371542
resolved "https://registry.yarnpkg.com/suspend-react/-/suspend-react-0.1.3.tgz#a52f49d21cfae9a2fb70bd0c68413d3f9d90768e"
15381543
integrity sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==
15391544

1540-
three-mesh-bvh@^0.6.0:
1541-
version "0.6.0"
1542-
resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.6.0.tgz#15523c335383df658dc60063a783fdd52d045dc5"
1543-
integrity sha512-4/oXeqVMLuN9/P0M3L5ezIVrFiXQXKvjVTErkiSYMjSaPoWfNPAwqulSgLf4bIUPn8/Lq3rmIJwxbCuD8qDobA==
1545+
three-mesh-bvh@^0.5.23:
1546+
version "0.5.23"
1547+
resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.5.23.tgz#08e5b629144b48b11acbd433519680e457d398ed"
1548+
integrity sha512-nyk+MskdyDgECqkxdv57UjazqqhrMi+Al9PxJN6yFtx1CTW4r0eCQ27FtyYKY5gCIWhxjtNfWYDPVy8lzx6LkA==
15441549

1545-
three-stdlib@^2.23.9:
1546-
version "2.23.12"
1547-
resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.23.12.tgz#f269398e3125c77bcd374d87f4c1da8d550e7f21"
1548-
integrity sha512-YFpuCu/ZVHBiK42bzEihZTA3tvEPQhaKE5tYej41AlNYXbwIWxO93fxYYrX7vs275s0yCKr6Zp6y7kI+mOklRQ==
1550+
three-stdlib@^2.23.5:
1551+
version "2.23.9"
1552+
resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.23.9.tgz#09c74fc6acced3d124e4f9d695156136c587a355"
1553+
integrity sha512-fYBClVGQptD7UZcoRZGNlR3sKcUW37hVPoEW1v68E4XuiwD0Ml/VqDUJ0yEMVE2DlooDvqgqv/rIcHC/B4N5pg==
15491554
dependencies:
15501555
"@types/draco3d" "^1.4.0"
15511556
"@types/offscreencanvas" "^2019.6.4"
@@ -1574,7 +1579,7 @@ to-fast-properties@^2.0.0:
15741579
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
15751580
integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
15761581

1577-
troika-three-text@^0.47.2:
1582+
troika-three-text@^0.47.1:
15781583
version "0.47.2"
15791584
resolved "https://registry.yarnpkg.com/troika-three-text/-/troika-three-text-0.47.2.tgz#fdf89059c010563bb829262b20c41f69ca79b712"
15801585
integrity sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng==

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"three": "^0.141.0",
6161
"typescript": "^4.5.5",
6262
"vite": "^3.0.5",
63-
"vitest": "^0.29.1"
63+
"vitest": "^0.34.3"
6464
},
6565
"dependencies": {
6666
"@types/webxr": "*",

src/Controllers.test.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { XRControllerModel } from './XRControllerModel'
88
import { XRControllerModelFactoryMock } from './mocks/XRControllerModelFactoryMock'
99
import { XRInputSourceMock } from './mocks/XRInputSourceMock'
1010
import { act } from '@react-three/test-renderer'
11+
import { Texture } from 'three'
1112

1213
vi.mock('./XRControllerModelFactory', async () => {
1314
const { XRControllerModelFactoryMock } = await vi.importActual<typeof import('./mocks/XRControllerModelFactoryMock')>(
@@ -141,4 +142,86 @@ describe('Controllers', () => {
141142
expect(disconnectSpy).not.toBeCalled()
142143
expect(xrControllerModelFactory?.initializeControllerModel).toBeCalledTimes(1)
143144
})
145+
146+
describe('envMap', () => {
147+
it("should not set env map if it's not provided in props", async () => {
148+
const store = createStoreMock()
149+
const xrControllerMock = new XRControllerMock(0)
150+
store.setState({ controllers: [xrControllerMock] })
151+
152+
await render(<Controllers />, { wrapper: createStoreProvider(store) })
153+
154+
const xrControllerModel = xrControllerMock.xrControllerModel
155+
156+
expect(xrControllerModel!.envMap).toBeNull()
157+
expect(xrControllerModel!.envMapIntensity).toBe(1)
158+
})
159+
160+
it("should set env map if it's provided in props", async () => {
161+
const store = createStoreMock()
162+
const xrControllerMock = new XRControllerMock(0)
163+
store.setState({ controllers: [xrControllerMock] })
164+
const envMap = new Texture()
165+
166+
await render(<Controllers envMap={envMap} />, { wrapper: createStoreProvider(store) })
167+
168+
const xrControllerModel = xrControllerMock.xrControllerModel
169+
170+
expect(xrControllerModel!.envMap).toBe(envMap)
171+
expect(xrControllerModel!.envMapIntensity).toBe(1)
172+
})
173+
174+
it("should only set env map intensity if it's provided in props", async () => {
175+
const store = createStoreMock()
176+
const xrControllerMock = new XRControllerMock(0)
177+
store.setState({ controllers: [xrControllerMock] })
178+
179+
await render(<Controllers envMapIntensity={0.5} />, { wrapper: createStoreProvider(store) })
180+
181+
const xrControllerModel = xrControllerMock.xrControllerModel
182+
183+
expect(xrControllerModel!.envMap).toBeNull()
184+
expect(xrControllerModel!.envMapIntensity).toBe(0.5)
185+
})
186+
187+
it("should change env map intensity if it's provided in props then updated to a different value", async () => {
188+
const store = createStoreMock()
189+
const xrControllerMock = new XRControllerMock(0)
190+
store.setState({ controllers: [xrControllerMock] })
191+
192+
const { rerender } = await render(<Controllers envMapIntensity={0.5} />, { wrapper: createStoreProvider(store) })
193+
const xrControllerModel = xrControllerMock.xrControllerModel
194+
await rerender(<Controllers envMapIntensity={0.6} />)
195+
196+
expect(xrControllerModel!.envMap).toBeNull()
197+
expect(xrControllerModel!.envMapIntensity).toBe(0.6)
198+
})
199+
200+
it("should remove env map if it's provided in props first, and then removed", async () => {
201+
const store = createStoreMock()
202+
const xrControllerMock = new XRControllerMock(0)
203+
store.setState({ controllers: [xrControllerMock] })
204+
const envMap = new Texture()
205+
206+
const { rerender } = await render(<Controllers envMap={envMap} />, { wrapper: createStoreProvider(store) })
207+
const xrControllerModel = xrControllerMock.xrControllerModel
208+
await rerender(<Controllers />)
209+
210+
expect(xrControllerModel!.envMap).toBeNull()
211+
})
212+
213+
it("should change env map intensity if it's provided in props then updated to a different value but envMap stays the same", async () => {
214+
const store = createStoreMock()
215+
const xrControllerMock = new XRControllerMock(0)
216+
store.setState({ controllers: [xrControllerMock] })
217+
const envMap = new Texture()
218+
219+
const { rerender } = await render(<Controllers envMap={envMap} envMapIntensity={0.5} />, { wrapper: createStoreProvider(store) })
220+
const xrControllerModel = xrControllerMock.xrControllerModel
221+
await rerender(<Controllers envMap={envMap} envMapIntensity={0.6} />)
222+
223+
expect(xrControllerModel!.envMap).toBe(envMap)
224+
expect(xrControllerModel!.envMapIntensity).toBe(0.6)
225+
})
226+
})
144227
})

0 commit comments

Comments
 (0)