import "../utils/postprocessing/EffectComposer"; import "../utils/postprocessing/RenderPass"; import "../utils/postprocessing/ShaderPass"; import "../utils/postprocessing/MaskPass"; import "../utils/shaders/CopyShader"; import "../utils/shaders/VignetteShader"; import qsTruthy from "../utils/qs_truthy"; const disabledByQueryString = qsTruthy("disableTunnel"); const CLAMP_SPEED = 0.01; const CLAMP_RADIUS = 0.001; const NO_TUNNEL_RADIUS = 10.0; const NO_TUNNEL_SOFTNESS = 0.0; function lerp(start, end, t) { return (1 - t) * start + t * end; } function f(t) { const x = t - 1; return 1 + x * x * x * x * x; } AFRAME.registerSystem("tunneleffect", { schema: { targetComponent: { type: "string", default: "character-controller" }, radius: { type: "number", default: 1.0, min: 0.25 }, minRadius: { type: "number", default: 0.3, min: 0.1 }, maxSpeed: { type: "number", default: 0.5, min: 0.1 }, softest: { type: "number", default: 0.1, min: 0.0 }, opacity: { type: "number", default: 1, min: 0.0 } }, init: function() { this.scene = this.el; this.isMoving = false; this.isVR = false; this.dt = 0; this.isPostProcessingReady = false; this.characterEl = document.querySelector(`a-entity[${this.data.targetComponent}]`); if (this.characterEl) { this._initPostProcessing = this._initPostProcessing.bind(this); this.characterEl.addEventListener("componentinitialized", this._initPostProcessing); } else { console.warn("Could not find target component."); } this._enterVR = this._enterVR.bind(this); this._exitVR = this._exitVR.bind(this); this.scene.addEventListener("enter-vr", this._enterVR); this.scene.addEventListener("exit-vr", this._exitVR); }, pause: function() { if (!this.characterEl) { return; } this.characterEl.removeEventListener("componentinitialized", this._initPostProcessing); this.scene.removeEventListener("enter-vr", this._enterVR); this.scene.removeEventListener("exit-vr", this._exitVR); }, play: function() { this.scene.addEventListener("enter-vr", this._enterVR); this.scene.addEventListener("exit-vr", this._exitVR); }, tick: function(t, dt) { this.dt = dt; if (disabledByQueryString || !this.isPostProcessingReady || !this.isVR) { return; } const { maxSpeed, minRadius, softest } = this.data; const characterSpeed = this.characterComponent.velocity.length(); const shaderRadius = this.vignettePass.uniforms["radius"].value || NO_TUNNEL_RADIUS; if (!this.enabled && characterSpeed > CLAMP_SPEED) { this.enabled = true; this._bindRenderFunc(); } else if ( this.enabled && characterSpeed < CLAMP_SPEED && Math.abs(NO_TUNNEL_RADIUS - shaderRadius) < CLAMP_RADIUS ) { this.enabled = false; this._exitTunnel(); } if (this.enabled) { const clampedSpeed = characterSpeed > maxSpeed ? maxSpeed : characterSpeed; const speedRatio = clampedSpeed / maxSpeed; this.targetRadius = lerp(NO_TUNNEL_RADIUS, minRadius, f(speedRatio)); this.targetSoftness = lerp(NO_TUNNEL_SOFTNESS, softest, f(speedRatio)); this._updateVignettePass(this.targetRadius, this.targetSoftness, this.data.opacity); } }, _exitTunnel: function() { this.scene.renderer.render = this.originalRenderFunc; this.isMoving = false; }, _initPostProcessing: function(event) { if (event.detail.name === this.data.targetComponent) { this.characterEl.removeEventListener("componentinitialized", this._initPostProcessing); this.characterComponent = this.characterEl.components[this.data.targetComponent]; this._initComposer(); } }, _enterVR: function() { this.isVR = true; //TODO: This is called in 2D mode when you press "f", which is bad }, _exitVR: function() { this._exitTunnel(); this.isVR = false; }, _initComposer: function() { this.renderer = this.scene.renderer; this.camera = this.scene.camera; this.originalRenderFunc = this.scene.renderer.render; this.isDigest = false; const render = this.scene.renderer.render; const system = this; this.postProcessingRenderFunc = function() { if (system.isDigest) { render.apply(this, arguments); } else { system.isDigest = true; system.composer.render(system.dt); system.isDigest = false; } }; this.composer = new THREE.EffectComposer(this.renderer); this.composer.resize(); this.scenePass = new THREE.RenderPass(this.scene.object3D, this.camera); this.vignettePass = new THREE.ShaderPass(THREE.VignetteShader); this._updateVignettePass(this.data.radius, this.data.softness, this.data.opacity); this.composer.addPass(this.scenePass); this.composer.addPass(this.vignettePass); this.isPostProcessingReady = true; }, _updateVignettePass: function(radius, softness, opacity) { const { width, height } = this.renderer.getSize(); const pixelRatio = this.renderer.getPixelRatio(); this.vignettePass.uniforms["radius"].value = radius; this.vignettePass.uniforms["softness"].value = softness; this.vignettePass.uniforms["opacity"].value = opacity; this.vignettePass["resolution"] = new THREE.Uniform(new THREE.Vector2(width * pixelRatio, height * pixelRatio)); if (!this.vignettePass.renderToScreen) { this.vignettePass.renderToScreen = true; } }, /** * use the render func of the effect composer when we need the postprocessing */ _bindRenderFunc: function() { if (this.postProcessingRenderFunc) { this.scene.renderer.render = this.postProcessingRenderFunc; } } });