Skip to content

Commit

Permalink
feat: path and callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
saitonakamura committed Dec 20, 2022
1 parent c8fa93e commit 085042b
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 70 deletions.
106 changes: 66 additions & 40 deletions src/libs/MotionControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,53 @@

import type { Object3D, XRGamepad, XRHandedness, XRInputSource } from 'three'

// https://github.com/immersive-web/webxr-input-profiles/blob/73d647040a827975a2100325afb3d13e0895b26b/packages/assets/schemas/visualResponses.schema.json
interface InputProfileVisualResponse {
interface GamepadIndices {
button: number
xAxis?: number
yAxis?: number
}

interface VisualResponseDescription {
componentProperty: string
states: string[]
valueNodeProperty: string
valueNodeName: string
property: string
minNodeName?: string
maxNodeName?: string
}

interface InputProfileGamepadIndices {
button: number
xAxis: number
yAxis: number
}
type VisualResponses = Record<string, VisualResponseDescription>

// https://github.com/immersive-web/webxr-input-profiles/blob/73d647040a827975a2100325afb3d13e0895b26b/packages/assets/schemas/layout.schema.json
interface InputProfileComponent {
interface ComponentDescription {
type: string
gamepadIndices: GamepadIndices
rootNodeName: string
touchPointNodeName: string
visualResponses: Record<string, InputProfileVisualResponse | null>
gamepadIndices: InputProfileGamepadIndices
visualResponses: VisualResponses
touchPointNodeName?: string
}
interface InputProfileLayout {

interface Components {
[componentKey: string]: ComponentDescription
}

interface LayoutDescription {
selectComponentId: string
components: Components
gamepadMapping: string
rootNodeName: string
assetPath: string
components: Record<string, InputProfileComponent>
}

// https://github.com/immersive-web/webxr-input-profiles/blob/73d647040a827975a2100325afb3d13e0895b26b/packages/assets/schemas/profile.schema.json
interface InputProfile {
type Layouts = Partial<Record<XRHandedness, LayoutDescription>>

export interface Profile {
profileId: string
fallbackProfileIds: string[]
layouts: Partial<Record<XRHandedness | 'left-right' | 'left-right-none', InputProfileLayout>>
layouts: Layouts
}

interface ProfilesList {
[profileId: string]: { path: string; deprecated?: boolean } | undefined
}

const MotionControllerConstants = {
Expand Down Expand Up @@ -93,7 +104,7 @@ async function fetchJsonFile(path: string): Promise<any> {
}
}

async function fetchProfilesList(basePath: string): Promise<any> {
async function fetchProfilesList(basePath: string): Promise<ProfilesList> {
if (!basePath) {
throw new Error('No basePath supplied')
}
Expand All @@ -108,7 +119,7 @@ async function fetchProfile(
basePath: string,
defaultProfile: string | null = null,
getAssetPath = true,
): Promise<any> {
): Promise<{ profile: Profile; assetPath: string | undefined }> {
if (!xrInputSource) {
throw new Error('No xrInputSource supplied')
}
Expand Down Expand Up @@ -153,7 +164,7 @@ async function fetchProfile(

const profile = await fetchJsonFile(match.profilePath)

let assetPath
let assetPath: string | undefined = undefined
if (getAssetPath) {
let layout
if ((xrInputSource.handedness as string) === 'any') {
Expand Down Expand Up @@ -189,7 +200,10 @@ const defaultComponentValues = {
* @param {number | undefined} x The original x coordinate in the range -1 to 1
* @param {number | undefined} y The original y coordinate in the range -1 to 1
*/
function normalizeAxes(x = 0, y = 0): { normalizedXAxis: number; normalizedYAxis: number } {
function normalizeAxes(
x: number | undefined = 0,
y: number | undefined = 0,
): { normalizedXAxis: number; normalizedYAxis: number } {
let xAxis = x
let yAxis = y

Expand Down Expand Up @@ -219,18 +233,18 @@ function normalizeAxes(x = 0, y = 0): { normalizedXAxis: number; normalizedYAxis
* to the named input changing, this object computes the appropriate weighting to use for
* interpolating between the range of motion nodes.
*/
class VisualResponse {
class VisualResponse implements VisualResponseDescription {
value: number | boolean
componentProperty: string
states: string[]
valueNodeName: string
valueNodeProperty: string
minNodeName: string | undefined
maxNodeName: string | undefined
minNodeName?: string
maxNodeName?: string
valueNode: Object3D | undefined
minNode: Object3D | undefined
maxNode: Object3D | undefined
constructor(visualResponseDescription: InputProfileVisualResponse) {
constructor(visualResponseDescription: VisualResponseDescription) {
this.componentProperty = visualResponseDescription.componentProperty
this.states = visualResponseDescription.states
this.valueNodeName = visualResponseDescription.valueNodeName
Expand Down Expand Up @@ -289,20 +303,27 @@ class VisualResponse {
}
}

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

type: string
gamepadIndices: GamepadIndices
rootNodeName: string
touchPointNodeName: string
visualResponses: Record<string, VisualResponse>
values: { state: string; button: number | undefined; xAxis: number | undefined; yAxis: number | undefined }
gamepadIndices: InputProfileGamepadIndices
touchPointNode: Object3D | undefined
touchPointNodeName?: string | undefined
touchPointNode?: Object3D

/**
* @param {string} componentId - Id of the component
* @param {InputProfileComponent} componentDescription - Description of the component to be created
*/
constructor(componentId: string, componentDescription: InputProfileComponent) {
constructor(componentId: string, componentDescription: ComponentDescription) {
if (
!componentId ||
!componentDescription ||
Expand Down Expand Up @@ -408,15 +429,15 @@ class Component {
class MotionController {
xrInputSource: XRInputSource
assetUrl: string
layoutDescription: InputProfileLayout | undefined
layoutDescription: LayoutDescription
id: string
components: Record<string, Component>
/**
* @param {Object} xrInputSource - The XRInputSource to build the MotionController around
* @param {Object} profile - The best matched profile description for the supplied xrInputSource
* @param {Object} assetUrl
* @param {XRInputSource} xrInputSource - The XRInputSource to build the MotionController around
* @param {Profile} profile - The best matched profile description for the supplied xrInputSource
* @param {string} assetUrl
*/
constructor(xrInputSource: XRInputSource, profile: InputProfile, assetUrl: string) {
constructor(xrInputSource: XRInputSource, profile: Profile, assetUrl: string) {
if (!xrInputSource) {
throw new Error('No xrInputSource supplied')
}
Expand All @@ -425,15 +446,20 @@ class MotionController {
throw new Error('No profile supplied')
}

if (!profile.layouts[xrInputSource.handedness]) {
throw new Error('No layout for ' + xrInputSource.handedness + ' handedness')
}

this.xrInputSource = xrInputSource
this.assetUrl = assetUrl
this.id = profile.profileId

// Build child components as described in the profile description
this.layoutDescription = profile.layouts[xrInputSource.handedness]
this.layoutDescription = profile.layouts[xrInputSource.handedness]!

this.components = {}
Object.keys(this.layoutDescription!.components).forEach((componentId) => {
const componentDescription = this.layoutDescription!.components[componentId]
Object.keys(this.layoutDescription.components).forEach((componentId) => {
const componentDescription = this.layoutDescription.components[componentId]
this.components[componentId] = new Component(componentId, componentDescription)
})

Expand Down
87 changes: 57 additions & 30 deletions src/webxr/XRControllerModelFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import { fetchProfile, MotionController, MotionControllerConstants } from '../li
const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles'
const DEFAULT_PROFILE = 'generic-trigger'

const applyEnvironmentMap = (envMap: Texture, obj: Object3D): void => {
obj.traverse((child) => {
if (child instanceof Mesh && 'envMap' in child.material) {
child.material.envMap = envMap
child.material.needsUpdate = true
}
})
}

class XRControllerModel extends Object3D {
envMap: Texture | null
motionController: MotionController | null
Expand All @@ -23,12 +32,7 @@ class XRControllerModel extends Object3D {
}

this.envMap = envMap
this.traverse((child) => {
if ((child as Mesh).isMesh) {
;((child as Mesh).material as MeshBasicMaterial).envMap = this.envMap
;((child as Mesh).material as MeshBasicMaterial).needsUpdate = true
}
})
applyEnvironmentMap(this.envMap, this)

return this
}
Expand Down Expand Up @@ -56,12 +60,20 @@ class XRControllerModel extends Object3D {
if (!valueNode) return

// Calculate the new properties based on the weight supplied
if (valueNodeProperty === MotionControllerConstants.VisualResponseProperty.VISIBILITY) {
valueNode.visible = value as boolean
} else if (valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM) {
valueNode.quaternion.slerpQuaternions(minNode!.quaternion, maxNode!.quaternion, value as number)

valueNode.position.lerpVectors(minNode!.position, maxNode!.position, value as number)
if (
valueNodeProperty === MotionControllerConstants.VisualResponseProperty.VISIBILITY &&
typeof value === 'boolean'
) {
valueNode.visible = value
} else if (
valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM &&
minNode &&
maxNode &&
typeof value === 'number'
) {
valueNode.quaternion.slerpQuaternions(minNode.quaternion, maxNode.quaternion, value)

valueNode.position.lerpVectors(minNode.position, maxNode.position, value)
}
})
})
Expand All @@ -78,7 +90,7 @@ function findNodes(motionController: MotionController, scene: Object3D): void {
Object.values(motionController.components).forEach((component) => {
const { type, touchPointNodeName, visualResponses } = component

if (type === MotionControllerConstants.ComponentType.TOUCHPAD) {
if (type === MotionControllerConstants.ComponentType.TOUCHPAD && touchPointNodeName) {
component.touchPointNode = scene.getObjectByName(touchPointNodeName)
if (component.touchPointNode) {
// Attach a touch dot to the touchpad.
Expand All @@ -96,9 +108,13 @@ function findNodes(motionController: MotionController, scene: Object3D): void {
const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse

// If animating a transform, find the two nodes to be interpolated between.
if (valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM) {
visualResponse.minNode = scene.getObjectByName(minNodeName!)
visualResponse.maxNode = scene.getObjectByName(maxNodeName!)
if (
valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM &&
minNodeName &&
maxNodeName
) {
visualResponse.minNode = scene.getObjectByName(minNodeName)
visualResponse.maxNode = scene.getObjectByName(maxNodeName)

// If the extents cannot be found, skip this animation
if (!visualResponse.minNode) {
Expand Down Expand Up @@ -127,12 +143,7 @@ function addAssetSceneToControllerModel(controllerModel: XRControllerModel, scen

// Apply any environment map that the mesh already has set.
if (controllerModel.envMap) {
scene.traverse((child) => {
if ((child as Mesh).isMesh) {
;((child as Mesh).material as MeshBasicMaterial).envMap = this.envMap
;((child as Mesh).material as MeshBasicMaterial).needsUpdate = true
}
})
applyEnvironmentMap(controllerModel.envMap, scene)
}

// Add the glTF scene to the controllerModel.
Expand All @@ -143,9 +154,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 @@ -154,7 +165,11 @@ class XRControllerModelFactory {
}
}

createControllerModel(controller: Group): XRControllerModel {
createControllerModel(
controller: Group,
onMotionControllerCreated?: (motionContoller: MotionController) => void,
onMotionControllerDestroyed?: () => void,
): XRControllerModel {
const controllerModel = new XRControllerModel()
let scene: Object3D | null = null

Expand All @@ -165,13 +180,17 @@ class XRControllerModelFactory {

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

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

addAssetSceneToControllerModel(controllerModel, scene!)
addAssetSceneToControllerModel(controllerModel, scene)
} else {
if (!this.gltfLoader) {
throw new Error('GLTFLoader not set.')
Expand All @@ -181,15 +200,20 @@ class XRControllerModelFactory {
this.gltfLoader.load(
controllerModel.motionController.assetUrl,
(asset: { scene: Object3D }) => {
this._assetCache[controllerModel.motionController!.assetUrl] = asset
if (!controllerModel.motionController) {
console.warn('motionController gone while gltf load, bailing...')
return
}

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

scene = asset.scene.clone()

addAssetSceneToControllerModel(controllerModel, scene!)
addAssetSceneToControllerModel(controllerModel, scene)
},
null,
() => {
throw new Error(`Asset ${controllerModel.motionController!.assetUrl} missing or malformed.`)
throw new Error(`Asset ${controllerModel.motionController?.assetUrl} missing or malformed.`)
},
)
}
Expand All @@ -201,8 +225,11 @@ class XRControllerModelFactory {

controller.addEventListener('disconnected', () => {
controllerModel.motionController = null
controllerModel.remove(scene!)
if (scene) {
controllerModel.remove(scene)
}
scene = null
onMotionControllerDestroyed?.()
})

return controllerModel
Expand Down

0 comments on commit 085042b

Please sign in to comment.