var registerComponent = require('../core/component').registerComponent; var controllerUtils = require('../utils/tracked-controls'); var DEFAULT_CAMERA_HEIGHT = require('../constants').DEFAULT_CAMERA_HEIGHT; var THREE = require('../lib/three'); var DEFAULT_HANDEDNESS = require('../constants').DEFAULT_HANDEDNESS; // Vector from eyes to elbow (divided by user height). var EYES_TO_ELBOW = {x: 0.175, y: -0.3, z: -0.03}; // Vector from eyes to elbow (divided by user height). var FOREARM = {x: 0, y: 0, z: -0.175}; // Due to unfortunate name collision, add empty touches array to avoid Daydream error. var EMPTY_DAYDREAM_TOUCHES = {touches: []}; var EVENTS = { AXISMOVE: 'axismove', BUTTONCHANGED: 'buttonchanged', BUTTONDOWN: 'buttondown', BUTTONUP: 'buttonup', TOUCHSTART: 'touchstart', TOUCHEND: 'touchend' }; /** * Tracked controls component. * Wrap the gamepad API for pose and button states. * Select the appropriate controller and apply pose to the entity. * Observe button states and emit appropriate events. * * @property {number} controller - Index of controller in array returned by Gamepad API. * Only used if hand property is not set. * @property {string} id - Selected controller among those returned by Gamepad API. * @property {number} hand - If multiple controllers found with id, choose the one with the * given value for hand. If set, we ignore 'controller' property */ module.exports.Component = registerComponent('tracked-controls-webvr', { schema: { autoHide: {default: true}, controller: {default: 0}, id: {type: 'string', default: ''}, hand: {type: 'string', default: ''}, idPrefix: {type: 'string', default: ''}, orientationOffset: {type: 'vec3'}, // Arm model parameters when not 6DoF. armModel: {default: false}, headElement: {type: 'selector'} }, init: function () { // Copy variables back to tracked-controls for backwards compatibility. // Some 3rd components rely on them. this.axis = this.el.components['tracked-controls'].axis = [0, 0, 0]; this.buttonStates = this.el.components['tracked-controls'].buttonStates = {}; this.changedAxes = []; this.targetControllerNumber = this.data.controller; this.axisMoveEventDetail = {axis: this.axis, changed: this.changedAxes}; this.deltaControllerPosition = new THREE.Vector3(); this.controllerQuaternion = new THREE.Quaternion(); this.controllerEuler = new THREE.Euler(); this.updateGamepad(); this.buttonEventDetails = {}; }, tick: function (time, delta) { var mesh = this.el.getObject3D('mesh'); // Update mesh animations. if (mesh && mesh.update) { mesh.update(delta / 1000); } this.updateGamepad(); this.updatePose(); this.updateButtons(); }, /** * Return default user height to use for non-6DOF arm model. */ defaultUserHeight: function () { return DEFAULT_CAMERA_HEIGHT; }, /** * Return head element to use for non-6DOF arm model. */ getHeadElement: function () { return this.data.headElement || this.el.sceneEl.camera.el; }, /** * Handle update controller match criteria (such as `id`, `idPrefix`, `hand`, `controller`) */ updateGamepad: function () { var data = this.data; var controller = controllerUtils.findMatchingControllerWebVR( this.system.controllers, data.id, data.idPrefix, data.hand, data.controller ); this.controller = controller; // Legacy handle to the controller for old components. this.el.components['tracked-controls'].controller = controller; if (this.data.autoHide) { this.el.object3D.visible = !!this.controller; } }, /** * Applies an artificial arm model to simulate elbow to wrist positioning * based on the orientation of the controller. * * @param {object} controllerPosition - Existing vector to update with controller position. */ applyArmModel: function (controllerPosition) { // Use controllerPosition and deltaControllerPosition to avoid creating variables. var controller = this.controller; var controllerEuler = this.controllerEuler; var controllerQuaternion = this.controllerQuaternion; var deltaControllerPosition = this.deltaControllerPosition; var hand; var headEl; var headObject3D; var pose; var userHeight; headEl = this.getHeadElement(); headObject3D = headEl.object3D; userHeight = this.defaultUserHeight(); pose = controller.pose; hand = (controller ? controller.hand : undefined) || DEFAULT_HANDEDNESS; // Use camera position as head position. controllerPosition.copy(headObject3D.position); // Set offset for degenerate "arm model" to elbow. deltaControllerPosition.set( EYES_TO_ELBOW.x * (hand === 'left' ? -1 : hand === 'right' ? 1 : 0), EYES_TO_ELBOW.y, // Lower than our eyes. EYES_TO_ELBOW.z); // Slightly out in front. // Scale offset by user height. deltaControllerPosition.multiplyScalar(userHeight); // Apply camera Y rotation (not X or Z, so you can look down at your hand). deltaControllerPosition.applyAxisAngle(headObject3D.up, headObject3D.rotation.y); // Apply rotated offset to position. controllerPosition.add(deltaControllerPosition); // Set offset for degenerate "arm model" forearm. Forearm sticking out from elbow. deltaControllerPosition.set(FOREARM.x, FOREARM.y, FOREARM.z); // Scale offset by user height. deltaControllerPosition.multiplyScalar(userHeight); // Apply controller X/Y rotation (tilting up/down/left/right is usually moving the arm). if (pose.orientation) { controllerQuaternion.fromArray(pose.orientation); } else { controllerQuaternion.copy(headObject3D.quaternion); } controllerEuler.setFromQuaternion(controllerQuaternion); controllerEuler.set(controllerEuler.x, controllerEuler.y, 0); deltaControllerPosition.applyEuler(controllerEuler); // Apply rotated offset to position. controllerPosition.add(deltaControllerPosition); }, /** * Read pose from controller (from Gamepad API), apply transforms, apply to entity. */ updatePose: function () { var controller = this.controller; var data = this.data; var object3D = this.el.object3D; var pose; var vrDisplay = this.system.vrDisplay; var standingMatrix; if (!controller) { return; } // Compose pose from Gamepad. pose = controller.pose; if (pose.position) { object3D.position.fromArray(pose.position); } else { // Controller not 6DOF, apply arm model. if (data.armModel) { this.applyArmModel(object3D.position); } } if (pose.orientation) { object3D.quaternion.fromArray(pose.orientation); } // Apply transforms, if 6DOF and in VR. if (vrDisplay && pose.position) { standingMatrix = this.el.sceneEl.renderer.xr.getStandingMatrix(); object3D.matrix.compose(object3D.position, object3D.quaternion, object3D.scale); object3D.matrix.multiplyMatrices(standingMatrix, object3D.matrix); object3D.matrix.decompose(object3D.position, object3D.quaternion, object3D.scale); } object3D.rotateX(this.data.orientationOffset.x * THREE.Math.DEG2RAD); object3D.rotateY(this.data.orientationOffset.y * THREE.Math.DEG2RAD); object3D.rotateZ(this.data.orientationOffset.z * THREE.Math.DEG2RAD); }, /** * Handle button changes including axes, presses, touches, values. */ updateButtons: function () { var buttonState; var controller = this.controller; var id; if (!controller) { return; } // Check every button. for (id = 0; id < controller.buttons.length; ++id) { // Initialize button state. if (!this.buttonStates[id]) { this.buttonStates[id] = {pressed: false, touched: false, value: 0}; } if (!this.buttonEventDetails[id]) { this.buttonEventDetails[id] = {id: id, state: this.buttonStates[id]}; } buttonState = controller.buttons[id]; this.handleButton(id, buttonState); } // Check axes. this.handleAxes(); }, /** * Handle presses and touches for a single button. * * @param {number} id - Index of button in Gamepad button array. * @param {number} buttonState - Value of button state from 0 to 1. * @returns {boolean} Whether button has changed in any way. */ handleButton: function (id, buttonState) { var changed; changed = this.handlePress(id, buttonState) | this.handleTouch(id, buttonState) | this.handleValue(id, buttonState); if (!changed) { return false; } this.el.emit(EVENTS.BUTTONCHANGED, this.buttonEventDetails[id], false); return true; }, /** * An axis is an array of values from -1 (up, left) to 1 (down, right). * Compare each component of the axis to the previous value to determine change. * * @returns {boolean} Whether axes changed. */ handleAxes: function () { var changed = false; var controllerAxes = this.controller.axes; var i; var previousAxis = this.axis; var changedAxes = this.changedAxes; // Check if axis changed. this.changedAxes.splice(0, this.changedAxes.length); for (i = 0; i < controllerAxes.length; ++i) { changedAxes.push(previousAxis[i] !== controllerAxes[i]); if (changedAxes[i]) { changed = true; } } if (!changed) { return false; } this.axis.splice(0, this.axis.length); for (i = 0; i < controllerAxes.length; i++) { this.axis.push(controllerAxes[i]); } this.el.emit(EVENTS.AXISMOVE, this.axisMoveEventDetail, false); return true; }, /** * Determine whether a button press has occured and emit events as appropriate. * * @param {string} id - ID of the button to check. * @param {object} buttonState - State of the button to check. * @returns {boolean} Whether button press state changed. */ handlePress: function (id, buttonState) { var evtName; var previousButtonState = this.buttonStates[id]; // Not changed. if (buttonState.pressed === previousButtonState.pressed) { return false; } evtName = buttonState.pressed ? EVENTS.BUTTONDOWN : EVENTS.BUTTONUP; this.el.emit(evtName, this.buttonEventDetails[id], false); previousButtonState.pressed = buttonState.pressed; return true; }, /** * Determine whether a button touch has occured and emit events as appropriate. * * @param {string} id - ID of the button to check. * @param {object} buttonState - State of the button to check. * @returns {boolean} Whether button touch state changed. */ handleTouch: function (id, buttonState) { var evtName; var previousButtonState = this.buttonStates[id]; // Not changed. if (buttonState.touched === previousButtonState.touched) { return false; } evtName = buttonState.touched ? EVENTS.TOUCHSTART : EVENTS.TOUCHEND; this.el.emit(evtName, this.buttonEventDetails[id], false, EMPTY_DAYDREAM_TOUCHES); previousButtonState.touched = buttonState.touched; return true; }, /** * Determine whether a button value has changed. * * @param {string} id - Id of the button to check. * @param {object} buttonState - State of the button to check. * @returns {boolean} Whether button value changed. */ handleValue: function (id, buttonState) { var previousButtonState = this.buttonStates[id]; // Not changed. if (buttonState.value === previousButtonState.value) { return false; } previousButtonState.value = buttonState.value; return true; } });