From b7380128ed7d9dde535f957d89b50d9bf6296b34 Mon Sep 17 00:00:00 2001 From: Lee Stemkoski Date: Thu, 24 Feb 2022 15:43:54 -0500 Subject: [PATCH] refactoring Quest-related code --- js/controller-listener.js | 153 +++++++------ js/player-move.js | 56 ++--- js/raycaster-controller-grabber.js | 132 ------------ js/raycaster-extras.js | 332 +++++++++++++++++++++++++++++ js/raycaster-graphics.js | 187 ---------------- quest-interact.html | 68 +++--- quest-move.html | 21 +- quest-music.html | 103 +++++---- quest-raycaster.html | 49 ++--- quest.html | 54 +++-- 10 files changed, 596 insertions(+), 559 deletions(-) delete mode 100644 js/raycaster-controller-grabber.js create mode 100644 js/raycaster-extras.js delete mode 100644 js/raycaster-graphics.js diff --git a/js/controller-listener.js b/js/controller-listener.js index 2c903c6..8cb6719 100644 --- a/js/controller-listener.js +++ b/js/controller-listener.js @@ -2,85 +2,99 @@ AFRAME.registerComponent('controller-listener', { schema: { - hand: {type: 'string', default: "right"}, + leftControllerId: {type: 'string', default: "#left-controller"}, + rightControllerId: {type: 'string', default: "#right-controller"}, }, init: function() { - // common to both controllers + this.leftController = document.querySelector(this.data.leftControllerId); + this.rightController = document.querySelector(this.data.rightControllerId); + + this.leftAxisX = 0; + this.leftAxisY = 0; + this.leftTrigger = {pressed: false, pressing: false, released: false, value: 0}; + this.leftGrip = {pressed: false, pressing: false, released: false, value: 0}; - this.axisX = 0; - this.axisY = 0; - this.trigger = {pressed: false, pressing: false, released: false, value: 0}; - this.grip = {pressed: false, pressing: false, released: false, value: 0}; + this.rightAxisX = 0; + this.rightAxisY = 0; + this.rightTrigger = {pressed: false, pressing: false, released: false, value: 0}; + this.rightGrip = {pressed: false, pressing: false, released: false, value: 0}; - // only on one controller + this.buttonA = {pressed: false, pressing: false, released: false}; + this.buttonB = {pressed: false, pressing: false, released: false}; + this.buttonX = {pressed: false, pressing: false, released: false}; + this.buttonY = {pressed: false, pressing: false, released: false}; - if (this.data.hand == "right") - { - this.buttonA = {pressed: false, pressing: false, released: false}; - this.buttonB = {pressed: false, pressing: false, released: false}; - } + // event listeners + let self = this; - if (this.data.hand == "left") - { - this.buttonX = {pressed: false, pressing: false, released: false}; - this.buttonY = {pressed: false, pressing: false, released: false}; - } + // left controller - // event listeners + this.leftController.addEventListener('thumbstickmoved', function(event) + { self.leftAxisX = event.detail.x; + self.leftAxisY = event.detail.y; } ); - let self = this; + this.leftController.addEventListener("triggerdown", function(event) + { self.leftTrigger.pressed = true; } ); + this.leftController.addEventListener("triggerup", function(event) + { self.leftTrigger.released = true; } ); + this.leftController.addEventListener('triggerchanged', function (event) + { self.leftTrigger.value = event.detail.value; } ); - this.el.addEventListener('thumbstickmoved', function(event) - { self.axisX = event.detail.x; - self.axisY = event.detail.y; } ); - - this.el.addEventListener("triggerdown", function(event) - { self.trigger.pressed = true; } ); - this.el.addEventListener("triggerup", function(event) - { self.trigger.released = true; } ); - this.el.addEventListener('triggerchanged', function (event) - { self.trigger.value = event.detail.value; } ); - - this.el.addEventListener("gripdown", function(event) - { self.grip.pressed = true; } ); - this.el.addEventListener("gripup", function(event) - { self.grip.released = true; } ); - this.el.addEventListener('gripchanged', function (event) - { self.grip.value = event.detail.value; } ); - - if (this.data.hand == "right") - { - this.el.addEventListener("abuttondown", function(event) - { self.buttonA.pressed = true; } ); - this.el.addEventListener("abuttonup", function(event) - { self.buttonA.released = true; } ); - - this.el.addEventListener("bbuttondown", function(event) - { self.buttonB.pressed = true; } ); - this.el.addEventListener("bbuttonup", function(event) - { self.buttonB.released = true; } ); - } - - if (this.data.hand == "left") - { - this.el.addEventListener("xbuttondown", function(event) + this.leftController.addEventListener("gripdown", function(event) + { self.leftGrip.pressed = true; } ); + this.leftController.addEventListener("gripup", function(event) + { self.leftGrip.released = true; } ); + this.leftController.addEventListener('gripchanged', function (event) + { self.leftGrip.value = event.detail.value; } ); + + this.leftController.addEventListener("xbuttondown", function(event) { self.buttonX.pressed = true; } ); - this.el.addEventListener("xbuttonup", function(event) + this.leftController.addEventListener("xbuttonup", function(event) { self.buttonX.released = true; } ); - this.el.addEventListener("ybuttondown", function(event) + this.leftController.addEventListener("ybuttondown", function(event) { self.buttonY.pressed = true; } ); - this.el.addEventListener("ybuttonup", function(event) + this.leftController.addEventListener("ybuttonup", function(event) { self.buttonY.released = true; } ); - } + + // right controller + + this.rightController.addEventListener('thumbstickmoved', function(event) + { self.rightAxisX = event.detail.x; + self.rightAxisY = event.detail.y; } ); + + this.rightController.addEventListener("triggerdown", function(event) + { self.rightTrigger.pressed = true; } ); + this.rightController.addEventListener("triggerup", function(event) + { self.rightTrigger.released = true; } ); + this.rightController.addEventListener('triggerchanged', function (event) + { self.rightTrigger.value = event.detail.value; } ); + + this.rightController.addEventListener("gripdown", function(event) + { self.rightGrip.pressed = true; } ); + this.rightController.addEventListener("gripup", function(event) + { self.rightGrip.released = true; } ); + this.rightController.addEventListener('gripchanged', function (event) + { self.rightGrip.value = event.detail.value; } ); + + this.rightController.addEventListener("abuttondown", function(event) + { self.buttonA.pressed = true; } ); + this.rightController.addEventListener("abuttonup", function(event) + { self.buttonA.released = true; } ); + + this.rightController.addEventListener("bbuttondown", function(event) + { self.buttonB.pressed = true; } ); + this.rightController.addEventListener("bbuttonup", function(event) + { self.buttonB.released = true; } ); }, updateButtonState: function( stateObject ) { // if button was recently pressed: - // first pressing becomes true, then on neck tick, pressed becomes false. + // on first tick, pressing becomes true, + // then on next tick, pressed becomes false. if (stateObject.pressed) { if (!stateObject.pressing) @@ -90,7 +104,8 @@ AFRAME.registerComponent('controller-listener', { } // if button was recently released: - // first pressing becomes false, then on next tick, released becomes false. + // on first tick, pressing becomes false, + // then on next tick, released becomes false. if (stateObject.released) { if (stateObject.pressing) @@ -102,20 +117,16 @@ AFRAME.registerComponent('controller-listener', { tick: function() { - this.updateButtonState( this.trigger ); - this.updateButtonState( this.grip ); + this.updateButtonState( this.leftTrigger ); + this.updateButtonState( this.leftGrip ); - if (this.data.hand == "right") - { - this.updateButtonState( this.buttonA ); - this.updateButtonState( this.buttonB ); - } + this.updateButtonState( this.rightTrigger ); + this.updateButtonState( this.rightGrip ); - if (this.data.hand == "left") - { - this.updateButtonState( this.buttonX ); - this.updateButtonState( this.buttonY ); - } + this.updateButtonState( this.buttonA ); + this.updateButtonState( this.buttonB ); + this.updateButtonState( this.buttonX ); + this.updateButtonState( this.buttonY ); } }); diff --git a/js/player-move.js b/js/player-move.js index df855af..753e658 100644 --- a/js/player-move.js +++ b/js/player-move.js @@ -1,7 +1,14 @@ AFRAME.registerComponent("player-move", { + schema: + { + controllerListenerId: {type: 'string', default: "#controller-data"}, + }, + init: function() { + this.controllerData = document.querySelector(this.data.controllerListenerId).components["controller-listener"]; + this.clock = new THREE.Clock(); this.moveSpeed = 1; // units per second @@ -12,26 +19,25 @@ AFRAME.registerComponent("player-move", { // used when getting world position of controllers this.tempVector1 = new THREE.Vector3(); this.tempVector2 = new THREE.Vector3(); + + this.enabled = true; }, tick: function() { + // always update deltaTime! let deltaTime = this.clock.getDelta(); - // get data from quest controllers; - // requires the "controller-listener" component - let leftController = document.querySelector("#left-controller-entity"); - let leftData = leftController.components["controller-listener"]; - - let rightController = document.querySelector("#right-controller-entity"); - let rightData = rightController.components["controller-listener"]; + if ( !this.enabled ) + return; // ===================================================================== // moving on horizontal (XZ) plane // ===================================================================== // move with left joystick - let leftJoystickLength = Math.sqrt(leftData.axisX * leftData.axisX + leftData.axisY * leftData.axisY); + let leftJoystickLength = Math.sqrt(this.controllerData.leftAxisX * this.controllerData.leftAxisX + + this.controllerData.leftAxisY * this.controllerData.leftAxisY ); if ( leftJoystickLength > 0.001 ) { @@ -40,14 +46,14 @@ AFRAME.registerComponent("player-move", { this.el.sceneEl.camera.getWorldDirection(cameraDirection); let cameraAngle = Math.atan2(cameraDirection.z, cameraDirection.x); - let leftJoystickAngle = Math.atan2(leftData.axisY, leftData.axisX); + let leftJoystickAngle = Math.atan2(this.controllerData.leftAxisY, this.controllerData.leftAxisX); let moveAngle = cameraAngle + leftJoystickAngle; let moveDistance = this.moveSpeed * deltaTime; // move faster if pressing trigger at same time - moveDistance *= (1 + 9 * leftData.trigger.value); + moveDistance *= (1 + 9 * this.controllerData.leftTrigger.value); // convert move distance and angle to right and forward amounts // scale by magnitude of joystick press (smaller press moves player slower) @@ -66,21 +72,21 @@ AFRAME.registerComponent("player-move", { // press right joystick left/right to turn left/right by N degrees; // joystick must return to rest/center position before turning again - if ( Math.abs(rightData.axisX) < 0.10 ) + if ( Math.abs(this.controllerData.rightAxisX) < 0.10 ) { this.turnReady = true; } if ( this.turnReady ) { - if ( rightData.axisX > 0.90 ) + if ( this.controllerData.rightAxisX > 0.90 ) { let rot = this.el.getAttribute("rotation"); rot.y -= this.turnAngle; this.el.setAttribute("rotation", rot); this.turnReady = false; } - if ( rightData.axisX < -0.90 ) + if ( this.controllerData.rightAxisX < -0.90 ) { let rot = this.el.getAttribute("rotation"); rot.y += this.turnAngle; @@ -95,20 +101,20 @@ AFRAME.registerComponent("player-move", { // ===================================================================== // hold trigger + grab, then hold A/X to pull player - if ( rightData.trigger.pressing && rightData.grip.pressing ) + if ( this.controllerData.rightTrigger.pressing && this.controllerData.rightGrip.pressing ) { - // let rightHandCurrentPos = rightController.getAttribute("position"); - rightController.object3D.getWorldPosition(this.tempVector1); + // store rightHandCurrentPosition in tempVector1 + this.controllerData.rightController.object3D.getWorldPosition(this.tempVector1); - if ( !rightData.buttonA.pressing ) + if ( !this.controllerData.buttonA.pressing ) { this.rightHandPreviousX = this.tempVector1.x; this.rightHandPreviousY = this.tempVector1.y; this.rightHandPreviousZ = this.tempVector1.z; } - else // if ( rightData.buttonA.pressing ) + else // if ( this.controllerData.buttonA.pressing ) { - // let playerPos = this.el.getAttribute("position"); + // store playerPosition in tempVector2 this.el.object3D.getWorldPosition(this.tempVector2); this.tempVector2.x -= (this.tempVector1.x - this.rightHandPreviousX); @@ -126,20 +132,20 @@ AFRAME.registerComponent("player-move", { } } - if ( leftData.trigger.pressing && leftData.grip.pressing ) + if ( this.controllerData.leftTrigger.pressing && this.controllerData.leftGrip.pressing ) { - // let leftHandCurrentPos = leftController.getAttribute("position"); - leftController.object3D.getWorldPosition(this.tempVector1); + // store leftHandCurrentPosition in tempVector1 + this.controllerData.leftController.object3D.getWorldPosition(this.tempVector1); - if ( !leftData.buttonX.pressing ) + if ( !this.controllerData.buttonX.pressing ) { this.leftHandPreviousX = this.tempVector1.x; this.leftHandPreviousY = this.tempVector1.y; this.leftHandPreviousZ = this.tempVector1.z; } - else // if ( rightData.buttonX.pressing ) + else // if ( this.controllerData..buttonX.pressing ) { - // let playerPos = this.el.getAttribute("position") + // store playerPosition in tempVector2 this.el.object3D.getWorldPosition(this.tempVector2); this.tempVector2.x -= (this.tempVector1.x - this.leftHandPreviousX); diff --git a/js/raycaster-controller-grabber.js b/js/raycaster-controller-grabber.js deleted file mode 100644 index 4c5367f..0000000 --- a/js/raycaster-controller-grabber.js +++ /dev/null @@ -1,132 +0,0 @@ -// attach this to the raycaster element, -// use it to grab any object with classes "raycaster-target" and "grabbable" -AFRAME.registerComponent("raycaster-controller-grabber", { - init: function () - { - this.grabbedElement = null; - - this.rightController = document.querySelector("#right-controller-entity"); - - this.tempVector = new THREE.Vector3(); - - // use when moving grabbed object along raycaster beam - this.clock = new THREE.Clock(); - this.moveSpeed = 1; // units per second - }, - - tick: function() - { - let deltaTime = this.clock.getDelta(); - - this.rightData = this.rightController.components["controller-listener"]; - - this.raycaster = this.el.components["raycaster"]; - this.raycasterGraphics = this.el.components["raycaster-graphics"]; - - if ( this.rightData.grip.pressed && - this.raycaster.intersectionDetail.intersections && - this.raycaster.intersectionDetail.intersections.length > 0 && - this.grabbedElement == null ) - { - // get the intersected entity - let entity = this.raycaster.intersectionDetail.els[0]; - - // if it has the "grabbable" class, then - if ( entity.classList.contains("grabbable") ) - { - // Attach grabbed entity to this object (controller). - // Note: not changing the A-Frame DOM tree, because this is temporary - // and will be added back to the scene when dropped. - this.el.object3D.attach( entity.object3D ); - - // raycaster-graphics keeps setting cursorEntity visible true, - // so force cursor hidden by making setting children visibility to false - this.raycasterGraphics.cursorCenter.setAttribute("visible", false); - this.raycasterGraphics.cursorBorder.setAttribute("visible", false); - - // turn off emission (set by raycaster-hover-glow) - entity.setAttribute("material", "emissive", "#000000"); - - // set grabbed element - this.grabbedElement = entity; - } - - } - - // perform actions while element is grabbed - if ( this.grabbedElement != null ) - { - // pushing/pulling grabbed object - if ( this.rightData.axisY != 0 ) - { - // grab point is already in world coordinates - let point = this.raycasterGraphics.cursorEntity.getAttribute("position"); - // convert controller position to world coordinates also - this.el.object3D.getWorldPosition(this.tempVector); - - // find distance from grabbed object to controller - let dx = point.x - this.tempVector.x; - let dy = point.y - this.tempVector.y; - let dz = point.z - this.tempVector.z; - let distance = Math.sqrt(dx*dx + dy*dy + dz*dz); - - // if not pulling entity that is too close, then okay to move it - if ( !(this.rightData.axisY > 0 && distance < 0.05) ) - { - // move grabbed object along raycaster beam - let angle = this.raycasterGraphics.beamAngleX; - let moveDistance = this.moveSpeed * deltaTime * this.rightData.axisY; - let moveY = Math.sin(angle) * moveDistance; - let moveZ = Math.cos(angle) * moveDistance; - - let grabbedPos = this.grabbedElement.getAttribute("position"); - grabbedPos.y += moveY; - grabbedPos.z += moveZ; - this.grabbedElement.setAttribute("position", grabbedPos); - - // repeat and move (animate) texture of beam entity - let material = this.raycasterGraphics.beamEntity.getAttribute("material"); - material.repeat.x = 2; - material.repeat.y = 30; - material.offset.y -= 10 * moveDistance; - this.raycasterGraphics.beamEntity.setAttribute("material", material); - } - } - else // if not pushing/pulling, revert beam graphics to solid line - { - let material = this.raycasterGraphics.beamEntity.getAttribute("material"); - material.repeat.x = 2; - material.repeat.y = 1; - material.offset.y = 0.001; // weird bug when setting to 0; does not change - this.raycasterGraphics.beamEntity.setAttribute("material", material); - } - - if ( this.rightData.grip.released ) - { - // attach element back to root scene - this.grabbedElement.sceneEl.object3D.attach( this.grabbedElement.object3D ); - - // revert previous changes - let material = this.raycasterGraphics.beamEntity.getAttribute("material"); - material.repeat.x = 2; - material.repeat.y = 1; - material.offset.y = 0.001; - this.raycasterGraphics.beamEntity.setAttribute("material", material); - - this.raycasterGraphics.cursorCenter.setAttribute("visible", true); - this.raycasterGraphics.cursorBorder.setAttribute("visible", true); - - if ( this.grabbedElement.components["raycaster-hover"] && - this.grabbedElement.components["raycaster-hover"].data.glowOnHover) - { - this.grabbedElement.setAttribute("material", "emissive", "#444444"); - } - - this.grabbedElement = null; - } - - } // end of "grabbedElement" - - } // end of function tick() - -}); diff --git a/js/raycaster-extras.js b/js/raycaster-extras.js new file mode 100644 index 0000000..95c1304 --- /dev/null +++ b/js/raycaster-extras.js @@ -0,0 +1,332 @@ +/** + * raycaster-extras: + * - draws a fading beam along raycaster direction + * - beam color changes while pressing right trigger or right grip + * - draws a cursor marker at first raycaster intersection point + */ + +AFRAME.registerComponent('raycaster-extras', { + + schema: + { + controllerListenerId: {type: 'string', default: "#controller-data"}, + + beamRadius: {type: 'float', default: 0.003}, + beamLength: {type: 'float', default: 0.400}, + beamColor: {type: 'color', default: "white"}, + beamOpacity: {type: 'float', default: 0.50}, + beamImageSrc: {type: 'string', default: "#gradient"}, + cursorRadius: {type: 'float', default: 0.020}, + cursorColor: {type: 'color', default: "white"}, + }, + + init: function () + { + // disable default raycaster line + this.raycaster = this.el.components["raycaster"]; + this.raycaster.data.showLine = false; + this.raycaster.data.lineOpacity = 0.0; + + + // draw light beam: thin textured cylinder along raycaster direction + this.beamEntity = document.createElement("a-cylinder"); + this.beamEntity.setAttribute("radius", this.data.beamRadius); + this.beamEntity.setAttribute("height", this.data.beamLength); + // these are used to set beam position/rotation in tick function + this.currentBeamDirection = null; + this.currentBeamOrigin = null; + this.vectorsEqual = function(v, w) + { return ((v.x == w.x) && (v.y == w.y) && (v.z == w.z)); } + this.beamEntity.setAttribute("material", "shader: flat; transparent: true; repeat: 2 1;"); + this.beamEntity.setAttribute("material", "src", this.data.beamImageSrc); + this.beamEntity.setAttribute("material", "color", this.data.beamColor); + this.beamEntity.setAttribute("material", "opacity", this.data.beamOpacity); + this.el.appendChild(this.beamEntity); + + + // draw a sphere with border to illustrate closest raycaster intersection point + this.cursorEntity = document.createElement('a-entity'); + this.cursorEntity.setAttribute("id", "cursor-ball"); + // attach to scene + document.querySelector('a-scene').appendChild(this.cursorEntity); + + this.cursorBorder = document.createElement('a-sphere'); + this.cursorBorder.setAttribute("radius", this.data.cursorRadius); + this.cursorBorder.setAttribute("material", "shader: flat; color: black; side: back;"); + this.cursorBorder.setAttribute("overlay", ""); + this.cursorEntity.appendChild(this.cursorBorder); + + this.cursorCenter = document.createElement('a-sphere'); + this.cursorCenter.setAttribute("radius", this.data.cursorRadius * 0.80); + this.cursorCenter.setAttribute("material", "shader: flat; side: front;"); + this.cursorCenter.setAttribute("material", "color", this.data.cursorColor); + this.cursorCenter.setAttribute("overlay", ""); + this.cursorEntity.appendChild(this.cursorCenter); + + this.controllerData = document.querySelector(this.data.controllerListenerId).components["controller-listener"]; + + this.focusedElement = null; + this.grabbedElement = null; + + // calculate translation vector for moving grabbed object along beam + this.tempVector = new THREE.Vector3(); + this.tempVector1 = new THREE.Vector3(); + this.tempVector2 = new THREE.Vector3(); + // use when moving grabbed object along raycaster beam + this.clock = new THREE.Clock(); + this.moveSpeed = 1; + + }, + + tick: function () + { + let deltaTime = this.clock.getDelta(); + + // change color of beam when interacting with trigger or grip button ================================== + + if ( this.controllerData.rightTrigger.pressing || this.controllerData.rightGrip.pressing ) + this.beamEntity.setAttribute("material", "color", "cyan"); + else + this.beamEntity.setAttribute("material", "color", this.data.beamColor); + + // calculate position and rotation of beam ============================================================ + + // based on model-specific values that customize raycaster line; + // note: this data may change when switching from browser to VR mode, so need to keep checking in tick() + let raycasterConfig = this.el.getAttribute("raycaster"); + let currentRayDirection = raycasterConfig.direction; + let currentRayOrigin = raycasterConfig.origin; + + if ( this.currentBeamDirection == null || this.currentBeamOrigin == null || + !this.vectorsEqual(this.currentBeamOrigin, currentRayOrigin) || + !this.vectorsEqual(this.currentBeamDirection, currentRayDirection) ) + { + + // align beam rotation with ray direction angle + // (beam is always only rotated around x-axis) + let beamAngleX = 180 + Math.atan2(currentRayDirection.z, currentRayDirection.y) * 180/Math.PI; + let rot = {x: beamAngleX, y: 0, z: 0}; + this.beamEntity.setAttribute("rotation", rot); + this.currentBeamDirection = currentRayDirection; + + this.beamAngleX = beamAngleX; + + // align beam position with ray origin point + // and shift so beam cylinder end is at origin + let angleRad = beamAngleX * Math.PI/180; + let cylinderShift = this.data.beamLength / 2.05; + let pos = { x: currentRayOrigin.x, + y: currentRayOrigin.y - Math.cos(angleRad) * cylinderShift, + z: currentRayOrigin.z - Math.sin(angleRad) * cylinderShift }; + this.beamEntity.setAttribute("position", pos); + this.currentBeamOrigin = currentRayOrigin; + } + + // update which element has focus =================================================================== + + if ( this.raycaster.intersectionDetail.intersections && this.raycaster.intersectionDetail.intersections.length > 0 ) + this.focusedElement = this.raycaster.intersectionDetail.els[0]; + else + this.focusedElement = null; + + // move cursor entity to point of intersection ====================================================== + + // Strange bug: + // If raycaster is intersecting an element, + // and then raycaster also starts to intersect another element behind the first, + // the "intersections" array becomes empty. + // This does not happen if second intersection is closer. + + // Hacky workaround: default raycaster sets far = 100; + // when there is an intersection, reduce far to current distance (plus epsilon) + // so it is not possible to suddenly intersect an object behind current intersect. + // Also implements registering only one intersection at a time. + + if ( this.focusedElement != null ) + { + this.cursorEntity.setAttribute("visible", true) + + let point = this.raycaster.intersectionDetail.intersections[0].point; + this.cursorEntity.setAttribute("position", + {x: point.x, y: point.y, z: point.z} ); + + // shorten raycaster + let dist = this.raycaster.intersectionDetail.intersections[0].distance; + this.el.setAttribute("raycaster", "far", dist + 0.1); + } + else + { + this.cursorEntity.setAttribute("visible", false) + this.el.setAttribute("raycaster", "far", 20); + } + + // grab element ===================================================================================== + + + if ( this.controllerData.rightGrip.pressed && + this.focusedElement != null && this.grabbedElement == null ) + { + // if it can be grabbed... + if ( this.focusedElement.components["raycaster-target"] && + this.focusedElement.components["raycaster-target"].data.canGrab ) + { + // Attach grabbed entity to this object (controller). + // Note: not changing the A-Frame DOM tree, because this is temporary + // and will be added back to the scene when dropped. + this.el.object3D.attach( this.focusedElement.object3D ); + + // raycaster-graphics keeps setting cursorEntity visible true, + // so force cursor hidden by making setting children visibility to false + // why? easier to focus on object without cursor in the way + this.cursorCenter.setAttribute("visible", false); + this.cursorBorder.setAttribute("visible", false); + + // turn off emission (set by raycaster-hover-glow) + this.focusedElement.setAttribute("material", "emissive", "#000000"); + + // focused element is now also the grabbed element + this.grabbedElement = this.focusedElement; + this.grabbedElement.components["raycaster-target"].isGrabbed = true; + } + + } + + // perform actions on grabbed element =============================================================== + + + if ( this.grabbedElement != null ) + { + // pushing/pulling grabbed object --------------------------------------------------------------- + if ( this.controllerData.rightAxisY != 0 ) + { + // do all calculations in world coordinates + this.el.object3D.getWorldPosition(this.tempVector1); + this.cursorEntity.object3D.getWorldPosition(this.tempVector2); + + // find distance from grabbed object to controller + this.tempVector.subVectors( this.tempVector2, this.tempVector1 ); + let distance = this.tempVector.length(); + + // if not pulling entity that is too close, then okay to move it + if ( !(this.controllerData.rightAxisY > 0 && distance < 0.05) ) + { + + let moveDistance = this.moveSpeed * deltaTime * this.controllerData.rightAxisY; + this.tempVector.setLength(moveDistance); + + + // temporarily attach element back to root scene + this.grabbedElement.sceneEl.object3D.attach( this.grabbedElement.object3D ); + // translate + this.grabbedElement.object3D.position.sub( this.tempVector ); + // reattach to controller + this.el.object3D.attach( this.grabbedElement.object3D ); + + + // repeat and move (animate) texture of beam entity + let material = this.beamEntity.getAttribute("material"); + material.repeat.x = 2; + material.repeat.y = 30; + material.offset.y -= 10 * moveDistance; + this.beamEntity.setAttribute("material", material); + + } + } + else // if not pushing/pulling, revert beam graphics to solid line ------------------------------ + { + let material = this.beamEntity.getAttribute("material"); + material.repeat.x = 2; + material.repeat.y = 1; + material.offset.y = 0.001; // weird bug when setting to 0; does not change + this.beamEntity.setAttribute("material", material); + } + + // drop grabbed element ------------------------------------------------------------------------- + if ( this.controllerData.rightGrip.released ) + { + // attach element back to root scene + this.grabbedElement.sceneEl.object3D.attach( this.grabbedElement.object3D ); + + // revert previous changes + let material = this.beamEntity.getAttribute("material"); + material.repeat.x = 2; + material.repeat.y = 1; + material.offset.y = 0.001; + this.beamEntity.setAttribute("material", material); + + this.cursorCenter.setAttribute("visible", true); + this.cursorBorder.setAttribute("visible", true); + + this.grabbedElement.components["raycaster-target"].isGrabbed = false; + + if ( this.grabbedElement.components["raycaster-target"].data.glowOnHover ) + this.grabbedElement.setAttribute("material", "emissive", "#444444"); + + this.grabbedElement = null; + } + + } + + // ================================================================================================== + + } +}); + + + +// The "overlay" component forces objects to render last, +// so they not occluded by any part of an opaque objects. + +// This is being used for the raycaster-graphics' cursor +// so that it is always fully visible to the user +// (more reliable than just a billboarded texture). +AFRAME.registerComponent("overlay", { + init: function () + { + this.el.sceneEl.renderer.sortObjects = true; + this.el.object3D.renderOrder = 100; + this.el.components.material.material.depthTest = false; + } +}); + + +// set a variable to indicate when object has focus (targeted by raycaster) +// optional: make objects brighter when raycaster intersects them +AFRAME.registerComponent('raycaster-target', { + + schema: + { + glowOnHover: {type: 'boolean', default: true}, + canGrab: {type: 'boolean', default: false}, + }, + + init: function () + { + // assumes that raycaster's object parameter includes class "raycaster-target" + // in order to register intersections of ray with this object + this.el.classList.add("raycaster-target"); + + this.hasFocus = false; + this.isGrabbed = false; + + let self = this; + + // this happens once, when intersection begins + this.el.addEventListener("raycaster-intersected", function(event) + { + self.hasFocus = true; + if (self.data.glowOnHover) + self.el.setAttribute("material", "emissive", "#444444"); + } + ); + + // this happens once, when intersection ends + this.el.addEventListener("raycaster-intersected-cleared", function(event) + { + self.hasFocus = false; + if (self.data.glowOnHover) + self.el.setAttribute("material", "emissive", "#000000"); + } + ); + } +}); diff --git a/js/raycaster-graphics.js b/js/raycaster-graphics.js deleted file mode 100644 index 5500feb..0000000 --- a/js/raycaster-graphics.js +++ /dev/null @@ -1,187 +0,0 @@ -AFRAME.registerComponent('raycaster-graphics', { - - schema: - { - beamRadius: {type: 'float', default: 0.003}, - beamLength: {type: 'float', default: 0.400}, - beamColor: {type: 'color', default: "white"}, - beamOpacity: {type: 'float', default: 0.50}, - beamImageSrc: {type: 'string', default: "#gradient"}, - cursorRadius: {type: 'float', default: 0.020}, - cursorColor: {type: 'color', default: "white"} - }, - - init: function () - { - // disable default raycaster line - this.raycaster = this.el.components["raycaster"]; - this.raycaster.data.showLine = false; - this.raycaster.data.lineOpacity = 0.0; - - - // draw light beam: thin textured cylinder along raycaster direction - this.beamEntity = document.createElement("a-cylinder"); - this.beamEntity.setAttribute("radius", this.data.beamRadius); - this.beamEntity.setAttribute("height", this.data.beamLength); - // these are used to set beam position/rotation in tick function - this.currentBeamDirection = null; - this.currentBeamOrigin = null; - this.vectorsEqual = function(v, w) - { return ((v.x == w.x) && (v.y == w.y) && (v.z == w.z)); } - this.beamEntity.setAttribute("material", "shader: flat; transparent: true; repeat: 2 1;"); - this.beamEntity.setAttribute("material", "src", this.data.beamImageSrc); - this.beamEntity.setAttribute("material", "color", this.data.beamColor); - this.beamEntity.setAttribute("material", "opacity", this.data.beamOpacity); - this.el.appendChild(this.beamEntity); - - - // draw a sphere with border to illustrate closest raycaster intersection point - this.cursorEntity = document.createElement('a-entity'); - this.cursorEntity.setAttribute("id", "cursor-ball"); - // attach to scene - document.querySelector('a-scene').appendChild(this.cursorEntity); - - this.cursorBorder = document.createElement('a-sphere'); - this.cursorBorder.setAttribute("radius", this.data.cursorRadius); - this.cursorBorder.setAttribute("material", "shader: flat; color: black; side: back;"); - this.cursorBorder.setAttribute("overlay", ""); - this.cursorEntity.appendChild(this.cursorBorder); - - this.cursorCenter = document.createElement('a-sphere'); - this.cursorCenter.setAttribute("radius", this.data.cursorRadius * 0.80); - this.cursorCenter.setAttribute("material", "shader: flat; side: front;"); - this.cursorCenter.setAttribute("material", "color", this.data.cursorColor); - this.cursorCenter.setAttribute("overlay", ""); - this.cursorEntity.appendChild(this.cursorCenter); - - this.rightController = document.querySelector("#right-controller-entity"); - }, - - tick: function () - { - this.rightData = this.rightController.components["controller-listener"]; - // change color of beam when interacting with trigger or grip button - if ( this.rightData.trigger.pressing || this.rightData.grip.pressing ) - this.beamEntity.setAttribute("material", "color", "cyan"); - else - this.beamEntity.setAttribute("material", "color", this.data.beamColor); - - // calculate position and rotation of beam - // based on model-specific values that customize raycaster line; - // this data may change when switching from browser to VR mode - let raycasterConfig = this.el.getAttribute("raycaster"); - let currentRayDirection = raycasterConfig.direction; - let currentRayOrigin = raycasterConfig.origin; - - if ( this.currentBeamDirection == null || this.currentBeamOrigin == null || - !this.vectorsEqual(this.currentBeamOrigin, currentRayOrigin) || - !this.vectorsEqual(this.currentBeamDirection, currentRayDirection) ) - { - - // align beam rotation with ray direction angle - // (beam is always only rotated around x-axis) - let beamAngleX = 180 + Math.atan2(currentRayDirection.z, currentRayDirection.y) * 180/Math.PI; - let rot = {x: beamAngleX, y: 0, z: 0}; - this.beamEntity.setAttribute("rotation", rot); - this.currentBeamDirection = currentRayDirection; - - this.beamAngleX = beamAngleX; - - // align beam position with ray origin point - // and shift so beam cylinder end is at origin - let angleRad = beamAngleX * Math.PI/180; - let cylinderShift = this.data.beamLength / 2.05; - let pos = { x: currentRayOrigin.x, - y: currentRayOrigin.y - Math.cos(angleRad) * cylinderShift, - z: currentRayOrigin.z - Math.sin(angleRad) * cylinderShift }; - this.beamEntity.setAttribute("position", pos); - this.currentBeamOrigin = currentRayOrigin; - } - - - // move cursor entity to point of intersection - - // Strange bug: - // If raycaster is intersecting an element, - // and then raycaster also starts to intersect another element behind the first, - // the "intersections" array becomes empty. - // This does not happen if second intersection is closer. - - // Hacky workaround: default raycaster sets far = 100; - // when there is an intersection, reduce far to current distance (plus epsilon) - // so it is not possible to suddenly intersect an object behind current intersect. - // Also implements registering only one intersection at a time. - - if ( this.raycaster.intersectionDetail.intersections && - this.raycaster.intersectionDetail.intersections.length > 0) - { - this.cursorEntity.setAttribute("visible", true) - - let point = this.raycaster.intersectionDetail.intersections[0].point; - - let pos = this.cursorEntity.getAttribute("position"); - pos.x = point.x; - pos.y = point.y; - pos.z = point.z; - this.cursorEntity.setAttribute("position", pos) - - // shorten raycaster - let dist = this.raycaster.intersectionDetail.intersections[0].distance; - this.el.setAttribute("raycaster", "far", dist + 0.1); - } - else - { - this.cursorEntity.setAttribute("visible", false) - this.el.setAttribute("raycaster", "far", 100); - } - } -}); - -// The "overlay" component forces objects to render last, -// so they not occluded by any part of an opaque objects. - -// This is being used for the raycaster-graphics' cursor -// so that it is always fully visible to the user -// (more reliable that a billboarded texture). -AFRAME.registerComponent("overlay", { - init: function () - { - this.el.sceneEl.renderer.sortObjects = true; - this.el.object3D.renderOrder = 100; - this.el.components.material.material.depthTest = false; - } -}); - -// set a variable to indicate when object has focus (targeted by raycaster) -// optional: make objects brighter when raycaster intersects them -AFRAME.registerComponent('raycaster-hover', { - - schema: - { - hasFocus: {type: 'boolean', default: false}, - glowOnHover: {type: 'boolean', default: true}, - }, - - init: function () - { - let self = this; - - // this happens once, when intersection begins - this.el.addEventListener("raycaster-intersected", function(event) - { - self.data.hasFocus = true; - if (self.data.glowOnHover) - self.el.setAttribute("material", "emissive", "#444444"); - } - ); - - // this happens once, when intersection ends - this.el.addEventListener("raycaster-intersected-cleared", function(event) - { - self.data.hasFocus = false; - if (self.data.glowOnHover) - self.el.setAttribute("material", "emissive", "#000000"); - } - ); - } -}); diff --git a/quest-interact.html b/quest-interact.html index 17ef940..d9c7598 100644 --- a/quest-interact.html +++ b/quest-interact.html @@ -8,8 +8,7 @@ - - + @@ -21,21 +20,20 @@ { this.colors = ["red", "orange", "yellow", "green", "blue", "violet"]; - this.rightController = document.querySelector("#right-controller-entity"); - this.rightData = this.rightController.components["controller-listener"]; - this.hoverData = this.el.components["raycaster-hover"].data; + this.controllerData = document.querySelector("#controller-data").components["controller-listener"]; + this.hoverData = this.el.components["raycaster-target"]; }, tick: function() { - if (this.hoverData.hasFocus && this.rightData.trigger.pressed ) + if (this.hoverData.hasFocus && this.controllerData.rightTrigger.pressed ) { let index = Math.floor( this.colors.length * Math.random() ); let color = this.colors[index]; this.el.setAttribute("color", color); } - if (!this.hoverData.hasFocus || this.rightData.trigger.released) + if (!this.hoverData.hasFocus || this.controllerData.rightTrigger.released) { this.el.setAttribute("color", "#CCCCCC"); } @@ -56,25 +54,30 @@ color = "#001337"> - - + + + + + + id="left-controller" + oculus-touch-controls="hand: left"> + raycaster-extras="controllerListenerId: #controller-data; + beamImageSrc: #gradient; beamLength: 0.5;"> @@ -83,8 +86,7 @@ p="2" q="3" radius="0.5" radius-tubular="0.1" position = "-2.5 1.5 -4" color="#CC3333" - class="raycaster-target" - raycaster-hover> + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> @@ -136,8 +133,7 @@ position = "0 1 -2" color = "#EEEEEE" material = "src: #border;" - class="raycaster-target grabbable" - raycaster-hover + raycaster-target="canGrab: true;" raycaster-color-change> @@ -146,8 +142,7 @@ position = "-1 1 -2" color = "#EEEEEE" material = "src: #border;" - class="raycaster-target grabbable" - raycaster-hover + raycaster-target="canGrab: true;" raycaster-color-change> @@ -156,8 +151,7 @@ position = "1 1 -2" color = "#EEEEEE" material = "src: #border;" - class="raycaster-target grabbable" - raycaster-hover + raycaster-target="canGrab: true;" raycaster-color-change> diff --git a/quest-move.html b/quest-move.html index 03ecb6a..ac1ac46 100644 --- a/quest-move.html +++ b/quest-move.html @@ -19,20 +19,27 @@ color = "#001337"> - + + + + + id="left-controller" + oculus-touch-controls="hand: left"> + id="right-controller" + oculus-touch-controls="hand: right"> diff --git a/quest-music.html b/quest-music.html index 5524b24..5622d70 100644 --- a/quest-music.html +++ b/quest-music.html @@ -8,8 +8,7 @@ - - + @@ -18,33 +17,31 @@ // uses other custom components: raycaster-graphics, raycaster-controller-grabber AFRAME.registerComponent("music-player", { + + schema: + { + controllerListenerId: {type: 'string', default: "#controller-data"}, + }, + init: function () { + this.controllerData = document.querySelector(this.data.controllerListenerId).components["controller-listener"]; + // set up the physical entity this.base = document.createElement("a-entity"); this.base.setAttribute("geometry", - { primitive: "box", width: 2.0, height: 1.60, depth: 0.04 } ); - this.base.setAttribute("material", {color: "#CCCCCC"}); - this.base.setAttribute("class", "raycaster-target grabbable" ); + { primitive: "box", width: 2.20, height: 1.80, depth: 0.04 } ); + this.base.setAttribute("material", {color: "#111111"}); + this.base.setAttribute("raycaster-target", "canGrab: true;" ); // add component this.el.setAttribute("sound", {positional: false, volume: 0.50}); this.el.appendChild(this.base); - // add a box to serve as border - let baseBorder = document.createElement("a-entity"); - baseBorder.setAttribute("geometry", - { primitive: "box", width: 2.2, height: 1.8, depth: 0.04 } ); - baseBorder.setAttribute("material", "color", "black"); - baseBorder.setAttribute("position", "0 0 -0.01"); - baseBorder.setAttribute("class", "raycaster-target" ); - this.base.appendChild(baseBorder); - // add another box to frame button controls let buttonBase = document.createElement("a-entity"); buttonBase.setAttribute("geometry", { primitive: "box", width: 1.999, height: 0.20, depth: 0.04 } ); buttonBase.setAttribute("material", {color: "#444444"}); - buttonBase.setAttribute("class", "raycaster-target" ); buttonBase.setAttribute("position", {x: 0, y: -0.70, z: 0.01}); this.base.appendChild(buttonBase); @@ -56,8 +53,7 @@ button.setAttribute("material", "color", "#EEEEEE"); button.setAttribute("material", "src", materialSrc); button.setAttribute("position", {x: posX, y: posY, z: posZ}); - button.setAttribute("class", "raycaster-target"); - button.setAttribute("raycaster-hover", ""); + button.setAttribute("raycaster-target", ""); return button; } @@ -100,10 +96,6 @@ // images should be approximately 900 x 500 this.imageArea.setAttribute("position", {x: 0, y: 0.20, z: 0.04}); this.base.appendChild(this.imageArea); - - // this.imageArea.setAttribute("material", "src", "url(music/ChauSara/image.jpg)"); - - this.rightController = document.querySelector("#right-controller-entity"); let self = this; @@ -167,12 +159,16 @@ tick: function() { - this.rightData = this.rightController.components["controller-listener"]; - - if (this.rightData.trigger.pressed) + + if (this.controllerData.rightTrigger.pressed) { + if ( this.buttonIcon == "wait" ) + { + // song loading in process; can not play song or skip ahead; + // just wait patiently... + } // determine which button was clicked (if any) - if ( this.playButton.components["raycaster-hover"].data.hasFocus ) + else if ( this.playButton.components["raycaster-target"].hasFocus ) { if ( this.buttonIcon == "play" ) { @@ -186,19 +182,15 @@ this.playButton.setAttribute("material", "src", "#iconPlay"); this.buttonIcon = "play"; } - else if ( this.buttonIcon == "wait" ) - { - // just wait patiently. - } } - else if ( this.replayButton.components["raycaster-hover"].data.hasFocus ) + else if ( this.replayButton.components["raycaster-target"].hasFocus ) { this.el.components["sound"].stopSound(); this.el.components["sound"].playSound(); this.playButton.setAttribute("material", "src", "#iconPause"); this.buttonIcon = "pause"; } - else if ( this.nextButton.components["raycaster-hover"].data.hasFocus ) + else if ( this.nextButton.components["raycaster-target"].hasFocus ) { this.el.components["sound"].stopSound(); this.musicDataIndex++; @@ -206,7 +198,7 @@ this.musicDataIndex = 0; this.loadMusic( this.musicDataIndex ); } - else if ( this.previousButton.components["raycaster-hover"].data.hasFocus ) + else if ( this.previousButton.components["raycaster-target"].hasFocus ) { this.el.components["sound"].stopSound(); this.musicDataIndex--; @@ -214,14 +206,14 @@ this.musicDataIndex = this.musicDataList.length - 1; this.loadMusic( this.musicDataIndex ); } - else if ( this.volumeDownButton.components["raycaster-hover"].data.hasFocus ) + else if ( this.volumeDownButton.components["raycaster-target"].hasFocus ) { let volume = this.el.getAttribute("sound").volume; if (volume >= 0.10) volume -= 0.10; this.el.setAttribute("sound", "volume", volume); } - else if ( this.volumeUpButton.components["raycaster-hover"].data.hasFocus ) + else if ( this.volumeUpButton.components["raycaster-target"].hasFocus ) { let volume = this.el.getAttribute("sound").volume; if (volume <= 0.90) @@ -258,32 +250,37 @@ - - + + + + + + id="left-controller" + oculus-touch-controls="hand: left"> + raycaster-extras="controllerListenerId: #controller-data; + beamImageSrc: #gradient; beamLength: 0.5;"> + music-player="controllerListenerId: #controller-data;"> + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> diff --git a/quest-raycaster.html b/quest-raycaster.html index dbd19a7..0754575 100644 --- a/quest-raycaster.html +++ b/quest-raycaster.html @@ -8,7 +8,7 @@ - + @@ -23,34 +23,40 @@ color = "#001337"> - - + + + + + + id="left-controller" + oculus-touch-controls="hand: left"> + raycaster-extras="controllerListenerId: #controller-data; + beamImageSrc: #gradient; beamLength: 0.5;"> + + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> + raycaster-target> diff --git a/quest.html b/quest.html index c894721..135e87a 100644 --- a/quest.html +++ b/quest.html @@ -13,28 +13,32 @@