-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #450 from mozilla/enhancement/locomotion
Tunnel Vision
- Loading branch information
Showing
11 changed files
with
604 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | ||
} | ||
}); |
Oops, something went wrong.