diff --git a/com/control/gaze-touch-to-click.js b/com/control/gaze-touch-to-click.js new file mode 100644 index 0000000..a92aacb --- /dev/null +++ b/com/control/gaze-touch-to-click.js @@ -0,0 +1,47 @@ +// gaze + fuse-to-click on mobile VR +// gaze + tap-to-click on mobile AR +// also adds 'eye'-tracking (mouseover) functionality to display popovers + +AFRAME.registerComponent('gaze-touch-to-click',{ + schema:{ + spawn:{type:'boolean',default:false}, + }, + events:{ + "fusing": function(e){ + if( e.detail.mouseEvent ) return // ignore click event + console.dir(e) + + } + }, + setGazer: function(state, fuse){ + if( !AFRAME.utils.device.isMobile() ) return + let cam = document.querySelector("[camera]") + if( state ){ + if( cam.innerHTML.match(/cursor/) ) return; // avoid duplicate calls + cam.innerHTML = ` + ` + cam.querySelector('#cursor').setAttribute("geometry","primitive: ring; radiusInner: 0.02; radiusOuter: 0.03") + }else{ + cam.querySelector('#cursor').removeAttribute("geometry") + if( document.querySelector('[cursor]') ) { + document.querySelector('[cursor]').setAttribute("visible",false) + } + } + }, + init:function(data){ + this.setGazer(true); + + document.querySelector("a-scene").addEventListener('exit-vr', () => this.setGazer(false,false) ) + document.querySelector("a-scene").addEventListener('enter-vr', () => this.setGazer(true,true) ) + document.querySelector("a-scene").addEventListener('enter-ar', () => this.setGazer(true,false) ) + document.querySelector("a-scene").addEventListener('exit-ar', () => this.setGazer(false,false) ) + + } +}); diff --git a/com/control/no-look-controls-in-xr.js b/com/control/no-look-controls-in-xr.js new file mode 100644 index 0000000..50e6fc0 --- /dev/null +++ b/com/control/no-look-controls-in-xr.js @@ -0,0 +1,67 @@ +// look-controls turns off autoUpdateMatrix (of player) which +// will break repositionining the VR rig (teleporting and other stuff) +// overriding this below is easier then adding updateMatrixWorld() everywhere else +// (or rolling your own look-controls and diverting from mainbranch) + +AFRAME.registerComponent('patch-look-controls',{ + + init: function(){ + alert("fjo"); //dEventListener('loaded', () => this.patchLookControls() ) + }, + + patchLookControls: function(){ +alert("ja!") + let lk = document.querySelector('[look-controls]') + if( !lk ) return + lk = lk.components['look-controls'] + + lk.onEnterVR = function () { + var sceneEl = this.el.sceneEl; + if (!sceneEl.checkHeadsetConnected()) { return; } + this.saveCameraPose(); + this.el.object3D.position.set(0, 0, 0); + this.el.object3D.rotation.set(0, 0, 0); + if (sceneEl.hasWebXR) { + // this.el.object3D.matrixAutoUpdate = false; + this.el.object3D.updateMatrix(); + } + } + + /** + * Restore the pose. + */ + lk.onExitVR = function () { + if (!this.el.sceneEl.checkHeadsetConnected()) { return; } + this.restoreCameraPose(); + this.previousHMDPosition.set(0, 0, 0); + this.el.object3D.matrixAutoUpdate = true; + } + + // it also needs to apply the offset (in case the #rot was used in URLS) + + lk.updateOrientation = function () { + var object3D = this.el.object3D; + var pitchObject = this.pitchObject; + var yawObject = this.yawObject; + var sceneEl = this.el.sceneEl; + + // In VR or AR mode, THREE is in charge of updating the camera pose. + if ((sceneEl.is('vr-mode') || sceneEl.is('ar-mode')) && sceneEl.checkHeadsetConnected()) { + // With WebXR THREE applies headset pose to the object3D internally. + return; + } + + this.updateMagicWindowOrientation(); + + let offsetX = object3D.rotation.offset ? object3D.rotation.offset.x : 0 + let offsetY = object3D.rotation.offset ? object3D.rotation.offset.y : 0 + + // On mobile, do camera rotation with touch events and sensors. + object3D.rotation.x = this.magicWindowDeltaEuler.x + offsetX + pitchObject.rotation.x; + object3D.rotation.y = this.magicWindowDeltaEuler.y + offsetY + yawObject.rotation.y; + object3D.rotation.z = this.magicWindowDeltaEuler.z; + object3D.matrixAutoUpdate = true + } + + } +}) diff --git a/com/control/patch-look-controls.js b/com/control/patch-look-controls.js new file mode 100644 index 0000000..7192c48 --- /dev/null +++ b/com/control/patch-look-controls.js @@ -0,0 +1,65 @@ +// look-controls turns off autoUpdateMatrix (of player) which +// will break repositionining the VR rig (teleporting and other stuff) +// overriding this below is easier then adding updateMatrixWorld() everywhere else +// (or rolling your own look-controls and diverting from mainbranch) + + +// this component will re-attach a patched look-controls +AFRAME.registerComponent('patch-look-controls',{ + + init: function(){ + let scene = AFRAME.scenes[0] + let el = document.querySelector('[look-controls]') + el.removeAttribute("look-controls") + + AFRAME.components['look-controls'].Component.prototype.onEnterVR = function () { + var sceneEl = this.el.sceneEl; + if (!sceneEl.checkHeadsetConnected()) { return; } + this.saveCameraPose(); + this.el.object3D.position.set(0, 0, 0); + this.el.object3D.rotation.set(0, 0, 0); + if (sceneEl.hasWebXR) { + // this.el.object3D.matrixAutoUpdate = false; + this.el.object3D.updateMatrix(); + } + } + + /** + * Restore the pose. + */ + AFRAME.components['look-controls'].Component.prototype.onExitVR = function () { + if (!this.el.sceneEl.checkHeadsetConnected()) { return; } + this.restoreCameraPose(); + this.previousHMDPosition.set(0, 0, 0); + this.el.object3D.matrixAutoUpdate = true; + } + + // it also needs to apply the offset (in case the #rot was used in URLS) + + AFRAME.components['look-controls'].Component.prototype.updateOrientation = function () { + var object3D = this.el.object3D; + var pitchObject = this.pitchObject; + var yawObject = this.yawObject; + var sceneEl = this.el.sceneEl; + + // In VR or AR mode, THREE is in charge of updating the camera pose. + if ((sceneEl.is('vr-mode') || sceneEl.is('ar-mode')) && sceneEl.checkHeadsetConnected()) { + // With WebXR THREE applies headset pose to the object3D internally. + return; + } + + this.updateMagicWindowOrientation(); + + let offsetX = object3D.rotation.offset ? object3D.rotation.offset.x : 0 + let offsetY = object3D.rotation.offset ? object3D.rotation.offset.y : 0 + + // On mobile, do camera rotation with touch events and sensors. + object3D.rotation.x = this.magicWindowDeltaEuler.x + offsetX + pitchObject.rotation.x; + object3D.rotation.y = this.magicWindowDeltaEuler.y + offsetY + yawObject.rotation.y; + object3D.rotation.z = this.magicWindowDeltaEuler.z; + object3D.matrixAutoUpdate = true + } + + el.setAttribute('look-controls','') + } +}) diff --git a/com/control/pinch-to-teleport.js b/com/control/pinch-to-teleport.js new file mode 100644 index 0000000..27f4c5a --- /dev/null +++ b/com/control/pinch-to-teleport.js @@ -0,0 +1,27 @@ +// poor man's way to move forward using hand gesture pinch + +window.AFRAME.registerComponent('pinch-to-teleport', { + schema:{ + rig: {type: "selector"} + }, + init: function(){ + this.el.addEventListener("pinchended", () => { + // get the cameras world direction + let direction = new THREE.Vector3() + this.el.sceneEl.camera.getWorldDirection(direction); + // multiply the direction by a "speed" factor + direction.multiplyScalar(0.4) + // get the current position + var pos = player.getAttribute("position") + // add the direction vector + pos.x += direction.x + pos.z += direction.z + // set the new position + this.data.rig.setAttribute("position", pos); + // !!! NOTE - it would be more efficient to do the + // position change on the players THREE.Object: + // `player.object3D.position.add(direction)` + // but it would break "getAttribute("position") + }) + }, +}) diff --git a/com/control/pressable.js b/com/control/pressable.js new file mode 100644 index 0000000..85d8675 --- /dev/null +++ b/com/control/pressable.js @@ -0,0 +1,72 @@ +// this makes WebXR hand controls able to click things (by touching it) + +AFRAME.registerComponent('pressable', { + schema: { + pressDistance: { + default: 0.01 + } + }, + init: function() { + this.worldPosition = new THREE.Vector3(); + this.fingerWorldPosition = new THREE.Vector3(); + this.raycaster = new THREE.Raycaster() + this.handEls = document.querySelectorAll('[hand-tracking-controls]'); + this.pressed = false; + this.distance = -1 + // we throttle by distance, to support scenes with loads of clickable objects (far away) + this.tick = this.throttleByDistance( () => this.detectPress() ) + }, + throttleByDistance: function(f){ + return function(){ + if( this.distance < 0 ) return f() // first call + if( !f.tid ){ + let x = this.distance + let y = x*(x*0.05)*1000 // parabolic curve + f.tid = setTimeout( function(){ + f.tid = null + f() + }, y ) + } + } + }, + detectPress: function(){ + var handEls = this.handEls; + var handEl; + let minDistance = 5 + + // compensate for xrf-get AFRAME component (which references non-reparented buffergeometries from the 3D model) + let object3D = this.el.object3D.child || this.el.object3D + + for (var i = 0; i < handEls.length; i++) { + handEl = handEls[i]; + let indexTipPosition = handEl.components['hand-tracking-controls'].indexTipPosition + // Apply the relative position to the parent's world position + handEl.object3D.updateMatrixWorld(); + handEl.object3D.getWorldPosition( this.fingerWorldPosition ) + this.fingerWorldPosition.add( indexTipPosition ) + + this.raycaster.far = this.data.pressDistance + // Create a direction vector (doesnt matter because it is supershort for 'touch' purposes) + const direction = new THREE.Vector3(1.0,0,0); + this.raycaster.set(this.fingerWorldPosition, direction) + intersects = this.raycaster.intersectObjects([object3D],true) + + object3D.getWorldPosition(this.worldPosition) + + distance = this.fingerWorldPosition.distanceTo(this.worldPosition) + minDistance = distance < minDistance ? distance : minDistance + + if (intersects.length ){ + if( !this.pressed ){ + this.el.emit('pressedstarted'); + this.el.emit('click'); + this.pressed = setTimeout( () => { + this.el.emit('pressedended'); + this.pressed = null + },300) + } + } + } + this.distance = minDistance + } +}); diff --git a/com/control/wearable.js b/com/control/wearable.js new file mode 100644 index 0000000..0babf4c --- /dev/null +++ b/com/control/wearable.js @@ -0,0 +1,26 @@ +window.AFRAME.registerComponent('xrf-wear', { + schema:{ + el: {type:"selector"}, + position: {type:"vec3"}, + rotation: {type:"vec3"} + }, + init: function(){ + $('a-scene').addEventListener('enter-vr', (e) => this.wear(e) ) + $('a-scene').addEventListener('exit-vr', (e) => this.unwear(e) ) + }, + wear: function(){ + if( !this.wearable ){ + let d = this.data + this.wearable = new THREE.Group() + this.el.object3D.children.map( (c) => this.wearable.add(c) ) + this.wearable.position.set( d.position.x, d.position.y, d.position.z) + this.wearable.rotation.set( d.rotation.x, d.rotation.y, d.rotation.z) + } + this.data.el.object3D.add(this.wearable) + }, + unwear: function(){ + this.data.el.remove(this.wearable) + this.wearable.children.map( (c) => this.el.object3D.add(c) ) + delete this.wearable + } +})