diff --git a/.defaults.env b/.defaults.env index 1978f94e2c..fc3f779160 100644 --- a/.defaults.env +++ b/.defaults.env @@ -32,3 +32,4 @@ DEFAULT_SCENE_SID="JGLt8DP" IMMERS_SERVER="https://localhost:8081" IMMERS_SCOPE="modAdditive" +IMMERS_ALLOW_GUESTS="true" diff --git a/src/assets/images/sprites/action/immers-bg-hover.png b/src/assets/images/sprites/action/immers-bg-hover.png new file mode 100644 index 0000000000..c123567f98 Binary files /dev/null and b/src/assets/images/sprites/action/immers-bg-hover.png differ diff --git a/src/assets/images/sprites/action/immers-bg.png b/src/assets/images/sprites/action/immers-bg.png new file mode 100644 index 0000000000..15521bbd03 Binary files /dev/null and b/src/assets/images/sprites/action/immers-bg.png differ diff --git a/src/assets/images/spritesheets/sprite-system-action-spritesheet.json b/src/assets/images/spritesheets/sprite-system-action-spritesheet.json index 690c4eb3d0..a822f84477 100644 --- a/src/assets/images/spritesheets/sprite-system-action-spritesheet.json +++ b/src/assets/images/spritesheets/sprite-system-action-spritesheet.json @@ -285,7 +285,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "inspect-action.png": + "immers-bg-hover.png": { "frame": {"x":328,"y":738,"w":64,"h":64}, "rotated": false, @@ -293,7 +293,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "mute-action.png": + "immers-bg.png": { "frame": {"x":408,"y":738,"w":64,"h":64}, "rotated": false, @@ -301,7 +301,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "next.png": + "inspect-action.png": { "frame": {"x":488,"y":738,"w":64,"h":64}, "rotated": false, @@ -309,7 +309,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "pin-action.png": + "mute-action.png": { "frame": {"x":568,"y":738,"w":64,"h":64}, "rotated": false, @@ -317,7 +317,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "prev.png": + "next.png": { "frame": {"x":648,"y":738,"w":64,"h":64}, "rotated": false, @@ -325,7 +325,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "recenter-action.png": + "pin-action.png": { "frame": {"x":728,"y":738,"w":64,"h":64}, "rotated": false, @@ -333,7 +333,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "record-action-alpha.png": + "prev.png": { "frame": {"x":882,"y":8,"w":64,"h":64}, "rotated": false, @@ -341,7 +341,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "record-action.png": + "recenter-action.png": { "frame": {"x":882,"y":88,"w":64,"h":64}, "rotated": false, @@ -349,7 +349,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "remove-action.png": + "record-action-alpha.png": { "frame": {"x":882,"y":168,"w":64,"h":64}, "rotated": false, @@ -357,7 +357,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "rotate-action.png": + "record-action.png": { "frame": {"x":882,"y":248,"w":64,"h":64}, "rotated": false, @@ -365,7 +365,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "scale-action.png": + "remove-action.png": { "frame": {"x":882,"y":328,"w":64,"h":64}, "rotated": false, @@ -373,7 +373,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "serialize-action.png": + "rotate-action.png": { "frame": {"x":882,"y":408,"w":64,"h":64}, "rotated": false, @@ -381,7 +381,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "snap-page.png": + "scale-action.png": { "frame": {"x":882,"y":488,"w":64,"h":64}, "rotated": false, @@ -389,7 +389,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "spawn_message.png": + "serialize-action.png": { "frame": {"x":882,"y":568,"w":64,"h":64}, "rotated": false, @@ -397,7 +397,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "spawn_message_dark-hover.png": + "snap-page.png": { "frame": {"x":882,"y":648,"w":64,"h":64}, "rotated": false, @@ -405,7 +405,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "spawn_message_dark.png": + "spawn_message.png": { "frame": {"x":882,"y":728,"w":64,"h":64}, "rotated": false, @@ -413,7 +413,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "stop-action.png": + "spawn_message_dark-hover.png": { "frame": {"x":8,"y":818,"w":64,"h":64}, "rotated": false, @@ -421,7 +421,7 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "undo-action.png": + "spawn_message_dark.png": { "frame": {"x":88,"y":818,"w":64,"h":64}, "rotated": false, @@ -429,13 +429,29 @@ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} }, - "unmute-action.png": + "stop-action.png": { "frame": {"x":168,"y":818,"w":64,"h":64}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, "sourceSize": {"w":64,"h":64} + }, + "undo-action.png": + { + "frame": {"x":248,"y":818,"w":64,"h":64}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, + "sourceSize": {"w":64,"h":64} + }, + "unmute-action.png": + { + "frame": {"x":328,"y":818,"w":64,"h":64}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, + "sourceSize": {"w":64,"h":64} } } } \ No newline at end of file diff --git a/src/assets/images/spritesheets/sprite-system-action-spritesheet.png b/src/assets/images/spritesheets/sprite-system-action-spritesheet.png index 9736cf33a6..cf316b5474 100644 Binary files a/src/assets/images/spritesheets/sprite-system-action-spritesheet.png and b/src/assets/images/spritesheets/sprite-system-action-spritesheet.png differ diff --git a/src/assets/images/spritesheets/sprite-system-notice-spritesheet.png b/src/assets/images/spritesheets/sprite-system-notice-spritesheet.png index ca956f8e6e..893da77263 100644 Binary files a/src/assets/images/spritesheets/sprite-system-notice-spritesheet.png and b/src/assets/images/spritesheets/sprite-system-notice-spritesheet.png differ diff --git a/src/components/block-button.js b/src/components/block-button.js index 23c7391086..6a9b8b0ddb 100644 --- a/src/components/block-button.js +++ b/src/components/block-button.js @@ -11,14 +11,19 @@ AFRAME.registerComponent("block-button", { this.el.emit("immers-block", { clientId: this.owner }); }; this.onScopeChange = () => { - if (this.el.sceneEl.states.includes("immers-scope-addBlocks")) { + if ( + this.playerEl?.getAttribute("player-info").immersId && + this.el.sceneEl.states.includes("immers-scope-addBlocks") + ) { this.textEl.setAttribute("text", "value", "Block"); } else { this.textEl.setAttribute("text", "value", "Hide"); } }; NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { + this.playerEl = networkedEl; this.owner = networkedEl.components.networked.data.owner; + this.playerEl.addEventListener("immers-id-changed", this.onScopeChange); }); this.onScopeChange(); }, @@ -27,11 +32,17 @@ AFRAME.registerComponent("block-button", { this.el.object3D.addEventListener("interact", this.onClick); this.el.sceneEl.addEventListener("stateadded", this.onScopeChange); this.el.sceneEl.addEventListener("stateremoved", this.onScopeChange); + if (this.playerEl) { + this.playerEl.addEventListener("immers-id-changed", this.onScopeChange); + } }, pause() { this.el.object3D.removeEventListener("interact", this.onClick); this.el.sceneEl.removeEventListener("stateremoved", this.onScopeChange); + if (this.playerEl) { + this.playerEl.removeEventListener("immers-id-changed", this.onScopeChange); + } }, block(clientId) { diff --git a/src/components/immers/immers-follow-button.js b/src/components/immers/immers-follow-button.js index ad47ac6641..5f533bed57 100644 --- a/src/components/immers/immers-follow-button.js +++ b/src/components/immers/immers-follow-button.js @@ -6,6 +6,9 @@ AFRAME.registerComponent("immers-follow-button", { schema: { relation: { type: "string", default: "none", oneOf: ["none", "request", "friend", "pending"] } }, init() { + this.showIfLoggedIn = () => { + this.el.object3D.visible = !!this.playerEl?.getAttribute("player-info").immersId; + }; NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.playerEl = networkedEl; this.playerEl.addEventListener("stateadded", this.onState); @@ -14,6 +17,8 @@ AFRAME.registerComponent("immers-follow-button", { } else if (this.playerEl.is("immers-follow-request")) { this.el.setAttribute("immers-follow-button", { relation: "request" }); } + this.showIfLoggedIn(); + this.playerEl.addEventListener("immers-id-changed", this.showIfLoggedIn); }); this.textEl = this.el.querySelector("[text]"); // avoid accidental double clicks @@ -48,6 +53,7 @@ AFRAME.registerComponent("immers-follow-button", { this.el.object3D.addEventListener("interact", this.onClick); if (this.playerEl) { this.playerEl.addEventListener("stateadded", this.onState); + this.playerEl.addEventListener("immers-id-changed", this.showIfLoggedIn); } }, @@ -73,6 +79,7 @@ AFRAME.registerComponent("immers-follow-button", { this.el.object3D.removeEventListener("interact", this.onClick); if (this.playerEl) { this.playerEl.removeEventListener("stateadded", this.onState); + this.playerEl.removeEventListener("immers-id-changed", this.showIfLoggedIn); } }, diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js index b89cba9418..84c2e1a4f2 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -10,6 +10,7 @@ AFRAME.registerComponent("in-world-hud", { this.spawn = this.el.querySelector(".spawn"); this.pen = this.el.querySelector(".penhud"); this.cameraBtn = this.el.querySelector(".camera-btn"); + this.immersBtn = this.el.querySelector(".immers-btn"); this.inviteBtn = this.el.querySelector(".invite-btn"); this.background = this.el.querySelector(".bg"); this.notificationText = this.el.querySelector("#hud-presence-notification"); @@ -24,6 +25,7 @@ AFRAME.registerComponent("in-world-hud", { this.pen.setAttribute("icon-button", "disabled", !window.APP.hubChannel.can("spawn_drawing")); this.cameraBtn.setAttribute("icon-button", "disabled", !window.APP.hubChannel.can("spawn_camera")); } + this.immersBtn.object3D.visible = !this.el.sceneEl.is("immers-connected"); }; this.onStateChange = evt => { @@ -52,6 +54,10 @@ AFRAME.registerComponent("in-world-hud", { this.el.emit("action_toggle_camera"); }; + this.onImmersClick = () => { + this.el.emit("action_immers_register"); + }; + this.onInviteClick = () => { this.el.emit("action_invite"); }; @@ -73,6 +79,7 @@ AFRAME.registerComponent("in-world-hud", { this.pen.object3D.addEventListener("interact", this.onPenClick); this.cameraBtn.object3D.addEventListener("interact", this.onCameraClick); this.inviteBtn.object3D.addEventListener("interact", this.onInviteClick); + this.immersBtn.object3D.addEventListener("interact", this.onImmersClick); }, pause() { @@ -86,5 +93,6 @@ AFRAME.registerComponent("in-world-hud", { this.pen.object3D.removeEventListener("interact", this.onPenClick); this.cameraBtn.object3D.removeEventListener("interact", this.onCameraClick); this.inviteBtn.object3D.removeEventListener("interact", this.onInviteClick); + this.immersBtn.object3D.removeEventListener("interact", this.onImmersClick); } }); diff --git a/src/components/media-views.js b/src/components/media-views.js index c016971723..29af6553b3 100644 --- a/src/components/media-views.js +++ b/src/components/media-views.js @@ -17,6 +17,7 @@ import { applyPersistentSync } from "../utils/permissions-utils"; import { refreshMediaMirror, getCurrentMirroredMedia } from "../utils/mirror-utils"; import { detect } from "detect-browser"; import semver from "semver"; +import { makeChromaKeyMaterial } from "./unseen/chroma-key-material"; import qsTruthy from "../utils/qs_truthy"; @@ -297,7 +298,7 @@ AFRAME.registerComponent("media-video", { this.isSnapping = false; this.videoIsLive = null; // value null until we've determined if the video is live or not. this.onSnapImageLoaded = () => (this.isSnapping = false); - + /* this.el.setAttribute("hover-menu__video", { template: "#video-hover-menu", isFlat: true }); this.el.components["hover-menu__video"].getHoverMenu().then(menu => { // If we got removed while waiting, do nothing. @@ -327,7 +328,7 @@ AFRAME.registerComponent("media-video", { this.updateHoverMenu(); this.updatePlaybackState(); }); - + */ NAF.utils .getNetworkedEntity(this.el) .then(networkedEl => { @@ -660,7 +661,7 @@ AFRAME.registerComponent("media-video", { const projection = this.data.projection; if (!this.mesh || projection !== oldData.projection) { - const material = new THREE.MeshBasicMaterial(); + const material = src.startsWith("hubs://") ? makeChromaKeyMaterial(texture) : new THREE.MeshBasicMaterial(); let geometry; diff --git a/src/components/unseen/animation-sync.js b/src/components/unseen/animation-sync.js new file mode 100644 index 0000000000..5b975a3a6c --- /dev/null +++ b/src/components/unseen/animation-sync.js @@ -0,0 +1,197 @@ +export const UNSEEN_EVENT = "unseen::animation"; +export const UNSEEN_CHANNEL = "unseen::animation"; + +const FLOWER_ACTIONS = ["decay2", "decay1", "closed", "bloom1", "bloom2"]; +const TRANSITION_DUR = 13000; + +/** + * hide/show performer + * id = document.querySelector('[media-frame]').getAttribute('media-frame').targetId +document.getElementById(id).object3D.visible + */ + +window.AFRAME.registerSystem("unseen-animation-sync", { + init() { + this.lastAction = "closed"; + this.flowerState = 2; + this.isTransitioning = false; + this.blurState = false; + if (window.NAF.connection.isConnected()) { + this.setupNetwork(); + } else { + document.body.addEventListener("connected", () => this.setupNetwork(), { once: true }); + } + if (this.sceneEl.is("loaded")) { + this.onSceneLoaded(); + } else { + const sceneStateListener = ({ detail }) => { + if (detail === "loaded") { + this.onSceneLoaded(); + this.sceneEl.removeEventListener("stateadded", sceneStateListener); + } + }; + this.sceneEl.addEventListener("stateadded", sceneStateListener); + } + + this.sceneEl.addEventListener(UNSEEN_EVENT, event => this.triggerAnimation(event)); + }, + triggerAnimation(event) { + if (this.isTransitioning) { + return; + } + let action = event.detail.action; + let newFlowerState = this.flowerState; + switch (action) { + case "blur": + if (this.blurState) { + action = "unblur"; + } + break; + case "bloom": + newFlowerState = Math.min(this.flowerState + 1, FLOWER_ACTIONS.length - 1); + action = FLOWER_ACTIONS[newFlowerState]; + break; + case "decay": + newFlowerState = Math.max(this.flowerState - 1, 0); + action = FLOWER_ACTIONS[newFlowerState]; + break; + case "closed": + newFlowerState = 2; + break; + case "begin": + case "end": + newFlowerState = 2; + break; + } + const data = { + action, + newFlowerState, + sound: event.detail.action + }; + this.doAnimation(data); + if (window.NAF.connection.isConnected()) { + window.NAF.connection.broadcastDataGuaranteed(UNSEEN_CHANNEL, data); + } + }, + doAnimation({ action, newFlowerState, sound }) { + console.log("doAnimation", { action, newFlowerState, sound }); + let transitionNeeded = false; + const performerVideo = this.getVideoEl(); + this.playSceneSound(`${sound}-sound`); + // blur is an independent toggle + if ((action === "blur" || action === "unblur") && performerVideo) { + this.blurState = action === "blur"; + const start = this.blurState ? 0.0001 : 0.2; + const end = this.blurState ? 0.2 : 0.0001; + performerVideo.setAttribute( + "animation", + `property: components.media-video.mesh.material.uniforms.blurWidth.value; from: ${start}; to: ${end}; dur: ${TRANSITION_DUR}; easing: easeInOutQuad;` + ); + return; + } + + if (action === "begin") { + // return to initial flower state + action = "closed"; + if (performerVideo) { + performerVideo.object3D.visible = true; + } + } else if (action === "end" && performerVideo) { + performerVideo.object3D.visible = false; + } + + const toHide = FLOWER_ACTIONS[this.flowerState]; + if (action === this.lastAction) { + // ignore repeats to avoid disappearing the flowers + return; + } + + for (const piece of this.sceneEl.querySelectorAll("#environment-scene a-entity")) { + if (!piece.className.startsWith(action)) { + continue; + } + transitionNeeded = true; + // make sure there's no flash of fully opaque flower at the start + window.setTimeout(() => piece.setAttribute("visible", true), 250); + piece.setAttribute( + "animation", + `property: model-relative-opacity.opacityFactor; from: 0; to: 1; dur: ${TRANSITION_DUR}; easing: easeOutQuad;` + ); + // updating render order to avoid sudden jumps in transition + // start drawing the outgoing flower first and then switch at the midpoint + piece.object3D.traverse(o => { + if (o.isMesh) { + o.renderOrder = -1; + window.setTimeout(() => { + o.renderOrder = -5; + }, TRANSITION_DUR / 2); + } + }); + } + + if (toHide && toHide !== action) { + transitionNeeded = true; + for (const piece of this.sceneEl.querySelectorAll("#environment-scene a-entity")) { + if (!piece.className.startsWith(toHide)) { + continue; + } + window.setTimeout(() => piece.setAttribute("visible", false), TRANSITION_DUR); + piece.setAttribute( + "animation", + `property: model-relative-opacity.opacityFactor; from: 1; to: 0; dur: ${TRANSITION_DUR}; easing: easeInQuad;` + ); + piece.object3D.traverse(o => { + if (o.isMesh) { + o.renderOrder = -5; + window.setTimeout(() => { + o.renderOrder = -1; + }, TRANSITION_DUR / 2); + } + }); + } + } + + this.lastAction = action; + this.flowerState = newFlowerState; + if (transitionNeeded) { + this.isTransitioning = true; + window.setTimeout(() => { + this.isTransitioning = false; + }, TRANSITION_DUR); + } + }, + setupNetwork() { + window.NAF.connection.subscribeToDataChannel(UNSEEN_CHANNEL, (_, dataType, data) => { + this.doAnimation(data); + }); + }, + playSceneSound(objectName) { + try { + const mediaNode = document.querySelector("#environment-root").object3D.getObjectByName(objectName); + if (mediaNode) { + mediaNode.el.components["media-video"].video.play(); + } else { + console.log(`No sound object found for ${objectName}`); + } + } catch (err) { + console.error("Error playing sound: ", err.message); + } + }, + onSceneLoaded() { + this.performerFrameEl = document.querySelector("[media-frame]"); + // draw flowers before cube to avoid transparency issues + FLOWER_ACTIONS.map(flowerType => { + for (const flower of this.sceneEl.querySelectorAll(`.${flowerType}`)) { + flower.object3D.traverse(o => { + if (o.isMesh) { + o.renderOrder = -5; + } + }); + } + }); + }, + getVideoEl() { + const id = this.performerFrameEl?.getAttribute("media-frame").targetId; + return id && document.getElementById(id); + } +}); diff --git a/src/components/unseen/chroma-key-material.js b/src/components/unseen/chroma-key-material.js new file mode 100644 index 0000000000..aa14a55615 --- /dev/null +++ b/src/components/unseen/chroma-key-material.js @@ -0,0 +1,63 @@ +const vertexShader = [ + "varying vec2 vUv;", + "void main(void)", + "{", + "vUv = uv;", + "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", + "gl_Position = projectionMatrix * mvPosition;", + "}" +].join("\n"); + +const fragmentShader = ` + uniform sampler2D map; + uniform vec3 chroma; + uniform int samples; + uniform float blurWidth; + uniform float period; + uniform float threshold; + varying vec2 vUv; + + void main(void) + { + vec3 tColor; + vec2 pos = vec2(0, 0); + for( float i=0.0;i { + this.prepareMap(); + this.update(); + }); + }, + prepareMap: function() { + this.traverseMesh(node => { + this.nodeMap[node.uuid] = node.material.opacity; + }); + this.mapIsPrepared = true; + }, + update: function() { + this.traverseMesh(node => { + node.material.opacity = /*this.nodeMap[node.uuid] * */ this.data.opacityFactor; + node.material.transparent = node.material.opacity < 1.0; + node.material.needsUpdate = true; + }); + }, + remove: function() { + this.traverseMesh(node => { + node.material.opacity = 1; //this.nodeMap[node.uuid]; + node.material.transparent = node.material.opacity < 1.0; + node.material.needsUpdate = true; + }); + }, + traverseMesh: function(func) { + const mesh = this.el.object3D; + if (!mesh) { + return; + } + mesh.traverse(node => { + if (node.isMesh) { + func(node); + } + }); + } +}); diff --git a/src/components/video-texture-target.js b/src/components/video-texture-target.js index 9fe2c0a533..cafc7be023 100644 --- a/src/components/video-texture-target.js +++ b/src/components/video-texture-target.js @@ -81,6 +81,30 @@ AFRAME.registerComponent("video-texture-source", { } }); +const vertexShader = [ + "varying vec2 vUv;", + "void main(void)", + "{", + "vUv = uv;", + "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", + "gl_Position = projectionMatrix * mvPosition;", + "}" +].join("\n"); + +const fragmentShader = [ + "uniform sampler2D map;", + "uniform vec3 chroma;", + "varying vec2 vUv;", + "void main(void)", + "{", + "vec3 tColor = texture2D( map, vUv ).rgb;", + "float a = length(tColor - chroma);", + // 20% similar (0.2 * 1.732) + "if (a < 0.34) discard;", + "gl_FragColor = vec4(tColor, 1);", + "}" +].join("\n"); + /** * @component video-texture-target * This component is intended to be used on entities with a mesh/skinned mesh Object3D @@ -105,6 +129,16 @@ AFRAME.registerComponent("video-texture-target", { ); }, + setMaterial(material) { + if (this.el.object3DMap.skinnedmesh) { + this.el.object3DMap.skinnedmesh.material = material; + } else if (this.el.object3DMap.mesh) { + this.el.object3DMap.mesh.material = material; + } else { + this.el.object3D.material = material; + } + }, + init() { const material = this.getMaterial(); @@ -164,9 +198,33 @@ AFRAME.registerComponent("video-texture-target", { const texture = new THREE.VideoTexture(video); texture.flipY = false; texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; texture.encoding = THREE.sRGBEncoding; - this.applyTexture(texture); + const newMaterial = new THREE.ShaderMaterial({ + uniforms: { + chroma: { + value: { x: 0, y: 0, z: 0 } + }, + map: { + value: texture + } + }, + vertexShader, + fragmentShader, + transparent: false + }); + + // if (this.data.targetBaseColorMap) { + // material.map = texture; + // } + + // if (this.data.targetEmissiveMap) { + // material.emissiveMap = texture; + // } + this.setMaterial(newMaterial); + // material.needsUpdate = true; + // this.applyTexture(texture); }); } else { if (material.map && material.map !== this.originalMap) { diff --git a/src/hub.html b/src/hub.html index b35cde868c..549f60cd5f 100644 --- a/src/hub.html +++ b/src/hub.html @@ -186,8 +186,10 @@ - - + + + + @@ -1247,6 +1249,24 @@ class="hud camera-btn" material="alphaTest:0.1;" > + diff --git a/src/hub.js b/src/hub.js index 4aa6e4c204..8db6e418cd 100644 --- a/src/hub.js +++ b/src/hub.js @@ -160,6 +160,8 @@ import "./systems/capture-system"; import "./systems/listed-media"; import "./systems/linked-media"; import { SOUND_CHAT_MESSAGE } from "./systems/sound-effects-system"; +import "./components/unseen/animation-sync"; +import "./components/unseen/model-relative-opacity"; import "./gltf-component-mappings"; diff --git a/src/message-dispatch.js b/src/message-dispatch.js index 1da3bc008e..d3f5189b20 100644 --- a/src/message-dispatch.js +++ b/src/message-dispatch.js @@ -182,6 +182,24 @@ export default class MessageDispatch extends EventTarget { } } break; + case "bloom": + this.scene.emit("unseen::animation", { action: "bloom" }); + break; + case "decay": + this.scene.emit("unseen::animation", { action: "decay" }); + break; + case "closed": + this.scene.emit("unseen::animation", { action: "closed" }); + break; + case "blur": + this.scene.emit("unseen::animation", { action: "blur" }); + break; + case "begin": + this.scene.emit("unseen::animation", { action: "begin" }); + break; + case "end": + this.scene.emit("unseen::animation", { action: "end" }); + break; } }; } diff --git a/src/react-components/room/ImmersFeedSidebarContainer.js b/src/react-components/room/ImmersFeedSidebarContainer.js index 7694da1f71..baa8b75a56 100644 --- a/src/react-components/room/ImmersFeedSidebarContainer.js +++ b/src/react-components/room/ImmersFeedSidebarContainer.js @@ -259,3 +259,17 @@ export function ImmersFeedToolbarButtonContainer(props) { /> ); } + +export function ImmersRegisterToolbarButtonContainer(props) { + return ( + } + preset="basic" + small + title="Register your Immers Space account to save your avatar and make friends" + label={} + /> + ); +} diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js index c1c9333730..f9afa4333c 100644 --- a/src/react-components/room/ImmersReact.js +++ b/src/react-components/room/ImmersReact.js @@ -4,11 +4,15 @@ import classNames from "classnames"; import { getMessageComponent } from "./ChatSidebar"; import chatStyles from "./ChatSidebar.scss"; import styles from "./ImmersReact.scss"; -import { FormattedRelativeTime } from "react-intl"; +import { FormattedMessage, FormattedRelativeTime } from "react-intl"; import { proxiedUrlFor } from "../../utils/media-url-utils"; import immersLogo from "../../assets/images/immers_logo.png"; import merge from "deepmerge"; import { ImmersFeedContext } from "./ImmersFeedSidebarContainer"; +import { Modal } from "../modal/Modal"; +import { Button } from "../input/Button"; +import { Column } from "../layout/Column"; +import { CloseButton } from "../input/CloseButton"; function proxyAndGetMessageComponent(message) { // media urls need proxy to pass CSP & CORS @@ -187,3 +191,38 @@ export function ImmersPermissionUpgradeButton({ role }) { ImmersPermissionUpgradeButton.propTypes = { role: PropTypes.string }; + +export function ImmersClaimAccountModal({ scene, startImmersAuth, onClose }) { + const onAuthDone = ({ detail }) => { + if (detail === "immers-authorizing") { + scene.removeEventListener("stateremoved", onAuthDone); + onClose(); + } + }; + const handleClick = event => { + scene.addEventListener("stateremoved", onAuthDone); + startImmersAuth(event); + }; + return ( + }> + +

+ Create an account that you can use to login anywhere in the Immers Space metaverse. You'll save your current + avatar and be able to start adding friends. +

+ +
+
+ ); +} + +ImmersClaimAccountModal.propTypes = { + scene: PropTypes.object, + startImmersAuth: PropTypes.func, + onClose: PropTypes.func +}; diff --git a/src/react-components/room/RoomEntryModal.js b/src/react-components/room/RoomEntryModal.js index 961bdfd3f6..5c3959a42f 100644 --- a/src/react-components/room/RoomEntryModal.js +++ b/src/react-components/room/RoomEntryModal.js @@ -21,7 +21,9 @@ export function RoomEntryModal({ className, roomName, showLoginToImmers, + showGuestEntry, onLoginToImmers, + onGuestEntry, showJoinRoom, onJoinRoom, showEnterOnDevice, @@ -52,7 +54,7 @@ export function RoomEntryModal({

{roomName}

- {showLoginToImmers && ( + {showLoginToImmers ? ( <> - -

Login or create a free account to join this space

+ {showGuestEntry ? ( + + ) : ( + <> + +

Login or create a free account to join this space

+ + )} + + ) : ( + <> + {showJoinRoom && ( + + )} + {showEnterOnDevice && ( + + )} - )} - {showJoinRoom && ( - - )} - {showEnterOnDevice && ( - )} {showSpectate && ( )} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 546b543b86..b28984c26d 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -96,8 +96,10 @@ import { SignInMessages } from "./auth/SignInModal"; import { ImmersFeedContextProvider, ImmersFeedSidebarContainer, - ImmersFeedToolbarButtonContainer + ImmersFeedToolbarButtonContainer, + ImmersRegisterToolbarButtonContainer } from "./room/ImmersFeedSidebarContainer"; +import { ImmersClaimAccountModal } from "./room/ImmersReact"; const avatarEditorDebug = qsTruthy("avatarEditorDebug"); @@ -210,6 +212,8 @@ class UIRoot extends Component { videoShareMediaSource: null, showVideoShareFailed: false, + guestEntry: false, + objectInfo: null, objectSrc: "", sidebarId: null, @@ -381,6 +385,7 @@ class UIRoot extends Component { this.playerRig = scene.querySelector("#avatar-rig"); scene.addEventListener("action_media_tweet", this.onTweet); + scene.addEventListener("action_immers_register", this.onImmersRegister); } UNSAFE_componentWillMount() { @@ -394,6 +399,7 @@ class UIRoot extends Component { this.props.scene.removeEventListener("share_video_disabled", this.onShareVideoDisabled); this.props.scene.removeEventListener("share_video_failed", this.onShareVideoFailed); this.props.scene.removeEventListener("action_media_tweet", this.onTweet); + this.props.scene.removeEventListener("action_immers_register", this.onImmersRegister); this.props.store.removeEventListener("statechanged", this.storeUpdated); window.removeEventListener("concurrentload", this.onConcurrentLoad); window.removeEventListener("idle_detected", this.onIdleDetected); @@ -750,6 +756,19 @@ class UIRoot extends Component { }); }; + showImmersRegister = () => { + this.showNonHistoriedDialog(ImmersClaimAccountModal, { + startImmersAuth: this.props.startImmersAuth, + scene: this.props.scene + }); + }; + + onImmersRegister = () => { + handleExitTo2DInterstitial(true, () => {}).then(() => { + this.showImmersRegister(); + }); + }; + onChangeScene = () => { this.props.performConditionalSignIn( () => this.props.hubChannel.can("update_hub"), @@ -808,9 +827,12 @@ class UIRoot extends Component { const { hasAcceptedProfile, hasChangedName } = this.props.store.state.activity; const promptForNameAndAvatarBeforeEntry = this.props.hubIsBound ? !hasAcceptedProfile : !hasChangedName; const pageIsMonetized = !!document.querySelector("meta[name=monetization]"); - const showLogin = !this.props.isImmersConnected; + const showLogin = !this.props.isImmersConnected && !this.state.guestEntry; + const showGuestEntry = configs.IMMERS_ALLOW_GUESTS !== "false"; // monetized users can bypass room limit - const canEnter = this.props.isImmersConnected && (!this.props.entryDisallowed || this.props.isMonetized); + const canEnter = !this.props.entryDisallowed || !!this.props.isMonetized; + // only show when joining is not possible to reduce number of choices shown + const canSpectate = !showLogin && !canEnter; // TODO: What does onEnteringCanceled do? return ( <> @@ -820,6 +842,8 @@ class UIRoot extends Component { roomName={this.props.hub.name} showLoginToImmers={showLogin} onLoginToImmers={this.props.startImmersAuth} + showGuestEntry={showGuestEntry} + onGuestEntry={() => this.setState({ guestEntry: !this.state.guestEntry })} showJoinRoom={!this.state.waitingOnAudio && canEnter} onJoinRoom={() => { if (promptForNameAndAvatarBeforeEntry || !this.props.forcedVREntryType) { @@ -838,7 +862,7 @@ class UIRoot extends Component { }} showEnterOnDevice={!this.state.waitingOnAudio && canEnter && !isMobileVR} onEnterOnDevice={() => this.attemptLink()} - showSpectate={false} + showSpectate={!this.state.waitingOnAudio && canSpectate} onSpectate={() => this.setState({ watching: true })} showOptions={this.props.hubChannel.canOrWillIfCreator("update_hub")} onOptions={() => { @@ -1591,8 +1615,10 @@ class UIRoot extends Component { )} this.toggleSidebar("chat")} /> - {this.props.isImmersConnected && ( + {this.props.isImmersConnected ? ( this.toggleSidebar("feed")} /> + ) : ( + )} {entered && isMobileVR && ( diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js index ec78672a56..71ca6540cc 100644 --- a/src/scene-entry-manager.js +++ b/src/scene-entry-manager.js @@ -172,7 +172,11 @@ export default class SceneEntryManager { _setPlayerInfoFromProfile = async (force = false) => { const avatarId = this.store.state.profile.avatarId; const immersId = this.store.state.profile.id; - if (!force && this._lastFetchedAvatarId === avatarId) return; // Avoid continually refetching based upon state changing + if (!force && this._lastFetchedAvatarId === avatarId) { + // share immersId with room if registered after join + this.avatarRig.setAttribute("player-info", { immersId }); + return; // Avoid continually refetching avatar based upon state changing + } this._lastFetchedAvatarId = avatarId; const avatarSrc = await getAvatarSrc(avatarId); diff --git a/src/storage/store.js b/src/storage/store.js index 501ad26424..080bf128c8 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -246,7 +246,7 @@ export default class Store extends EventTarget { creatorAssignmentTokens: [], embedTokens: [], onLoadActions: [], - preferences: {} + preferences: { onlyShowNametagsInFreeze: true } }); // Temporary fix for distorted audio in Safari. diff --git a/src/systems/character-controller-system.js b/src/systems/character-controller-system.js index 5f720e9f6a..814d1a87ed 100644 --- a/src/systems/character-controller-system.js +++ b/src/systems/character-controller-system.js @@ -231,6 +231,24 @@ export class CharacterControllerSystem { } const userinput = AFRAME.scenes[0].systems.userinput; + if (userinput.get(paths.actions.unseenBloom)) { + this.avatarRig.messageDispatch.dispatch("/bloom"); + } + if (userinput.get(paths.actions.unseenDecay)) { + this.avatarRig.messageDispatch.dispatch("/decay"); + } + if (userinput.get(paths.actions.unseenClosed)) { + this.avatarRig.messageDispatch.dispatch("/closed"); + } + if (userinput.get(paths.actions.unseenBlur)) { + this.avatarRig.messageDispatch.dispatch("/blur"); + } + if (userinput.get(paths.actions.unseenBegin)) { + this.avatarRig.messageDispatch.dispatch("/begin"); + } + if (userinput.get(paths.actions.unseenEnd)) { + this.avatarRig.messageDispatch.dispatch("/end"); + } const wasFlying = this.fly; if (userinput.get(paths.actions.toggleFly)) { this.shouldLandWhenPossible = false; diff --git a/src/systems/exit-on-blur.js b/src/systems/exit-on-blur.js index 15a37be4c5..70b2513367 100644 --- a/src/systems/exit-on-blur.js +++ b/src/systems/exit-on-blur.js @@ -29,6 +29,7 @@ AFRAME.registerSystem("exit-on-blur", { if ( this.isOculusBrowser && this.enteredVR && + !this.el.is("immers-authorizing") && (this.lastTimeoutCheck === 0 || t - this.lastTimeoutCheck >= 1000.0) // Don't do this clear every frame, slow. ) { this.lastTimeoutCheck = t; diff --git a/src/systems/userinput/bindings/keyboard-mouse-user.js b/src/systems/userinput/bindings/keyboard-mouse-user.js index 50d5eaff6d..ee179172fb 100644 --- a/src/systems/userinput/bindings/keyboard-mouse-user.js +++ b/src/systems/userinput/bindings/keyboard-mouse-user.js @@ -102,6 +102,36 @@ export const keyboardMouseUserBindings = addSetsToBindings({ dest: { value: paths.actions.toggleFly }, xform: xforms.rising }, + { + src: { value: paths.device.keyboard.key("[") }, + dest: { value: paths.actions.unseenBloom }, + xform: xforms.rising + }, + { + src: { value: paths.device.keyboard.key("]") }, + dest: { value: paths.actions.unseenDecay }, + xform: xforms.rising + }, + { + src: { value: paths.device.keyboard.key("\\") }, + dest: { value: paths.actions.unseenClosed }, + xform: xforms.rising + }, + { + src: { value: paths.device.keyboard.key("*") }, + dest: { value: paths.actions.unseenBlur }, + xform: xforms.rising + }, + { + src: { value: paths.device.keyboard.key("b") }, + dest: { value: paths.actions.unseenBegin }, + xform: xforms.rising + }, + { + src: { value: paths.device.keyboard.key("n") }, + dest: { value: paths.actions.unseenEnd }, + xform: xforms.rising + }, { src: { value: paths.device.keyboard.key("`") }, dest: { value: paths.actions.toggleUI }, diff --git a/src/systems/userinput/paths.js b/src/systems/userinput/paths.js index e44107e92d..74434fa69b 100644 --- a/src/systems/userinput/paths.js +++ b/src/systems/userinput/paths.js @@ -129,6 +129,13 @@ paths.actions.spawnEmoji3 = "/actions/spawnEmoji3"; paths.actions.spawnEmoji4 = "/actions/spawnEmoji4"; paths.actions.spawnEmoji5 = "/actions/spawnEmoji5"; paths.actions.spawnEmoji6 = "/actions/spawnEmoji6"; +paths.actions.unseenBloom = "/actions/unseenBloom"; +paths.actions.unseenDecay = "/actions/unseenDecay"; +paths.actions.unseenClosed = "/actions/unseenClosed"; +paths.actions.unseenBlur = "/actions/unseenBlur"; +paths.actions.unseenBegin = "/actions/unseenBegin"; +paths.actions.unseenEnd = "/actions/unseenEnd"; + paths.haptics = {}; paths.haptics.actuators = {}; paths.haptics.actuators.left = "/haptics/actuators/left"; diff --git a/src/utils/configs.js b/src/utils/configs.js index f69e795e1a..9cac618092 100644 --- a/src/utils/configs.js +++ b/src/utils/configs.js @@ -18,6 +18,7 @@ let isAdmin = false; "SHORTLINK_DOMAIN", "IMMERS_SERVER", "IMMERS_SCOPE", + "IMMERS_ALLOW_GUESTS", "BASE_ASSETS_PATH" ].forEach(x => { const el = document.querySelector(`meta[name='env:${x.toLowerCase()}']`); diff --git a/src/utils/immers.js b/src/utils/immers.js index c96ac857db..fe327ddd7d 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -288,21 +288,31 @@ export async function auth(store, scope) { response_type: "token", scope: scope || preferredScope }); + if (evt.currentTarget.classList.contains("registration")) { + redirectParams.set("tab", "Register"); + if (store.state.activity.hasAcceptedProfile) { + // if registering after joining, pre-fill form with current name + redirectParams.set("me", `${store.state.profile.displayName}[${localImmer}]`); + } + } // users handle may be passed from previous immer or cached but with expired token if (handle || store.state.profile.handle) { // pass to auth to prefill login form redirectParams.set("me", handle || store.state.profile.handle); } - if (evt.currentTarget.classList.contains("registration")) { - redirectParams.set("tab", "Register"); - } + redirect.search = redirectParams.toString(); popup = window.open(redirect, "immersLoginPopup", features); if (!popup) { alert("Could not open login window. Please check if popup was blocked and allow it"); } else { - hubScene?.addState("immers-authorizing"); - popup.onunload = () => hubScene?.removeState("immers-authorizing"); + hubScene.addState("immers-authorizing"); + const closedCheckInterval = window.setInterval(() => { + if (popup.closed) { + window.clearInterval(closedCheckInterval); + hubScene.removeState("immers-authorizing"); + } + }, 100); } } }; @@ -454,7 +464,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat } }); - scene.addEventListener("avatar_updated", async () => { + const updateProfileNameAndAvi = async () => { const profile = store.state.profile; const update = {}; // disable the first-time entry name & avatar prompt @@ -479,7 +489,12 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat // update cached copy of profile Object.assign(actorObj, update); } - }); + }; + scene.addEventListener("avatar_updated", updateProfileNameAndAvi); + // registered after joining, save selected avatar & name to profile + if (!actorAvi && store.state.activity.hasAcceptedProfile) { + updateProfileNameAndAvi(); + } // entity interactions scene.addEventListener("immers-id-changed", event => setFriendState(event.detail, event.target)); @@ -580,6 +595,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat .catch(err => console.error(`Error sharing chat: ${err.message}`)); }); const immersReAuth = scope => resetAuth(store, remountUI, scope); + hubScene.addState("immers-connected"); remountUI({ immersMessageDispatch, immersScopes: authorizedScopes, isImmersConnected: true, immersReAuth }); } } diff --git a/webpack.config.js b/webpack.config.js index 9478c5d050..3359042417 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -634,6 +634,7 @@ module.exports = async (env, argv) => { POSTGREST_SERVER: process.env.POSTGREST_SERVER, IMMERS_SERVER: process.env.IMMERS_SERVER, IMMERS_SCOPE: process.env.IMMERS_SCOPE, + IMMERS_ALLOW_GUESTS: process.env.IMMERS_ALLOW_GUESTS, APP_CONFIG: appConfig }) })