Skip to content

Commit

Permalink
Merge pull request #450 from mozilla/enhancement/locomotion
Browse files Browse the repository at this point in the history
Tunnel Vision
  • Loading branch information
tony056 authored Sep 7, 2018
2 parents 5cea39d + b52512d commit e875207
Show file tree
Hide file tree
Showing 11 changed files with 604 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ This will allow the CSP checks to pass that are served up by Reticulum so you ca
- `disable_telemetry` - If `true` disables Sentry telemetry.
- `log_filter` - A `debug` style filter for setting the logging level.
- `debug` - If `true` performs verbose logging of Janus and NAF traffic.
- `disableTunnel` - Tunnel vision is on by default. Disable the tunnel vision by this parameter.

## Additional Resources

Expand Down
1 change: 1 addition & 0 deletions src/hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import "./components/cardboard-controls";
import "./components/cursor-controller";

import "./components/nav-mesh-helper";
import "./systems/tunnel-effect";

import "./components/tools/pen";
import "./components/tools/networked-drawing";
Expand Down
163 changes: 163 additions & 0 deletions src/systems/tunnel-effect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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.25, 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() {
this.scene.renderer.render = this.postProcessingRenderFunc;
}
});
167 changes: 167 additions & 0 deletions src/utils/postprocessing/EffectComposer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
THREE.EffectComposer = function(renderer, renderTarget) {
this.renderer = renderer;
this.delta = 0;
window.addEventListener("vrdisplaypresentchange", this.resize.bind(this));

if (renderTarget === undefined) {
const parameters = {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
stencilBuffer: false
};

const size = renderer.getDrawingBufferSize();
renderTarget = new THREE.WebGLRenderTarget(size.width, size.height, parameters);
renderTarget.texture.name = "EffectComposer.rt1";
}

this.renderTarget1 = renderTarget;
this.renderTarget2 = renderTarget.clone();
this.renderTarget2.texture.name = "EffectComposer.rt2";

this.writeBuffer = this.renderTarget1;
this.readBuffer = this.renderTarget2;

this.passes = [];
this.maskActive = false;

// dependencies

if (THREE.CopyShader === undefined) {
console.error("THREE.EffectComposer relies on THREE.CopyShader");
}

if (THREE.ShaderPass === undefined) {
console.error("THREE.EffectComposer relies on THREE.ShaderPass");
}

this.copyPass = new THREE.ShaderPass(THREE.CopyShader);
};

Object.assign(THREE.EffectComposer.prototype, {
swapBuffers: function(pass) {
if (pass.needsSwap) {
if (this.maskActive) {
const context = this.renderer.context;
context.stencilFunc(context.NOTEQUAL, 1, 0xffffffff);
this.copyPass.render(this.renderer, this.writeBuffer, this.readBuffer, this.delta);
context.stencilFunc(context.EQUAL, 1, 0xffffffff);
}

const tmp = this.readBuffer;
this.readBuffer = this.writeBuffer;
this.writeBuffer = tmp;
}

if (THREE.MaskPass !== undefined) {
if (pass instanceof THREE.MaskPass) {
this.maskActive = true;
} else if (pass instanceof THREE.ClearMaskPass) {
this.maskActive = false;
}
}
},

addPass: function(pass) {
this.passes.push(pass);
const size = this.renderer.getDrawingBufferSize();
pass.setSize(size.width, size.height);
},

insertPass: function(pass, index) {
this.passes.splice(index, 0, pass);
},

render: function(delta, starti) {
const maskActive = this.maskActive;
let pass;
let i;
const il = this.passes.length;
const scope = this;
let currentOnAfterRender;
this.delta = delta;

for (i = starti || 0; i < il; i++) {
pass = this.passes[i];
if (pass.enabled === false) continue;

// If VR mode is enabled and rendering the whole scene is required.
// The pass renders the scene and and postprocessing is resumed before
// submitting the frame to the headset by using the onAfterRender callback.
if (this.renderer.vr.enabled && pass.scene) {
currentOnAfterRender = pass.scene.onAfterRender;
pass.scene.onAfterRender = function() {
// Disable stereo rendering when doing postprocessing
// on a render target.
scope.renderer.vr.enabled = false;
scope.render(delta, i + 1, maskActive);

// Renable vr mode.
scope.renderer.vr.enabled = true;
};

pass.render(this.renderer, this.writeBuffer, this.readBuffer);

// Restore onAfterRender
pass.scene.onAfterRender = currentOnAfterRender;
this.swapBuffers(pass);
return;
}

pass.render(this.renderer, this.writeBuffer, this.readBuffer);
this.swapBuffers(pass);
}
},

reset: function(renderTarget) {
if (renderTarget === undefined) {
const size = this.renderer.getDrawingBufferSize();
renderTarget = this.renderTarget1.clone();
renderTarget.setSize(size.width, size.height);
}

this.renderTarget1.dispose();
this.renderTarget2.dispose();
this.renderTarget1 = renderTarget;
this.renderTarget2 = renderTarget.clone();

this.writeBuffer = this.renderTarget1;
this.readBuffer = this.renderTarget2;
},

setSize: function(width, height) {
this.renderTarget1.setSize(width, height);
this.renderTarget2.setSize(width, height);
for (let i = 0; i < this.passes.length; i++) {
this.passes[i].setSize(width, height);
}
},

resize: function() {
const rendererSize = this.renderer.getDrawingBufferSize();
this.setSize(rendererSize.width, rendererSize.height);
}
});

THREE.Pass = function() {
// if set to true, the pass is processed by the composer
this.enabled = true;

// if set to true, the pass indicates to swap read and write buffer after rendering
this.needsSwap = true;

// if set to true, the pass clears its buffer before rendering
this.clear = false;

// if set to true, the result of the pass is rendered to screen
this.renderToScreen = false;
};

Object.assign(THREE.Pass.prototype, {
setSize: function() {},

render: function() {
console.error("THREE.Pass: .render() must be implemented in derived pass.");
}
});
Loading

0 comments on commit e875207

Please sign in to comment.