Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve xr controller model factory #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 31 additions & 31 deletions src/libs/MotionControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ interface GamepadIndices {
yAxis?: number
}

type ComponentState = 'default' | 'touched' | 'pressed'

type ComponentProperty = 'button' | 'xAxis' | 'yAxis' | 'state'

type ValueNodeProperty = 'transform' | 'visibility'

type ComponentType = 'trigger' | 'squeeze' | 'touchpad' | 'thumbstick' | 'button'

interface VisualResponseDescription {
componentProperty: string
states: string[]
valueNodeProperty: string
componentProperty: ComponentProperty
states: ComponentState[]
valueNodeProperty: ValueNodeProperty
valueNodeName: string
minNodeName?: string
maxNodeName?: string
Expand All @@ -22,7 +30,7 @@ interface VisualResponseDescription {
type VisualResponses = Record<string, VisualResponseDescription>

interface ComponentDescription {
type: string
type: ComponentType
gamepadIndices: GamepadIndices
rootNodeName: string
visualResponses: VisualResponses
Expand Down Expand Up @@ -58,28 +66,28 @@ const MotionControllerConstants = {
NONE: 'none',
LEFT: 'left',
RIGHT: 'right',
}),
} as const),

ComponentState: Object.freeze({
DEFAULT: 'default',
TOUCHED: 'touched',
PRESSED: 'pressed',
}),
} as const),

ComponentProperty: Object.freeze({
BUTTON: 'button',
X_AXIS: 'xAxis',
Y_AXIS: 'yAxis',
STATE: 'state',
}),
} as const),

ComponentType: Object.freeze({
TRIGGER: 'trigger',
SQUEEZE: 'squeeze',
TOUCHPAD: 'touchpad',
THUMBSTICK: 'thumbstick',
BUTTON: 'button',
}),
} as const),

ButtonTouchThreshold: 0.05,

Expand All @@ -88,7 +96,7 @@ const MotionControllerConstants = {
VisualResponseProperty: Object.freeze({
TRANSFORM: 'transform',
VISIBILITY: 'visibility',
}),
} as const),
}

/**
Expand Down Expand Up @@ -184,8 +192,15 @@ async function fetchProfile(
return { profile, assetPath }
}

interface ComponentValues {
button: number | undefined
state: ComponentState
xAxis: number | undefined
yAxis: number | undefined
}

/** @constant {Object} */
const defaultComponentValues = {
const defaultComponentValues: ComponentValues = {
xAxis: 0,
yAxis: 0,
button: 0,
Expand Down Expand Up @@ -235,10 +250,10 @@ function normalizeAxes(
*/
class VisualResponse implements VisualResponseDescription {
value: number | boolean
componentProperty: string
states: string[]
componentProperty: ComponentProperty
states: ComponentState[]
valueNodeName: string
valueNodeProperty: string
valueNodeProperty: ValueNodeProperty
minNodeName?: string
maxNodeName?: string
valueNode: Object3D | undefined
Expand Down Expand Up @@ -268,17 +283,7 @@ class VisualResponse implements VisualResponseDescription {
* @param {number | undefined} button - The reported value of the component's button
* @param {string} state - The component's active state
*/
updateFromComponent({
xAxis,
yAxis,
button,
state,
}: {
xAxis?: number
yAxis?: number
button?: number
state: string
}): void {
updateFromComponent({ xAxis, yAxis, button, state }: ComponentValues): void {
const { normalizedXAxis, normalizedYAxis } = normalizeAxes(xAxis, yAxis)
switch (this.componentProperty) {
case MotionControllerConstants.ComponentProperty.X_AXIS:
Expand All @@ -305,14 +310,9 @@ class VisualResponse implements VisualResponseDescription {

class Component implements ComponentDescription {
id: string
values: {
state: string
button: number | undefined
xAxis: number | undefined
yAxis: number | undefined
}
values: ComponentValues

type: string
type: ComponentType
gamepadIndices: GamepadIndices
rootNodeName: string
visualResponses: Record<string, VisualResponse>
Expand Down
152 changes: 85 additions & 67 deletions src/webxr/XRControllerModelFactory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Mesh, Object3D, SphereGeometry, MeshBasicMaterial } from 'three'
import type { Texture, Group } from 'three'
import type { Texture } from 'three'
// @ts-ignore
import { GLTFLoader } from '../loaders/GLTFLoader'
import { fetchProfile, MotionController, MotionControllerConstants } from '../libs/MotionControllers'
Expand All @@ -16,14 +16,17 @@ const applyEnvironmentMap = (envMap: Texture, obj: Object3D): void => {
})
}

class XRControllerModel extends Object3D {
export class XRControllerModel extends Object3D {
envMap: Texture | null
motionController: MotionController | null
scene: Object3D | null

constructor() {
super()

this.motionController = null
this.envMap = null
this.scene = null
}

setEnvironmentMap(envMap: Texture): XRControllerModel {
Expand All @@ -37,6 +40,28 @@ class XRControllerModel extends Object3D {
return this
}

addScene(scene: Object3D): void {
if (!this.motionController) {
console.warn('scene tried to add, but no motion controller')
return
}

this.scene = scene
addAssetSceneToControllerModel(this, scene)
this.dispatchEvent({
type: 'motionControllerLinkedToScene',
data: this.motionController,
})
}

addMotionController(motionController: MotionController): void {
this.motionController = motionController
this.dispatchEvent({
type: 'motionControllerCreated',
data: motionController,
})
}

/**
* Polls data from the XRInputSource and updates the model's components to match
* the real world data
Expand Down Expand Up @@ -78,6 +103,21 @@ class XRControllerModel extends Object3D {
})
})
}

disconnect(): void {
this.motionController = null
this.dispatchEvent({
type: 'motionControllerDestroyed',
})
if (this.scene) {
this.remove(this.scene)
}
this.scene = null
}

dispose(): void {
this.disconnect()
}
}

/**
Expand Down Expand Up @@ -154,9 +194,9 @@ class XRControllerModelFactory {
gltfLoader: GLTFLoader
path: string
private _assetCache: Record<string, { scene: Object3D } | undefined>
constructor(gltfLoader: GLTFLoader = null) {
constructor(gltfLoader: GLTFLoader = null, path = DEFAULT_PROFILES_PATH) {
this.gltfLoader = gltfLoader
this.path = DEFAULT_PROFILES_PATH
this.path = path
this._assetCache = {}

// If a GLTFLoader wasn't supplied to the constructor create a new one.
Expand All @@ -165,77 +205,55 @@ class XRControllerModelFactory {
}
}

createControllerModel(controller: Group): XRControllerModel {
const controllerModel = new XRControllerModel()
let scene: Object3D | null = null
initializeControllerModel(controllerModel: XRControllerModel, event: any): void {
const xrInputSource = event.data

const onConnected = (event: any): void => {
const xrInputSource = event.data
if (xrInputSource.targetRayMode !== 'tracked-pointer' || !xrInputSource.gamepad) return

if (xrInputSource.targetRayMode !== 'tracked-pointer' || !xrInputSource.gamepad) return

fetchProfile(xrInputSource, this.path, DEFAULT_PROFILE)
.then(({ profile, assetPath }) => {
if (!assetPath) {
throw new Error('no asset path')
}

controllerModel.motionController = new MotionController(xrInputSource, profile, assetPath)

const assetUrl = controllerModel.motionController.assetUrl

const cachedAsset = this._assetCache[assetUrl]
if (cachedAsset) {
scene = cachedAsset.scene.clone()

addAssetSceneToControllerModel(controllerModel, scene)
} else {
if (!this.gltfLoader) {
throw new Error('GLTFLoader not set.')
}
fetchProfile(xrInputSource, this.path, DEFAULT_PROFILE)
.then(({ profile, assetPath }) => {
if (!assetPath) {
throw new Error('no asset path')
}

this.gltfLoader.setPath('')
this.gltfLoader.load(
controllerModel.motionController.assetUrl,
(asset: { scene: Object3D }) => {
if (!controllerModel.motionController) {
console.warn('motionController gone while gltf load, bailing...')
return
}
const motionController = new MotionController(xrInputSource, profile, assetPath)
controllerModel.addMotionController(motionController)

this._assetCache[assetUrl] = asset
const assetUrl = motionController.assetUrl

scene = asset.scene.clone()
const cachedAsset = this._assetCache[assetUrl]
if (cachedAsset) {
const scene = cachedAsset.scene.clone()

addAssetSceneToControllerModel(controllerModel, scene)
},
null,
() => {
throw new Error(`Asset ${assetUrl} missing or malformed.`)
},
)
controllerModel.addScene(scene)
} else {
if (!this.gltfLoader) {
throw new Error('GLTFLoader not set.')
}
})
.catch((err) => {
console.warn(err)
})
}

controller.addEventListener('connected', onConnected)

const onDisconnected = (): void => {
controller.removeEventListener('connected', onConnected)
controller.removeEventListener('disconnected', onDisconnected)
controllerModel.motionController = null
if (scene) {
controllerModel.remove(scene)
}
scene = null
}

controller.addEventListener('disconnected', onDisconnected)

return controllerModel
this.gltfLoader.setPath('')
this.gltfLoader.load(
assetUrl,
(asset: { scene: Object3D }) => {
if (!controllerModel.motionController) {
console.warn('motionController gone while gltf load, bailing...')
return
}

this._assetCache[assetUrl] = asset
const scene = asset.scene.clone()
controllerModel.addScene(scene)
},
null,
() => {
throw new Error(`Asset ${assetUrl} missing or malformed.`)
},
)
}
})
.catch((err) => {
console.warn(err)
})
}
}

Expand Down