Skip to content

Commit 8005cb1

Browse files
committed
feat: anchor, iwer emulate, hit test, dom overlay
fix: xr screen input, teleport pointer enabled by default
1 parent 5369e7f commit 8005cb1

26 files changed

+787
-266
lines changed

packages/xr/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@
2626
"fix:prettier": "prettier --write src",
2727
"fix:eslint": "eslint 'src/**/*.ts' --fix"
2828
},
29+
"peerDependencies": {
30+
"three": "*"
31+
},
2932
"dependencies": {
33+
"@iwer/devui": "^0.1.0",
3034
"@pmndrs/pointer-events": "workspace:^",
35+
"iwer": "^1.0.3",
3136
"meshline": "^3.3.1",
3237
"zustand": "^4.5.2"
3338
},

packages/xr/src/anchor.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Matrix4, Quaternion, Vector3 } from 'three'
2+
import { XRStore } from './store.js'
3+
4+
const OneVector = new Vector3(1, 1, 1)
5+
const ZeroVector = new Vector3(1, 1, 1)
6+
const NeutralQuaternion = new Quaternion()
7+
8+
const matrixHelper1 = new Matrix4()
9+
const matrixHelper2 = new Matrix4()
10+
const quaternionHelper = new Quaternion()
11+
const positionHelper = new Vector3()
12+
13+
const directionHelper = new Vector3()
14+
15+
/*
16+
export async function loadXRPersistentAnchor(session: XRSession, id: string): Promise<XRAnchor | undefined> {
17+
const anchorId = localStorage.getItem(id)
18+
if (anchorId == null || session == null) {
19+
return undefined
20+
}
21+
if (!('restorePersistentAnchor' in session)) {
22+
console.warn(`"restorePersistentAnchor" not supported`)
23+
return undefined
24+
}
25+
return (session.restorePersistentAnchor as (id: string) => Promise<XRAnchor>)(anchorId)
26+
}
27+
28+
export async function requestXRPersistentAnchor(
29+
store: XRStore<any>,
30+
id: string,
31+
options: XRAnchorOptions,
32+
abortRef?: { current: boolean },
33+
): Promise<XRAnchor | undefined> {
34+
const anchor = await requestXRAnchor(store, options)
35+
if (anchor == null || abortRef?.current === true) {
36+
return undefined
37+
}
38+
if (!('requestPersistentHandle' in anchor)) {
39+
console.warn(`"requestPersistentHandle" not supported`)
40+
return undefined
41+
}
42+
const anchorHandle = await (anchor.requestPersistentHandle as () => Promise<string>)()
43+
if (anchor == null || (abortRef?.current as boolean | undefined) === true) {
44+
return undefined
45+
}
46+
localStorage.setItem(id, anchorHandle)
47+
return anchor
48+
}
49+
50+
export async function deleteXRPersistentAnchor(store: XRStore<any>, id: string) {
51+
const { session } = store.getState()
52+
if (session == null) {
53+
return
54+
}
55+
if (!('deletePersistentAnchor' in session)) {
56+
console.warn(`"deletePersistentAnchor" not supported`)
57+
return undefined
58+
}
59+
return (session.deletePersistentAnchor as (id: string) => Promise<undefined>)(id)
60+
}*/
61+
62+
export type XRAnchorOptions = XRAnchorWorldOptions | XRAnchorSpaceOptions | XRAnchorHitTestResultOptions
63+
64+
export type XRAnchorWorldOptions = {
65+
relativeTo: 'world'
66+
worldPosition: Vector3
67+
worldQuaternion: Quaternion
68+
frame?: XRFrame
69+
}
70+
71+
export type XRAnchorHitTestResultOptions = {
72+
relativeTo: 'hit-test-result'
73+
hitTestResult: XRHitTestResult
74+
offsetPosition?: Vector3
75+
offsetQuaternion?: Quaternion
76+
}
77+
export type XRAnchorSpaceOptions = {
78+
relativeTo: 'space'
79+
space: XRSpace
80+
offsetPosition?: Vector3
81+
offsetQuaternion?: Quaternion
82+
frame?: XRFrame
83+
}
84+
85+
export async function requestXRAnchor(store: XRStore<any>, options: XRAnchorOptions): Promise<XRAnchor | undefined> {
86+
if (options.relativeTo === 'hit-test-result') {
87+
directionHelper.set(0, 0, 1)
88+
if (options.offsetQuaternion != null) {
89+
directionHelper.applyQuaternion(options.offsetQuaternion)
90+
}
91+
return options.hitTestResult.createAnchor?.(
92+
new XRRigidTransform({ ...options.offsetPosition }, { ...directionHelper }),
93+
)
94+
}
95+
let frame: XRFrame
96+
let space: XRSpace
97+
if (options.relativeTo === 'world') {
98+
frame = options.frame ?? (await store.requestFrame())
99+
const { origin, originReferenceSpace } = store.getState()
100+
if (originReferenceSpace == null) {
101+
return undefined
102+
}
103+
space = originReferenceSpace
104+
const { worldPosition, worldQuaternion } = options
105+
if (origin != null) {
106+
//compute vectorHelper and quaternionHelper in the local space of the origin
107+
matrixHelper1.copy(origin.matrixWorld).invert()
108+
matrixHelper2.compose(worldPosition, worldQuaternion, OneVector).multiply(matrixHelper1)
109+
matrixHelper2.decompose(positionHelper, quaternionHelper, directionHelper)
110+
111+
quaternionHelper.setFromRotationMatrix(matrixHelper2)
112+
} else {
113+
positionHelper.copy(worldPosition)
114+
quaternionHelper.copy(worldQuaternion)
115+
}
116+
} else {
117+
frame = options.frame ?? (await store.requestFrame())
118+
space = options.space
119+
const { offsetPosition, offsetQuaternion } = options
120+
positionHelper.copy(offsetPosition ?? ZeroVector)
121+
quaternionHelper.copy(offsetQuaternion ?? NeutralQuaternion)
122+
}
123+
directionHelper.set(0, 0, 1)
124+
directionHelper.applyQuaternion(quaternionHelper)
125+
return frame.createAnchor?.(new XRRigidTransform({ ...positionHelper }, { ...directionHelper }), space)
126+
}

packages/xr/src/controller/layout.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,15 @@ type XRControllerProfilesList = Record<string, { path: string }>
5050
const DefaultDefaultControllerProfileId = 'generic-trigger'
5151

5252
export type XRControllerLayoutLoaderOptions = {
53+
/**
54+
* where to load the controller profiles and models from
55+
* @default 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/'
56+
*/
5357
baseAssetPath?: string
58+
/**
59+
* profile id that is used if no matching profile id is found
60+
* @default 'generic-trigger'
61+
*/
5462
defaultControllerProfileId?: string
5563
}
5664

packages/xr/src/controller/model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ export async function loadXRControllerModel(layout: XRControllerLayout, loader:
99
}
1010

1111
export type XRControllerModelOptions = {
12+
/**
13+
* allows to configure whether the controller is rendered to the color buffer
14+
* can be used to show the real controller in AR passthrough mode
15+
*/
1216
colorWrite?: boolean
17+
/**
18+
* allows to configure the render order of the controller model
19+
* @default undefined
20+
*/
1321
renderOrder?: number
1422
}
1523

packages/xr/src/controller/state.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1-
import { Object3D } from 'three'
21
import { XRControllerGamepadState, updateXRControllerGamepadState } from './gamepad.js'
3-
import { XRControllerLayout, XRControllerLayoutLoader } from './layout.js'
4-
5-
export type XRControllerState = {
6-
type: 'controller'
7-
inputSource: XRInputSource
8-
gamepad: XRControllerGamepadState
9-
layout: XRControllerLayout
10-
object?: Object3D
11-
}
2+
import { XRControllerLayoutLoader } from './layout.js'
3+
import { XRControllerState } from '../input.js'
124

135
export async function createXRControllerState(
146
inputSource: XRInputSource,
157
layoutLoader: XRControllerLayoutLoader,
8+
events: ReadonlyArray<XRInputSourceEvent>,
169
): Promise<XRControllerState> {
1710
const layout = await layoutLoader.load(inputSource.profiles, inputSource.handedness)
1811
const gamepad: XRControllerGamepadState = {}
1912
updateXRControllerGamepadState(gamepad, inputSource, layout)
2013
return {
14+
events,
2115
type: 'controller',
2216
inputSource,
2317
gamepad,

packages/xr/src/default.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,29 @@ export type DefaultXRHandTouchPointerOptions = TouchPointerOptions & {
2222
}
2323

2424
export type DefaultXRControllerOptions = {
25+
/**
26+
* provide options to the <XRControllerModel/>
27+
* `false` disables the model
28+
* @default true
29+
*/
2530
model?: boolean | XRControllerModelOptions
31+
/**
32+
* provide options to the <DefaultXRInputSourceGrabPointer/>
33+
* `false` disables the grab pointer
34+
* @default true
35+
*/
2636
grabPointer?: boolean | DefaultXRInputSourceGrabPointerOptions
37+
/**
38+
* provide options to the <DefaultXRInputSourceRayPointer/>
39+
* `false` disables the ray pointer
40+
* @default true
41+
*/
2742
rayPointer?: boolean | DefaultXRInputSourceRayPointerOptions
43+
/**
44+
* provide options to the <DefaultXRInputSourceTeleportPointer/>
45+
* `false` disables the teleport pointer
46+
* @default false
47+
*/
2848
teleportPointer?: boolean | DefaultXRInputSourceTeleportPointerOptions
2949
}
3050

packages/xr/src/emulate.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { XRDevice, metaQuest3, metaQuest2, metaQuestPro, oculusQuest1 } from 'iwer'
2+
import { DevUI } from '@iwer/devui'
3+
4+
const configurations = { metaQuest3, metaQuest2, metaQuestPro, oculusQuest1 }
5+
6+
export type EmulatorType = keyof typeof configurations
7+
8+
export function emulate(type: EmulatorType) {
9+
const xrdevice = new XRDevice(configurations[type])
10+
xrdevice.ipd = 0
11+
xrdevice.installRuntime()
12+
new DevUI(xrdevice)
13+
}

packages/xr/src/hand/model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ import { clone as cloneSkeleton } from 'three/examples/jsm/utils/SkeletonUtils.j
88
const DefaultDefaultXRHandProfileId = 'generic-hand'
99

1010
export type XRHandLoaderOptions = {
11+
/**
12+
* where to load the hand models from
13+
* @default 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/'
14+
*/
1115
baseAssetPath?: string
16+
/**
17+
* profile id that is used if no matching profile id is found
18+
* @default 'generic-hand'
19+
*/
1220
defaultXRHandProfileId?: string
1321
}
1422

packages/xr/src/hand/state.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
import { Object3D, WebXRManager } from 'three'
2-
import { XRHandPoseState, createHandPoseState, updateXRHandPoseState } from './pose.js'
1+
import { WebXRManager } from 'three'
2+
import { createHandPoseState, updateXRHandPoseState } from './pose.js'
33
import { XRHandLoaderOptions, getXRHandAssetPath } from './model.js'
4+
import type { XRHandState } from '../input.js'
45

56
export type XRHandInputSource = XRInputSource & { hand: XRHand }
67

78
export function isXRHandInputSource(inputSource: XRInputSource): inputSource is XRHandInputSource {
89
return inputSource.hand != null
910
}
1011

11-
export type XRHandState = {
12-
type: 'hand'
13-
inputSource: XRHandInputSource
14-
pose: XRHandPoseState
15-
assetPath: string
16-
object?: Object3D
17-
}
18-
19-
export function createXRHandState(inputSource: XRInputSource, options: XRHandLoaderOptions | undefined): XRHandState {
12+
export function createXRHandState(
13+
inputSource: XRInputSource,
14+
options: XRHandLoaderOptions | undefined,
15+
events: ReadonlyArray<XRInputSourceEvent>,
16+
): XRHandState {
2017
return {
2118
type: 'hand',
2219
inputSource: inputSource as XRHandInputSource,
2320
pose: createHandPoseState(inputSource.hand!),
2421
assetPath: getXRHandAssetPath(inputSource.handedness, options),
22+
events,
2523
}
2624
}
2725

packages/xr/src/hand/visual.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Object3D } from 'three'
2-
import { GetXRSpace } from '../space.js'
32

43
const joints: Array<XRHandJoint> = [
54
'wrist',
@@ -32,7 +31,7 @@ const joints: Array<XRHandJoint> = [
3231
export function createUpdateXRHandVisuals(
3332
hand: XRHand,
3433
handModel: Object3D,
35-
referenceSpace: GetXRSpace,
34+
referenceSpace: XRSpace | (() => XRSpace | undefined),
3635
): (frame: XRFrame | undefined) => void {
3736
const buffer = new Float32Array(hand.size * 16)
3837
const jointObjects = joints.map((joint) => {
@@ -44,11 +43,8 @@ export function createUpdateXRHandVisuals(
4443
return jointObject
4544
})
4645
return (frame) => {
47-
if (frame == null) {
48-
return
49-
}
5046
const resolvedReferenceSpace = typeof referenceSpace === 'function' ? referenceSpace() : referenceSpace
51-
if (resolvedReferenceSpace == null) {
47+
if (frame == null || resolvedReferenceSpace == null) {
5248
return
5349
}
5450
frame.fillPoses(hand.values(), resolvedReferenceSpace, buffer)

0 commit comments

Comments
 (0)