UNPKG

aframe

Version:

A web framework for building virtual reality experiences.

491 lines (438 loc) 19.6 kB
import * as THREE from 'three'; import { registerComponent } from '../core/component.js'; import { AFRAME_CDN_ROOT } from '../constants/index.js'; import { checkControllerPresentAndSetup, emitIfAxesChanged, onButtonEvent } from '../utils/tracked-controls.js'; // Prefix for Gen1 and Gen2 Oculus Touch Controllers. var GAMEPAD_ID_PREFIX = 'oculus-touch'; // First generation model URL. var TOUCH_CONTROLLER_MODEL_BASE_URL = AFRAME_CDN_ROOT + 'controllers/oculus/oculus-touch-controller-'; var META_CONTROLLER_MODEL_BASE_URL = AFRAME_CDN_ROOT + 'controllers/meta/'; var OCULUS_TOUCH_CONFIG = { left: { modelUrl: TOUCH_CONTROLLER_MODEL_BASE_URL + 'left.gltf', rayOrigin: {origin: {x: 0.002, y: -0.005, z: -0.03}, direction: {x: 0, y: -0.8, z: -1}}, modelPivotOffset: new THREE.Vector3(-0.005, 0.036, -0.037), modelPivotRotation: new THREE.Euler(Math.PI / 4.5, 0, 0) }, right: { modelUrl: TOUCH_CONTROLLER_MODEL_BASE_URL + 'right.gltf', rayOrigin: {origin: {x: -0.002, y: -0.005, z: -0.03}, direction: {x: 0, y: -0.8, z: -1}}, modelPivotOffset: new THREE.Vector3(0.005, 0.036, -0.037), modelPivotRotation: new THREE.Euler(Math.PI / 4.5, 0, 0) } }; var CONTROLLER_DEFAULT = 'oculus-touch'; var CONTROLLER_PROPERTIES = { 'oculus-touch': OCULUS_TOUCH_CONFIG, 'oculus-touch-v2': { left: { modelUrl: TOUCH_CONTROLLER_MODEL_BASE_URL + 'gen2-left.gltf', rayOrigin: {origin: {x: -0.006, y: -0.03, z: -0.04}, direction: {x: 0, y: -0.9, z: -1}}, modelPivotOffset: new THREE.Vector3(0, -0.007, -0.021), modelPivotRotation: new THREE.Euler(-Math.PI / 4, 0, 0) }, right: { modelUrl: TOUCH_CONTROLLER_MODEL_BASE_URL + 'gen2-right.gltf', rayOrigin: {origin: {x: 0.006, y: -0.03, z: -0.04}, direction: {x: 0, y: -0.9, z: -1}}, modelPivotOffset: new THREE.Vector3(0, -0.007, -0.021), modelPivotRotation: new THREE.Euler(-Math.PI / 4, 0, 0) } }, 'oculus-touch-v3': { left: { modelUrl: TOUCH_CONTROLLER_MODEL_BASE_URL + 'v3-left.glb', rayOrigin: { origin: {x: 0.0065, y: -0.0186, z: -0.05}, direction: {x: 0.12394785839500175, y: -0.5944043672340157, z: -0.7945567170519814} }, modelPivotOffset: new THREE.Vector3(0, 0, 0), modelPivotRotation: new THREE.Euler(0, 0, 0) }, right: { modelUrl: TOUCH_CONTROLLER_MODEL_BASE_URL + 'v3-right.glb', rayOrigin: { origin: {x: -0.0065, y: -0.0186, z: -0.05}, direction: {x: -0.12394785839500175, y: -0.5944043672340157, z: -0.7945567170519814} }, modelPivotOffset: new THREE.Vector3(0, 0, 0), modelPivotRotation: new THREE.Euler(0, 0, 0) } }, 'meta-quest-touch-pro': { left: { modelUrl: META_CONTROLLER_MODEL_BASE_URL + 'quest-touch-pro-left.glb', rayOrigin: { origin: {x: 0.0065, y: -0.0186, z: -0.05}, direction: {x: 0.12394785839500175, y: -0.5944043672340157, z: -0.7945567170519814} }, modelPivotOffset: new THREE.Vector3(0, 0, 0), modelPivotRotation: new THREE.Euler(0, 0, 0) }, right: { modelUrl: META_CONTROLLER_MODEL_BASE_URL + 'quest-touch-pro-right.glb', rayOrigin: { origin: {x: -0.0065, y: -0.0186, z: -0.05}, direction: {x: -0.12394785839500175, y: -0.5944043672340157, z: -0.7945567170519814} }, modelPivotOffset: new THREE.Vector3(0, 0, 0), modelPivotRotation: new THREE.Euler(0, 0, 0) } }, 'meta-quest-touch-plus': { left: { modelUrl: META_CONTROLLER_MODEL_BASE_URL + 'quest-touch-plus-left.glb', rayOrigin: { origin: {x: 0.0065, y: -0.0186, z: -0.05}, direction: {x: 0.12394785839500175, y: -0.5944043672340157, z: -0.7945567170519814} }, modelPivotOffset: new THREE.Vector3(0, 0, 0), modelPivotRotation: new THREE.Euler(0, 0, 0) }, right: { modelUrl: META_CONTROLLER_MODEL_BASE_URL + 'quest-touch-plus-right.glb', rayOrigin: { origin: {x: -0.0065, y: -0.0186, z: -0.05}, direction: {x: -0.12394785839500175, y: -0.5944043672340157, z: -0.7945567170519814} }, modelPivotOffset: new THREE.Vector3(0, 0, 0), modelPivotRotation: new THREE.Euler(0, 0, 0) } } }; var INPUT_MAPPING = { left: { axes: {thumbstick: [2, 3]}, buttons: ['trigger', 'grip', 'none', 'thumbstick', 'xbutton', 'ybutton', 'surface'] }, right: { axes: {thumbstick: [2, 3]}, buttons: ['trigger', 'grip', 'none', 'thumbstick', 'abutton', 'bbutton', 'surface'] } }; /** * Meta Touch controls (formerly Oculus Touch) * Interface with Meta Touch controllers and map Gamepad events to * controller buttons: thumbstick, trigger, grip, xbutton, ybutton, surface * Load a controller model and highlight the pressed buttons. */ var componentConfig = { schema: { hand: {default: 'left'}, buttonColor: {type: 'color', default: '#999'}, // Off-white. buttonTouchColor: {type: 'color', default: '#8AB'}, buttonHighlightColor: {type: 'color', default: '#2DF'}, // Light blue. model: {default: true}, controllerType: {default: 'auto', oneOf: ['auto', 'oculus-touch', 'oculus-touch-v2', 'oculus-touch-v3']} }, after: ['tracked-controls'], mapping: INPUT_MAPPING, bindMethods: function () { this.onButtonChanged = this.onButtonChanged.bind(this); this.onThumbstickMoved = this.onThumbstickMoved.bind(this); this.onModelLoaded = this.onModelLoaded.bind(this); this.onControllersUpdate = this.onControllersUpdate.bind(this); this.checkIfControllerPresent = this.checkIfControllerPresent.bind(this); this.onAxisMoved = this.onAxisMoved.bind(this); }, init: function () { var self = this; this.onButtonDown = function (evt) { onButtonEvent(evt.detail.id, 'down', self, self.data.hand); }; this.onButtonUp = function (evt) { onButtonEvent(evt.detail.id, 'up', self, self.data.hand); }; this.onButtonTouchStart = function (evt) { onButtonEvent(evt.detail.id, 'touchstart', self, self.data.hand); }; this.onButtonTouchEnd = function (evt) { onButtonEvent(evt.detail.id, 'touchend', self, self.data.hand); }; this.controllerPresent = false; this.previousButtonValues = {}; this.bindMethods(); this.triggerEuler = new THREE.Euler(); }, addEventListeners: function () { var el = this.el; el.addEventListener('buttonchanged', this.onButtonChanged); el.addEventListener('buttondown', this.onButtonDown); el.addEventListener('buttonup', this.onButtonUp); el.addEventListener('touchstart', this.onButtonTouchStart); el.addEventListener('touchend', this.onButtonTouchEnd); el.addEventListener('axismove', this.onAxisMoved); el.addEventListener('model-loaded', this.onModelLoaded); el.addEventListener('thumbstickmoved', this.onThumbstickMoved); this.controllerEventsActive = true; }, removeEventListeners: function () { var el = this.el; el.removeEventListener('buttonchanged', this.onButtonChanged); el.removeEventListener('buttondown', this.onButtonDown); el.removeEventListener('buttonup', this.onButtonUp); el.removeEventListener('touchstart', this.onButtonTouchStart); el.removeEventListener('touchend', this.onButtonTouchEnd); el.removeEventListener('axismove', this.onAxisMoved); el.removeEventListener('model-loaded', this.onModelLoaded); el.removeEventListener('thumbstickmoved', this.onThumbstickMoved); this.controllerEventsActive = false; }, checkIfControllerPresent: function () { var controllerObject3D = this.controllerObject3D; if (controllerObject3D) { controllerObject3D.visible = false; } checkControllerPresentAndSetup(this, GAMEPAD_ID_PREFIX, { hand: this.data.hand, iterateControllerProfiles: true }); }, play: function () { this.checkIfControllerPresent(); this.addControllersUpdateListener(); }, pause: function () { this.removeEventListeners(); this.removeControllersUpdateListener(); }, loadModel: function (controller) { var data = this.data; var controllerId; if (!data.model) { return; } // If model has been already loaded if (this.controllerObject3D) { this.controllerObject3D.visible = true; this.el.setObject3D('mesh', this.controllerObject3D); return; } // Set the controller display model based on the data passed in. this.displayModel = CONTROLLER_PROPERTIES[data.controllerType] || CONTROLLER_PROPERTIES[CONTROLLER_DEFAULT]; // If the developer is asking for auto-detection, use the retrieved displayName to identify the specific unit. if (data.controllerType === 'auto') { controllerId = CONTROLLER_DEFAULT; var controllersPropertiesIds = Object.keys(CONTROLLER_PROPERTIES); for (var i = 0; i < controller.profiles.length; i++) { if (controllersPropertiesIds.indexOf(controller.profiles[i]) !== -1) { controllerId = controller.profiles[i]; break; } } this.displayModel = CONTROLLER_PROPERTIES[controllerId]; } var modelUrl = this.displayModel[data.hand].modelUrl; this.isTouchV3orPROorPlus = this.displayModel === CONTROLLER_PROPERTIES['oculus-touch-v3'] || this.displayModel === CONTROLLER_PROPERTIES['meta-quest-touch-pro'] || this.displayModel === CONTROLLER_PROPERTIES['meta-quest-touch-plus']; this.el.setAttribute('gltf-model', modelUrl); }, injectTrackedControls: function (controller) { var data = this.data; var id = GAMEPAD_ID_PREFIX; this.el.setAttribute('tracked-controls', { id: id, hand: data.hand, handTrackingEnabled: false, iterateControllerProfiles: true }); this.loadModel(controller); }, addControllersUpdateListener: function () { this.el.sceneEl.addEventListener('controllersupdated', this.onControllersUpdate, false); }, removeControllersUpdateListener: function () { this.el.sceneEl.removeEventListener('controllersupdated', this.onControllersUpdate, false); }, onControllersUpdate: function () { this.checkIfControllerPresent(); }, onButtonChanged: function (evt) { var button = this.mapping[this.data.hand].buttons[evt.detail.id]; if (!button) { return; } // move the button meshes if (this.isTouchV3orPROorPlus) { this.onButtonChangedV3orPROorPlus(evt); } else { var buttonMeshes = this.buttonMeshes; var analogValue; if (button === 'trigger' || button === 'grip') { analogValue = evt.detail.state.value; } if (buttonMeshes) { if (button === 'trigger' && buttonMeshes.trigger) { buttonMeshes.trigger.rotation.x = this.originalXRotationTrigger - analogValue * (Math.PI / 26); } if (button === 'grip' && buttonMeshes.grip) { analogValue *= this.data.hand === 'left' ? -1 : 1; buttonMeshes.grip.position.x = this.originalXPositionGrip + analogValue * 0.004; } } } // Pass along changed event with button state, using the button mapping for convenience. this.el.emit(button + 'changed', evt.detail.state); }, onButtonChangedV3orPROorPlus: function (evt) { var button = this.mapping[this.data.hand].buttons[evt.detail.id]; var buttonObjects = this.buttonObjects; var analogValue; if (!buttonObjects || !buttonObjects[button]) { return; } analogValue = evt.detail.state.value; buttonObjects[button].quaternion.slerpQuaternions( this.buttonRanges[button].min.quaternion, this.buttonRanges[button].max.quaternion, analogValue ); buttonObjects[button].position.lerpVectors( this.buttonRanges[button].min.position, this.buttonRanges[button].max.position, analogValue ); }, onModelLoaded: function (evt) { if (evt.target !== this.el || !this.data.model) { return; } if (this.isTouchV3orPROorPlus) { this.onTouchV3orPROorPlusModelLoaded(evt); } else { // All oculus headset controller models prior to the Quest 2 (i.e., Oculus Touch V3) // used a consistent format that is handled here var controllerObject3D = this.controllerObject3D = evt.detail.model; var buttonMeshes; buttonMeshes = this.buttonMeshes = {}; buttonMeshes.grip = controllerObject3D.getObjectByName('buttonHand'); this.originalXPositionGrip = buttonMeshes.grip && buttonMeshes.grip.position.x; buttonMeshes.trigger = controllerObject3D.getObjectByName('buttonTrigger'); this.originalXRotationTrigger = buttonMeshes.trigger && buttonMeshes.trigger.rotation.x; buttonMeshes.thumbstick = controllerObject3D.getObjectByName('stick'); buttonMeshes.xbutton = controllerObject3D.getObjectByName('buttonX'); buttonMeshes.abutton = controllerObject3D.getObjectByName('buttonA'); buttonMeshes.ybutton = controllerObject3D.getObjectByName('buttonY'); buttonMeshes.bbutton = controllerObject3D.getObjectByName('buttonB'); } for (var button in this.buttonMeshes) { if (this.buttonMeshes[button]) { cloneMeshMaterial(this.buttonMeshes[button]); } } this.applyOffset(evt.detail.model); this.el.emit('controllermodelready', { name: 'meta-touch-controls', model: this.data.model, rayOrigin: this.displayModel[this.data.hand].rayOrigin }); }, applyOffset: function (model) { model.position.copy(this.displayModel[this.data.hand].modelPivotOffset); model.rotation.copy(this.displayModel[this.data.hand].modelPivotRotation); }, onTouchV3orPROorPlusModelLoaded: function (evt) { var controllerObject3D = this.controllerObject3D = evt.detail.model; var buttonObjects = this.buttonObjects = {}; var buttonMeshes = this.buttonMeshes = {}; var buttonRanges = this.buttonRanges = {}; buttonMeshes.grip = controllerObject3D.getObjectByName('squeeze'); buttonObjects.grip = controllerObject3D.getObjectByName('xr_standard_squeeze_pressed_value'); buttonRanges.grip = { min: controllerObject3D.getObjectByName('xr_standard_squeeze_pressed_min'), max: controllerObject3D.getObjectByName('xr_standard_squeeze_pressed_max') }; buttonObjects.grip.minX = buttonObjects.grip.position.x; buttonMeshes.thumbstick = controllerObject3D.getObjectByName('thumbstick'); buttonObjects.thumbstick = controllerObject3D.getObjectByName('xr_standard_thumbstick_pressed_value'); buttonRanges.thumbstick = { min: controllerObject3D.getObjectByName('xr_standard_thumbstick_pressed_min'), max: controllerObject3D.getObjectByName('xr_standard_thumbstick_pressed_max') }; buttonObjects.thumbstickXAxis = controllerObject3D.getObjectByName('xr_standard_thumbstick_xaxis_pressed_value'); buttonRanges.thumbstickXAxis = { min: controllerObject3D.getObjectByName('xr_standard_thumbstick_xaxis_pressed_min'), max: controllerObject3D.getObjectByName('xr_standard_thumbstick_xaxis_pressed_max') }; buttonObjects.thumbstickYAxis = controllerObject3D.getObjectByName('xr_standard_thumbstick_yaxis_pressed_value'); buttonRanges.thumbstickYAxis = { min: controllerObject3D.getObjectByName('xr_standard_thumbstick_yaxis_pressed_min'), max: controllerObject3D.getObjectByName('xr_standard_thumbstick_yaxis_pressed_max') }; buttonMeshes.trigger = controllerObject3D.getObjectByName('trigger'); buttonObjects.trigger = controllerObject3D.getObjectByName('xr_standard_trigger_pressed_value'); buttonRanges.trigger = { min: controllerObject3D.getObjectByName('xr_standard_trigger_pressed_min'), max: controllerObject3D.getObjectByName('xr_standard_trigger_pressed_max') }; buttonRanges.trigger.diff = { x: Math.abs(buttonRanges.trigger.max.rotation.x) - Math.abs(buttonRanges.trigger.min.rotation.x), y: Math.abs(buttonRanges.trigger.max.rotation.y) - Math.abs(buttonRanges.trigger.min.rotation.y), z: Math.abs(buttonRanges.trigger.max.rotation.z) - Math.abs(buttonRanges.trigger.min.rotation.z) }; var button1 = this.data.hand === 'left' ? 'x' : 'a'; var button2 = this.data.hand === 'left' ? 'y' : 'b'; var button1id = button1 + 'button'; var button2id = button2 + 'button'; buttonMeshes[button1id] = controllerObject3D.getObjectByName(button1 + '_button'); buttonObjects[button1id] = controllerObject3D.getObjectByName(button1 + '_button_pressed_value'); buttonRanges[button1id] = { min: controllerObject3D.getObjectByName(button1 + '_button_pressed_min'), max: controllerObject3D.getObjectByName(button1 + '_button_pressed_max') }; buttonMeshes[button2id] = controllerObject3D.getObjectByName(button2 + '_button'); buttonObjects[button2id] = controllerObject3D.getObjectByName(button2 + '_button_pressed_value'); buttonRanges[button2id] = { min: controllerObject3D.getObjectByName(button2 + '_button_pressed_min'), max: controllerObject3D.getObjectByName(button2 + '_button_pressed_max') }; }, onAxisMoved: function (evt) { emitIfAxesChanged(this, this.mapping[this.data.hand].axes, evt); }, onThumbstickMoved: function (evt) { if (!this.buttonMeshes || !this.buttonMeshes.thumbstick) { return; } if (this.isTouchV3orPROorPlus) { this.updateThumbstickTouchV3orPROorPlus(evt); return; } for (var axis in evt.detail) { this.buttonObjects.thumbstick.rotation[this.axisMap[axis]] = this.buttonRanges.thumbstick.originalRotation[this.axisMap[axis]] - (Math.PI / 8) * evt.detail[axis] * (axis === 'y' || this.data.hand === 'right' ? -1 : 1); } }, axisMap: { y: 'x', x: 'z' }, updateThumbstickTouchV3orPROorPlus: function (evt) { var normalizedXAxis = (evt.detail.x + 1.0) / 2.0; this.buttonObjects.thumbstickXAxis.quaternion.slerpQuaternions( this.buttonRanges.thumbstickXAxis.min.quaternion, this.buttonRanges.thumbstickXAxis.max.quaternion, normalizedXAxis ); var normalizedYAxis = (evt.detail.y + 1.0) / 2.0; this.buttonObjects.thumbstickYAxis.quaternion.slerpQuaternions( this.buttonRanges.thumbstickYAxis.min.quaternion, this.buttonRanges.thumbstickYAxis.max.quaternion, normalizedYAxis ); }, updateModel: function (buttonName, evtName) { if (!this.data.model) { return; } this.updateButtonModel(buttonName, evtName); }, updateButtonModel: function (buttonName, state) { // update the button mesh colors var buttonMeshes = this.buttonMeshes; var button; var color; if (!buttonMeshes) { return; } if (buttonMeshes[buttonName]) { color = (state === 'up' || state === 'touchend') ? buttonMeshes[buttonName].originalColor || this.data.buttonColor : state === 'touchstart' ? this.data.buttonTouchColor : this.data.buttonHighlightColor; button = buttonMeshes[buttonName]; button.material.color.set(color); } } }; registerComponent('oculus-touch-controls', componentConfig); export var Component = registerComponent('meta-touch-controls', componentConfig); /** * Some of the controller models share the same material for different parts (buttons, triggers...). * In order to change their color independently we have to create separate materials. */ function cloneMeshMaterial (object3d) { object3d.traverse(function (node) { var newMaterial; if (node.type !== 'Mesh') return; newMaterial = node.material.clone(); object3d.originalColor = node.material.color; node.material.dispose(); node.material = newMaterial; }); }