UNPKG

aframe

Version:

A web framework for building virtual reality experiences.

406 lines (350 loc) 13.3 kB
/* global XRHand */ import * as THREE from 'three'; import { registerComponent } from '../core/component.js'; import { AEntity } from '../core/a-entity.js'; import { checkControllerPresentAndSetup } from '../utils/tracked-controls.js'; import { AFRAME_CDN_ROOT } from '../constants/index.js'; var LEFT_HAND_MODEL_URL = AFRAME_CDN_ROOT + 'controllers/oculus-hands/v4/left.glb'; var RIGHT_HAND_MODEL_URL = AFRAME_CDN_ROOT + 'controllers/oculus-hands/v4/right.glb'; var JOINTS = [ 'wrist', 'thumb-metacarpal', 'thumb-phalanx-proximal', 'thumb-phalanx-distal', 'thumb-tip', 'index-finger-metacarpal', 'index-finger-phalanx-proximal', 'index-finger-phalanx-intermediate', 'index-finger-phalanx-distal', 'index-finger-tip', 'middle-finger-metacarpal', 'middle-finger-phalanx-proximal', 'middle-finger-phalanx-intermediate', 'middle-finger-phalanx-distal', 'middle-finger-tip', 'ring-finger-metacarpal', 'ring-finger-phalanx-proximal', 'ring-finger-phalanx-intermediate', 'ring-finger-phalanx-distal', 'ring-finger-tip', 'pinky-finger-metacarpal', 'pinky-finger-phalanx-proximal', 'pinky-finger-phalanx-intermediate', 'pinky-finger-phalanx-distal', 'pinky-finger-tip' ]; var WRIST_INDEX = 0; var THUMB_TIP_INDEX = 4; var INDEX_TIP_INDEX = 9; var PINCH_START_DISTANCE = 0.015; var PINCH_END_DISTANCE = 0.02; /** * Controls for hand tracking */ export var Component = registerComponent('hand-tracking-controls', { schema: { hand: {default: 'right', oneOf: ['left', 'right']}, modelStyle: {default: 'mesh', oneOf: ['dots', 'mesh']}, modelColor: {default: 'white'}, modelOpacity: {default: 1.0} }, after: ['tracked-controls'], bindMethods: function () { this.onControllersUpdate = this.onControllersUpdate.bind(this); this.checkIfControllerPresent = this.checkIfControllerPresent.bind(this); this.removeControllersUpdateListener = this.removeControllersUpdateListener.bind(this); }, addEventListeners: function () { this.el.addEventListener('model-loaded', this.onModelLoaded); for (var i = 0; i < this.jointEls.length; ++i) { this.jointEls[i].object3D.visible = true; } }, removeEventListeners: function () { this.el.removeEventListener('model-loaded', this.onModelLoaded); for (var i = 0; i < this.jointEls.length; ++i) { this.jointEls[i].object3D.visible = false; } }, init: function () { var sceneEl = this.el.sceneEl; var webxrData = sceneEl.getAttribute('webxr'); var optionalFeaturesArray = webxrData.optionalFeatures; if (optionalFeaturesArray.indexOf('hand-tracking') === -1) { optionalFeaturesArray.push('hand-tracking'); sceneEl.setAttribute('webxr', webxrData); } this.wristObject3D = new THREE.Object3D(); this.el.sceneEl.object3D.add(this.wristObject3D); this.onModelLoaded = this.onModelLoaded.bind(this); this.onChildAttached = this.onChildAttached.bind(this); this.jointEls = []; this.controllerPresent = false; this.isPinched = false; this.pinchEventDetail = { position: new THREE.Vector3(), wristRotation: new THREE.Quaternion() }; this.indexTipPosition = new THREE.Vector3(); this.hasPoses = false; this.jointPoses = new Float32Array(16 * JOINTS.length); this.jointRadii = new Float32Array(JOINTS.length); this.bindMethods(); this.updateReferenceSpace = this.updateReferenceSpace.bind(this); this.el.sceneEl.addEventListener('enter-vr', this.updateReferenceSpace); this.el.sceneEl.addEventListener('exit-vr', this.updateReferenceSpace); this.el.addEventListener('child-attached', this.onChildAttached); this.wristObject3D.visible = false; }, onChildAttached: function (evt) { this.addChildEntity(evt.detail.el); }, update: function () { this.updateModelMaterial(); }, updateModelMaterial: function () { var jointEls = this.jointEls; var skinnedMesh = this.skinnedMesh; var transparent = !(this.data.modelOpacity === 1.0); if (skinnedMesh) { this.skinnedMesh.material.color.set(this.data.modelColor); this.skinnedMesh.material.transparent = transparent; this.skinnedMesh.material.opacity = this.data.modelOpacity; } for (var i = 0; i < jointEls.length; i++) { jointEls[i].setAttribute('material', { color: this.data.modelColor, transparent: transparent, opacity: this.data.modelOpacity }); } }, updateReferenceSpace: function () { var self = this; var xrSession = this.el.sceneEl.xrSession; this.referenceSpace = undefined; if (!xrSession) { return; } var referenceSpaceType = self.el.sceneEl.systems.webxr.sessionReferenceSpaceType; xrSession.requestReferenceSpace(referenceSpaceType).then(function (referenceSpace) { self.referenceSpace = referenceSpace; }).catch(function (error) { self.el.sceneEl.systems.webxr.warnIfFeatureNotRequested(referenceSpaceType, 'tracked-controls-webxr uses reference space ' + referenceSpaceType); throw error; }); }, checkIfControllerPresent: function () { var data = this.data; var hand = data.hand ? data.hand : undefined; checkControllerPresentAndSetup( this, '', {hand: hand, iterateControllerProfiles: true, handTracking: true}); }, play: function () { this.checkIfControllerPresent(); this.addControllersUpdateListener(); }, tick: function () { var sceneEl = this.el.sceneEl; var controller = this.el.components['tracked-controls'] && this.el.components['tracked-controls'].controller; var frame = sceneEl.frame; var trackedControlsWebXR = this.el.components['tracked-controls']; var referenceSpace = this.referenceSpace; if (!controller || !frame || !referenceSpace || !trackedControlsWebXR) { return; } this.hasPoses = false; if (controller.hand) { this.el.object3D.position.set(0, 0, 0); this.el.object3D.rotation.set(0, 0, 0); this.hasPoses = frame.fillPoses(controller.hand.values(), referenceSpace, this.jointPoses) && frame.fillJointRadii(controller.hand.values(), this.jointRadii); this.updateHandModel(); this.detectGesture(); this.updateWristObject(); } }, updateWristObject: (function () { var jointPose = new THREE.Matrix4(); return function () { var wristObject3D = this.wristObject3D; if (!wristObject3D || !this.hasPoses) { return; } jointPose.fromArray(this.jointPoses, WRIST_INDEX * 16); wristObject3D.position.setFromMatrixPosition(jointPose); wristObject3D.quaternion.setFromRotationMatrix(jointPose); }; })(), updateHandModel: function () { this.wristObject3D.visible = true; this.el.object3D.visible = true; if (this.data.modelStyle === 'dots') { this.updateHandDotsModel(); } if (this.data.modelStyle === 'mesh') { this.updateHandMeshModel(); } }, getBone: function (name) { var bones = this.bones; for (var i = 0; i < bones.length; i++) { if (bones[i].name === name) { return bones[i]; } } return null; }, updateHandMeshModel: (function () { var jointPose = new THREE.Matrix4(); return function () { var i = 0; var jointPoses = this.jointPoses; var controller = this.el.components['tracked-controls'] && this.el.components['tracked-controls'].controller; if (!controller || !this.mesh) { return; } this.mesh.visible = false; if (!this.hasPoses) { return; } for (var inputjoint of controller.hand.values()) { var bone = this.getBone(inputjoint.jointName); if (bone != null) { this.mesh.visible = true; jointPose.fromArray(jointPoses, i * 16); bone.position.setFromMatrixPosition(jointPose); bone.quaternion.setFromRotationMatrix(jointPose); } i++; } }; })(), updateHandDotsModel: function () { var jointPoses = this.jointPoses; var jointRadii = this.jointRadii; var controller = this.el.components['tracked-controls'] && this.el.components['tracked-controls'].controller; var jointEl; var object3D; for (var i = 0; i < controller.hand.size; i++) { jointEl = this.jointEls[i]; object3D = jointEl.object3D; jointEl.object3D.visible = this.hasPoses; if (!this.hasPoses) { continue; } object3D.matrix.fromArray(jointPoses, i * 16); object3D.matrix.decompose(object3D.position, object3D.rotation, object3D.scale); jointEl.setAttribute('scale', {x: jointRadii[i], y: jointRadii[i], z: jointRadii[i]}); } }, detectGesture: function () { this.detectPinch(); }, detectPinch: (function () { var thumbTipPosition = new THREE.Vector3(); var jointPose = new THREE.Matrix4(); return function () { var indexTipPosition = this.indexTipPosition; var pinchEventDetail = this.pinchEventDetail; if (!this.hasPoses) { return; } thumbTipPosition.setFromMatrixPosition(jointPose.fromArray(this.jointPoses, THUMB_TIP_INDEX * 16)); indexTipPosition.setFromMatrixPosition(jointPose.fromArray(this.jointPoses, INDEX_TIP_INDEX * 16)); pinchEventDetail.wristRotation.setFromRotationMatrix(jointPose.fromArray(this.jointPoses, WRIST_INDEX * 16)); var distance = indexTipPosition.distanceTo(thumbTipPosition); if (distance < PINCH_START_DISTANCE && this.isPinched === false) { this.isPinched = true; pinchEventDetail.position.copy(indexTipPosition).add(thumbTipPosition).multiplyScalar(0.5); this.el.emit('pinchstarted', pinchEventDetail); } if (distance > PINCH_END_DISTANCE && this.isPinched === true) { this.isPinched = false; pinchEventDetail.position.copy(indexTipPosition).add(thumbTipPosition).multiplyScalar(0.5); this.el.emit('pinchended', pinchEventDetail); } if (this.isPinched) { pinchEventDetail.position.copy(indexTipPosition).add(thumbTipPosition).multiplyScalar(0.5); this.el.emit('pinchmoved', pinchEventDetail); } }; })(), pause: function () { this.removeEventListeners(); this.removeControllersUpdateListener(); }, injectTrackedControls: function () { var el = this.el; var data = this.data; el.setAttribute('tracked-controls', { id: '', hand: data.hand, iterateControllerProfiles: true, handTrackingEnabled: true }); if (this.mesh) { if (this.mesh !== el.getObject3D('mesh')) { el.setObject3D('mesh', this.mesh); } return; } this.initDefaultModel(); }, addControllersUpdateListener: function () { this.el.sceneEl.addEventListener('controllersupdated', this.onControllersUpdate, false); }, removeControllersUpdateListener: function () { this.el.sceneEl.removeEventListener('controllersupdated', this.onControllersUpdate, false); }, onControllersUpdate: function () { var el = this.el; var controller; this.checkIfControllerPresent(); controller = el.components['tracked-controls'] && el.components['tracked-controls'].controller; if (!this.mesh) { return; } if (controller && controller.hand && (controller.hand instanceof XRHand)) { el.setObject3D('mesh', this.mesh); } }, initDefaultModel: function () { var data = this.data; if (data.modelStyle === 'dots') { this.initDotsModel(); } if (data.modelStyle === 'mesh') { this.initMeshHandModel(); } this.el.object3D.visible = true; this.wristObject3D.visible = true; }, initDotsModel: function () { // Add models just once. if (this.jointEls.length !== 0) { return; } for (var i = 0; i < JOINTS.length; ++i) { var jointEl = this.jointEl = document.createElement('a-entity'); jointEl.setAttribute('geometry', { primitive: 'sphere', radius: 1.0 }); jointEl.object3D.visible = false; this.el.appendChild(jointEl); this.jointEls.push(jointEl); } this.updateModelMaterial(); }, initMeshHandModel: function () { var modelURL = this.data.hand === 'left' ? LEFT_HAND_MODEL_URL : RIGHT_HAND_MODEL_URL; this.el.setAttribute('gltf-model', modelURL); }, onModelLoaded: function () { var mesh = this.mesh = this.el.getObject3D('mesh').children[0]; var skinnedMesh = this.skinnedMesh = mesh.getObjectByProperty('type', 'SkinnedMesh'); if (!this.skinnedMesh) { return; } this.bones = skinnedMesh.skeleton.bones; this.el.removeObject3D('mesh'); mesh.position.set(0, 0, 0); mesh.rotation.set(0, 0, 0); skinnedMesh.frustumCulled = false; skinnedMesh.material = new THREE.MeshStandardMaterial(); this.updateModelMaterial(); this.setupChildrenEntities(); this.el.setObject3D('mesh', mesh); }, setupChildrenEntities: function () { var childrenEls = this.el.children; for (var i = 0; i < childrenEls.length; ++i) { if (!(childrenEls[i] instanceof AEntity)) { continue; } this.addChildEntity(childrenEls[i]); } }, addChildEntity: function (childEl) { if (!(childEl instanceof AEntity)) { return; } this.wristObject3D.add(childEl.object3D); } });