diff --git a/src/assets/avatars/BotDom_Avatar.glb b/src/assets/avatars/BotDom_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..9c72e3d2fc170d6b0782d741eca441a62780daad Binary files /dev/null and b/src/assets/avatars/BotDom_Avatar.glb differ diff --git a/src/assets/stylesheets/room.scss b/src/assets/stylesheets/room.scss index 8f3cfb7b5ebf1553653649575d03c5e0a215956c..d042519b31409aa888dde3bbe83e69758fb1cb54 100644 --- a/src/assets/stylesheets/room.scss +++ b/src/assets/stylesheets/room.scss @@ -17,3 +17,10 @@ bottom: 20px; } +.a-canvas.a-grab-cursor:hover { + cursor: none; +} + +.a-canvas.a-grab-cursor:active { + cursor: none; +} diff --git a/src/components/debug.js b/src/components/debug.js new file mode 100644 index 0000000000000000000000000000000000000000..77202f03295be677b9156e1ff16f11100ab19351 --- /dev/null +++ b/src/components/debug.js @@ -0,0 +1,25 @@ +AFRAME.registerComponent("lifecycle-checker", { + schema: { + tick: { default: false } + }, + init: function() { + console.log("init", this.el); + }, + update: function() { + console.log("update", this.el); + }, + tick: function() { + if (this.data.tick) { + console.log("tick", this.el); + } + }, + remove: function() { + console.log("remove", this.el); + }, + pause: function() { + console.log("pause", this.el); + }, + play: function() { + console.log("play", this.el); + } +}); diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js index 3b4f323df72d83cfbe0d03ab00094318485eed22..0a1990bd8ede46f4e5bbdde5cfced182ed4f5a3d 100644 --- a/src/components/ik-controller.js +++ b/src/components/ik-controller.js @@ -7,29 +7,27 @@ AFRAME.registerComponent("ik-root", { rightController: { type: "string", default: ".right-controller" } }, update(oldData) { - let updated = false; - if (this.data.camera !== oldData.camera) { this.camera = this.el.querySelector(this.data.camera); - updated = true; } if (this.data.leftController !== oldData.leftController) { this.leftController = this.el.querySelector(this.data.leftController); - updated = true; } if (this.data.rightController !== oldData.rightController) { this.rightController = this.el.querySelector(this.data.rightController); - updated = true; - } - - if (updated) { - this.el.querySelector("[ik-controller]").components["ik-controller"].updateIkRoot(this); } } }); +function findIKRoot(entity) { + while (entity && !(entity.components && entity.components["ik-root"])) { + entity = entity.parentNode; + } + return entity && entity.components["ik-root"]; +} + AFRAME.registerComponent("ik-controller", { schema: { leftEye: { type: "string", default: ".LeftEye" }, @@ -65,6 +63,8 @@ AFRAME.registerComponent("ik-controller", { this.rootToChest = new Matrix4(); this.invRootToChest = new Matrix4(); + this.ikRoot = findIKRoot(this.el); + this.hands = { left: { lastVisible: true, @@ -124,10 +124,6 @@ AFRAME.registerComponent("ik-controller", { .negate(); }, - updateIkRoot(ikRoot) { - this.ikRoot = ikRoot; - }, - tick(time, dt) { if (!this.ikRoot) { return; diff --git a/src/components/player-info.js b/src/components/player-info.js new file mode 100644 index 0000000000000000000000000000000000000000..8cb265a67b9c67ac26510d6b8a03efb59703c931 --- /dev/null +++ b/src/components/player-info.js @@ -0,0 +1,31 @@ +AFRAME.registerComponent("player-info", { + schema: { + displayName: { type: "string" }, + avatar: { type: "string" } + }, + init() { + this.applyProperties = this.applyProperties.bind(this); + }, + play() { + this.el.addEventListener("model-loaded", this.applyProperties); + }, + pause() { + this.el.removeEventListener("model-loaded", this.applyProperties); + }, + update(oldProps) { + this.applyProperties(); + }, + applyProperties() { + const nametagEl = this.el.querySelector(".nametag"); + if (this.data.displayName && nametagEl) { + nametagEl.setAttribute("text", { + value: this.data.displayName + }); + } + + const modelEl = this.el.querySelector(".model"); + if (this.data.avatar && modelEl) { + modelEl.setAttribute("src", this.data.avatar); + } + } +}); diff --git a/src/components/super-cursor.js b/src/components/super-cursor.js index d1b28b0f1566d43fe68e40d1fc0d9e0ecc13be65..e498a1acc2ad9b99c9131134bd17560bee1af531 100644 --- a/src/components/super-cursor.js +++ b/src/components/super-cursor.js @@ -2,42 +2,55 @@ AFRAME.registerComponent("super-cursor", { dependencies: ["raycaster"], schema: { cursor: { type: "selector" }, - maxDistance: { type: "number", default: 3 }, - minDistance: { type: "number", default: 0.5 } + camera: { type: "selector" }, + maxDistance: { default: 3 }, + minDistance: { default: 0.5 }, + cursorColorHovered: { default: "#FF0000" }, + cursorColorUnhovered: { efault: "#FFFFFF" } }, init: function() { this.isGrabbing = false; + this.isInteractable = false; this.wasIntersecting = false; this.currentDistance = this.data.maxDistance; this.currentDistanceMod = 0; this.enabled = true; - this.isGrabbing = false; this.origin = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.point = new THREE.Vector3(); - }, + this.mousePos = new THREE.Vector2(); + + this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); - play: function() { this.mouseDownListener = this._handleMouseDown.bind(this); + this.mouseMoveListener = this._handleMouseMove.bind(this); this.mouseUpListener = this._handleMouseUp.bind(this); this.wheelListener = this._handleWheel.bind(this); this.enterVRListener = this._handleEnterVR.bind(this); - this.exitVRListener = this._handleExitVR.bind(this); + this.exitVRListener = this._handleExitVR.bind(this); + }, + play: function() { document.addEventListener("mousedown", this.mouseDownListener); + document.addEventListener("mousemove", this.mouseMoveListener); document.addEventListener("mouseup", this.mouseUpListener); document.addEventListener("wheel", this.wheelListener); window.addEventListener("enter-vr", this.enterVRListener); window.addEventListener("exit-vr", this.exitVRListener); + + this._enable(); }, pause: function() { document.removeEventListener("mousedown", this.mouseDownListener); + document.removeEventListener("mousemove", this.mouseMoveListener); document.removeEventListener("mouseup", this.mouseUpListener); document.removeEventListener("wheel", this.wheelListener); window.removeEventListener("enter-vr", this.enterVRListener); window.removeEventListener("exit-vr", this.exitVRListener); + + this._disable(); }, tick: function() { @@ -46,14 +59,21 @@ AFRAME.registerComponent("super-cursor", { } this.isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start"); - let isIntersecting = false; + + const camera = this.data.camera.components.camera.camera; + const raycaster = this.el.components.raycaster.raycaster; + raycaster.setFromCamera(this.mousePos, camera); + this.origin = raycaster.ray.origin; + this.direction = raycaster.ray.direction; + this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction }); + + let intersection = null; if (!this.isGrabbing) { const intersections = this.el.components.raycaster.intersections; if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) { - isIntersecting = true; - this.point = intersections[0].point; - this.data.cursor.object3D.position.copy(this.point); + intersection = intersections[0]; + this.data.cursor.object3D.position.copy(intersection.point); this.currentDistance = intersections[0].distance; this.currentDistanceMod = 0; } else { @@ -61,50 +81,69 @@ AFRAME.registerComponent("super-cursor", { } } - if (this.isGrabbing || !isIntersecting) { - const head = this.el.object3D; - head.getWorldPosition(this.origin); - head.getWorldDirection(this.direction); + if (this.isGrabbing || !intersection) { const distance = Math.min( Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod), this.data.maxDistance ); this.currentDistanceMod = this.currentDistance - distance; - this.direction.multiplyScalar(-distance); + this.direction.multiplyScalar(distance); this.point.addVectors(this.origin, this.direction); this.data.cursor.object3D.position.copy(this.point); } - if ((this.isGrabbing || isIntersecting) && !this.wasIntersecting) { + this.isInteractable = intersection && intersection.object.el.className === "interactable"; + + if ((this.isGrabbing || this.isInteractable) && !this.wasIntersecting) { this.wasIntersecting = true; - this.data.cursor.setAttribute("material", {color: "#00FF00"}); - } else if (!this.isGrabbing && !isIntersecting && this.wasIntersecting) { + this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered }); + } else if (!this.isGrabbing && !this.isInteractable && this.wasIntersecting) { this.wasIntersecting = false; - this.data.cursor.setAttribute("material", {color: "#00EFFF"}); + this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); } }, _handleMouseDown: function(e) { + if (this.isInteractable) { + const lookControls = this.data.camera.components["look-controls"]; + lookControls.pause(); + } this.data.cursor.emit("action_grab", {}); }, + _handleMouseMove: function(e) { + this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1); + }, + _handleMouseUp: function(e) { + const lookControls = this.data.camera.components["look-controls"]; + lookControls.play(); this.data.cursor.emit("action_release", {}); }, _handleWheel: function(e) { if (this.isGrabbing) this.currentDistanceMod += e.deltaY / 10; - }, + }, _handleEnterVR: function(e) { if (AFRAME.utils.device.checkHeadsetConnected() || AFRAME.utils.device.isMobile()) { - this.enabled = false; - this.data.cursor.setAttribute("visible", false); + this._disable(); } }, _handleExitVR: function(e) { + this._enable(); + }, + + _enable: function() { this.enabled = true; this.data.cursor.setAttribute("visible", true); + this.el.setAttribute("raycaster", { enabled: true }); }, + + _disable: function() { + this.enabled = false; + this.data.cursor.setAttribute("visible", false); + this.el.setAttribute("raycaster", { enabled: false }); + } }); diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index 42d7c9b48f3067e87f0242aebc15b7ff07332eca..72d24eff785b9eebed42c60e2e4feb21e659468a 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -21,7 +21,7 @@ AFRAME.registerComponent("super-spawner", { remove: function() { for (let entity of this.entities.keys()) { const data = this.entities.get(entity); - entity.removeEventListener("componentinitialized", data.componentinItializedListener); + entity.removeEventListener("componentinitialized", data.componentinInitializedListener); entity.removeEventListener("bodyloaded", data.bodyLoadedListener); } @@ -34,18 +34,18 @@ AFRAME.registerComponent("super-spawner", { entity.setAttribute("networked", "template:" + this.data.template); - const componentinItializedListener = this._handleComponentInitialzed.bind(this, entity); + const componentinInitializedListener = this._handleComponentInitialzed.bind(this, entity); const bodyLoadedListener = this._handleBodyLoaded.bind(this, entity); this.entities.set(entity, { hand: hand, componentInitialized: false, bodyLoaded: false, - componentinItializedListener: componentinItializedListener, + componentinInitializedListener: componentinInitializedListener, bodyLoadedListener: bodyLoadedListener }); - entity.addEventListener("componentinitialized", componentinItializedListener); + entity.addEventListener("componentinitialized", componentinInitializedListener); entity.addEventListener("body-loaded", bodyLoadedListener); const pos = this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.getAttribute("position"); @@ -71,7 +71,7 @@ AFRAME.registerComponent("super-spawner", { data.hand.emit("action_grab", { targetEntity: entity }); entity.emit("grab-start", { hand: data.hand }); - entity.removeEventListener("componentinitialized", data.componentinItializedListener); + entity.removeEventListener("componentinitialized", data.componentinInitializedListener); entity.removeEventListener("body-loaded", data.bodyLoadedListener); this.entities.delete(entity); diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js index 3c9c07031eecbf21069569b0f2738fe48afcea25..e88f7a76bf6ee4218f37997d9497e576b3bed55a 100644 --- a/src/elements/a-gltf-entity.js +++ b/src/elements/a-gltf-entity.js @@ -22,19 +22,21 @@ AFRAME.AGLTFEntity = { // From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87 // Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573 function cloneGltf(gltf) { - const clone = { - animations: gltf.animations, - scene: gltf.scene.clone(true) - }; - const skinnedMeshes = {}; - gltf.scene.traverse(node => { + if (!node.name) { + node.name = node.uuid; + } if (node.isSkinnedMesh) { skinnedMeshes[node.name] = node; } }); + const clone = { + animations: gltf.animations, + scene: gltf.scene.clone(true) + }; + const cloneBones = {}; const cloneSkinnedMeshes = {}; @@ -68,18 +70,24 @@ function cloneGltf(gltf) { return clone; } -const inflateEntities = function(classPrefix, parentEl, node) { +const inflateEntities = function(parentEl, node) { // setObject3D mutates the node's parent, so we have to copy const children = node.children.slice(0); const el = document.createElement("a-entity"); // Remove invalid CSS class name characters. - const className = node.name.replace(/[^\w-]/g, ""); - el.classList.add(classPrefix + className); + const className = (node.name || node.uuid).replace(/[^\w-]/g, ""); + el.classList.add(className); parentEl.appendChild(el); - // Copy over transform to the THREE.Group and reset the actual transform of the Object3D + // AFRAME rotation component expects rotations in YXZ, convert it + if (node.rotation.order !== "YXZ") { + node.rotation.setFromQuaternion(node.quaternion, "YXZ"); + } + + // Copy over the object's transform to the THREE.Group and reset the actual transform of the Object3D + // all updates to the object should be done through the THREE.Group wrapper el.setAttribute("position", { x: node.position.x, y: node.position.y, @@ -95,10 +103,8 @@ const inflateEntities = function(classPrefix, parentEl, node) { y: node.scale.y, z: node.scale.z }); - - node.position.set(0, 0, 0); - node.rotation.set(0, 0, 0); - node.scale.set(1, 1, 1); + node.matrixAutoUpdate = false; + node.matrix.identity(); el.setObject3D(node.type.toLowerCase(), node); @@ -116,7 +122,6 @@ const inflateEntities = function(classPrefix, parentEl, node) { } const entityComponents = node.userData.components; - if (entityComponents) { for (const prop in entityComponents) { if (entityComponents.hasOwnProperty(prop)) { @@ -130,116 +135,184 @@ const inflateEntities = function(classPrefix, parentEl, node) { } children.forEach(childNode => { - inflateEntities(classPrefix, el, childNode); + inflateEntities(el, childNode); }); -}; -function attachTemplate(templateEl) { - const selector = templateEl.getAttribute("data-selector"); - const targetEls = templateEl.parentNode.querySelectorAll(selector); - const clone = document.importNode(templateEl.content, true); - const templateRoot = clone.firstElementChild; + return el; +}; +function attachTemplate(root, { selector, templateRoot }) { + const targetEls = root.querySelectorAll(selector); for (const el of targetEls) { + const root = templateRoot.cloneNode(true); // Merge root element attributes with the target element - for (const { name, value } of templateRoot.attributes) { + for (const { name, value } of root.attributes) { el.setAttribute(name, value); } // Append all child elements - for (const child of templateRoot.children) { - el.appendChild(document.importNode(child, true)); + for (const child of root.children) { + el.appendChild(child); } } } +function nextTick() { + return new Promise(resolve => { + setTimeout(resolve, 0); + }); +} + +function cachedLoadGLTF(src, onProgress) { + return new Promise((resolve, reject) => { + // Load the gltf model from the cache if it exists. + if (GLTFCache[src]) { + // Use a cloned copy of the cached model. + resolve(cloneGltf(GLTFCache[src])); + } else { + // Otherwise load the new gltf model. + new THREE.GLTFLoader().load( + src, + model => { + if (!GLTFCache[src]) { + // Store a cloned copy of the gltf model. + GLTFCache[src] = cloneGltf(model); + } + resolve(model); + }, + onProgress, + reject + ); + } + }); +} + AFRAME.registerElement("a-gltf-entity", { prototype: Object.create(AFRAME.AEntity.prototype, { load: { - value() { + async value() { if (this.hasLoaded || !this.parentEl) { return; } - // Get the src url. - let src = this.getAttribute("src"); - - // If the src attribute is a selector, get the url from the asset item. - if (src.charAt(0) === "#") { - const assetEl = document.getElementById(src.substring(1)); - - const fallbackSrc = assetEl.getAttribute("src"); - const highSrc = assetEl.getAttribute("high-src"); - const lowSrc = assetEl.getAttribute("low-src"); + // The code above and below this are from AEntity.prototype.load, we need to monkeypatch in gltf loading mid function + this.loadTemplates(); + await this.applySrc(this.getAttribute("src")); + // - if (highSrc && window.APP.quality === "high") { - src = highSrc; - } else if (lowSrc && window.APP.quality === "low") { - src = lowSrc; - } else { - src = fallbackSrc; + AFRAME.ANode.prototype.load.call(this, () => { + // Check if entity was detached while it was waiting to load. + if (!this.parentEl) { + return; } - } - const onLoad = gltfModel => { - if (!GLTFCache[src]) { - // Store a cloned copy of the gltf model. - GLTFCache[src] = cloneGltf(gltfModel); + this.updateComponents(); + if (this.isScene || this.parentEl.isPlaying) { + this.play(); } + }); + } + }, - this.model = gltfModel.scene || gltfModel.scenes[0]; - this.model.animations = gltfModel.animations; + loadTemplates: { + value() { + this.templates = []; + this.querySelectorAll(":scope > template").forEach(templateEl => + this.templates.push({ + selector: templateEl.getAttribute("data-selector"), + templateRoot: document.importNode(templateEl.content.firstElementChild, true) + }) + ); + } + }, + + applySrc: { + async value(src) { + try { + // If the src attribute is a selector, get the url from the asset item. + if (src && src.charAt(0) === "#") { + const assetEl = document.getElementById(src.substring(1)); + + const fallbackSrc = assetEl.getAttribute("src"); + const highSrc = assetEl.getAttribute("high-src"); + const lowSrc = assetEl.getAttribute("low-src"); + + if (highSrc && window.APP.quality === "high") { + src = highSrc; + } else if (lowSrc && window.APP.quality === "low") { + src = lowSrc; + } else { + src = fallbackSrc; + } + } - this.setObject3D("mesh", this.model); - this.emit("model-loaded", { format: "gltf", model: this.model }); + if (src === this.lastSrc) return; + this.lastSrc = src; - if (this.getAttribute("inflate")) { - inflate(this.model, finalizeLoad); - } else { - finalizeLoad(); + if (!src) { + if (this.inflatedEl) { + console.warn("gltf-entity set to an empty source, unloading inflated model."); + this.removeInflatedEl(); + } + return; } - }; - const inflate = (model, callback) => { - inflateEntities("", this, model); - this.querySelectorAll(":scope > template").forEach(attachTemplate); + const model = await cachedLoadGLTF(src); - // Wait one tick for the appended custom elements to be connected before calling finalizeLoad - setTimeout(callback, 0); - }; + // If we started loading something else already + // TODO: there should be a way to cancel loading instead + if (src != this.lastSrc) return; - const finalizeLoad = () => { - AFRAME.ANode.prototype.load.call(this, () => { - // Check if entity was detached while it was waiting to load. - if (!this.parentEl) { - return; - } + // If we had inflated something already before, clean that up + this.removeInflatedEl(); - this.updateComponents(); - if (this.isScene || this.parentEl.isPlaying) { - this.play(); - } - }); - }; + this.model = model.scene || model.scenes[0]; + this.model.animations = model.animations; + + this.setObject3D("mesh", this.model); - // Load the gltf model from the cache if it exists. - const gltf = GLTFCache[src]; + if (this.getAttribute("inflate")) { + this.inflatedEl = inflateEntities(this, this.model); + // TODO: Still don't fully understand the lifecycle here and how it differs between browsers, we should dig in more + // Wait one tick for the appended custom elements to be connected before attaching templates + await nextTick(); + if (src != this.lastSrc) return; // TODO: there must be a nicer pattern for this + this.templates.forEach(attachTemplate.bind(null, this)); + } - if (gltf) { - // Use a cloned copy of the cached model. - const clonedGltf = cloneGltf(gltf); - onLoad(clonedGltf); - return; + this.emit("model-loaded", { format: "gltf", model: this.model }); + } catch (e) { + const message = (e && e.message) || "Failed to load glTF model"; + console.error(message); + this.emit("model-error", { format: "gltf", src }); + } + } + }, + + removeInflatedEl: { + value() { + if (this.inflatedEl) { + this.inflatedEl.parentNode.removeChild(this.inflatedEl); + delete this.inflatedEl; } + } + }, - // Otherwise load the new gltf model. - new THREE.GLTFLoader().load(src, onLoad, undefined /* onProgress */, error => { - // On glTF load error + attributeChangedCallback: { + value(attr, oldVal, newVal) { + if (attr === "src") { + this.applySrc(newVal); + } + AFRAME.AEntity.prototype.attributeChangedCallback.call(this, attr, oldVal, newVal); + } + }, - const message = error && error.message ? error.message : "Failed to load glTF model"; - console.warn(message); - this.emit("model-error", { format: "gltf", src }); - }); + setAttribute: { + value(attr, arg1, arg2) { + if (attr === "src") { + this.applySrc(arg1); + } + AFRAME.AEntity.prototype.setAttribute.call(this, attr, arg1, arg2); } } }) diff --git a/src/network-schemas.js b/src/network-schemas.js index 4632a5933807b1e97132c726fcc335afa6b68964..54cc0b63ba40efed17baf43c0d3554b1ea2fa4bc 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -5,6 +5,7 @@ function registerNetworkSchemas() { "position", "rotation", "scale", + "player-info", { selector: ".camera", component: "position" @@ -36,11 +37,6 @@ function registerNetworkSchemas() { { selector: ".right-controller", component: "visible" - }, - { - selector: ".nametag", - component: "text", - property: "value" } ] }); diff --git a/src/room.html b/src/room.html index 339b2ec76d4d14327619bd6d31c32d37a82ae517..4f1e132b9eb3a2ce94afdfd757fb87f2cde5ddb6 100644 --- a/src/room.html +++ b/src/room.html @@ -30,7 +30,7 @@ high-src="./assets/avatars/BotDefault_Avatar.glb" low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb" ></a-progressive-asset> - + <a-asset-item id="bot-dom-mesh" response-type="arraybuffer" src="./assets/avatars/BotDom_Avatar.glb"></a-asset-item> <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item> <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="./assets/environments/MeetingSpace1_mesh.glb"></a-asset-item> @@ -48,16 +48,16 @@ </template> <template id="remote-avatar-template"> - <a-entity ik-root> + <a-entity ik-root player-info> <a-entity class="camera"></a-entity> <a-entity class="left-controller"></a-entity> <a-entity class="right-controller"></a-entity> - <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller > + <a-gltf-entity class="model" inflate="true"> <template data-selector=".RootScene"> - <a-entity animation-mixer ></a-entity> + <a-entity ik-controller animation-mixer></a-entity> </template> <template data-selector=".Neck"> @@ -81,7 +81,7 @@ </a-entity> </template> - <template selector=".LeftHand"> + <template data-selector=".LeftHand"> <a-entity personal-space-invader ></a-entity> </template> @@ -96,13 +96,12 @@ <a-entity gltf-model="#interactable-duck" scale="2 2 2" - class="collidable" + class="interactable" super-networked-interactable="counter: #counter; mass: 5;" body="type: dynamic; mass: 5; shape: box;" grabbable stretchable="useWorldPosition: true;" - > - </a-entity> + ></a-entity> </template> <a-mixin id="super-hands" @@ -123,21 +122,24 @@ <a-entity gltf-model="#interactable-duck" scale="2 2 2" - class="collidable" - class="collidable" + class="interactable" super-spawner="template: #interactable-template;" position="2.5 1.2 0" body="mass: 0; type: static; shape: box;" ></a-entity> - <a-sphere - id="3d-cursor" - material="color: #00EFFF" - radius=0.02 - static-body="shape: sphere;" - mixin="super-hands" - ></a-sphere> - + <a-entity + id="super-cursor" + super-cursor="cursor: #3d-cursor; camera: #player-camera;" + raycaster="objects: .collidable, .interactable; far: 10;" + > + <a-sphere + id="3d-cursor" + radius=0.02 + static-body="shape: sphere;" + mixin="super-hands" + ></a-sphere> + </a-entity> <!-- Player Rig --> <a-entity @@ -147,6 +149,7 @@ wasd-to-analog2d character-controller="pivot: #player-camera" ik-root + player-info > <a-entity id="player-camera" @@ -154,14 +157,8 @@ camera position="0 1.6 0" personal-space-bubble - > - <a-entity - id="super-cursor" - super-cursor="cursor: #3d-cursor" - position="0 0 0" - raycaster="objects: .collidable; direction: 0 0 -1;" - ></a-entity> - </a-entity> + look-controls + ></a-entity> <a-entity id="player-left-controller" @@ -181,9 +178,13 @@ haptic-feedback ></a-entity> - <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller > + <a-gltf-entity class="model" inflate="true"> <template data-selector=".RootScene"> - <a-entity animation-mixer animated-robot-hands ></a-entity> + <a-entity + ik-controller + animated-robot-hands + animation-mixer + ></a-entity> </template> <template data-selector=".Neck"> @@ -298,8 +299,8 @@ height="35" width="35" static-body - class="collidable"> - </a-plane> + class="collidable" + ></a-plane> </a-scene> <div id="ui-root" class="ui"></div> diff --git a/src/room.js b/src/room.js index be559c209526c4255b12dffe8165ec333ab9249c..ab05f114958dc0a0885681ab790d870aab263d50 100644 --- a/src/room.js +++ b/src/room.js @@ -37,6 +37,8 @@ import "./components/layers"; import "./components/spawn-controller"; import "./components/animated-robot-hands"; import "./components/hide-when-quality"; +import "./components/player-info"; +import "./components/debug"; import "./components/animation-mixer"; import "./components/loop-animation"; @@ -100,7 +102,7 @@ const concurrentLoadDetector = new ConcurrentLoadDetector(); concurrentLoadDetector.start(); // Always layer in any new default profile bits -store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) }}) +store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } }); async function exitScene() { const scene = document.querySelector("a-scene"); @@ -108,15 +110,21 @@ async function exitScene() { document.body.removeChild(scene); } -function setNameTagFromStore() { - const myNametag = document.querySelector("#player-rig .nametag"); - myNametag.setAttribute("text", "value", store.state.profile.display_name); +function updatePlayerInfoFromStore() { + const qs = queryString.parse(location.search); + const playerRig = document.querySelector("#player-rig"); + playerRig.setAttribute("player-info", { + displayName: store.state.profile.display_name, + avatar: qs.avatar || "#bot-skinned-mesh" + }); } async function enterScene(mediaStream, enterInVR) { const scene = document.querySelector("a-scene"); - document.querySelector("a-scene canvas").classList.remove("blurred") - scene.setAttribute("networked-scene", "adapter: janus; audio: true; debug: true; connectOnLoad: false;"); + const playerRig = document.querySelector("#player-rig"); + const qs = queryString.parse(location.search); + + document.querySelector("a-scene canvas").classList.remove("blurred"); registerNetworkSchemas(); if (enterInVR) { @@ -125,9 +133,11 @@ async function enterScene(mediaStream, enterInVR) { AFRAME.registerInputActions(inGameActions, "default"); - document.querySelector("#player-camera").setAttribute("look-controls", "pointerLockEnabled: true;"); - scene.setAttribute("networked-scene", { + adapter: "janus", + audio: true, + debug: true, + connectOnLoad: false, room: qs.room && !isNaN(parseInt(qs.room, 10)) ? parseInt(qs.room, 10) : 1, serverURL: process.env.JANUS_SERVER }); @@ -136,13 +146,12 @@ async function enterScene(mediaStream, enterInVR) { scene.setAttribute("stats", true); } - if (isMobile || qsTruthy("mobile")) { - const playerRig = document.querySelector("#player-rig"); + if (isMobile || qsTruthy(qs.mobile)) { playerRig.setAttribute("virtual-gamepad-controls", {}); } - setNameTagFromStore(); - store.addEventListener('statechanged', setNameTagFromStore); + updatePlayerInfoFromStore(); + store.addEventListener("statechanged", updatePlayerInfoFromStore); const avatarScale = parseInt(qs.avatar_scale, 10); @@ -200,8 +209,7 @@ async function enterScene(mediaStream, enterInVR) { } } -function onConnect() { -} +function onConnect() {} function mountUI(scene) { const qs = queryString.parse(location.search); @@ -209,16 +217,21 @@ function mountUI(scene) { const forcedVREntryType = qs.vr_entry_type || null; const enableScreenSharing = qsTruthy("enable_screen_sharing"); - const uiRoot = ReactDOM.render(<UIRoot {...{ - scene, - enterScene, - exitScene, - concurrentLoadDetector, - disableAutoExitOnConcurrentLoad, - forcedVREntryType, - enableScreenSharing, - store - }} />, document.getElementById("ui-root")); + const uiRoot = ReactDOM.render( + <UIRoot + {...{ + scene, + enterScene, + exitScene, + concurrentLoadDetector, + disableAutoExitOnConcurrentLoad, + forcedVREntryType, + enableScreenSharing, + store + }} + />, + document.getElementById("ui-root") + ); getAvailableVREntryTypes().then(availableVREntryTypes => { uiRoot.setState({ availableVREntryTypes });