Skip to content

Commit

Permalink
wip: working toward ability to use other XR input controller models
Browse files Browse the repository at this point in the history
  • Loading branch information
lojjic committed Jun 1, 2020
1 parent 7264b0c commit 115e1c7
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 73 deletions.
7 changes: 7 additions & 0 deletions packages/troika-xr/src/facade/GripFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ import { utils } from 'troika-core'
import BasicGrip from './grip-models/BasicGrip.js'
import OculusTouchGrip from './grip-models/OculusTouchGrip.js'
import { copyXRPoseToFacadeProps } from '../XRUtils.js'
//import { HandsGrip } from './grip-models/HandsGrip.js'


const PROFILE_MODELS = [
/*{
match: xrInputSource => {
return true //TODO
},
facade: HandsGrip
},*/
{
match: xrInputSource => {
return /Oculus/.test(navigator.userAgent) || (
Expand Down
80 changes: 80 additions & 0 deletions packages/troika-xr/src/facade/grip-models/GLTFFacade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Object3DFacade } from 'troika-3d'
import { Group } from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

export class GLTFFacade extends Object3DFacade {
constructor (parent) {
super(parent, new Group())
this.url = null
this.rootTransform = null
this.autoDispose = true
}

afterUpdate () {
let { url } = this
if (url !== this._url) {
this._url = url
this.removeObjects()
if (url) {
let loader = new GLTFLoader()
loader.setCrossOrigin('anonymous')
loader.load(
url,
result => {
this.onLoad(result)
},
null,
err => {
console.error('Failed loading controller model', err)
}
)
}
}
super.afterUpdate()
}

onLoad (gltf) {
if (this.threeObject) {
gltf = this.normalize(gltf)
let root = gltf.scene
if (this.rootTransform) {
root.applyMatrix4(this.rootTransform)
}
this.threeObject.add(root)
this.gltf = gltf
this.afterUpdate()
}
}

normalize(gltf) {
return gltf
}

removeObjects () {
const { gltf } = this
if (gltf && gltf.scene) {
if (this.autoDispose) {
gltf.scene.traverse(({ geometry, material }) => {
if (geometry) {
geometry.dispose()
}
if (material) {
if (material.texture) {
material.texture.dispose()
}
material.dispose()
}
})
}
if (this.threeObject) {
this.threeObject.remove(gltf.scene)
}
this.gltf = null
}
}

destructor () {
this.removeObjects()
super.destructor()
}
}
101 changes: 101 additions & 0 deletions packages/troika-xr/src/facade/grip-models/HandsGrip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { GLTFFacade } from './GLTFFacade.js'
import { AnimationMixer, Clock, LoopOnce, Matrix4 } from 'three'
import { BUTTON_SQUEEZE, BUTTON_TRIGGER } from '../../XRStandardGamepadMapping.js'


function getModelUrl (hand) {
return `https://cdn.aframe.io/controllers/hands/${hand}HandHigh.glb`
}

// AFrame models are built assuming a targetRaySpace, but we use gripSpace, so
// these rootTransforms are offsets from targetRaySpace to gripSpace (as measured
// using controllers in Oculus browser), plus the Z rotation also applied by AFrame.
const MODEL_PARAMS = {
left: {
url: getModelUrl('left'),
rootTransform: new Matrix4().set(0.9891952390780486, -0.055079804712322655, -0.135864030013094, 0, 0.134251874636802, 0.7126619830767638, 0.68854141828729, 0, 0.058900417593749754, -0.6993419911774881, 0.7123564071509296, 0, 0.022191358380132697, 0.3674491931926873, 0.46970496839747344, 1).multiply(new Matrix4().makeRotationZ(Math.PI / 2))
},
right: {
url: getModelUrl('right'),
rootTransform: new Matrix4().set(0.9991057478311871, -0.021268630839894498, 0.03654256702889458, 0, -0.010756123291139397, 0.7079848583317349, 0.7061457664971519, 0, -0.04089033411730017, -0.7059072584331106, 0.7071229985273267, 0, 0.014017117995823086, 0.35926713373523733, 0.5145142484388896, 1).multiply(new Matrix4().makeRotationZ(-Math.PI / 2))
}
}

export class HandsGrip extends GLTFFacade {
constructor (parent) {
super(parent)
this.xrInputSource = null
this.currentClip = 'Open'
}

afterUpdate () {
let hand = this.xrInputSource.handedness
if (hand !== 'left' && hand !== 'right') {
hand = 'left'
}
if (hand !== this._hand) {
this._hand = hand
Object.assign(this, MODEL_PARAMS[hand])
}

let mixer = this._mixer
if (mixer) {
let currentClip = this._animationsByName.get(this.getClipName())
if (currentClip !== this._prevClip) {
// adapted from https://github.com/aframevr/aframe/blob/master/src/components/hand-controls.js#playAnimation
mixer.stopAllAction()

let to = mixer.clipAction(currentClip)
to.clampWhenFinished = true
to.loop = LoopOnce
to.timeScale = currentClip ? 1 : -1
to.time = currentClip ? currentClip.duration : 0

let from = this._prevClip ? mixer.clipAction(this._prevClip) : null
if (from) {
from.crossFadeTo(to, 0.1, true)
}
to.play()

this._prevClip = currentClip
}

mixer.update(this._clock.getDelta())
}

super.afterUpdate()
}

onLoad(gltf) {
this._mixer = new AnimationMixer(gltf.scene.children[0])
this._clock = new Clock()

this._animationsByName = new Map()
gltf.animations.forEach(clip => {
this._animationsByName.set(clip.name, clip)
})

super.onLoad(gltf)
}

getClipName() {
let gamepad = this.xrInputSource.gamepad
let buttons = gamepad && gamepad.buttons
if (buttons) {
let grabbing = buttons[BUTTON_SQUEEZE] && buttons[BUTTON_SQUEEZE].pressed
let triggering = buttons[BUTTON_TRIGGER] && (buttons[BUTTON_TRIGGER].touched || buttons[BUTTON_TRIGGER].pressed)
let thumbTouching = false
for (let i = 2; i < buttons.length; i++) {
if (buttons[i] && (buttons[i].touched || buttons[i].pressed)) {
thumbTouching = true
break
}
}
return grabbing ? (
thumbTouching ? (triggering ? 'Fist' : 'Point') : (triggering ? 'Thumb Up' : 'Point + Thumb')
) : triggering ? 'Hold' : 'Open'
} else {
return 'Open'
}
}
}
115 changes: 42 additions & 73 deletions packages/troika-xr/src/facade/grip-models/OculusTouchGrip.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,56 @@
import { Object3DFacade } from 'troika-3d'
import { Group, Ray, Vector3 } from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { Euler, Matrix4, Quaternion, Vector3 } from 'three'
import { GLTFFacade } from './GLTFFacade.js'

const MODEL_GEN = 'gen2'

function getModelUrl(gen, hand) {
function getModelUrl (gen, hand) {
return `https://cdn.aframe.io/controllers/oculus/oculus-touch-controller-${gen}-${hand}.gltf`
}

const MODEL_PARAMS = {
gen1: {
left: {
url: getModelUrl('gen1', 'left'),
position: [0, 0, -0.1],
rotation: [0, 0, 0]
transform: new Matrix4().compose(
new Vector3(0, 0, -0.1),
new Quaternion(),
new Vector3(1, 1, 1)
)
},
right: {
url: getModelUrl('gen1', 'right'),
position: [0, 0, -0.1],
rotation: [0, 0, 0]
transform: new Matrix4().compose(
new Vector3(0, 0, -0.1),
new Quaternion(),
new Vector3(1, 1, 1)
)
}
},
gen2: {
left: {
url: getModelUrl('gen2', 'left'),
position: [0.01, -0.01, -0.05],
rotation: [-0.67, 0, 0]
transform: new Matrix4().compose(
new Vector3(0.01, -0.01, -0.05),
new Quaternion().setFromEuler(new Euler(-0.67, 0, 0)),
new Vector3(1, 1, 1)
)
},
right: {
url: getModelUrl('gen2', 'right'),
position: [-0.01, -0.01, -0.05],
rotation: [-0.67, 0, 0]
transform: new Matrix4().compose(
new Vector3(-0.01, -0.01, -0.05),
new Quaternion().setFromEuler(new Euler(-0.67, 0, 0)),
new Vector3(1, 1, 1)
)
}
}
}

// TODO define mapping here that will allow us to pass in a set of active button
// ids and highlight their corresponding meshes when pressed...
const buttonIdsToMeshNames = {

}
const buttonIdsToMeshNames = {}



class OculusTouchGrip extends Object3DFacade {
class OculusTouchGrip extends GLTFFacade {
/**
* @property bodyColor
* @property emissive
Expand All @@ -52,77 +59,45 @@ class OculusTouchGrip extends Object3DFacade {
* @property buttonActiveColor
*/

constructor(parent) {
super(parent, new Group())
constructor (parent) {
super(parent)

this.xrInputSource = null
this.bodyColor = 0x999999
this.buttonColor = 0xffffff
this.buttonActiveColor = 0xccffcc
this.emissiveIntensity = 0.3
}

afterUpdate() {
afterUpdate () {
let hand = this.xrInputSource.handedness
if (hand !== 'left' && hand !== 'right') {
hand = 'left'
}
if (hand !== this._hand) {
this._hand = hand
this.removeObjects()
new GLTFLoader().load(
MODEL_PARAMS[MODEL_GEN][hand].url,
this.addObjects.bind(this),
null,
err => {
console.error('Failed loading controller model', err)
}
)
this.url = MODEL_PARAMS[MODEL_GEN][hand].url
this.rootTransform = MODEL_PARAMS[MODEL_GEN][hand].transform
}

this.updateMaterials()
super.afterUpdate()
}

addObjects(gltf) {
if (this.threeObject) {
const root = this.rootObj = gltf.scene
root.position.fromArray(MODEL_PARAMS[MODEL_GEN][this._hand].position)
root.rotation.fromArray(MODEL_PARAMS[MODEL_GEN][this._hand].rotation)
this.threeObject.add(root)

// Find all the individual meshes
this.meshes = Object.create(null)
root.traverse(obj => {
if (obj.isMesh) {
obj.material = obj.material.clone() //workaround for some meshes sharing a material instance
this.meshes[obj.name] = obj
}
})
this.afterUpdate()
}
}

removeObjects() {
const {rootObj, meshes} = this
if (rootObj) {
this.threeObject.remove(rootObj)
this.rootObj = null
}
if (meshes) {
for (let name in meshes) {
const {geometry, material} = meshes[name]
geometry.dispose()
if (material.texture) {
material.texture.dispose()
}
material.dispose()
normalize (gltf) {
// Track all the individual meshes
this.meshes = Object.create(null)
gltf.scene.traverse(obj => {
if (obj.isMesh) {
obj.material = obj.material.clone() //workaround for some meshes sharing a material instance
this.meshes[obj.name] = obj
}
this.meshes = null
}
})
return gltf
}

updateMaterials() {
const {meshes} = this
updateMaterials () {
const { meshes } = this
if (meshes) {
for (let name in meshes) {
const color = name === 'body' ? this.bodyColor : this.buttonColor
Expand All @@ -136,12 +111,6 @@ class OculusTouchGrip extends Object3DFacade {
}
}
}

destructor () {
this.removeObjects()
super.destructor()
}
}


export default OculusTouchGrip

0 comments on commit 115e1c7

Please sign in to comment.