Three.js post-processing(后处理)

1,239 阅读3分钟

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(),这个渲染过程也被封装成一个PassRenderPass

也就是说,一个后处理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中,有两个缓冲区,分别是readBufferwriteBuffer

EffectComposer会按照顺序依次调用Passrender方法,并且将readBufferwriteBuffer交给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.needsSwaptrue

我们到代码里看一下: 这段代码进行了精简,删掉了对于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的标志。passrender方法中判断是否要渲染到屏幕。看一下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)