diff --git a/src/components/controls-shape-offset.js b/src/components/controls-shape-offset.js new file mode 100644 index 0000000000000000000000000000000000000000..7ce8764332f0e8b7b6116add6a8e8178a84121f6 --- /dev/null +++ b/src/components/controls-shape-offset.js @@ -0,0 +1,45 @@ +import { CONTROLLER_OFFSETS } from "./hand-controls2.js"; + +AFRAME.registerComponent("controls-shape-offset", { + schema: { + additionalOffset: { default: { x: 0, y: -0.03, z: -0.04 } } + }, + init: function() { + this.controller = null; + this.shapeAdded = false; + + this._handleControllerConnected = this._handleControllerConnected.bind(this); + this.el.addEventListener("controllerconnected", this._handleControllerConnected); + }, + + remove: function() { + this.el.removeEventListener("controllerconnected", this._handleControllerConnected); + }, + + tick: function() { + if (!this.shapeAdded && this.controller) { + this.shapeAdded = true; + const hasOffset = CONTROLLER_OFFSETS.hasOwnProperty(this.controller); + const offset = hasOffset ? CONTROLLER_OFFSETS[this.controller] : CONTROLLER_OFFSETS.default; + const position = new THREE.Vector3(); + const quaternion = new THREE.Quaternion(); + const scale = new THREE.Vector3(); + offset.decompose(position, quaternion, scale); + position.add(this.data.additionalOffset); + quaternion.conjugate(); + + const shape = { + shape: "sphere", + radius: "0.02", + orientation: quaternion, + offset: position + }; + + this.el.setAttribute("shape", shape); + } + }, + + _handleControllerConnected: function(e) { + this.controller = e.detail.name; + } +}); diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index 572b17d91f606912b612fb3e1b8e440f33be46a0..f17be4e82617ce0ac609ed34c83d77959bae0ab1 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -10,6 +10,7 @@ AFRAME.registerComponent("cursor-controller", { camera: { type: "selector" }, playerRig: { type: "selector" }, gazeTeleportControls: { type: "selector" }, + haptic: { type: "selector" }, physicalHandSelector: { type: "string" }, handedness: { default: "right", oneOf: ["right", "left"] }, maxDistance: { default: 3 }, @@ -78,16 +79,13 @@ AFRAME.registerComponent("cursor-controller", { }, play: function() { - if (!this.inVR && this.isMobile && !this.hasPointingDevice) { - document.addEventListener("touchstart", this._handleTouchStart); - document.addEventListener("touchmove", this._handleTouchMove); - document.addEventListener("touchend", this._handleTouchEnd); - } else { - document.addEventListener("mousedown", this._handleMouseDown); - document.addEventListener("mousemove", this._handleMouseMove); - document.addEventListener("mouseup", this._handleMouseUp); - document.addEventListener("wheel", this._handleWheel); - } + document.addEventListener("touchstart", this._handleTouchStart); + document.addEventListener("touchmove", this._handleTouchMove); + document.addEventListener("touchend", this._handleTouchEnd); + document.addEventListener("mousedown", this._handleMouseDown); + document.addEventListener("mousemove", this._handleMouseMove); + document.addEventListener("mouseup", this._handleMouseUp); + document.addEventListener("wheel", this._handleWheel); window.addEventListener("enter-vr", this._handleEnterVR); window.addEventListener("exit-vr", this._handleExitVR); @@ -258,6 +256,8 @@ AFRAME.registerComponent("cursor-controller", { }, _handleTouchStart: function(e) { + if (!this.isMobile || this.hasPointingDevice) return; + const touch = e.touches[0]; if (touch.clientY / window.innerHeight >= 0.8) return true; this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1); @@ -303,6 +303,8 @@ AFRAME.registerComponent("cursor-controller", { }, _handleTouchMove: function(e) { + if (!this.isMobile || this.hasPointingDevice) return; + for (let i = 0; i < e.touches.length; i++) { const touch = e.touches[i]; if (touch.clientY / window.innerHeight >= 0.8) return true; @@ -312,6 +314,8 @@ AFRAME.registerComponent("cursor-controller", { }, _handleTouchEnd: function(e) { + if (!this.isMobile || this.hasPointingDevice) return; + for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; const thisTouchDidNotDriveMousePos = @@ -326,6 +330,8 @@ AFRAME.registerComponent("cursor-controller", { }, _handleMouseDown: function() { + if (this.isMobile && !this.hasPointingDevice) return; + if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) { this._setLookControlsEnabled(false); this.data.cursor.emit("cursor-grab", {}); @@ -335,10 +341,14 @@ AFRAME.registerComponent("cursor-controller", { }, _handleMouseMove: function(e) { + if (this.isMobile && !this.hasPointingDevice) return; + this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1); }, _handleMouseUp: function() { + if (this.isMobile && !this.hasPointingDevice) return; + this._setLookControlsEnabled(true); this.data.cursor.emit("cursor-release", {}); this._endTeleport(); diff --git a/src/components/duck.js b/src/components/duck.js new file mode 100644 index 0000000000000000000000000000000000000000..50f24de70f4a74855d5346d3bc543c1f765b7eb0 --- /dev/null +++ b/src/components/duck.js @@ -0,0 +1,40 @@ +AFRAME.registerComponent("duck", { + schema: { + initialForce: { default: 0 }, + maxForce: { default: 6.5 }, + maxScale: { default: 5 } + }, + + init: function() { + this.system = this.el.sceneEl.systems.physics; + this.hasBody = false; + this.position = new CANNON.Vec3(); + this.force = new CANNON.Vec3(0, this.data.initialForce, 0); + this.initialScale = this.el.object3D.scale.x; + this.maxScale = this.data.maxScale * this.initialScale; + }, + + play: function() { + this.system.addComponent(this); + }, + + pause: function() { + this.system.removeComponent(this); + }, + + beforeStep: function() { + if (this.el.body) { + const currentScale = this.el.object3D.scale.x; + const ratio = Math.min(1, (currentScale - this.initialScale) / (this.maxScale - this.initialScale)); + const force = ratio * this.data.maxForce; + if (force > 0) { + const angle = Math.random() * Math.PI * 2; + const x = Math.cos(angle); + const z = Math.sin(angle); + this.force.set(x, force, z); + this.position.set(x * 0.01, 0, z * 0.01); + this.el.body.applyForce(this.force, this.position); + } + } + } +}); diff --git a/src/components/hand-controls2.js b/src/components/hand-controls2.js index af86cd5da272ea5aee5c60e75578da84a13aaef5..4eef8a499cabdf1804c73bccccba800d9629b9a2 100644 --- a/src/components/hand-controls2.js +++ b/src/components/hand-controls2.js @@ -10,7 +10,7 @@ const POSES = { mrpDown: "mrpDown" }; -const CONTROLLER_OFFSETS = { +export const CONTROLLER_OFFSETS = { default: new THREE.Matrix4(), "oculus-touch-controls": new THREE.Matrix4().makeTranslation(0, -0.015, 0.04), "vive-controls": new THREE.Matrix4().compose( diff --git a/src/components/haptic-feedback.js b/src/components/haptic-feedback.js index 35a79cd9555e2864e5c496f671fcb9ddd8831345..d7db51d5e8c8f7235d89510d1245dfb5a9587d1a 100644 --- a/src/components/haptic-feedback.js +++ b/src/components/haptic-feedback.js @@ -10,7 +10,7 @@ AFRAME.registerComponent("haptic-feedback", { }, init: function() { - this.pulse = this.pulse.bind(this); + this.handlePulse = this.handlePulse.bind(this); this.getActuator = this.getActuator.bind(this); this.getActuator().then(actuator => { this.actuator = actuator; @@ -37,21 +37,28 @@ AFRAME.registerComponent("haptic-feedback", { }, play: function() { - this.el.addEventListener(this.data.hapticEventName, this.pulse); + this.el.addEventListener(this.data.hapticEventName, this.handlePulse); }, pause: function() { - this.el.removeEventListener(this.data.hapticEventName, this.pulse); + this.el.removeEventListener(this.data.hapticEventName, this.handlePulse); }, - pulse: function(event) { + handlePulse: function(event) { const { intensity } = event.detail; - if (!strengthForIntensity[intensity]) { + let strength; + + if (strengthForIntensity[intensity]) { + this.pulse(strengthForIntensity[intensity]); + } else if (Number(intensity) === intensity) { + this.pulse(intensity); + } else { console.warn(`Invalid intensity : ${intensity}`); - return; } + }, + pulse: function(intensity) { if (this.actuator) { - this.actuator.pulse(strengthForIntensity[intensity], 15); + this.actuator.pulse(intensity, 15); } } }); diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index a5baed53dd2f3f5404a81a26150904353434e9e9..0d9fc26dea6304e7ea66f165f76a62bd7044b2d3 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -1,10 +1,12 @@ AFRAME.registerComponent("super-networked-interactable", { schema: { mass: { default: 1 }, + hapticsMassVelocityFactor: { default: 0.1 }, counter: { type: "selector" } }, init: function() { + this.system = this.el.sceneEl.systems.physics; this.counter = this.data.counter.components["networked-counter"]; this.hand = null; @@ -21,12 +23,23 @@ AFRAME.registerComponent("super-networked-interactable", { this.ownershipLostListener = this._onOwnershipLost.bind(this); this.el.addEventListener("grab-start", this.grabStartListener); this.el.addEventListener("ownership-lost", this.ownershipLostListener); + this.system.addComponent(this); }, remove: function() { this.counter.deregister(this.el); this.el.removeEventListener("grab-start", this.grabStartListener); this.el.removeEventListener("ownership-lost", this.ownershipLostListener); + this.system.removeComponent(this); + }, + + afterStep: function() { + if (this.el.is("grabbed") && this.hand && this.hand.components.hasOwnProperty("haptic-feedback")) { + const hapticFeedback = this.hand.components["haptic-feedback"]; + let velocity = this.el.body.velocity.lengthSquared() * this.el.body.mass * this.data.hapticsMassVelocityFactor; + velocity = Math.min(1, velocity); + hapticFeedback.pulse(velocity); + } }, _onGrabStart: function(e) { diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index 9d36c6583f01909e0db08f93450c78cd78c3c3aa..27fee415c1b3735e43bae322f78a2b631e657633 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -3,11 +3,13 @@ AFRAME.registerComponent("super-spawner", { template: { default: "" }, useCustomSpawnPosition: { default: false }, spawnPosition: { type: "vec3" }, - events: { default: ["cursor-grab", "action_grab"] } + events: { default: ["cursor-grab", "action_grab"] }, + spawnCooldown: { default: 1 } }, init: function() { this.entities = new Map(); + this.timeout = null; }, play: function() { @@ -17,19 +19,26 @@ AFRAME.registerComponent("super-spawner", { pause: function() { this.el.removeEventListener("grab-start", this.handleGrabStart); + + clearTimeout(this.timeout); + this.timeout = null; + this.el.setAttribute("visible", true); }, remove: function() { for (const entity of this.entities.keys()) { const data = this.entities.get(entity); entity.removeEventListener("componentinitialized", data.componentinInitializedListener); - entity.removeEventListener("bodyloaded", data.bodyLoadedListener); + entity.removeEventListener("body-loaded", data.bodyLoadedListener); } this.entities.clear(); }, _handleGrabStart: function(e) { + if (this.timeout) { + return; + } const hand = e.detail.hand; const entity = document.createElement("a-entity"); @@ -51,6 +60,14 @@ AFRAME.registerComponent("super-spawner", { const pos = this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.getAttribute("position"); entity.setAttribute("position", pos); this.el.sceneEl.appendChild(entity); + + if (this.data.spawnCooldown > 0) { + this.el.setAttribute("visible", false); + this.timeout = setTimeout(() => { + this.el.setAttribute("visible", true); + this.timeout = null; + }, this.data.spawnCooldown * 1000); + } }, _handleComponentInitialzed: function(entity, e) { diff --git a/src/hub.html b/src/hub.html index 6b759eb1ab65fb7bef6b43c2b3c4f261a8a50576..9b47dcb372b47b9d5fd277b69a703e413543b4ed 100644 --- a/src/hub.html +++ b/src/hub.html @@ -21,7 +21,7 @@ <a-scene networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;" - physics + physics="gravity: -6;" mute-mic="eventSrc: a-scene; toggleEvents: action_mute" personal-space-bubble="debug: false;" > @@ -89,6 +89,13 @@ personal-space-invader="radius: 0.15; useMaterial: true;" bone-visibility > + <a-cylinder + static-body + radius="0.13" + height="0.2" + position="0 0.07 0.05" + visible="false" + ></a-cylinder> </a-entity> </template> @@ -108,10 +115,11 @@ gltf-model-plus="src: #interactable-duck; inflate: true;" scale="2 2 2" class="interactable" - super-networked-interactable="counter: #counter; mass: 5;" - body="type: dynamic; shape: none; mass: 5;" + super-networked-interactable="counter: #counter; mass: 1;" + body="type: dynamic; shape: none; mass: 1;" grabbable stretchable="useWorldPosition: true;" + duck ></a-entity> </template> @@ -137,6 +145,7 @@ super-spawner="template: #interactable-template;" position="2.9 1.2 0" body="mass: 0; type: static; shape: box;" + collision-filter="collisionForces: false;" ></a-entity> <a-entity @@ -230,7 +239,11 @@ hitOpacity: 0.3; missOpacity: 0.2;" haptic-feedback - ></a-entity> + body="type: static; shape: none;" + mixin="super-hands" + controls-shape-offset + > + </a-entity> <a-entity id="player-right-controller" @@ -247,6 +260,9 @@ hitOpacity: 0.3; missOpacity: 0.2;" haptic-feedback + body="type: static; shape: none;" + mixin="super-hands" + controls-shape-offset ></a-entity> <a-entity gltf-model-plus="inflate: true;" class="model"> @@ -273,7 +289,7 @@ <template data-selector=".LeftHand"> <a-entity bone-visibility> - <a-entity + <!-- <a-entity id="left-super-hand" event-repeater=" events: action_grab, action_release, action_primary_down, action_primary_up; @@ -281,13 +297,13 @@ static-body="shape: sphere; sphereRadius: 0.02" mixin="super-hands" position="0 0.05 0" - ></a-entity> + ></a-entity> --> </a-entity> </template> <template data-selector=".RightHand"> <a-entity bone-visibility> - <a-entity +<!-- <a-entity id="right-super-hand" event-repeater=" events: action_grab, action_release, action_primary_down, action_primary_up; @@ -295,7 +311,7 @@ static-body="shape: sphere; sphereRadius: 0.02" mixin="super-hands" position="0 -0.05 0" - ></a-entity> + ></a-entity> --> </a-entity> </template> diff --git a/src/hub.js b/src/hub.js index a412bb4d909087dfd627a994df1ec214f4444dd7..4a3946382e0c0637a48295f72a3ec89fe89fc6a3 100644 --- a/src/hub.js +++ b/src/hub.js @@ -83,6 +83,8 @@ import "./components/super-networked-interactable"; import "./components/networked-counter"; import "./components/super-spawner"; import "./components/event-repeater"; +import "./components/controls-shape-offset"; +import "./components/duck"; import "./components/cursor-controller";