Three.js post-processing(后处理)
后处理就是对WebGLRenderer.render(scene, camera)的渲染2D图片进行处理。可以把多个后处理进行组合,按照顺序执行,每个处理过程,被称为Pass。
通过EffectComposer创建后处理pipeline
Three.js提供了构建后处理pipeline的工具,EffectComposer。可以通过EffectComposer.add(pass),将多个Pass以此添加到EffectComposer。
之后,调用EffectComposer.render()就可以启动pipeline,依次使用Pass进行处理。
既然后处理是对场景渲染后的图片进行处理,那么,后处理的起点就是渲染场景,即renderer.render(),这个渲染过程也被封装成一个Pass,RenderPass。
也就是说,一个后处理pipeline的第一个Pass,是RenderPass。
大致的使用流程如下:
// 创建composer
function createRenderPipeline(renderer: WebGLRenderer, scene: Scene, camera: Camera) {
const composer = new EffectComposer(renderer)
const renderPass = new RenderPass()
composer.add(renderPass)
const pass1 = new XXXPass()
composer.add(pass1)
const pass2 = new YYYPass()
composer.add(pass2)
...
return composer
}
// 在动画循环中调用composer.render
function animate() {
const dt = clock.getDelta()
composer.render(dt)
}
EffectComposer原理及源码
在EffectComposer中,有两个缓冲区,分别是readBuffer和writeBuffer。
EffectComposer会按照顺序依次调用Pass的render方法,并且将readBuffer和writeBuffer交给pass。
每个pass的职责就是从readBuffer中读取上一个Pass的处理结果,处理之后,放入readBuffer或者writeBuffer。
因为下一个Pass是从readBuffer读取,所以,如果这个Pass将处理结果放到writeBuffer,就需要设置pass.needsSwap = true,这样,EffectComposer就会在这一次处理后,将两个缓冲区进行交换,这样,readBuffer就指向了处理的结果,下一个Pass仍然从readBuffer读取。
// EffectComposer.js
swapBuffers() {
const tmp = this.readBuffer;
this.readBuffer = this.writeBuffer;
this.writeBuffer = tmp;
}
总结一下就是,在自定义一个Pass时,如果处理后的结果仍然写到readBuffer,那么pass.needsSwap设置为false;如果处理后的结果写到writeBuffer,就设置pass.needsSwap为true。
我们到代码里看一下:
这段代码进行了精简,删掉了对于MaskPass的特殊处理,不影响整体流程
// EffectComposer.js
// class EffectComposer
render(deltaTime) {
// deltaTime value is in seconds
if (deltaTime === undefined) {
deltaTime = this.clock.getDelta();
}
// 记录renderer当前的target,之后需要恢复
const currentRenderTarget = this.renderer.getRenderTarget();
// 遍历所有pass 按顺序执行
for (let i = 0, il = this.passes.length; i < il; i++) {
const pass = this.passes[i];
// 跳过enabled === false的pass
if (pass.enabled === false) continue;
// 是否渲染到屏幕情况
// 1. composer.renderToScreen === true
// 2. 如果是最后一个enabled的pass
pass.renderToScreen = (this.renderToScreen && this.isLastEnabledPass(i));
// 将缓冲区、renderer传递给pass进行处理
pass.render(this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive);
// 如果pass将结果写在了writeBuffer 就会将needsSwap置为true
if (pass.needsSwap) {
this.swapBuffers();
}
}
// 恢复之前记录的renderTarget
this.renderer.setRenderTarget(currentRenderTarget);
}
还有最后一个问题,怎么渲染到屏幕的?通过上面的代码,可以看到,composer会在调用pass.render之前,给pass设置一个renderToScreen的标志。pass在render方法中判断是否要渲染到屏幕。看一下RenderPass的代码:同样进行了精简
constructor( scene, camera, overrideMaterial, clearColor, clearAlpha ) {
super();
// 结果直接写道readBuffer
this.needsSwap = false;
//...
}
render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) {
// 处理是否渲染到屏幕
// 如果不渲染到屏幕 就渲染到readBuffer
renderer.setRenderTarget(this.renderToScreen ? null : readBuffer);
if (this.clear) renderer.clear(renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil);
renderer.render(this.scene, this.camera);
}
对后处理结果进行截图
如果没有使用后处理,直接使用renderer,可以通过渲染到renderTarget的方式进行截图。但是添加后处理之后,怎么截图呢?最后一个Pass直接将结果渲染到“屏幕”,没有办法通过readBuffer拿到最后的渲染图片。
再回头看一下EffectComposer.render()中,对于渲染到屏幕相关的代码:
pass.renderToScreen = (this.renderToScreen && this.isLastEnabledPass(i));
只要将composer.renderToScreen置为false,这样,就不会渲染到屏幕啦!最终的渲染结果会保留在readBuffer中,readBuffer是一个RenderTarget类的实例。之后的步骤就不再赘述,如果对截图功能感兴趣,可以看
WebGLRenderTarget与截图功能 - 掘金 (juejin.cn)