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')
};