跳到主要内容

渲染后处理

在本章中,我们将看到 Three.js 的一个主要特性,到目前为止我们尚未涉及的功能:渲染后处理。通过渲染后处理,您可以在场景渲染后添加额外的效果。例如,您可以添加一个效果,使场景看起来像是在旧电视上显示的,或者您可以添加模糊和泛光效果。 本章我们将讨论以下主要内容:

  • 为后处理设置 Three.js
  • 由 Three.js 提供的一些基本后处理通道,如 BloomPass 和 FilmPass
  • 使用蒙版将效果应用于场景的部分
  • 使用 ShaderPass 添加更多基本后处理效果,如深褐色滤镜、镜像效果和颜色调整
  • 使用 ShaderPass 进行各种模糊效果和更高级的滤镜
  • 通过编写简单的着色器创建自定义后处理效果

在第1章《使用 Three.js 创建您的第一个 3D 场景》的"引入 requestAnimationFrame"部分, 我们设置了一个我们在整本书中都使用的渲染循环,以渲染和动画化我们的场景。 为了进行后处理,我们需要对此设置进行一些更改,以允许 Three.js 进行最终渲染的后处理。 在第一部分中,我们将看看如何做到这一点。

为后处理设置 Three.js

要为 Three.js 进行后处理设置,我们必须对我们当前的设置进行一些更改,如下所示:

  1. 创建 EffectComposer,用于添加后处理通道。
  2. 配置 EffectComposer,使其能够渲染我们的场景并应用任何额外的后处理步骤。
  3. 在渲染循环中,使用 EffectComposer 渲染场景,应用配置的后处理步骤,并显示输出。

与往常一样,我们将展示一个示例,您可以用来进行实验并根据自己的需要进行调整。 本章的第一个示例可以从 basic-setup.html 中访问。 您可以使用右上角的菜单修改此示例中使用的后处理步骤的属性。 在此示例中,我们将渲染第9章《动画和移动相机》中的蘑菇人,并为其添加 RGB 移位效果, 如下所示:

这个效果是通过使用 ShaderPassEffectComposer 一起在场景渲染后添加的。 在屏幕右侧的菜单中,您可以配置此效果并启用 DotScreenShader 效果。 在接下来的几节中,我们将解释上述列表中的各个步骤。

创建 THREE.EffectComposer

要使 EffectComposer 正常工作,我们首先需要可以与其一起使用的效果。 Three.js 自带了许多您可以使用的效果和着色器。 在本章中,我们将展示其中大部分,但要获取完整的概述,请查看 GitHub 上以下两个目录:

  • 效果通道:https://github.com/mrdoob/three.js/tree/dev/examples/ jsm/postprocessing
  • 着色器:https://github.com/mrdoob/three.js/tree/dev/examples/jsm/shaders

要在场景中使用这些效果,您需要导入它们:

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js'
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
import { RGBShiftShader } from 'three/examples/jsm/shaders/RGBShiftShader.js'
import { DotScreenShader } from 'three/examples/jsm/shaders/DotScreenShader.js'
import { CopyShader } from 'three/examples/jsm/shaders/CopyShader.js'

在上述代码块的导入部分,我们导入了主要的 EffectComposer, 以及可以与此 EffectComposer 一起使用的不同数量的后处理通道和着色器。 一旦我们有了这些,设置 EffectComposer 就像这样:

const composer = new EffectComposer(renderer)

如您所见,效果合成器接受的唯一参数是渲染器。 接下来,我们将向这个合成器添加各种通道。

配置 THREE.EffectComposer 进行后处理

每个通道都是按照添加到 THREE.EffectComposer 的顺序执行的。 我们添加的第一个通道是 RenderPass。 这个通道使用提供的摄像机渲染我们的场景,但尚未将其输出到屏幕:

const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

通过 addPass 函数,我们将 RenderPass 添加到 EffectComposer。 下一步是添加另一个通道,它将以 RenderPass 的结果作为输入,应用其变换,并将其结果输出到屏幕。 并非所有可用的通道都允许这样做,但我们在此示例中使用的通道可以:

const effect1 = new ShaderPass(DotScreenShader)
effect1.uniforms['scale'].value = 10
effect1.enabled = false
const effect2 = new ShaderPass(RGBShiftShader)
effect2.uniforms['amount'].value = 0.015
effect2.enabled = false
const composer = new EffectComposer(renderer)
composer.addPass(new RenderPass(scene, camera))
composer.addPass(effect1)
composer.addPass(effect2)

在这个示例中,我们向 composer 添加了两个效果。 首先使用 RenderPass 渲染场景,然后应用 DotScreenShader,最后应用 RGBShiftShader

更新渲染循环

现在我们只需更新渲染循环,以便使用 EffectComposer 进行渲染, 而不是通过正常的 WebGLRenderer 进行:

const render = () => {
requestAnimationFrame(render);
composer.render();
}

我们所做的唯一修改是删除 renderer.render(scene, camera) 并将其替换为 composer.render()。 这将调用 EffectComposer 上的 render 函数,进而使用传入的 THREE.WebGLRenderer, 结果是我们在屏幕上看到输出:

备注

在应用渲染通道后使用控件

您仍然可以使用正常的控件在场景中移动。 在本章中看到的所有效果都是在渲染场景后应用的。 通过这个基本的设置,我们将在接下来的几节中查看可用的后处理通道。

后处理通道

Three.js 自带一些可直接与 THREE.EffectComposer 一起使用的后处理通道。

备注

使用简单的 GUI 进行实验 本章中展示的大多数着色器和通道都是可配置的。 当您想要应用其中之一时,通常最简单的方法就是添加一个简单的用户界面,以便您可以调整属性。 这样,您可以看到适用于特定场景的良好设置是什么样的。

以下列表显示了 Three.js 中可用的所有后处理通道:

  • AdaptiveToneMappingPass:此渲染通道根据场景中的光量调整亮度。
  • BloomPass:这是一种效果,使较亮的区域渗入较暗的区域。这模拟了相机被极亮的光所淹没的效果。
  • BokehPass:这为场景添加了浸透效果。具有浸透效果时,场景的前景处于焦点,而其他地方处于模糊状态。
  • ClearPass:这个通道清除当前的纹理缓冲区。
  • CubeTexturePass:这可用于在场景中渲染一个天空盒。
  • DotScreenPass:这应用了一层黑点,表示原始图像跨越屏幕。
  • FilmPass:这通过应用扫描线和扭曲来模拟电视屏幕。
  • GlitchPass:这在屏幕上显示一个电子故障,以随机时间间隔。
  • HalfTonePass:这向场景添加了半色调效果。具有半色调效果时,场景以各种大小的彩色字形(圆圈、方形等)呈现。
  • LUTPass:使用 LUTPass,您可以在渲染后对场景进行颜色校正(本章未展示)。
  • MaskPass:这允许您向当前图像应用蒙版。后续的通道仅应用于蒙版区域。
  • OutlinePass:这呈现场景中对象的轮廓。
  • RenderPass:这根据提供的场景和摄像机渲染场景。
  • SAOPass:这提供运行时环境光遮蔽。
  • SMAAPass:这向场景添加抗锯齿效果。
  • SSAARenderPass:这向场景添加抗锯齿效果。
  • SSAOPass:这提供了执行运行时环境光遮蔽的另一种方法。
  • SSRPass:此通道允许您创建反射对象。
  • SavePass:执行此通道时,它会复制当前渲染步骤的副本,以供以后使用。这个通道在实践中并不是很有用,我们在任何示例中都不会使用它。
  • ShaderPass:这允许您为高级或自定义的后处理通道传递自定义着色器。
  • TAARenderPass:这向场景添加抗锯齿效果。
  • TexturePass:这将当前的合成器状态存储在一个纹理中,您可以将其用作其他 EffectComposer 实例的输入。
  • UnrealBloomPass:这与 THREE.BloomPass 相同,但效果类似于虚幻 3D 引擎中使用的效果。

让我们从一些简单的通道开始。

简单的后处理通道

对于简单的通道,我们将看看使用 FilmPassBloomPassDotScreenPass 可以做什么。 对于这些通道,有一个示例(multi-passes.html),可以让您尝试这些通道并查看它们如何以不同方式影响原始输出。以下截图显示了示例:

在此示例中,您可以同时看到四个场景,在每个场景中,都添加了不同的后处理通道。 左上角的场景显示了 BloomPass,右下角的场景显示了 DotScreenPass, 左下角的场景显示了 FilmPass。右上角的场景显示了原始渲染。

在这个示例中,我们还使用了 THREE.ShaderPassTHREE.TexturePass, 以将原始渲染的输出重复用作其他三个场景的输入。 这样,我们只需渲染场景一次。 因此,在我们查看各个通道之前,让我们先看看这两个通道, 如下所示:

const effectCopy = new ShaderPass(CopyShader)
const renderedSceneComposer = new EffectComposer(renderer)
renderedSceneComposer.addPass(new RenderPass(scene, camera))
renderedSceneComposer.addPass(new ShaderPass (GammaCorrectionShader))
renderedSceneComposer.addPass(effectCopy)
renderedSceneComposer.renderToScreen = false
const texturePass = new TexturePass(renderedSceneComposer.renderTarget2.texture)

在此代码片段中,我们设置了 EffectComposer,它将输出默认场景(右上角的场景)。 该合成器有三个通道:

  • RenderPass:此通道渲染场景。
  • 带有 GammaCorrectionShaderShaderPass:确保输出的颜色是正确的。 如果在应用效果后,场景的颜色看起来不正确,此着色器将对其进行更正。
  • 带有 CopyShaderShaderPass:将输出渲染到屏幕上(如果我们将 renderToScreen 属性设置为 true, 则不进行任何进一步的后处理)。

如果查看示例,您会发现我们四次显示了相同的场景,但每次都应用了不同的效果。 我们也可以使用 RenderPass 四次从头渲染场景,但这有点浪费,因为我们只需重用第一个合成器的输出即可。 为此,我们创建了 TexturePass,并传递了 composer.renderTarget2.texture 的值。此属性包含以纹理形式呈现的场景,我们可以将其传递给 TexturePass。 现在,我们可以将 texturePass 变量用作其他合成器的输入,而无需从头渲染场景。 现在,让我们首先看看 FilmPass 以及如何使用 TexturePass 的结果。

使用 THREE.FilmPass 创建类似电视的效果

要创建 FilmPass,我们使用以下代码片段:

const filmPass = new FilmPass();
const filmPassComposer = new EffectComposer(renderer);
filmPassComposer.addPass(texturePass);
filmPassComposer.addPass(filmPass);

使用 TexturePass 的唯一步骤是将其添加为合成器中的第一个通道。 接下来,我们只需添加 FilmPass,效果就会被应用。 FilmPass 可以接受四个额外的参数,如下所示:

  • noiseIntensity:此属性允许您控制场景的颗粒感。
  • scanlinesIntensityFilmPass 向场景添加了多条扫描线(参见 scanLinesCount)。 通过此属性,您可以定义这些扫描线的显著程度。
  • scanLinesCount:可以使用此属性控制显示的扫描线数。
  • grayscale:如果设置为 true,则输出将转换为灰度。

实际上,有两种方法可以传递这些参数。 在这个示例中,我们将它们作为构造函数的参数传递,但您也可以直接设置它们, 如下所示:

effectFilm.uniforms.grayscale.value = controls.grayscale;
effectFilm.uniforms.nIntensity.value = controls.noiseIntensity;
effectFilm.uniforms.sIntensity.value = controls.scanlinesIntensity;
effectFilm.uniforms.sCount.value = controls.scanlinesCount;

在这种方法中,我们使用 uniforms 属性,该属性直接与 WebGL 通信。 在“使用 THREE.ShaderPass 进行自定义效果”部分, 我们将深入研究 uniforms;现在,您只需要知道,通过这种方式, 您可以更新后处理通道和着色器的配置,并直接查看结果。

此通道的结果如下图所示:

接下来的效果是泛光效果,您可以在图 11.3 屏幕左上角看到。

使用 THREE.BloomPass 向场景添加泛光效果

左上角显示的效果称为泛光效果。当应用泛光效果时,场景的亮区域将变得更加突出,并渗入较暗的区域。 创建 BloomPass 的代码如下:

const bloomPass = new BloomPass();
const effectCopy = new ShaderPass(CopyShader);
const bloomPassComposer = new EffectComposer(renderer);
bloomPassComposer.addPass(texturePass);
bloomPassComposer.addPass(bloomPass);
bloomPassComposer.addPass(effectCopy);

如果将其与我们使用 FilmPassEffectComposer 进行比较, 您会注意到我们添加了额外的通道 effectCopy。 这一步不添加任何特殊效果,只是将最后一个通道的输出复制到屏幕上。 我们需要添加这一步,因为 BloomPass 不会直接渲染到屏幕。

以下表格列出了可以在 BloomPass 上设置的属性:

  • strength:这是泛光效果的强度。此值越高,亮区域越亮,渗入较暗区域的程度就越大。
  • kernelSize:这是核的大小。这是在单个步骤中模糊的区域的大小。如果将此值设置得更高,则将包括更多像素以确定特定点的效果。
  • sigma:使用 sigma 属性,您可以控制泛光效果的锐度。值越高,泛光效果看起来越模糊。
  • resolutionresolution 属性定义了创建泛光效果的精度。如果将其设置得太低,结果将显得块状。

更好的理解这些属性的方法是通过使用前述示例 multi-passes.html 进行实验。 以下截图显示了 sigma 大小和强度较高的泛光效果: 接下来,我们将看一下 DotScreenPass 效果。

将场景输出为一组点

使用 DotScreenPass 与使用 BloomPass 非常相似。 我们刚刚看到了 BloomPass 的效果。现在让我们看看 DotScreenPass 的代码:

const dotScreenPass = new DotScreenPass();
const dotScreenPassComposer = new EffectComposer(renderer);
dotScreenPassComposer.addPass(texturePass);
dotScreenPassComposer.addPass(dotScreenPass);

对于这种效果,我们不需要使用 effectCopy 将结果输出到屏幕上。 DotScreenPass 也可以使用以下几个属性进行配置:

  • center:使用 center 属性,您可以微调点的偏移方式。
  • angle:点以某种方式对齐。使用 angle 属性,您可以更改此对齐方式。
  • scale:通过这个属性,我们可以设置要使用的点的大小。缩放越低,点越大。

对于这个着色器,适用于其他着色器的也适用于这个着色器。通过实验更容易找到正确的设置,如下图所示:

在进入下一组简单的着色器之前,我们首先看一下如何在同一屏幕上渲染多个场景。

在同一屏幕上显示多个渲染器的输出

本节不会详细介绍如何使用后处理效果, 但将解释如何在同一屏幕上获取所有四个 EffectComposer 实例的输出。 首先,让我们看一下用于此示例的渲染循环:

const width = window.innerWidth || 2;
const height = window.innerHeight || 2;
const halfWidth = width / 2;
const halfHeight = height / 2;

const render = () => {
renderer.autoClear = false;
renderer.clear();

renderedSceneComposer.render();
renderer.setViewport(0, 0, halfWidth, halfHeight);

filmpassComposer.render();
renderer.setViewport(halfWidth, 0, halfWidth, halfHeight);

dotScreenPassComposer.render();
renderer.setViewport(0, halfHeight, halfWidth, halfHeight);

bloomPassComposer.render();
renderer.setViewport(halfWidth, halfHeight, halfWidth, halfHeight);

copyComposer.render();
requestAnimationFrame(() => render());
}

首先要注意的是,我们将 renderer.autoClear 属性设置为 false, 然后在渲染循环中显式调用 clear() 函数。 如果我们不在每次调用 render() 函数时执行此操作,先前渲染的屏幕部分将被清除。 通过这种方法,我们只在渲染循环的开始时清除所有内容。

为了避免所有合成器在相同的空间中渲染, 我们将渲染器的 viewport 函数(由我们的合成器使用)设置为屏幕的不同部分。 此函数接受四个参数:xywidthheight。 正如您在代码示例中所看到的,我们使用这个函数将屏幕分为四个区域,并使合成器渲染到它们各自的区域。 请注意,如果需要,您还可以在多个场景、相机和 WebGLRenderer 实例中使用此方法。 通过这种设置,渲染循环将每个四个 EffectComposer 对象分别渲染到屏幕的各自区域。 让我们快速看一下另外几个通道。

其他简单的通道

如果在浏览器中打开 multi-passes-2.html 示例,您将看到许多其他通道的效果:

我们在这里不会深入讨论,因为这些通道的配置方式与前几节中的通道相同。 在这个例子中,您可以看到以下效果:

  • 在左下角,您可以看到 OutlinePass。轮廓通道可用于为 THREE.Mesh 对象绘制轮廓。
  • 在右下角,显示了 GlitchPass。顾名思义,此通道提供技术渲染故障效果。
  • 在左上角,展示了 UnrealBloom 效果。
  • 在右上角,使用 HalftonePass 将渲染转换为一组点。

与本章中所有示例一样,您可以使用右侧菜单配置这些通道的各个属性。

要正确查看 OutlinePass,您可以将场景背景设置为黑色并稍微缩小视图:

到目前为止,我们已经看到了简单的效果,在下一节中,我们将了解如何使用蒙版将效果应用于屏幕的部分。

使用蒙版的高级 EffectComposer 流程

在先前的例子中,我们将后处理通道应用于整个屏幕。 然而,Three.js 也可以将通道仅应用于特定区域。 在本节中,我们将执行以下步骤:

  1. 创建一个用作背景图像的场景。
  2. 创建一个包含看起来像地球的球体的场景。
  3. 创建一个包含看起来像火星的球体的场景。
  4. 创建 EffectComposer,将这三个场景渲染为单个图像。
  5. 对渲染为火星的球体应用一种颜色效果。
  6. 对渲染为地球的球体应用一种棕褐色效果。

这听起来可能很复杂,但实际上实现起来非常容易。 首先,让我们看一下在 masks.html 示例中我们要达到的结果。 以下截图显示了这些步骤的结果:

首先,我们需要设置各种场景:

const sceneEarth = new THREE.Scene()
const sceneMars = new THREE.Scene()
const sceneBG = new THREE.Scene()

要创建地球和火星球体,我们只需使用正确的材质和纹理创建球体,并将它们添加到各自的场景中。 对于背景场景,我们加载一个纹理并将其设置为 sceneBG 的背景。

以下是代码示例(addEarthaddMars 只是为了保持代码清晰而创建的辅助函数;

它们使用 THREE.SphereGeometry 创建一个简单的 THREE.Mesh, 创建一些灯光,并将它们全部添加到 THREE.Scene):

sceneBG.background = new THREE.TextureLoader().load('/assets/textures/bg/starry-deep-outer-space-galaxy.jpg')
const earthAndLight = addEarth(sceneEarth)
sceneEarth.translateX(-16)
sceneEarth.scale.set(1.2, 1.2, 1.2)
const marsAndLight = addMars(sceneMars)
sceneMars.translateX(12)
sceneMars.translateY(6)
sceneMars.scale.set(0.2, 0.2, 0.2)

在这个例子中,我们使用场景的 background 属性添加了星空背景。 创建背景还有一种替代方法,我们可以使用 THREE.OrthographicCamera。 使用 THREE.OrthographicCamera 时,渲染对象的大小在离摄像机近或远时不会改变, 因此,通过将 THREE.PlaneGeometry 对象直接放在 THREE.rhoGraphicCamera 前面, 我们也可以创建背景。

现在我们有了三个场景,可以开始设置通道和 EffectComposer。 让我们首先看一下完整的通道链,然后再看各个通道:

var composer = new EffectComposer(renderer)
composer.renderTarget1.stencilBuffer = true
composer.renderTarget2.stencilBuffer = true
composer.addPass(bgRenderPass)
composer.addPass(earthRenderPass)
composer.addPass(marsRenderPass)
composer.addPass(marsMask)
composer.addPass(effectColorify)
composer.addPass(clearMask)
composer.addPass(earthMask)
composer.addPass(effectSepia)
composer.addPass(clearMask)
composer.addPass(effectCopy)

要使用蒙版,我们需要以稍微不同的方式创建 EffectComposer。 我们需要将内部使用的渲染目标的 stencilBuffer 属性设置为 true。 模板缓冲区是一种特殊类型的缓冲区,用于限制渲染区域。 因此,通过启用模板缓冲区,我们可以使用我们的蒙版。 让我们看一下添加的前三个通道。这三个通道分别渲染背景、地球场景和火星场景:

const bgRenderPass = new RenderPass(sceneBG, camera)
const earthRenderPass = new RenderPass(sceneEarth, camera)
earthRenderPass.clear = false
const marsRenderPass = new RenderPass(sceneMars, camera)
marsRenderPass.clear = false

这里没有什么新的,只是我们将两个通道的 clear 属性设置为 false。 如果我们不这样做,我们将只看到 marsRenderPass 渲染的输出, 因为它在开始渲染之前会清除所有内容。

如果回顾一下 EffectComposer 的代码, 接下来添加的是 marsMaskeffectColorifyclearMask 三个通道。 首先,我们来看看这三个通道是如何定义的:

const marsMask = new MaskPass(sceneMars, camera)
const effectColorify = new ShaderPass(ColorifyShader)
effectColorify.uniforms['color'].value.setRGB(0.5, 0.5, 1)
const clearMask = new ClearMaskPass()

这三个通道中的第一个是 MaskPass。 创建 MaskPass 对象时,您传入一个场景和一个摄像机,就像为 RenderPass 所做的那样。 MaskPass 对象将在内部渲染此场景,但不会在屏幕上显示此渲染的场景,而是使用内部渲染的场景创建蒙版。 当将 MaskPass 对象添加到 EffectComposer 时,所有后续的通道将仅应用于由 MaskPass 定义的蒙版, 直到遇到 ClearMaskPass 步骤。 在这个例子中,这意味着 effectColorify 通道,它添加了蓝色光晕, 仅应用于 sceneMars 中渲染的对象。

我们使用相同的方法对地球对象应用棕褐色滤镜。 我们首先基于地球场景创建一个蒙版,并在 EffectComposer 中使用该蒙版。 在使用 MaskPass 后,我们添加要应用的效果(在这种情况下是 effectSepia), 一旦完成,我们添加 ClearMaskPass 以再次移除蒙版。

对于此特定的 EffectComposer 的最后一步是我们已经见过的步骤。 我们需要将最终结果复制到屏幕上,我们再次使用 effectCopy 通道。 通过这种设置,我们可以应用我们希望成为总屏幕一部分的效果。 请注意,如果火星场景和地球场景重叠,这些效果将应用于屏幕的那一部分:

在使用 MaskPass 时,还有一个有趣的额外属性,那就是 inverse 属性。 如果将此属性设置为 true,则蒙版将被反转。 换句话说,效果将应用于除了传递给 MaskPass 的场景之外的所有内容。 这在下面的截图中展示出来,其中我们将 earthMaskinverse 属性设置为 true

在我们讨论 ShaderPass 之前,我们将看两个提供更高级效果的通道:BokehPassSSAOPass

高级通道 – 背景虚化

通过 BokehPass,您可以为场景添加背景虚化效果。 在背景虚化效果中,场景的一部分处于焦点状态,而其余部分看起来模糊。 要查看此效果的实际效果,您可以打开 bokeh.html 示例:

打开示例时,最初整个场景都会显得模糊。 通过右侧的 Bokeh 控件,您可以将焦点值设置为希望处于焦点状态的场景部分, 并调整光圈属性以确定应该处于焦点状态的区域大小。 通过滑动焦点,您可以使前景中的立方体组处于焦点状态,如下所示:

或者,如果将焦点滑动得更远,我们可以聚焦于红色的立方体:

而且,如果将焦点滑动得更远,我们可以聚焦于场景远端的绿色立方体组:

BokehPass 可以像我们迄今为止看到的其他通道一样使用:

const params = {
focus: 10,
aspect: camera.aspect,
aperture: 0.0002,
maxblur: 1
};
const renderPass = new RenderPass(scene, camera);
const bokehPass = new BokehPass(scene, camera, params)
bokehPass.renderToScreen = true;
const composer = new EffectComposer(renderer);
composer.addPass(renderPass);
composer.addPass(bokehPass);

达到期望的效果可能需要对属性进行一些微调。

高级通道 – 环境光遮蔽

在第 10 章《加载和使用纹理》中, 我们讨论了使用预先烘焙的环境光遮蔽贴图(aoMap)直接应用基于环境光的阴影。 环境光遮蔽涉及到您在物体上看到的阴影和光强变化,因为物体的各个部分并不都接收相同数量的环境光。 除了在材质上使用 aoMap 之外,还可以使用 EffectComposer 上的通道来获得相同的效果。 如果您打开 ambient-occlusion.html 示例,您将看到使用 SSAOPass 的结果:

没有应用环境光遮蔽滤镜的类似场景似乎非常平淡,如下所示:

请注意,如果使用此功能,必须注意应用程序的整体性能,因为这是一个非常 GPU 密集的通道。

到目前为止,我们已经使用了 Three.js 提供的标准通道来实现我们的效果。 Three.js 还提供了 THREE.ShaderPass,它可用于定制效果,并配备有许多可用于实验的着色器。

使用 THREE.ShaderPass 进行自定义效果

通过 THREE.ShaderPass,我们可以通过传递自定义着色器来对场景应用大量额外的效果。 Three.js 配备了一组可以与此 THREE.ShaderPass 一起使用的着色器,它们将在本节中列出。 我们将本节分为三个部分。第一组涉及简单的着色器。 所有这些着色器都可以通过打开 shaderpass-simple.html 示例进行查看和配置:

  • BleachBypassShader:创建漂白副本效果。使用此效果,将在图像上应用类似银的覆盖。
  • BlendShader:这不是单独应用的着色器,而是允许您将两个纹理混合在一起。 例如,您可以使用此着色器将一个场景的渲染平滑地混合到另一个场景中(在 shaderpass-simple.html 中未显示)。
  • BrightnessContrastShader:允许您更改图像的亮度和对比度。
  • ColorifyShader:对屏幕应用颜色叠加。我们在 mask 示例中已经看到过这个。
  • ColorCorrectionShader:使用此着色器,您可以更改颜色分布。
  • GammaCorrectionShader:对渲染场景应用伽马校正。这使用固定的伽马系数为2。请注意,您还可以通过使用 gammaFactor、gammaInput 和 gammaOutput 属性直接在 THREE.WebGLRenderer 上设置伽马校正。
  • HueSaturationShader:允许您更改颜色的色调和饱和度。
  • KaleidoShader:为场景添加一个类似万花筒的效果,围绕场景中心提供径向反射。
  • LuminosityShaderLuminosityHighPassShader:提供亮度效果,显示场景的亮度。
  • MirrorShader:为屏幕的一部分创建镜像效果。
  • PixelShader:创建像素化效果。
  • RGBShiftShader:此着色器将颜色的红色、绿色和蓝色组件分离。
  • SepiaShader:在屏幕上创建类似赭色的效果。
  • SobelOperatorShader:提供边缘检测。
  • VignetteShader:应用晕影效果。此效果在图像中心周围显示深色边框。

接下来,我们将看一下提供一些与模糊相关的效果的着色器。这些效果可以通过 shaderpass-blurs.html 示例进行实验:

  • HorizontalBlurShaderVerticalBlurShader:这些对整个场景应用模糊效果。
  • HorizontalTiltShiftShaderVerticalTiltShiftShader:重新创建倾斜移位效果。 通过倾斜移位效果,可以通过确保仅图像的一部分清晰来创建看起来微缩的场景。
  • FocusShader:这是一个简单的着色器,其结果是一个中心区域的清晰渲染,边缘模糊。

最后,还有一些我们不会详细讨论的着色器;我们仅列出它们是为了完整性。 这些着色器主要由另一个着色器或本章开头讨论的着色器通道内部使用:

  • THREE.FXAAShader:此着色器在后处理阶段应用抗锯齿效果。如果在渲染过程中应用抗锯齿效果太昂贵,则使用此效果。
  • THREE.ConvolutionShader:此着色器在 BloomPass 渲染通道内部使用。
  • THREE.DepthLimitedBlurShader:此着色器在 SAOPass 中用于环境光遮蔽。
  • THREE.HalftoneShader:此着色器在 HalftonePass 内部使用。
  • THREE.SAOShader:此提供以着色器形式的环境光遮蔽。
  • THREE.SSAOShader:此提供以着色器形式的环境光遮蔽的替代方法。
  • THREE.SMAAShader:此向渲染场景提供抗锯齿效果。
  • THREE.ToneMapShader:此在 AdaptiveToneMappingPass 内部使用。
  • UnpackDepthRGBAShader:此可用于将来自 RGBA 纹理的编码深度值可视化为视觉颜色。 如果查看 Three.js 发行版的 Shaders 目录,可能会注意到

一些本章未列出的其他着色器。 这些着色器 - FresnelShaderOceanShaderParallaxShaderWaterRefractionShader - 不是用于后期处理的着色器, 而应使用我们在第 4 章《使用 Three.js 材质》中讨论的 THREE.ShaderMaterial 对象。

我们将从几个简单的着色器开始。

简单的着色器

为了尝试基本的着色器,我们创建了一个示例,您可以在其中玩弄大多数着色器并直接在场景中查看效果。 您可以在 shaders.html 找到此示例。以下截图显示了一些效果。

BrightnessContrastShader 效果如下:

SobelOperatorShader 效果检测轮廓:

使用 KaleidoShader 可以创建万花筒效果:

使用 MirrorShader 可以镜像场景的部分:

RGBShiftShader 效果如下:

使用 LuminosityHighPassShader 可以调整场景中的亮度:

要查看其他效果,请使用右侧的菜单查看它们的作用以及如何配置它们。 Three.js 还提供了一些专门用于添加模糊效果的着色器。 这些将在下一节中展示。

模糊着色器

同样,在本节中,我们不会深入代码;我们只会向您展示各种模糊着色器的结果。 您可以通过使用 shaders-blur.html 示例进行实验。 首先显示的两个着色器是 HorizontalBlurShaderVerticalBlurShader

另一种类似模糊的效果由 HorizontalTiltShiftShaderVerticalTiltShiftShader 提供。 此着色器不会使整个场景模糊,而仅会使小区域模糊。

这提供了一种称为倾斜移位的效果。这通常用于通过确保仅图像的一部分清晰来从正常照片创建微缩样式的场景。 以下截图显示了此效果:

最后,通过 FocusShader 提供了最后一种类似模糊的效果:

到目前为止,我们一直使用 Three.js 提供的着色器。 然而,也可以编写自己的着色器以用于 THREE.EffectComposer

创建自定义后期处理着色器

在本节中,您将学习如何创建自定义着色器,以便在后期处理中使用。 我们将创建两个不同的着色器。 第一个将把当前图像转换为灰度图像,而第二个将通过减少可用颜色的数量将图像转换为 8 位图像。

备注

顶点着色器和片段着色器

创建顶点着色器和片段着色器是一个非常广泛的主题。 在本节中,我们将只涉及这些着色器可以做什么以及它们的工作原理的表面知识。 有关更深入的信息,您可以在 http://www.khronos.org/webgl/ 上找到 WebGL 规范。 还有一个充满示例的附加资源, 可以在 https://www.shadertoy.com 或《着色器之书》(https://thebookofshaders.com/)上找到。

自定义灰度着色器

要为 Three.js(以及其他 WebGL 库)创建自定义着色器, 您必须创建两个组件:一个顶点着色器和一个片段着色器。 顶点着色器可用于更改单个顶点的位置,而片段着色器可用于确定单个像素的颜色。 对于后期处理着色器,我们只需要实现片段着色器,可以保留 Three.js 提供的默认顶点着色器。

在查看代码之前需要强调的一点是,GPU 支持多个着色器管线。 这意味着顶点着色器同时并行运行于多个顶点,片段着色器也是如此。

让我们首先查看将灰度效果应用于我们的图像的着色器的完整源代码(custom-shader.js):

export const CustomGrayScaleShader = {
  uniforms: {
    tDiffuse: { type: 't', value: null },
    rPower: { type: 'f', value: 0.2126 },
    gPower: { type: 'f', value: 0.7152 },
    bPower: { type: 'f', value: 0.0722 }
  },
  vertexShader: [
    'varying vec2 vUv;',
    'void main() {',
    'vUv = uv;',
    'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
    '}'
  ].join('\n'),
  fragmentShader: [
    'uniform float rPower;',
    'uniform float gPower;',
    'uniform float bPower;',
    'uniform sampler2D tDiffuse;',
    'varying vec2 vUv;',
    'void main() {',
    'vec4 texel = texture2D( tDiffuse, vUv );',
    'float gray = texel.r*rPower + texel.g*gPower + texel.b*bPower;',
    'gl_FragColor = vec4( vec3(gray), texel.w );',
    '}'
  ].join('\n')
}
备注

另一种定义着色器的方法

在第 4 章中,我们展示了如何在单独的独立文件中定义着色器。 在 Three.js 中,大多数着色器都遵循前面代码片段中看到的结构。 这两种方法都可以用于定义着色器代码。

正如在前面的代码块中所看到的,这不是 JavaScript。 编写着色器时,您将它们写成 OpenGL 着色语言(GLSL),它看起来很像 C 编程语言。 有关 GLSL 的更多信息,请访问 http://www.khronos.org/opengles/sdk/docs/manglsl/

首先,让我们看看顶点着色器:

  vertexShader: [
    'varying vec2 vUv;',
    'void main() {',
    'vUv = uv;',
    'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
    '}'
].join('\n'),

对于后期处理,这个着色器实际上不需要做任何事情。 前述代码是 Three.js 实现顶点着色器的标准方式。 它使用 projectionMatrix,这是从相机的投影, 以及 modelViewMatrix,它将对象的位置映射到世界位置, 以确定在屏幕上渲染顶点的位置。 对于后期处理,此代码片段中唯一有趣的事情是 uv 值, 该值指示从纹理中读取的 texel,使用 varying vec2 vUv 变量将其传递到片段着色器。

这可用于获取要在片段着色器中修改的像素。 现在,让我们看看片段着色器并了解代码在做什么。 我们将从以下变量声明开始:

  'uniform float rPower;',
  'uniform float gPower;',
  'uniform float bPower;',
  'uniform sampler2D tDiffuse;',
  'varying vec2 vUv;',

在这里,我们可以看到 uniform 属性的四个实例。 uniform 属性的实例具有从 JavaScript 传递到着色器的值, 这对于处理的每个片段都是相同的。

在这种情况下, 我们传递了三个浮点数,由类型 float 标识(用于确定要包含在最终灰度图像中的颜色的比率), 并且还传递了一个纹理(tDiffuse),由类型 tDiffuse 标识。 此纹理包含来自 EffectComposer 实例的前一次传递的图像。 Three.js 确保在将 tDiffuse 用作其名称时将此纹理传递给此着色器。 我们还可以通过自己从 JavaScript 设置 uniform 属性的其他实例。 在我们可以从 JavaScript 中使用这些 uniform 之前, 我们必须定义要向 JavaScript 公开哪些 uniform 属性。 这是在着色器文件的顶部完成的:

uniforms: {
"tDiffuse": { type: "t", value: null },
"rPower":   { type: "f", value: 0.2126 },
"gPower":   { type: "f", value: 0.7152 },
"bPower":   { type: "f", value: 0.0722 }
},

此时,我们可以从 Three.js 接收配置参数,该参数将提供当前渲染的输出。 让我们看看将每个像素转换为灰色像素的代码:

"void main() {",
"vec4 texel = texture2D( tDiffuse, vUv );",
"float gray = texel.r*rPower + texel.g*gPower + texel.b*bPower;", "gl_FragColor = vec4( vec3(gray), texel.w );"

这里发生的事情是我们从传入的纹理中获取正确的像素。 我们通过使用 texture2D 函数实现此目的, 其中我们传递我们当前的图像(tDiffuse)和我们要分析的像素(vUv)的位置。 结果是包含颜色和不透明度(texel.w)的 texel(来自纹理的像素)。 接下来,我们使用该 texelrgb 属性计算灰度值。 这个灰度值设置为 gl_FragColor 变量,最终显示在屏幕上。

好了,我们有了自己的自定义着色器。 此着色器的使用方式与我们在本章中已经看到的几次相同。 首先,我们只需要设置 EffectComposer,如下所示:

const effectCopy = new ShaderPass(CopyShader)
effectCopy.renderToScreen = true
const grayScaleShader = new ShaderPass (CustomGrayScaleShader)
const gammaCorrectionShader = new ShaderPass (GammaCorrectionShader)
const composer = new EffectComposer(renderer)
composer.addPass(new RenderPass(scene, camera))
composer.addPass(grayScaleShader)
composer.addPass(gammaCorrectionShader)
composer.addPass(effectCopy)

我们在渲染循环中调用 composer.render()。 如果我们想在运行时更改此着色器的属性,我们只需更新我们已定义的 uniforms 属性, 如下所示:

shaderPass.uniforms.rPower.value = ...;
shaderPass.uniforms.gPower.value = ...;
shaderPass.uniforms.bPower.value = ...;

结果可以在 custom-shaders-scene.html 中看到。以下截图显示了这个示例: 现在让我们创建另一个自定义着色器。 这一次,我们将将 24 位输出减少到较低的位数。

创建自定义位着色器

通常,颜色表示为 24 位值,这给了我们约 1600 万种不同的颜色。 在计算机的早期,这是不可能的,颜色通常表示为 8 位或 16 位颜色。 使用这个着色器,我们将自动将我们的 24 位输出转换为 4 位颜色深度(或任何您想要的深度)。

由于顶点着色器与我们之前的示例相同,我们将跳过顶点着色器,直接列出 uniforms 属性的定义:

uniforms: {
"tDiffuse": { type: "t", value: null },
"bitSize":    { type: "i", value: 4 }
}

fragmentShader 代码如下:

fragmentShader: [
  'uniform int bitSize;',
  'uniform sampler2D tDiffuse;',
  'varying vec2 vUv;',
  'void main() {',
  'vec4 texel = texture2D( tDiffuse, vUv );',
  'float n = pow(float(bitSize),2.0);',
  'float newR = floor(texel.r*n)/n;',
  'float newG = floor(texel.g*n)/n;',
  'float newB = floor(texel.b*n)/n;',
  'gl_FragColor = vec4( vec3(newR,newG,newB), 1.0);',
  '}'
  ].join('\n')

我们定义了两个 uniform 属性的实例,用于配置此着色器。 第一个是 Three.js 用于传递当前屏幕的,第二个由我们定义为整数(type:"i"), 用作我们希望将结果呈现为的颜色深度。 代码本身非常简单:

  1. 首先,我们根据传入的像素的 vUv 位置从 tDiffuse 纹理获取 texel
  2. 我们通过计算 2bitSize 次幂(pow(float(bitSize),2.0))来计算我们可以拥有的颜色数量。
  3. 接下来,我们通过将此值乘以 n, 四舍五入(floor(texel.r*n))并再次除以 n 来计算 texel 的颜色的新值。
  4. 结果设置为 gl_FragColor(红色、绿色和蓝色值以及不透明度),并显示在屏幕上。

您可以在与我们之前的自定义着色器相同的示例中查看此自定义着色器的结果, custom-shaders-scene.html。以下截图显示了此示例,其中我们将位大小设置为 4。 这意味着该模型仅以 16 种颜色呈现:

至此,关于后期处理的章节就完成了。

总结

在本章中,我们讨论了许多不同的后期处理选项。 正如您所见,创建 EffectComposer 并将通道链接在一起实际上非常容易。 您只需记住一些事情。并非所有通道都会在屏幕上输出。 如果要输出到屏幕,您始终可以使用带有 CopyShaderShaderPass。 向 EffectComposer 添加通道的顺序很重要。效果是按照那个顺序应用的。 如果要重用来自特定 EffectComposer 实例的结果,可以使用 TexturePass。 当您的 EffectComposer 中有多个 RenderPass 时,请确保将 clear 属性设置为 false。 否则,您将只看到最后一个 RenderPass 步骤的输出。 如果要仅将效果应用于特定对象,可以使用 MaskPass。 完成遮罩后,请使用 ClearMaskPass 清除遮罩。 除了 Three.js 提供的标准通道外,还有许多标准着色器可用。 您可以与 ShaderPass 一起使用这些着色器。 使用 Three.js 的标准方法创建后期处理的自定义着色器非常简单。 您只需创建一个片段着色器。

现在我们基本上已经涵盖了关于 Three.js 核心的所有知识。 在第 12 章中,我们将看到一个名为 Rapier.js 的库, 您可以使用它来将物理效果和声音应用于 Three.js 场景,以实现碰撞、重力和约束。