-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhancement: Tunnel Vision #450
Changes from 34 commits
99adaa7
0bd5a32
ea6f19e
aa824ac
682f89f
3b84442
661d0c0
9988451
c0d6d55
22bcc2f
c834cdf
6e7f751
690d0c5
6e56358
912139a
4bb5b0f
bdb4b36
009f802
76813ee
41e267e
25f0d9c
8e0f561
31914e6
a5eb72a
d3414d9
7f19a1d
8cdab8b
da5d4bc
d9c195d
c3f1976
08d4470
7a6090b
a8203ac
a304b8a
7b1fe60
487ae33
c2440b8
b52512d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
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 isDisabled = qsTruthy("disableTunnel"); | ||
const CLAMP_SPEED = 0.01; | ||
const CLAMP_RADIUS = 0.001; | ||
const CLAMP_SOFTNESS = 0.001; | ||
const FADE_TIMEOUT_MS = 300; | ||
const TARGET_RADIUS = 10.0; | ||
const TARGET_SOFTNESS = 0.0; | ||
|
||
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.7, min: 0.1 }, | ||
softness: { 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 (isDisabled || !this.isPostProcessingReady || !this.isVR) { | ||
return; | ||
} | ||
|
||
const characterSpeed = this.characterComponent.velocity.length(); | ||
if (characterSpeed < CLAMP_SPEED) { | ||
// the character stops, so we use the aframe default render func | ||
const r = this.vignettePass.uniforms["radius"].value; | ||
const softness = this.vignettePass.uniforms["softness"].value; | ||
if ( | ||
this.isMoving && | ||
Math.abs(r - TARGET_RADIUS) > CLAMP_RADIUS && | ||
Math.abs(softness - TARGET_SOFTNESS) > CLAMP_SOFTNESS | ||
) { | ||
if (!this.deltaR && !this.deltaS) { | ||
this.deltaR = TARGET_RADIUS - r; | ||
this.deltaS = softness - TARGET_SOFTNESS; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am still confused about why these are set once with some particular value for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the |
||
} | ||
const ratio = this.dt / FADE_TIMEOUT_MS; | ||
this._updateVignettePass(r + this.deltaR * ratio, softness - this.deltaS * ratio, this.data.opacity); | ||
} else { | ||
this._exitTunnel(); | ||
} | ||
return; | ||
} | ||
|
||
if (!this.isMoving) { | ||
this.isMoving = true; | ||
this._bindRenderFunc(); | ||
} | ||
const { radius, minRadius, maxSpeed, softness, opacity } = this.data; | ||
const r = (radius - minRadius) * (maxSpeed - characterSpeed) + minRadius; | ||
this._updateVignettePass(r, softness, 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately if you press the "f" key without a headset plugged in, the "entervr" event still fires. You go into full screen, but you're not actually in VR. If you press F11 you can go into fullscreen mode without going into VR, but we may want to fix this anyway because we don't mean to use the vignette pass when we're not in vr. |
||
}, | ||
|
||
_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; | ||
} | ||
}); |
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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to disable this effect when not in VR. |
||
|
||
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."); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On by default, then disabled with a query parameter will get you the most feedback. Add a note in README.md under the ##Query Params header.