Skip to content

Commit

Permalink
feat: support emulator options; breaking change: set default space in…
Browse files Browse the repository at this point in the history
… hand and controller to target ray space to align better with the webxr standard
  • Loading branch information
bbohlender committed Nov 1, 2024
1 parent 432aff6 commit f4b5f2d
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 93 deletions.
2 changes: 1 addition & 1 deletion examples/hit-test-anchor/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const store = createXRStore({
return (
<>
<XRHandModel />
<XRSpace space={state.inputSource.targetRaySpace}>
<XRSpace space="target-ray-space">
<XRHitTest onResults={onResults.bind(null, state.inputSource.handedness)} />
</XRSpace>
</>
Expand Down
17 changes: 15 additions & 2 deletions examples/room-with-shadows/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,21 @@ function Light() {
)
}

const store = createXRStore()
const store = createXRStore({
emulate: {
headset: {
position: [0, 1, 0],
},
controller: {
left: {
position: [-0.2, 1, -0.3],
},
right: {
position: [0.2, 1, -0.3],
},
},
},
})

export default function App() {
return (
Expand Down Expand Up @@ -66,7 +80,6 @@ export default function App() {
<Sphere position={[2, 4, -8]} scale={0.9} />
<Sphere position={[-2, 2, -8]} scale={0.8} />
<Sky inclination={0.52} scale={20} />
<XROrigin scale={2} position={[-3.5, -1.85, 3.5]} />
</XR>
</Canvas>
</>
Expand Down
4 changes: 1 addition & 3 deletions examples/secondary-input-sources/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ const store = createXRStore({
controller: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const hasHands = useXR((xr) => xr.inputSourceStates.find((state) => state.type === 'hand') != null)
// eslint-disable-next-line react-hooks/rules-of-hooks
const controllerState = useXRInputSourceStateContext()
return (
<>
<XRSpace space={controllerState.inputSource.targetRaySpace}>
<XRSpace space="target-ray-space">
<Suspense>
<Gltf
rotation-x={(-20 / 180) * Math.PI}
Expand Down
7 changes: 6 additions & 1 deletion packages/react/xr/src/controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { createPortal, useFrame } from '@react-three/fiber'
import { Object3D } from 'three'
import { useXRInputSourceStateContext } from './input.js'
import { XRSpace } from './space.js'

/**
* component for placing content in the controller anchored at a specific component such as the Thumbstick
Expand Down Expand Up @@ -88,7 +89,11 @@ export const XRControllerModel = forwardRef<Object3D, XRControllerModelOptions>(
[model, state.layout, state.gamepad],
)
useFrame(update)
return <primitive object={model} />
return (
<XRSpace space="grip-space">
<primitive object={model} />
</XRSpace>
)
})

const LoadXRControllerLayoutSymbol = Symbol('loadXRControllerLayout')
Expand Down
22 changes: 7 additions & 15 deletions packages/react/xr/src/default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
useRayPointer,
useTouchPointer,
} from './pointer.js'
import { XRSpace as XRSpaceImpl } from './space.js'
import { XRSpace as XRSpaceImpl, XRSpaceType } from './space.js'
import { xrInputSourceStateContext } from './contexts.js'
import { TeleportPointerRayModel } from './teleport.js'
import { createPortal, useFrame, useThree } from '@react-three/fiber'
Expand All @@ -51,7 +51,7 @@ export {

function DefaultXRInputSourceGrabPointer(
event: 'select' | 'squeeze',
getSpace: (source: XRInputSource) => XRSpace,
spaceType: XRSpaceType,
options: DefaultXRInputSourceGrabPointerOptions,
) {
const state = useContext(xrInputSourceStateContext)
Expand All @@ -63,7 +63,7 @@ function DefaultXRInputSourceGrabPointer(
usePointerXRInputSourceEvents(pointer, state.inputSource, event, state.events)
const cursorModelOptions = options.cursorModel
return (
<XRSpaceImpl ref={ref} space={getSpace(state.inputSource)}>
<XRSpaceImpl ref={ref} space={spaceType}>
{cursorModelOptions !== false && (
<PointerCursorModel pointer={pointer} opacity={defaultGrabPointerOpacity} {...spreadable(cursorModelOptions)} />
)}
Expand All @@ -82,11 +82,7 @@ function DefaultXRInputSourceGrabPointer(
* - `cursorModel` properties for configuring how the cursor should look
* - `radius` the size of the intersection sphere
*/
export const DefaultXRHandGrabPointer = DefaultXRInputSourceGrabPointer.bind(
null,
'select',
(inputSource) => inputSource.hand!.get('index-finger-tip')!,
)
export const DefaultXRHandGrabPointer = DefaultXRInputSourceGrabPointer.bind(null, 'select', 'index-finger-tip')

/**
* grab pointer for the XRController
Expand All @@ -99,11 +95,7 @@ export const DefaultXRHandGrabPointer = DefaultXRInputSourceGrabPointer.bind(
* - `cursorModel` properties for configuring how the cursor should look
* - `radius` the size of the intersection sphere
*/
export const DefaultXRControllerGrabPointer = DefaultXRInputSourceGrabPointer.bind(
null,
'squeeze',
(inputSource) => inputSource.gripSpace!,
)
export const DefaultXRControllerGrabPointer = DefaultXRInputSourceGrabPointer.bind(null, 'squeeze', 'grip-space')

/**
* ray pointer for the XRInputSource
Expand All @@ -128,7 +120,7 @@ export function DefaultXRInputSourceRayPointer(options: DefaultXRInputSourceRayP
const rayModelOptions = options.rayModel
const cursorModelOptions = options.cursorModel
return (
<XRSpaceImpl ref={ref} space={state.inputSource.targetRaySpace}>
<XRSpaceImpl ref={ref} space="target-ray-space">
{rayModelOptions !== false && (
<PointerRayModel pointer={pointer} opacity={defaultRayPointerOpacity} {...spreadable(rayModelOptions)} />
)}
Expand Down Expand Up @@ -345,7 +337,7 @@ export function DefaultXRInputSourceTeleportPointer(options: DefaultXRInputSourc
})
return (
<>
<XRSpaceImpl ref={ref} space={state.inputSource.targetRaySpace} />
<XRSpaceImpl ref={ref} space="target-ray-space" />
{createPortal(
<group ref={groupRef}>
{rayModelOptions !== false && (
Expand Down
22 changes: 11 additions & 11 deletions packages/react/xr/src/elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ function XRControllers() {
return null
}
return (
<XRSpace key={state.id} space={state.inputSource.gripSpace!}>
<xrInputSourceStateContext.Provider value={state}>
<xrInputSourceStateContext.Provider key={state.id} value={state}>
<XRSpace space="target-ray-space">
<Suspense>
{typeof ResolvedImpl === 'function' ? <ResolvedImpl /> : <DefaultXRController {...ResolvedImpl} />}
</Suspense>
</xrInputSourceStateContext.Provider>
</XRSpace>
</XRSpace>
</xrInputSourceStateContext.Provider>
)
})}
</>
Expand All @@ -95,13 +95,13 @@ function XRHands() {
return null
}
return (
<XRSpace key={objectToKey(state)} space={state.inputSource.hand.get('wrist')!}>
<xrInputSourceStateContext.Provider value={state}>
<xrInputSourceStateContext.Provider key={objectToKey(state)} value={state}>
<XRSpace space="target-ray-space">
<Suspense>
{typeof ResolvedImpl === 'function' ? <ResolvedImpl /> : <DefaultXRHand {...ResolvedImpl} />}
</Suspense>
</xrInputSourceStateContext.Provider>
</XRSpace>
</XRSpace>
</xrInputSourceStateContext.Provider>
)
})}
</>
Expand All @@ -125,7 +125,7 @@ function XRTransientPointers() {
return null
}
return (
<XRSpace key={objectToKey(state)} space={state.inputSource.targetRaySpace}>
<XRSpace key={objectToKey(state)} space="target-ray-space">
<xrInputSourceStateContext.Provider value={state}>
<Suspense>
{typeof ResolvedImpl === 'function' ? (
Expand All @@ -152,7 +152,7 @@ function XRGazes() {
<>
{gazeStates.map((state) => {
return (
<XRSpace key={objectToKey(state)} space={state.inputSource.targetRaySpace}>
<XRSpace key={objectToKey(state)} space="target-ray-space">
<xrInputSourceStateContext.Provider value={state}>
<Suspense>
{typeof Implementation === 'function' ? (
Expand All @@ -179,7 +179,7 @@ function XRScreenInputs() {
<>
{screenInputStates.map((state) => {
return (
<XRSpace key={objectToKey(state)} space={state.inputSource.targetRaySpace}>
<XRSpace key={objectToKey(state)} space="target-ray-space">
<xrInputSourceStateContext.Provider value={state}>
<Suspense>
{typeof Implementation === 'function' ? (
Expand Down
2 changes: 1 addition & 1 deletion packages/xr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"three": "*"
},
"dependencies": {
"@iwer/devui": "^0.1.0",
"@iwer/devui": "^0.2.0",
"@pmndrs/pointer-events": "workspace:^",
"iwer": "^1.0.3",
"meshline": "^3.3.1",
Expand Down
103 changes: 100 additions & 3 deletions packages/xr/src/emulate.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,110 @@
import { XRDevice, metaQuest3, metaQuest2, metaQuestPro, oculusQuest1 } from 'iwer'
import { DevUI } from '@iwer/devui'
import type { XRDeviceOptions } from 'iwer/lib/device/XRDevice'
import { Euler, Quaternion, Vector3, Vector3Tuple, Vector4Tuple } from 'three'

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

export type EmulatorType = keyof typeof configurations

export function emulate(type: EmulatorType) {
const xrdevice = new XRDevice(configurations[type])
xrdevice.ipd = 0
export type EmulatorTransformationOptions = {
position?: Vector3 | Vector3Tuple
rotation?: Euler | Vector3Tuple
quaternion?: Quaternion | Vector4Tuple
}

export type EmulatorOptions =
| EmulatorType
| ({
type?: EmulatorType
primaryInputMode?: XRDevice['primaryInputMode']
headset?: EmulatorTransformationOptions
controller?: Partial<Record<XRHandedness, EmulatorTransformationOptions>>
hand?: Partial<Record<XRHandedness, EmulatorTransformationOptions>>
} & Partial<Pick<XRDeviceOptions, 'ipd' | 'fovy' | 'stereoEnabled' | 'canvasContainer'>>)

const handednessList: Array<XRHandedness> = ['left', 'none', 'right']

export function emulate(options: EmulatorOptions) {
const type = typeof options === 'string' ? options : (options.type ?? 'metaQuest3')
const xrdevice = new XRDevice(configurations[type], typeof options === 'string' ? undefined : options)
if (typeof options != 'string') {
applyEmulatorTransformOptions(xrdevice, options.headset)
applyEmulatorInputSourcesOptions(xrdevice.hands, options.hand)
applyEmulatorInputSourcesOptions(xrdevice.controllers, options.controller)
xrdevice.primaryInputMode = options.primaryInputMode ?? 'controller'
}
xrdevice.ipd = typeof options === 'string' ? 0 : (options.ipd ?? 0)
xrdevice.installRuntime()
new DevUI(xrdevice)
return xrdevice
}

const eulerHelper = new Euler()
const quaternionHelper = new Quaternion()

function applyEmulatorInputSourcesOptions(
xrInputSources: XRDevice['controllers'] | XRDevice['hands'],
options: Partial<Record<XRHandedness, EmulatorTransformationOptions>> | undefined,
) {
if (options == null) {
return
}
for (const handedness of handednessList) {
applyEmulatorTransformOptions(xrInputSources[handedness], options[handedness])
}
}

function applyEmulatorTransformOptions(
target: XRDevice['controllers']['left'] | XRDevice['hands']['left'] | XRDevice,
options: EmulatorTransformationOptions | undefined,
) {
if (target == null || options == null) {
return
}
setVector(target.position, options.position)
setVector(eulerHelper, options.rotation)
setQuaternion(target.quaternion, quaternionHelper.setFromEuler(eulerHelper))
setQuaternion(target.quaternion, options.quaternion)
}

function setVector(
target: { x: number; y: number; z: number } | Euler,
value: Euler | Vector3 | Vector3Tuple | undefined,
) {
if (value == null) {
return
}
if (value instanceof Euler && target instanceof Euler) {
target.copy(value)
}
if (Array.isArray(value)) {
target.x = value[0]
target.y = value[1]
target.z = value[2]
return
}
target.x = value.x
target.y = value.y
target.z = value.z
}

function setQuaternion(
target: { x: number; y: number; z: number; w: number },
value: Quaternion | Vector4Tuple | undefined,
) {
if (value == null) {
return
}
if (Array.isArray(value)) {
target.x = value[0]
target.y = value[1]
target.z = value[2]
target.w = value[3]
return
}
target.x = value.x
target.y = value.y
target.z = value.z
target.w = value.w
}
Loading

0 comments on commit f4b5f2d

Please sign in to comment.