diff --git a/docs/components/daydream-controls.md b/docs/components/daydream-controls.md new file mode 100644 index 00000000000..4c6a6945d0d --- /dev/null +++ b/docs/components/daydream-controls.md @@ -0,0 +1,46 @@ +--- +title: daydream-controls +type: components +layout: docs +parent_section: components +--- + +[trackedcontrols]: ./tracked-controls.md + +The daydream-controls component interfaces with the Google Daydream controllers. +It wraps the [tracked-controls component][trackedcontrols] while adding button +mappings, events, and a Daydream controller model that highlights the touched +and/or pressed buttons (trackpad). + +## Example + +```html + + +``` + +## Value + +| Property | Description | Default | +|----------------------|----------------------------------------------------|---------| +| buttonColor | Button colors when not pressed. | #000000 | +| buttonTouchedColor | Button colors when touched. | #777777 | +| buttonHighlightColor | Button colors when pressed and active. | #FFFFFF | +| hand | The hand that will be tracked (i.e., right, left). | right | +| model | Whether the Daydream controller model is loaded. | true | +| rotationOffset | Offset to apply to model rotation. | 0 | + +## Events + +| Event Name | Description | +| ---------- | ----------- | +| trackpaddown | Trackpad pressed. | +| trackpadup | Trackpad released. | +| trackpadtouchstart | Trackpad touched. | +| trackpadtouchend | Trackpad not touched. | + +## Assets + +- [Controller OBJ](https://cdn.aframe.io/controllers/google/vr_controller_daydream.obj) +- [Controller MTL](https://cdn.aframe.io/controllers/google/vr_controller_daydream.mtl) + diff --git a/docs/components/hand-controls.md b/docs/components/hand-controls.md index 6287481e7dc..8087bcf06bc 100644 --- a/docs/components/hand-controls.md +++ b/docs/components/hand-controls.md @@ -8,10 +8,11 @@ parent_section: components [tracked]: ./tracked-controls.md [vive]: ./vive-controls.md [oculustouch]: ./oculus-touch-controls.md +[daydream]: ./daydream-controls.md The hand-controls gives tracked hands (using a prescribed model) with animated -gestures. hand-controls wraps the [vive-controls][vive] and -[oculus-touch-controls][oculustouch] components, which in turn wrap the +gestures. hand-controls wraps the [vive-controls][vive], [oculus-touch-controls][oculustouch], +and [daydream-controls][daydream] components, which in turn wrap the [tracked-controls component][tracked]. The component gives extra events and handles hand animations and poses. diff --git a/docs/components/tracked-controls.md b/docs/components/tracked-controls.md index 4cd1136a265..32ebf9b8d28 100644 --- a/docs/components/tracked-controls.md +++ b/docs/components/tracked-controls.md @@ -8,13 +8,17 @@ parent_section: components [handcontrols]: ./hand-controls.md [oculustouchcontrols]: ./oculus-touch-controls.md [vivecontrols]: ./vive-controls.md +[daydreamcontrols]: ./daydream-controls.md The tracked-controls component interfaces with tracked controllers. tracked-controls uses the Gamepad API to handle tracked controllers, and is abstracted by the [hand-controls component][handcontrols] as well as the -[vive-controls][vivecontrols] and [oculus-touch-controls][oculustouchcontrols] +[vive-controls][vivecontrols], [oculus-touch-controls][oculustouchcontrols], and +[daydream-controls][daydreamcontrols] components. This component elects the appropriate controller, applies pose to -the entity, observes buttons state and emits appropriate events. +the entity, observes buttons state and emits appropriate events. For non-6DOF controllers +such as [daydream-controls][daydreamcontrols], a primitive arm model is used to emulate +positional data. ## Example @@ -28,11 +32,17 @@ so using idPrefix for Vive / OpenVR controllers is recommended. ## Value -| Property | Description | Default Value | -|-------------|-------------------------------------------------------------- --|---------------| -| controller | Index of the controller in array returned by the Gamepad API. | 0 | -| id | Selects the controller from the Gamepad API using exact match. | | -| idPrefix | Selects the controller from the Gamepad API using prefix match. | | +| Property | Description | Default Value | +|-------------------|-------------------------------------------------------------- --|----------------------------| +| controller | Index of the controller in array returned by the Gamepad API. | 0 | +| id | Selects the controller from the Gamepad API using exact match. | | +| idPrefix | Selects the controller from the Gamepad API using prefix match. | | +| rotationOffset | Offset to add to model rotation. | 0 | +| headElement | Head element for arm model if needed (if not active camera). | | +| hand | Which hand to use, if arm model is needed. (left negates X) | right | +| eyesToElbow | Arm model vector from eyes to elbow as user height ratio. | {x:0.175, y:-0.3, z:-0.03} | +| forearm | Arm model vector for forearm as user height ratio. | {x:0, y:0, z:-0.175} | +| defaultUserHeight | Default user height (for cameras with zero). | 1.6 | ## Events diff --git a/src/components/daydream-controls.js b/src/components/daydream-controls.js new file mode 100644 index 00000000000..2fb13e09d3d --- /dev/null +++ b/src/components/daydream-controls.js @@ -0,0 +1,206 @@ +var registerComponent = require('../core/component').registerComponent; +var bind = require('../utils/bind'); +var isControllerPresent = require('../utils/tracked-controls').isControllerPresent; + +var DEFAULT_HANDEDNESS = require('../constants').DEFAULT_HANDEDNESS; + +var DAYDREAM_CONTROLLER_MODEL_BASE_URL = 'https://cdn.aframe.io/controllers/google/'; +var DAYDREAM_CONTROLLER_MODEL_OBJ_URL = DAYDREAM_CONTROLLER_MODEL_BASE_URL + 'vr_controller_daydream.obj'; +var DAYDREAM_CONTROLLER_MODEL_OBJ_MTL = DAYDREAM_CONTROLLER_MODEL_BASE_URL + 'vr_controller_daydream.mtl'; + +var GAMEPAD_ID_PREFIX = 'Daydream Controller'; + +/** + * Vive Controls Component + * Interfaces with vive controllers and maps Gamepad events to + * common controller buttons: trackpad, trigger, grip, menu and system + * It loads a controller model and highlights the pressed buttons + */ +module.exports.Component = registerComponent('daydream-controls', { + schema: { + hand: {default: DEFAULT_HANDEDNESS}, // This informs the degenerate arm model. + buttonColor: {type: 'color', default: '#000000'}, + buttonTouchedColor: {type: 'color', default: '#777777'}, + buttonHighlightColor: {type: 'color', default: '#FFFFFF'}, + model: {default: true}, + rotationOffset: {default: 0} // use -999 as sentinel value to auto-determine based on hand + }, + + // buttonId + // 0 - trackpad + // 1 - menu ( never dispatched on this layer ) + // 2 - system ( never dispatched on this layer ) + mapping: { + axis0: 'trackpad', + axis1: 'trackpad', + button0: 'trackpad', + button1: 'menu', + button2: 'system' + }, + + bindMethods: function () { + this.onModelLoaded = bind(this.onModelLoaded, this); + this.onControllersUpdate = bind(this.onControllersUpdate, this); + this.checkIfControllerPresent = bind(this.checkIfControllerPresent, this); + this.removeControllersUpdateListener = bind(this.removeControllersUpdateListener, this); + this.onGamepadConnected = bind(this.onGamepadConnected, this); + this.onGamepadDisconnected = bind(this.onGamepadDisconnected, this); + }, + + init: function () { + var self = this; + this.animationActive = 'pointing'; + this.onButtonDown = function (evt) { self.onButtonEvent(evt.detail.id, 'down'); }; + this.onButtonUp = function (evt) { self.onButtonEvent(evt.detail.id, 'up'); }; + this.onButtonTouchStart = function (evt) { self.onButtonEvent(evt.detail.id, 'touchstart'); }; + this.onButtonTouchEnd = function (evt) { self.onButtonEvent(evt.detail.id, 'touchend'); }; + this.onAxisMoved = bind(this.onAxisMoved, this); + this.controllerPresent = false; + this.everGotGamepadEvent = false; + this.lastControllerCheck = 0; + this.bindMethods(); + this.isControllerPresent = isControllerPresent; // to allow mock + }, + + addEventListeners: function () { + var el = this.el; + el.addEventListener('buttondown', this.onButtonDown); + el.addEventListener('buttonup', this.onButtonUp); + el.addEventListener('touchstart', this.onButtonTouchStart); + el.addEventListener('touchend', this.onButtonTouchEnd); + el.addEventListener('model-loaded', this.onModelLoaded); + el.addEventListener('axismove', this.onAxisMoved); + }, + + removeEventListeners: function () { + var el = this.el; + el.removeEventListener('buttondown', this.onButtonDown); + el.removeEventListener('buttonup', this.onButtonUp); + el.removeEventListener('touchstart', this.onButtonTouchStart); + el.removeEventListener('touchend', this.onButtonTouchEnd); + el.removeEventListener('model-loaded', this.onModelLoaded); + el.removeEventListener('axismove', this.onAxisMoved); + }, + + checkIfControllerPresent: function () { + var isPresent = this.isControllerPresent(this.el.sceneEl, GAMEPAD_ID_PREFIX, {hand: this.data.hand}); + if (isPresent === this.controllerPresent) { return; } + this.controllerPresent = isPresent; + if (isPresent) { this.injectTrackedControls(); } // inject track-controls + }, + + onGamepadConnected: function (evt) { + // for now, don't disable controller update listening, due to + // apparent issue with FF Nightly only sending one event and seeing one controller; + // this.everGotGamepadEvent = true; + // this.removeControllersUpdateListener(); + this.checkIfControllerPresent(); + }, + + onGamepadDisconnected: function (evt) { + // for now, don't disable controller update listening, due to + // apparent issue with FF Nightly only sending one event and seeing one controller; + // this.everGotGamepadEvent = true; + // this.removeControllersUpdateListener(); + this.checkIfControllerPresent(); + }, + + play: function () { + this.checkIfControllerPresent(); + window.addEventListener('gamepadconnected', this.onGamepadConnected, false); + window.addEventListener('gamepaddisconnected', this.onGamepadDisconnected, false); + this.addControllersUpdateListener(); + this.addEventListeners(); + }, + + pause: function () { + window.removeEventListener('gamepadconnected', this.onGamepadConnected, false); + window.removeEventListener('gamepaddisconnected', this.onGamepadDisconnected, false); + this.removeControllersUpdateListener(); + this.removeEventListeners(); + }, + + injectTrackedControls: function () { + var el = this.el; + var data = this.data; + el.setAttribute('tracked-controls', {idPrefix: GAMEPAD_ID_PREFIX, hand: data.hand, rotationOffset: data.rotationOffset}); + if (!this.data.model) { return; } + this.el.setAttribute('obj-model', { + obj: DAYDREAM_CONTROLLER_MODEL_OBJ_URL, + mtl: DAYDREAM_CONTROLLER_MODEL_OBJ_MTL + }); + }, + + addControllersUpdateListener: function () { + this.el.sceneEl.addEventListener('controllersupdated', this.onControllersUpdate, false); + }, + + removeControllersUpdateListener: function () { + this.el.sceneEl.removeEventListener('controllersupdated', this.onControllersUpdate, false); + }, + + onControllersUpdate: function () { + if (!this.everGotGamepadEvent) { this.checkIfControllerPresent(); } + }, + + // No need for onButtonChanged, since Daydream controller has no analog buttons. + + onModelLoaded: function (evt) { + var controllerObject3D = evt.detail.model; + var buttonMeshes; + if (!this.data.model) { return; } + buttonMeshes = this.buttonMeshes = {}; + buttonMeshes.menu = controllerObject3D.getObjectByName('AppButton_AppButton_Cylinder.004'); + buttonMeshes.system = controllerObject3D.getObjectByName('HomeButton_HomeButton_Cylinder.005'); + buttonMeshes.trackpad = controllerObject3D.getObjectByName('TouchPad_TouchPad_Cylinder.003'); + // Offset pivot point + controllerObject3D.position.set(0, 0, -0.04); + }, + + onAxisMoved: function (evt) { + if (evt.detail.axis[0] === 0 && evt.detail.axis[1] === 0) { return; } + this.el.emit('trackpadmoved', { x: evt.detail.axis[0], y: evt.detail.axis[1] }); + }, + + onButtonEvent: function (id, evtName) { + var buttonName = this.mapping['button' + id]; + var i; + if (Array.isArray(buttonName)) { + for (i = 0; i < buttonName.length; i++) { + this.el.emit(buttonName[i] + evtName); + } + } else { + this.el.emit(buttonName + evtName); + } + this.updateModel(buttonName, evtName); + }, + + updateModel: function (buttonName, evtName) { + var i; + if (!this.data.model) { return; } + if (Array.isArray(buttonName)) { + for (i = 0; i < buttonName.length; i++) { + this.updateButtonModel(buttonName[i], evtName); + } + } else { + this.updateButtonModel(buttonName, evtName); + } + }, + + updateButtonModel: function (buttonName, state) { + var buttonMeshes = this.buttonMeshes; + if (!buttonMeshes || !buttonMeshes[buttonName]) { return; } + var color; + switch (state) { + case 'down': + color = this.data.buttonHighlightColor; + break; + case 'touchstart': + color = this.data.buttonTouchedColor; + break; + default: + color = this.data.buttonColor; + } + buttonMeshes[buttonName].material.color.set(color); + } +}); diff --git a/src/components/hand-controls.js b/src/components/hand-controls.js index 86041df5440..5713d64c28f 100644 --- a/src/components/hand-controls.js +++ b/src/components/hand-controls.js @@ -124,6 +124,7 @@ module.exports.Component = registerComponent('hand-controls', { } el.setAttribute('vive-controls', controlConfiguration); el.setAttribute('oculus-touch-controls', controlConfiguration); + el.setAttribute('daydream-controls', controlConfiguration); el.setAttribute('blend-character-model', modelUrl); }, diff --git a/src/components/index.js b/src/components/index.js index abfdfd67bb5..aeb29bb30f7 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -2,6 +2,7 @@ require('./blend-character-model'); require('./camera'); require('./collada-model'); require('./cursor'); +require('./daydream-controls'); require('./geometry'); require('./gltf-model'); require('./hand-controls'); diff --git a/src/components/tracked-controls.js b/src/components/tracked-controls.js index a64effcdce3..a002dcf5b6d 100644 --- a/src/components/tracked-controls.js +++ b/src/components/tracked-controls.js @@ -1,8 +1,10 @@ var registerComponent = require('../core/component').registerComponent; var THREE = require('../lib/three'); +var DEFAULT_USER_HEIGHT = require('../constants').DEFAULT_USER_HEIGHT; -var ZERO_ORIENTATION = [0, 0, 0, 1]; -var ZERO_POSITION = [0, 0, 0]; +var DEFAULT_HANDEDNESS = require('../constants').DEFAULT_HANDEDNESS; +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}; // vector from eyes to elbow (divided by user height) /** * Tracked controls component. @@ -18,7 +20,9 @@ module.exports.Component = registerComponent('tracked-controls', { controller: {default: 0}, id: {type: 'string', default: ''}, idPrefix: {type: 'string', default: ''}, - rotationOffset: {default: 0} + rotationOffset: {default: 0}, + // Arm model parameters, to use when not 6DOF. (pose hasPosition false, no position) + headElement: {type: 'selector'} }, init: function () { @@ -46,6 +50,16 @@ module.exports.Component = registerComponent('tracked-controls', { this.updateButtons(); }, + /** + * Return default user height to use for non-6DOF arm model. + */ + defaultUserHeight: function () { return DEFAULT_USER_HEIGHT; }, // default user height (for cameras with zero) + + /** + * Return head element to use for non-6DOF arm model. + */ + getHeadElement: function () { return this.data.headElement || this.el.sceneEl.camera.el; }, + /** * Handle update to `id` or `idPrefix. */ @@ -63,6 +77,50 @@ module.exports.Component = registerComponent('tracked-controls', { this.controller = matchingControllers[data.controller]; }, + applyArmModel: function (controllerPosition) { + // Use controllerPosition and deltaControllerPosition to avoid creating yet more variables... + var controller = this.controller; + var pose = controller.pose; + var controllerQuaternion = this.controllerQuaternion; + var controllerEuler = this.controllerEuler; + var deltaControllerPosition = this.deltaControllerPosition; + var hand = (controller ? controller.hand : undefined) || DEFAULT_HANDEDNESS; + var headEl = this.getHeadElement(); + var headObject3D = headEl.object3D; + var headCamera = headEl.components.camera; + var userHeight = (headCamera ? headCamera.data.userHeight : 0) || this.defaultUserHeight(); + + // 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 your 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. + deltaControllerPosition.set(FOREARM.x, FOREARM.y, FOREARM.z); // forearm sticking out from elbow + // Scale offset by user height. + deltaControllerPosition.multiplyScalar(userHeight); + // Apply controller X and 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. */ @@ -70,36 +128,45 @@ module.exports.Component = registerComponent('tracked-controls', { var controller = this.controller; var controllerEuler = this.controllerEuler; var controllerPosition = this.controllerPosition; - var controllerQuaternion = this.controllerQuaternion; var currentPosition; var deltaControllerPosition = this.deltaControllerPosition; var dolly = this.dolly; var el = this.el; - var orientation; var pose; - var position; var standingMatrix = this.standingMatrix; var vrDisplay = this.system.vrDisplay; + var headEl = this.getHeadElement(); + var headObject3D = headEl.object3D; + var headCamera = headEl.components.camera; + var userHeight = (headCamera ? headCamera.data.userHeight : 0) || this.defaultUserHeight(); if (!controller) { return; } // Compose pose from Gamepad. pose = controller.pose; - orientation = pose.orientation || ZERO_ORIENTATION; - position = pose.position || ZERO_POSITION; - controllerQuaternion.fromArray(orientation); - dolly.quaternion.fromArray(orientation); - dolly.position.fromArray(position); + // If no orientation, use camera. + if (pose.orientation) { + dolly.quaternion.fromArray(pose.orientation); + } else { + dolly.quaternion.copy(headObject3D.quaternion); + } + if (pose.position) { + dolly.position.fromArray(pose.position); + } else { + // The controller is not 6DOF, so apply arm model. + this.applyArmModel(controllerPosition); + dolly.position.copy(controllerPosition); + } dolly.updateMatrix(); - // Apply transforms. - if (vrDisplay) { + // Apply transforms, if 6DOF and in VR. + if (pose.position && vrDisplay) { if (vrDisplay.stageParameters) { standingMatrix.fromArray(vrDisplay.stageParameters.sittingToStandingTransform); dolly.applyMatrix(standingMatrix); } else { // Apply default camera height - dolly.position.y += el.sceneEl.camera.el.getAttribute('camera').userHeight; + dolly.position.y += userHeight; dolly.updateMatrix(); } } @@ -115,7 +182,7 @@ module.exports.Component = registerComponent('tracked-controls', { z: THREE.Math.radToDeg(controllerEuler.z) + this.data.rotationOffset }); - // Apply position (as delta from previous Gamepad rotation). + // Apply position (as delta from previous Gamepad position). deltaControllerPosition.copy(controllerPosition).sub(this.previousControllerPosition); this.previousControllerPosition.copy(controllerPosition); currentPosition = el.getAttribute('position'); diff --git a/src/constants/index.js b/src/constants/index.js index 54c7ee83071..973d153a289 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,6 +1,7 @@ module.exports = { AFRAME_INJECTED: 'aframe-injected', DEFAULT_CAMERA_HEIGHT: 1.6, + DEFAULT_HANDEDNESS: 'right', animation: require('./animation'), keyboardevent: require('./keyboardevent') };