Skip to content

Commit f4b5f2d

Browse files
committed
feat: support emulator options; breaking change: set default space in hand and controller to target ray space to align better with the webxr standard
1 parent 432aff6 commit f4b5f2d

File tree

11 files changed

+203
-93
lines changed

11 files changed

+203
-93
lines changed

examples/hit-test-anchor/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const store = createXRStore({
3838
return (
3939
<>
4040
<XRHandModel />
41-
<XRSpace space={state.inputSource.targetRaySpace}>
41+
<XRSpace space="target-ray-space">
4242
<XRHitTest onResults={onResults.bind(null, state.inputSource.handedness)} />
4343
</XRSpace>
4444
</>

examples/room-with-shadows/src/App.jsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,21 @@ function Light() {
2525
)
2626
}
2727

28-
const store = createXRStore()
28+
const store = createXRStore({
29+
emulate: {
30+
headset: {
31+
position: [0, 1, 0],
32+
},
33+
controller: {
34+
left: {
35+
position: [-0.2, 1, -0.3],
36+
},
37+
right: {
38+
position: [0.2, 1, -0.3],
39+
},
40+
},
41+
},
42+
})
2943

3044
export default function App() {
3145
return (
@@ -66,7 +80,6 @@ export default function App() {
6680
<Sphere position={[2, 4, -8]} scale={0.9} />
6781
<Sphere position={[-2, 2, -8]} scale={0.8} />
6882
<Sky inclination={0.52} scale={20} />
69-
<XROrigin scale={2} position={[-3.5, -1.85, 3.5]} />
7083
</XR>
7184
</Canvas>
7285
</>

examples/secondary-input-sources/app.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ const store = createXRStore({
1111
controller: () => {
1212
// eslint-disable-next-line react-hooks/rules-of-hooks
1313
const hasHands = useXR((xr) => xr.inputSourceStates.find((state) => state.type === 'hand') != null)
14-
// eslint-disable-next-line react-hooks/rules-of-hooks
15-
const controllerState = useXRInputSourceStateContext()
1614
return (
1715
<>
18-
<XRSpace space={controllerState.inputSource.targetRaySpace}>
16+
<XRSpace space="target-ray-space">
1917
<Suspense>
2018
<Gltf
2119
rotation-x={(-20 / 180) * Math.PI}

packages/react/xr/src/controller.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { createPortal, useFrame } from '@react-three/fiber'
1616
import { Object3D } from 'three'
1717
import { useXRInputSourceStateContext } from './input.js'
18+
import { XRSpace } from './space.js'
1819

1920
/**
2021
* component for placing content in the controller anchored at a specific component such as the Thumbstick
@@ -88,7 +89,11 @@ export const XRControllerModel = forwardRef<Object3D, XRControllerModelOptions>(
8889
[model, state.layout, state.gamepad],
8990
)
9091
useFrame(update)
91-
return <primitive object={model} />
92+
return (
93+
<XRSpace space="grip-space">
94+
<primitive object={model} />
95+
</XRSpace>
96+
)
9297
})
9398

9499
const LoadXRControllerLayoutSymbol = Symbol('loadXRControllerLayout')

packages/react/xr/src/default.tsx

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
useRayPointer,
3030
useTouchPointer,
3131
} from './pointer.js'
32-
import { XRSpace as XRSpaceImpl } from './space.js'
32+
import { XRSpace as XRSpaceImpl, XRSpaceType } from './space.js'
3333
import { xrInputSourceStateContext } from './contexts.js'
3434
import { TeleportPointerRayModel } from './teleport.js'
3535
import { createPortal, useFrame, useThree } from '@react-three/fiber'
@@ -51,7 +51,7 @@ export {
5151

5252
function DefaultXRInputSourceGrabPointer(
5353
event: 'select' | 'squeeze',
54-
getSpace: (source: XRInputSource) => XRSpace,
54+
spaceType: XRSpaceType,
5555
options: DefaultXRInputSourceGrabPointerOptions,
5656
) {
5757
const state = useContext(xrInputSourceStateContext)
@@ -63,7 +63,7 @@ function DefaultXRInputSourceGrabPointer(
6363
usePointerXRInputSourceEvents(pointer, state.inputSource, event, state.events)
6464
const cursorModelOptions = options.cursorModel
6565
return (
66-
<XRSpaceImpl ref={ref} space={getSpace(state.inputSource)}>
66+
<XRSpaceImpl ref={ref} space={spaceType}>
6767
{cursorModelOptions !== false && (
6868
<PointerCursorModel pointer={pointer} opacity={defaultGrabPointerOpacity} {...spreadable(cursorModelOptions)} />
6969
)}
@@ -82,11 +82,7 @@ function DefaultXRInputSourceGrabPointer(
8282
* - `cursorModel` properties for configuring how the cursor should look
8383
* - `radius` the size of the intersection sphere
8484
*/
85-
export const DefaultXRHandGrabPointer = DefaultXRInputSourceGrabPointer.bind(
86-
null,
87-
'select',
88-
(inputSource) => inputSource.hand!.get('index-finger-tip')!,
89-
)
85+
export const DefaultXRHandGrabPointer = DefaultXRInputSourceGrabPointer.bind(null, 'select', 'index-finger-tip')
9086

9187
/**
9288
* grab pointer for the XRController
@@ -99,11 +95,7 @@ export const DefaultXRHandGrabPointer = DefaultXRInputSourceGrabPointer.bind(
9995
* - `cursorModel` properties for configuring how the cursor should look
10096
* - `radius` the size of the intersection sphere
10197
*/
102-
export const DefaultXRControllerGrabPointer = DefaultXRInputSourceGrabPointer.bind(
103-
null,
104-
'squeeze',
105-
(inputSource) => inputSource.gripSpace!,
106-
)
98+
export const DefaultXRControllerGrabPointer = DefaultXRInputSourceGrabPointer.bind(null, 'squeeze', 'grip-space')
10799

108100
/**
109101
* ray pointer for the XRInputSource
@@ -128,7 +120,7 @@ export function DefaultXRInputSourceRayPointer(options: DefaultXRInputSourceRayP
128120
const rayModelOptions = options.rayModel
129121
const cursorModelOptions = options.cursorModel
130122
return (
131-
<XRSpaceImpl ref={ref} space={state.inputSource.targetRaySpace}>
123+
<XRSpaceImpl ref={ref} space="target-ray-space">
132124
{rayModelOptions !== false && (
133125
<PointerRayModel pointer={pointer} opacity={defaultRayPointerOpacity} {...spreadable(rayModelOptions)} />
134126
)}
@@ -345,7 +337,7 @@ export function DefaultXRInputSourceTeleportPointer(options: DefaultXRInputSourc
345337
})
346338
return (
347339
<>
348-
<XRSpaceImpl ref={ref} space={state.inputSource.targetRaySpace} />
340+
<XRSpaceImpl ref={ref} space="target-ray-space" />
349341
{createPortal(
350342
<group ref={groupRef}>
351343
{rayModelOptions !== false && (

packages/react/xr/src/elements.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ function XRControllers() {
6868
return null
6969
}
7070
return (
71-
<XRSpace key={state.id} space={state.inputSource.gripSpace!}>
72-
<xrInputSourceStateContext.Provider value={state}>
71+
<xrInputSourceStateContext.Provider key={state.id} value={state}>
72+
<XRSpace space="target-ray-space">
7373
<Suspense>
7474
{typeof ResolvedImpl === 'function' ? <ResolvedImpl /> : <DefaultXRController {...ResolvedImpl} />}
7575
</Suspense>
76-
</xrInputSourceStateContext.Provider>
77-
</XRSpace>
76+
</XRSpace>
77+
</xrInputSourceStateContext.Provider>
7878
)
7979
})}
8080
</>
@@ -95,13 +95,13 @@ function XRHands() {
9595
return null
9696
}
9797
return (
98-
<XRSpace key={objectToKey(state)} space={state.inputSource.hand.get('wrist')!}>
99-
<xrInputSourceStateContext.Provider value={state}>
98+
<xrInputSourceStateContext.Provider key={objectToKey(state)} value={state}>
99+
<XRSpace space="target-ray-space">
100100
<Suspense>
101101
{typeof ResolvedImpl === 'function' ? <ResolvedImpl /> : <DefaultXRHand {...ResolvedImpl} />}
102102
</Suspense>
103-
</xrInputSourceStateContext.Provider>
104-
</XRSpace>
103+
</XRSpace>
104+
</xrInputSourceStateContext.Provider>
105105
)
106106
})}
107107
</>
@@ -125,7 +125,7 @@ function XRTransientPointers() {
125125
return null
126126
}
127127
return (
128-
<XRSpace key={objectToKey(state)} space={state.inputSource.targetRaySpace}>
128+
<XRSpace key={objectToKey(state)} space="target-ray-space">
129129
<xrInputSourceStateContext.Provider value={state}>
130130
<Suspense>
131131
{typeof ResolvedImpl === 'function' ? (
@@ -152,7 +152,7 @@ function XRGazes() {
152152
<>
153153
{gazeStates.map((state) => {
154154
return (
155-
<XRSpace key={objectToKey(state)} space={state.inputSource.targetRaySpace}>
155+
<XRSpace key={objectToKey(state)} space="target-ray-space">
156156
<xrInputSourceStateContext.Provider value={state}>
157157
<Suspense>
158158
{typeof Implementation === 'function' ? (
@@ -179,7 +179,7 @@ function XRScreenInputs() {
179179
<>
180180
{screenInputStates.map((state) => {
181181
return (
182-
<XRSpace key={objectToKey(state)} space={state.inputSource.targetRaySpace}>
182+
<XRSpace key={objectToKey(state)} space="target-ray-space">
183183
<xrInputSourceStateContext.Provider value={state}>
184184
<Suspense>
185185
{typeof Implementation === 'function' ? (

packages/xr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"three": "*"
3131
},
3232
"dependencies": {
33-
"@iwer/devui": "^0.1.0",
33+
"@iwer/devui": "^0.2.0",
3434
"@pmndrs/pointer-events": "workspace:^",
3535
"iwer": "^1.0.3",
3636
"meshline": "^3.3.1",

packages/xr/src/emulate.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,110 @@
11
import { XRDevice, metaQuest3, metaQuest2, metaQuestPro, oculusQuest1 } from 'iwer'
22
import { DevUI } from '@iwer/devui'
3+
import type { XRDeviceOptions } from 'iwer/lib/device/XRDevice'
4+
import { Euler, Quaternion, Vector3, Vector3Tuple, Vector4Tuple } from 'three'
35

46
const configurations = { metaQuest3, metaQuest2, metaQuestPro, oculusQuest1 }
57

68
export type EmulatorType = keyof typeof configurations
79

8-
export function emulate(type: EmulatorType) {
9-
const xrdevice = new XRDevice(configurations[type])
10-
xrdevice.ipd = 0
10+
export type EmulatorTransformationOptions = {
11+
position?: Vector3 | Vector3Tuple
12+
rotation?: Euler | Vector3Tuple
13+
quaternion?: Quaternion | Vector4Tuple
14+
}
15+
16+
export type EmulatorOptions =
17+
| EmulatorType
18+
| ({
19+
type?: EmulatorType
20+
primaryInputMode?: XRDevice['primaryInputMode']
21+
headset?: EmulatorTransformationOptions
22+
controller?: Partial<Record<XRHandedness, EmulatorTransformationOptions>>
23+
hand?: Partial<Record<XRHandedness, EmulatorTransformationOptions>>
24+
} & Partial<Pick<XRDeviceOptions, 'ipd' | 'fovy' | 'stereoEnabled' | 'canvasContainer'>>)
25+
26+
const handednessList: Array<XRHandedness> = ['left', 'none', 'right']
27+
28+
export function emulate(options: EmulatorOptions) {
29+
const type = typeof options === 'string' ? options : (options.type ?? 'metaQuest3')
30+
const xrdevice = new XRDevice(configurations[type], typeof options === 'string' ? undefined : options)
31+
if (typeof options != 'string') {
32+
applyEmulatorTransformOptions(xrdevice, options.headset)
33+
applyEmulatorInputSourcesOptions(xrdevice.hands, options.hand)
34+
applyEmulatorInputSourcesOptions(xrdevice.controllers, options.controller)
35+
xrdevice.primaryInputMode = options.primaryInputMode ?? 'controller'
36+
}
37+
xrdevice.ipd = typeof options === 'string' ? 0 : (options.ipd ?? 0)
1138
xrdevice.installRuntime()
1239
new DevUI(xrdevice)
40+
return xrdevice
41+
}
42+
43+
const eulerHelper = new Euler()
44+
const quaternionHelper = new Quaternion()
45+
46+
function applyEmulatorInputSourcesOptions(
47+
xrInputSources: XRDevice['controllers'] | XRDevice['hands'],
48+
options: Partial<Record<XRHandedness, EmulatorTransformationOptions>> | undefined,
49+
) {
50+
if (options == null) {
51+
return
52+
}
53+
for (const handedness of handednessList) {
54+
applyEmulatorTransformOptions(xrInputSources[handedness], options[handedness])
55+
}
56+
}
57+
58+
function applyEmulatorTransformOptions(
59+
target: XRDevice['controllers']['left'] | XRDevice['hands']['left'] | XRDevice,
60+
options: EmulatorTransformationOptions | undefined,
61+
) {
62+
if (target == null || options == null) {
63+
return
64+
}
65+
setVector(target.position, options.position)
66+
setVector(eulerHelper, options.rotation)
67+
setQuaternion(target.quaternion, quaternionHelper.setFromEuler(eulerHelper))
68+
setQuaternion(target.quaternion, options.quaternion)
69+
}
70+
71+
function setVector(
72+
target: { x: number; y: number; z: number } | Euler,
73+
value: Euler | Vector3 | Vector3Tuple | undefined,
74+
) {
75+
if (value == null) {
76+
return
77+
}
78+
if (value instanceof Euler && target instanceof Euler) {
79+
target.copy(value)
80+
}
81+
if (Array.isArray(value)) {
82+
target.x = value[0]
83+
target.y = value[1]
84+
target.z = value[2]
85+
return
86+
}
87+
target.x = value.x
88+
target.y = value.y
89+
target.z = value.z
90+
}
91+
92+
function setQuaternion(
93+
target: { x: number; y: number; z: number; w: number },
94+
value: Quaternion | Vector4Tuple | undefined,
95+
) {
96+
if (value == null) {
97+
return
98+
}
99+
if (Array.isArray(value)) {
100+
target.x = value[0]
101+
target.y = value[1]
102+
target.z = value[2]
103+
target.w = value[3]
104+
return
105+
}
106+
target.x = value.x
107+
target.y = value.y
108+
target.z = value.z
109+
target.w = value.w
13110
}

0 commit comments

Comments
 (0)