diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index be27631195188130eabe5ccd9829d00b3abd7e91..0947f60406f4f3d651b9134939c115d142a61013 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -11,7 +11,10 @@ AFRAME.registerComponent("cursor-controller", { maxDistance: { default: 3 }, minDistance: { default: 0 }, cursorColorHovered: { default: "#2F80ED" }, - cursorColorUnhovered: { default: "#FFFFFF" } + cursorColorUnhovered: { default: "#FFFFFF" }, + rayObject: { type: "selector" }, + useMousePos: { default: true }, + drawLine: { default: false } }, init: function() { @@ -22,7 +25,6 @@ AFRAME.registerComponent("cursor-controller", { this.currentDistance = this.data.maxDistance; this.currentDistanceMod = 0; this.mousePos = new THREE.Vector2(); - this.useMousePos = true; this.wasCursorHovered = false; this.origin = new THREE.Vector3(); this.direction = new THREE.Vector3(); @@ -35,6 +37,8 @@ AFRAME.registerComponent("cursor-controller", { this.moveCursor = this.moveCursor.bind(this); this.endInteraction = this.endInteraction.bind(this); this.changeDistanceMod = this.changeDistanceMod.bind(this); + this.setRaycasterWithMousePos = this.setRaycasterWithMousePos.bind(this); + this.updateDistanceAndTargetType = this.updateDistanceAndTargetType.bind(this); this._handleCursorLoaded = this._handleCursorLoaded.bind(this); this.data.cursor.addEventListener("loaded", this._handleCursorLoaded); @@ -49,65 +53,89 @@ AFRAME.registerComponent("cursor-controller", { this.setCursorVisibility(false); }, - tick: function() { - if (!this.enabled) { - return; + update: function() { + if (this.data.rayObject) { + this.rayObject = this.data.rayObject.object3D; } + }, - if (this.useMousePos) { - const camera = this.data.camera.components.camera.camera; - const raycaster = this.el.components.raycaster.raycaster; - raycaster.setFromCamera(this.mousePos, camera); - this.origin.copy(raycaster.ray.origin); - this.direction.copy(raycaster.ray.direction); - } else { - this.rayObject.getWorldPosition(this.origin); - this.rayObject.getWorldDirection(this.direction); - } - this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction }); - - const isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start"); - if (isGrabbing) { - const distance = Math.min( - this.data.maxDistance, - Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod) - ); - this.direction.multiplyScalar(distance); - this.data.cursor.object3D.position.addVectors(this.origin, this.direction); - } else { - this.currentDistanceMod = 0; - let intersection = null; - const intersections = this.el.components.raycaster.intersections; - if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) { - intersection = intersections[0]; - this.data.cursor.object3D.position.copy(intersection.point); - this.currentDistance = intersections[0].distance; + tick: (() => { + const rayObjectRotation = new THREE.Quaternion(); + + return function() { + if (!this.enabled) { + return; + } + + if (this.data.useMousePos) { + this.setRaycasterWithMousePos(); } else { - this.currentDistance = this.data.maxDistance; - this.direction.multiplyScalar(this.currentDistance); + //this.rayObject.updateMatrixWorld(); + rayObjectRotation.setFromRotationMatrix(this.rayObject.matrixWorld); + this.direction + .set(0, 0, 1) + .applyQuaternion(rayObjectRotation) + .normalize(); + this.origin.setFromMatrixPosition(this.rayObject.matrixWorld); + } + this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction }); + + const isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start"); + if (isGrabbing) { + const distance = Math.min( + this.data.maxDistance, + Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod) + ); + this.direction.multiplyScalar(distance); this.data.cursor.object3D.position.addVectors(this.origin, this.direction); + } else { + this.currentDistanceMod = 0; + this.updateDistanceAndTargetType(); + + const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI); + if (isTarget && !this.wasCursorHovered) { + this.wasCursorHovered = true; + this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered }); + } else if (!isTarget && this.wasCursorHovered) { + this.wasCursorHovered = false; + this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); + } } - if (!intersection) { - this.currentTargetType = TARGET_TYPE_NONE; - } else if (intersection.object.el.matches(".interactable, .interactable *")) { - this.currentTargetType = TARGET_TYPE_INTERACTABLE; - } else if (intersection.object.el.matches(".ui, .ui *")) { - this.currentTargetType = TARGET_TYPE_UI; + if (this.data.drawLine) { + this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() }); } + }; + })(), - const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI); - if (isTarget && !this.wasCursorHovered) { - this.wasCursorHovered = true; - this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered }); - } else if (!isTarget && this.wasCursorHovered) { - this.wasCursorHovered = false; - this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); - } + setRaycasterWithMousePos() { + const camera = this.data.camera.components.camera.camera; + const raycaster = this.el.components.raycaster.raycaster; + raycaster.setFromCamera(this.mousePos, camera); + this.origin.copy(raycaster.ray.origin); + this.direction.copy(raycaster.ray.direction); + this.el.setAttribute("raycaster", { origin: raycaster.ray.origin, direction: raycaster.ray.direction }); + }, + + updateDistanceAndTargetType() { + let intersection = null; + const intersections = this.el.components.raycaster.intersections; + if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) { + intersection = intersections[0]; + this.data.cursor.object3D.position.copy(intersection.point); + this.currentDistance = intersections[0].distance; + } else { + this.currentDistance = this.data.maxDistance; + this.direction.multiplyScalar(this.currentDistance); + this.data.cursor.object3D.position.addVectors(this.origin, this.direction); } - if (this.drawLine) { - this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() }); + if (!intersection) { + this.currentTargetType = TARGET_TYPE_NONE; + } else if (intersection.object.el.matches(".interactable, .interactable *")) { + this.currentTargetType = TARGET_TYPE_INTERACTABLE; + } else if (intersection.object.el.matches(".ui, .ui *")) { + this.currentTargetType = TARGET_TYPE_UI; } }, @@ -117,32 +145,14 @@ AFRAME.registerComponent("cursor-controller", { setCursorVisibility(visible) { this.data.cursor.setAttribute("visible", visible); - this.el.setAttribute("line", { visible: visible && this.drawLine }); + this.el.setAttribute("line", { visible: visible && this.data.drawLine }); }, forceCursorUpdate: function() { - // Update the ray and cursor positions - const raycasterComp = this.el.components.raycaster; - const raycaster = raycasterComp.raycaster; - const camera = this.data.camera.components.camera.camera; - const cursor = this.data.cursor; - raycaster.setFromCamera(this.mousePos, camera); - this.el.setAttribute("raycaster", { origin: raycaster.ray.origin, direction: raycaster.ray.direction }); - raycasterComp.checkIntersections(); - const intersections = raycasterComp.intersections; - if (intersections.length === 0 || intersections[0].distance > this.data.maxDistance) { - this.currentTargetType = TARGET_TYPE_NONE; - return; - } - const intersection = intersections[0]; - if (intersection.object.el.matches(".interactable, .interactable *")) { - this.currentTargetType = TARGET_TYPE_INTERACTABLE; - } else if (intersection.object.el.matches(".ui, .ui *")) { - this.currentTargetType = TARGET_TYPE_UI; - } - cursor.object3D.position.copy(intersection.point); - // Cursor position must be synced to physics before constraint is created - cursor.components["static-body"].syncToPhysics(); + this.setRaycasterWithMousePos(); + this.el.components.raycaster.checkIntersections(); + this.updateDistanceAndTargetType(); + this.data.cursor.components["static-body"].syncToPhysics(); }, startInteraction: function() { @@ -164,10 +174,8 @@ AFRAME.registerComponent("cursor-controller", { changeDistanceMod: function(delta) { const { minDistance, maxDistance } = this.data; const targetDistanceMod = this.currentDistanceMod + delta; - if (this.currentDistance - targetDistanceMod > maxDistance) { - return; - } - if (this.currentDistance - targetDistanceMod < minDistance) { + const moddedDistance = this.currentDistance - targetDistanceMod; + if (moddedDistance > maxDistance || moddedDistance < minDistance) { return; } this.currentDistanceMod = targetDistanceMod; diff --git a/src/components/input-configurator.js b/src/components/input-configurator.js index 4e7c2cfac7445f4a8c8fbdca6afc5a961d912185..b761deefe6fecef65714350763c05e0b6c8794d4 100644 --- a/src/components/input-configurator.js +++ b/src/components/input-configurator.js @@ -4,18 +4,28 @@ import GearVRMouseEventsHandler from "../utils/gearvr-mouse-events-handler.js"; import ActionEventHandler from "../utils/action-event-handler.js"; AFRAME.registerComponent("input-configurator", { + schema: { + cursorController: { type: "selector" }, + gazeTeleporter: { type: "selector" }, + camera: { type: "selector" }, + playerRig: { type: "selector" }, + leftController: { type: "selector" }, + rightController: { type: "selector" }, + leftControllerRayObject: { type: "string" }, + rightControllerRayObject: { type: "string" }, + gazeCursorRayObject: { type: "string" } + }, + init() { this.inVR = this.el.sceneEl.is("vr-mode"); this.isMobile = AFRAME.utils.device.isMobile(); this.eventHandlers = []; - this.controller = null; this.controllerQueue = []; this.hasPointingDevice = false; - this.gazeCursorRayObject = document.querySelector("#player-camera-reverse-z"); - this.cursor = document.querySelector("#cursor-controller").components["cursor-controller"]; - this.gazeTeleporter = document.querySelector("#gaze-teleport").components["teleport-controls"]; - this.cameraController = document.querySelector("#player-camera").components["pitch-yaw-rotator"]; - this.playerRig = document.querySelector("#player-rig"); + this.cursor = this.data.cursorController.components["cursor-controller"]; + this.gazeTeleporter = this.data.gazeTeleporter.components["teleport-controls"]; + this.cameraController = this.data.camera.components["pitch-yaw-rotator"]; + this.playerRig = this.data.playerRig; this.handedness = "right"; this.onEnterVR = this.onEnterVR.bind(this); @@ -26,21 +36,21 @@ AFRAME.registerComponent("input-configurator", { this.handleControllerConnected = this.handleControllerConnected.bind(this); this.handleControllerDisconnected = this.handleControllerDisconnected.bind(this); - this.el.sceneEl.addEventListener("enter-vr", this.onEnterVR); - this.el.sceneEl.addEventListener("exit-vr", this.onExitVR); - - this.tearDown(); this.configureInput(); }, play() { this.el.sceneEl.addEventListener("controllerconnected", this.handleControllerConnected); this.el.sceneEl.addEventListener("controllerdisconnected", this.handleControllerDisconnected); + this.el.sceneEl.addEventListener("enter-vr", this.onEnterVR); + this.el.sceneEl.addEventListener("exit-vr", this.onExitVR); }, pause() { this.el.sceneEl.removeEventListener("controllerconnected", this.handleControllerConnected); this.el.sceneEl.removeEventListener("controllerdisconnected", this.handleControllerDisconnected); + this.el.sceneEl.removeEventListener("enter-vr", this.onEnterVR); + this.el.sceneEl.removeEventListener("exit-vr", this.onExitVR); }, onEnterVR() { @@ -65,7 +75,6 @@ AFRAME.registerComponent("input-configurator", { this.lookOnMobile.el.removeComponent("look-on-mobile"); this.lookOnMobile = null; } - this.cameraController.pause(); this.cursorRequiresManagement = false; }, @@ -73,19 +82,21 @@ AFRAME.registerComponent("input-configurator", { const onAdded = e => { if (e.detail.name !== "look-on-mobile") return; this.lookOnMobile = this.el.sceneEl.components["look-on-mobile"]; - this.lookOnMobile.registerCameraController(this.cameraController); }; this.el.sceneEl.addEventListener("componentinitialized", onAdded); - this.el.sceneEl.setAttribute("look-on-mobile", ""); + // This adds look-on-mobile to the scene + this.el.sceneEl.setAttribute("look-on-mobile", "camera", this.data.camera); }, configureInput() { + this.actionEventHandler = new ActionEventHandler(this.el.sceneEl, this.cursor); + this.eventHandlers.push(this.actionEventHandler); + + this.cursor.el.setAttribute("cursor-controller", "useMousePos", !this.inVR); + if (this.inVR) { - this.cursor.useMousePos = false; + this.cameraController.pause(); this.cursorRequiresManagement = true; - this.hovered = false; - this.actionEventHandler = new ActionEventHandler(this.el.sceneEl, this.cursor); - this.eventHandlers.push(this.actionEventHandler); this.cursor.el.setAttribute("cursor-controller", "minDistance", 0); if (this.isMobile) { this.eventHandlers.push(new GearVRMouseEventsHandler(this.cursor, this.gazeTeleporter)); @@ -94,7 +105,6 @@ AFRAME.registerComponent("input-configurator", { } } else { this.cameraController.play(); - this.cursor.useMousePos = true; if (this.isMobile) { this.eventHandlers.push(new TouchEventsHandler(this.cursor, this.cameraController, this.cursor.el)); this.addLookOnMobile(); @@ -106,18 +116,8 @@ AFRAME.registerComponent("input-configurator", { }, tick() { - if (!this.cursorRequiresManagement) return; - - if (this.physicalHand) { - const state = this.physicalHand.components["super-hands"].state; - if (!this.hovered && state.has("hover-start") && !this.actionEventHandler.isCursorInteracting) { - this.cursor.disable(); - this.hovered = true; - } else if (this.hovered === true && !state.has("hover-start") && !state.has("grab-start")) { - this.cursor.enable(); - this.cursor.setCursorVisibility(!this.actionEventHandler.isTeleporting); - this.hovered = false; - } + if (this.cursorRequiresManagement && this.controller) { + this.actionEventHandler.manageCursorEnabled(); } }, @@ -148,7 +148,7 @@ AFRAME.registerComponent("input-configurator", { updateController: function() { this.hasPointingDevice = this.controllerQueue.length > 0 && this.inVR; - this.cursor.drawLine = this.hasPointingDevice; + this.cursor.el.setAttribute("cursor-controller", "drawLine", this.hasPointingDevice); this.cursor.setCursorVisibility(true); @@ -156,15 +156,16 @@ AFRAME.registerComponent("input-configurator", { const controllerData = this.controllerQueue[0]; const hand = controllerData.handedness; this.controller = controllerData.controller; - this.physicalHand = this.playerRig.querySelector(`#player-${hand}-controller`); - this.cursor.rayObject = this.controller.querySelector(`#player-${hand}-controller-reverse-z`).object3D; + this.cursor.el.setAttribute("cursor-controller", { + rayObject: this.hand === "left" ? this.data.leftControllerRayObject : this.data.rightControllerRayObject + }); } else { this.controller = null; - this.cursor.rayObject = this.gazeCursorRayObject.object3D; + this.cursor.el.setAttribute("cursor-controller", { rayObject: this.data.gazeCursorRayObject }); } if (this.actionEventHandler) { - this.actionEventHandler.setCursorController(this.controller); + this.actionEventHandler.setHandThatAlsoDrivesCursor(this.controller); } } }); diff --git a/src/components/look-on-mobile.js b/src/components/look-on-mobile.js index ff725f50749770e40f72122f021db1a91eeb923d..768df1acdf3073ba65192abc0e5250d9b8e58769 100644 --- a/src/components/look-on-mobile.js +++ b/src/components/look-on-mobile.js @@ -29,7 +29,8 @@ const average = a => { AFRAME.registerComponent("look-on-mobile", { schema: { horizontalLookSpeedRatio: { default: 0.4 }, // motion applied to camera / motion of polyfill object - verticalLookSpeedRatio: { default: 0.4 } // motion applied to camera / motion of polyfill object + verticalLookSpeedRatio: { default: 0.4 }, // motion applied to camera / motion of polyfill object + camera: { type: "selector" } }, init() { @@ -54,12 +55,12 @@ AFRAME.registerComponent("look-on-mobile", { this.polyfillObject = null; }, - onRotateX(e) { - this.pendingLookX = e.detail.value; + update() { + this.cameraController = this.data.camera.components["pitch-yaw-rotator"]; }, - registerCameraController(cameraController) { - this.cameraController = cameraController; + onRotateX(e) { + this.pendingLookX = e.detail.value; }, tick() { diff --git a/src/components/pitch-yaw-rotator b/src/components/pitch-yaw-rotator.js similarity index 56% rename from src/components/pitch-yaw-rotator rename to src/components/pitch-yaw-rotator.js index cc62dd1c5a2e8f8e77a5c52357fd7dda2bdb267b..87f4e646959ead44cec4b9223a52535081244240 100644 --- a/src/components/pitch-yaw-rotator +++ b/src/components/pitch-yaw-rotator.js @@ -1,3 +1,4 @@ +const degToRad = THREE.Math.degToRad; AFRAME.registerComponent("pitch-yaw-rotator", { schema: { minPitch: { default: -50 }, @@ -20,6 +21,10 @@ AFRAME.registerComponent("pitch-yaw-rotator", { tick() { this.rotation.x = this.pitch; this.rotation.y = this.yaw; - this.el.setAttribute("rotation", this.rotation); + + // Update rotation of object3D the same way the rotation component of aframe does, + // skipping the work that would be done if we used this.el.setAttribute("rotation", this.rotation); + this.el.object3D.rotation.set(degToRad(this.rotation.x), degToRad(this.rotation.y), degToRad(this.rotation.z)); + this.el.object3D.rotation.order = "YXZ"; } }); diff --git a/src/hub.html b/src/hub.html index 36708f68167224897d1e86d82be8596ae553ae40..24f7b5e8009fb064de253eaa0e9b7a71126b3674 100644 --- a/src/hub.html +++ b/src/hub.html @@ -42,7 +42,16 @@ personal-space-bubble="debug: false;" vr-mode-ui="enabled: false" pinch-to-move - input-configurator + input-configurator=" + gazeCursorRayObject: #player-camera-reverse-z; + cursorController: #cursor-controller; + gazeTeleporter: #gaze-teleport; + camera: #player-camera; + playerRig: #player-rig; + leftController: #player-left-controller; + leftControllerRayObject: #player-left-controller-reverse-z; + rightController: #player-right-controller; + rightControllerRayObject: #player-right-controller-reverse-z;" > <a-assets> @@ -203,10 +212,7 @@ id="cursor-controller" cursor-controller=" cursor: #cursor; - camera: #player-camera; - playerRig: #player-rig; - physicalHandSelector: #player-right-controller; - gazeTeleportControls: #gaze-teleport;" + camera: #player-camera; " raycaster="objects: .collidable, .interactable, .ui; far: 3;" line="visible: false; color: white; opacity: 0.2;" ></a-entity> diff --git a/src/utils/action-event-handler.js b/src/utils/action-event-handler.js index bd2ed13a1bd9a66f4d3b86312c97e76377e857a1..1884a7313bf88bc345eff106856c270403d2a74f 100644 --- a/src/utils/action-event-handler.js +++ b/src/utils/action-event-handler.js @@ -4,7 +4,8 @@ export default class ActionEventHandler { this.cursor = cursor; this.isCursorInteracting = false; this.isTeleporting = false; - this.cursorController = null; + this.handThatAlsoDrivesCursor = null; + this.hovered = false; this.addEventListeners = this.addEventListeners.bind(this); this.tearDown = this.tearDown.bind(this); @@ -15,6 +16,7 @@ export default class ActionEventHandler { this.onCardboardButtonDown = this.onCardboardButtonDown.bind(this); this.onCardboardButtonUp = this.onCardboardButtonUp.bind(this); this.onMoveDuck = this.onMoveDuck.bind(this); + this.manageCursorEnabled = this.manageCursorEnabled.bind(this); this.addEventListeners(); } @@ -42,12 +44,12 @@ export default class ActionEventHandler { this.cursor.changeDistanceMod(-e.detail.axis[1] / 8); } - setCursorController(cursorController) { - this.cursorController = cursorController; + setHandThatAlsoDrivesCursor(handThatAlsoDrivesCursor) { + this.handThatAlsoDrivesCursor = handThatAlsoDrivesCursor; } onGrab(e) { - if (this.cursorController && this.cursorController === e.target) { + if (this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { if (this.isCursorInteracting) { return; } else if (e.target.components["super-hands"].state.has("hover-start")) { @@ -64,7 +66,7 @@ export default class ActionEventHandler { } onRelease(e) { - if (this.isCursorInteracting && this.cursorController && this.cursorController === e.target) { + if (this.isCursorInteracting && this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { this.isCursorInteracting = false; this.cursor.endInteraction(); } else { @@ -73,7 +75,7 @@ export default class ActionEventHandler { } onPrimaryDown(e) { - if (this.cursorController && this.cursorController === e.target) { + if (this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { if (this.isCursorInteracting) { return; } else if (e.target.components["super-hands"].state.has("hover-start")) { @@ -92,7 +94,7 @@ export default class ActionEventHandler { } onPrimaryUp(e) { - const isCursorHand = this.cursorController && this.cursorController === e.target; + const isCursorHand = this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target; if (this.isCursorInteracting && isCursorHand) { this.isCursorInteracting = false; this.cursor.endInteraction(); @@ -141,4 +143,19 @@ export default class ActionEventHandler { gazeTeleport.emit(button + "up"); this.isTeleporting = false; } + + manageCursorEnabled() { + const handState = this.handThatAlsoDrivesCursor.components["super-hands"].state; + const handHoveredThisFrame = !this.hovered && handState.has("hover-start") && !this.isCursorInteracting; + const handStoppedHoveringThisFrame = + this.hovered === true && !handState.has("hover-start") && !handState.has("grab-start"); + if (handHoveredThisFrame) { + this.hovered = true; + this.cursor.disable(); + } else if (handStoppedHoveringThisFrame) { + this.hovered = false; + this.cursor.enable(); + this.cursor.setCursorVisibility(!this.isTeleporting); + } + } } diff --git a/src/utils/mouse-events-handler.js b/src/utils/mouse-events-handler.js index 9c6b71af828e2e09300db7decf5f9e3d9de0f4bc..b034006a1fafa9756cc94928288622a7b022bb07 100644 --- a/src/utils/mouse-events-handler.js +++ b/src/utils/mouse-events-handler.js @@ -28,9 +28,7 @@ export default class MouseEventsHandler { document.removeEventListener("mousemove", this.onMouseMove); document.removeEventListener("mouseup", this.onMouseUp); document.removeEventListener("wheel", this.onMouseWheel); - document.removeEventListener("contextmenu", e => { - e.preventDefault(); - }); + document.removeEventListener("contextmenu", this.onContextMenu); } setInverseMouseLook(invert) { @@ -42,9 +40,11 @@ export default class MouseEventsHandler { document.addEventListener("mousemove", this.onMouseMove); document.addEventListener("mouseup", this.onMouseUp); document.addEventListener("wheel", this.onMouseWheel); - document.addEventListener("contextmenu", e => { - e.preventDefault(); - }); + document.addEventListener("contextmenu", this.onContextMenu); + } + + onContextMenu(e) { + e.preventDefault(); } onMouseDown(e) {