后期处理(Post Processing)通常是指对 2D 图像应用某种效果或滤镜。 在 THREE.js 中我们有一个包含一堆网格物体的场景。 我们将该场景渲染为 2D 图像。 通常,该图像会直接渲染到画布中并显示在浏览器中,但我们可以将其渲染到渲染目标,然后在将结果绘制到画布之前对结果应用一些后处理效果。 之所以称为后处理,是因为它发生在主场景处理之后(后)。
后期处理的例子有 Instagram 滤镜、Photoshop 滤镜等……
推荐:用 NSDT编辑器 快速搭建可编程3D场景
THREE.js 有一些示例类来帮助设置后处理管道。 它的工作方式是创建一个 EffectComposer 并向其中添加多个 Pass 对象。 然后,调用 EffectComposer.render,它将场景渲染到渲染目标,然后应用每个通道。
每个通道(Pass)都可以是一些后期处理效果,例如添加晕影、模糊、应用光晕、应用胶片颗粒、调整色调、饱和度、对比度等…最后将结果渲染到画布上。
了解 EffectComposer 的功能非常重要。 它创建两个渲染目标。 我们称它们为 rtA 和 rtB。
然后,调用 EffectComposer.addPass 按照你想要应用的顺序添加每个通道。 然后像这样应用通道。
首先,你传递到 RenderPass 的场景被渲染到 rtA,然后 rtA 被传递到下一个通道,无论它是什么(BloomPass、FilmPass…)。 该过程使用 rtA 作为输入来执行其操作并将结果写入 rtB。 然后 rtB 被传递到下一个通道,该通道使用 rtB 作为输入并写回 rtA。 这贯穿所有的通道。
每个通道有 4 个基本选项
让我们举一个基本的例子。 我们将从有关响应性的文章中的示例开始。
为此,我们首先创建一个 EffectComposer。
const composer = new EffectComposer(renderer);
然后,作为第一个通道,我们添加一个 RenderPass,它将使用相机将场景渲染到第一个渲染目标中。
composer.addPass(new RenderPass(scene, camera));
接下来我们添加一个 BloomPass。 BloomPass 将其输入渲染到通常较小的渲染目标并模糊结果。 然后,它将模糊结果添加到原始输入之上。 这使得场景具有绽放效果。
const bloomPass = new BloomPass(
1, // strength
25, // kernel size
4, // sigma ?
256, // blur render target resolution
);
composer.addPass(bloomPass);
最后,我们有一个 FilmPass,可以在其输入之上绘制噪声和扫描线。
const filmPass = new FilmPass(
0.35, // noise intensity
0.025, // scanline intensity
648, // scanline count
false, // grayscale
);
filmPass.renderToScreen = true;
composer.addPass(filmPass);
由于 filmPass 是最后一个通道,我们将其 renderToScreen 属性设置为 true 以告诉它渲染到画布。 如果不设置此项,它将渲染到下一个渲染目标。
要使用这些类,我们需要导入一堆脚本。
import {EffectComposer} from '/examples/jsm/postprocessing/EffectComposer.js';
import {RenderPass} from '/examples/jsm/postprocessing/RenderPass.js';
import {BloomPass} from '/examples/jsm/postprocessing/BloomPass.js';
import {FilmPass} from '/examples/jsm/postprocessing/FilmPass.js';
对于几乎任何后期处理,都需要 EffectComposer.js 和 RenderPass.js。
我们需要做的最后一件事是使用 EffectComposer.render 而不是 WebGLRenderer.render 并告诉 EffectComposer 匹配画布的大小。
-function render(now) {
- time *= 0.001;
+let then = 0;
+function render(now) {
+ now *= 0.001; // convert to seconds
+ const deltaTime = now - then;
+ then = now;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
+ composer.setSize(canvas.width, canvas.height);
}
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * .1;
- const rot = time * speed;
+ const rot = now * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
- renderer.render(scene, camera);
+ composer.render(deltaTime);
requestAnimationFrame(render);
}
EffectComposer.render 采用 deltaTime,它是自渲染最后一帧以来的时间(以秒为单位)。 它将它传递给各种效果,以防它们中的任何一个被动画化。 在本例中, FilmPass 是动画的。
要在运行时更改效果参数通常需要设置 uniform的值。 让我们添加一个 gui 来调整一些参数。 弄清楚可以轻松调整哪些值以及如何调整它们需要深入研究该效果的代码。
查看 BloomPass.js 内部,我发现了这一行:
this.copyUniforms[ "opacity" ].value = strength;
所以我们可以通过如下代码来设置强度:
bloomPass.copyUniforms.opacity.value = someValue;
同样地,在 FilmPass.js 中我发现了这些行:
if ( grayscale !== undefined ) this.uniforms.grayscale.value = grayscale;
if ( noiseIntensity !== undefined ) this.uniforms.nIntensity.value = noiseIntensity;
if ( scanlinesIntensity !== undefined ) this.uniforms.sIntensity.value = scanlinesIntensity;
if ( scanlinesCount !== undefined ) this.uniforms.sCount.value = scanlinesCount;
这样就很清楚如何设置它们了。
让我们制作一个快速 GUI 来设置这些值
import {GUI} from '/examples/jsm/libs/lil-gui.module.min.js';
const gui = new GUI();
{
const folder = gui.addFolder('BloomPass');
folder.add(bloomPass.copyUniforms.opacity, 'value', 0, 2).name('strength');
folder.open();
}
{
const folder = gui.addFolder('FilmPass');
folder.add(filmPass.uniforms.grayscale, 'value').name('grayscale');
folder.add(filmPass.uniforms.nIntensity, 'value', 0, 1).name('noise intensity');
folder.add(filmPass.uniforms.sIntensity, 'value', 0, 1).name('scanline intensity');
folder.add(filmPass.uniforms.sCount, 'value', 0, 1000).name('scanline count');
folder.open();
}
现在我们可以调整这些设置了。
这是实现我们自己的效果的一小步。
后期处理效果使用着色器(Shader)。 着色器是用一种称为 GLSL(图形库着色语言)的语言编写的。 对于这些文章来说,回顾整个语言是一个太大的主题。 一些可以开始使用的资源可能是这篇文章,也可能是《着色器之书》。
我认为一个帮助你入门的示例会很有帮助,所以让我们制作一个简单的 GLSL 后处理着色器。 我们将制作一个可以将图像乘以颜色的图像。
对于后期处理,THREE.js 提供了一个有用的助手,称为 ShaderPass。 它需要一个带有定义顶点着色器、片段着色器和默认输入信息的对象。 它将处理设置从哪个纹理读取以获取上一个通道的结果以及渲染到何处:渲染目标或屏幕画布。
这是一个简单的后处理着色器,它将前一个通道的结果乘以颜色。
const colorShader = {
uniforms: {
tDiffuse: { value: null },
color: { value: new THREE.Color(0x88CCFF) },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform vec3 color;
void main() {
vec4 previousPassColor = texture2D(tDiffuse, vUv);
gl_FragColor = vec4(
previousPassColor.rgb * color,
previousPassColor.a);
}
`,
};
上面的 tDiffuse 是 ShaderPass 用于传递前一个通道的结果纹理的名称,因此我们几乎总是需要它。 然后我们将颜色声明为 THREE.js的 Color 类型。
接下来我们需要一个顶点着色器。 对于后期处理,此处显示的顶点着色器几乎是标准的,很少需要更改。 无需赘述太多细节(请参阅上面链接的文章),变量 uv、 projectionMatrix、 modelViewMatrix 和 position 均由 THREE.js 神奇地添加。
最后我们创建一个片段着色器。 在其中,我们使用此行从上一次传递中获取像素颜色
vec4 previousPassColor =texture2D(tDiffuse, vUv);
我们将它乘以我们的颜色并将 gl_FragColor 设置为结果
gl_FragColor = vec4(
previousPassColor.rgb * color,
previousPassColor.a);
添加一些简单的 GUI 来设置颜色的 3 个值:
const gui = new GUI();
gui.add(colorPass.uniforms.color.value, 'r', 0, 4).name('red');
gui.add(colorPass.uniforms.color.value, 'g', 0, 4).name('green');
gui.add(colorPass.uniforms.color.value, 'b', 0, 4).name('blue');
这就为我们提供了乘以颜色的简单后处理效果。
正如前面提到的,关于如何编写 GLSL 和自定义着色器的所有细节对于这些文章来说太多了。 如果你确实想了解 WebGL 本身如何工作,请查看这些文章。 另一个很棒的资源是阅读 THREE.js 存储库中现有的后处理着色器。 有些比其他更复杂,但如果你从较小的开始,就有望了解它们的工作原理。
不幸的是,THREE.js 存储库中的大多数后期处理效果都没有文档,因此要使用它们,你必须通读示例或效果本身的代码。 希望这些简单的示例和有关渲染目标的文章提供足够的上下文来开始。