第十一章渲染后处理

在本章中,我们将介绍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偏移效果,如下所示:

图11.1-使用后处理通道进行渲染

图11.1-使用后处理通道进行渲染

此效果是在使用 ShaderPass 和 EffectComposer 渲染场景后添加的。在屏幕右侧的菜单中,您可以配置此效果,也可以启用 DotScreenShader 效果。

在下面的部分中,我们将解释前面列表中的各个步骤。

创建 THREE.EffectComposer

为了让 EffectComposer 发挥作用,我们首先需要可以与它一起使用的效果。Three.js提供了大量可以使用的效果和着色器。在本章中,我们将展示其中的大部分,但要获得完整的概述,请查看GitHub上的以下两个目录:

Effect passes: https://github.com/mrdoob/three.js/tree/dev/examples/jsm/postprocessing

Shaders: 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)

正如您所看到的,效果生成器所采用的唯一参数是渲染器。接下来,我们将为这个效果生成器添加各种传递

配置 THRE.EffectComposer 进行后处理

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

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

使用addPass函数,我们将RenderPass添加到EffectComposer中。下一步是添加另一个过程,该过程将使用RenderPass的结果作为其输入,应用其转换,并将其结果输出到屏幕。并非所有可用的 passes 都允许这样做,但我们在本例中使用的 passes 做到了:

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。

更新渲染循环

我们只需要对渲染循环进行一个小的修改,以使用作曲家而不是 THREE.WebGLRenderer:

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

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

image-20230529211235658

图11.2-使用多个后处理通道进行渲染

在应用渲染传递后使用控件

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

后处理通道

Three.js 附带了许多后处理通道,您可以直接与 THREE.EffectComposer

使用一个简单的GUI来进行实验

本章中显示的大多数着色器和通道都可以被配置。当您想自己应用一个时,通常最简单的方法是添加一个简单的UI,允许您处理属性。这样,您就可以看到特定场景的良好设置。

以下列表显示了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引擎中使用的效果。

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

简单的后处理通道

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

image-20230529212141758

图11.3-应用于一个场景的三个简单通道

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

在这个例子中,我们也使用 THREE.ShaderPass 和 THREE.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,它将输出默认场景(右上角的场景)。这位 composer 有三个通道:

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

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

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

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

const filmpass = new FilmPass()
const filmpassComposer = new EffectComposer(renderer)
filmpassComposer.addPass(texturePass)
filmpassComposer.addPass(filmpass)

使用TexturePass需要采取的唯一步骤是将其添加为 composer 中的第一个通道。接下来,我们只需添加 FilmPass,效果就会应用。FilmPass 可以采用四个附加参数,如以下列表所示:

  • noiseIntensity:此属性允许您控制场景的颗粒感。
  • scanlinesIntensity:FilmPass 为场景添加了许多扫描线(参见 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 用于自定义效果部分,我们将讨论创建自定义着色器,我们将更深入地了解制服;现在,您只需要知道,通过这种方式,您可以更新后处理通道和着色器的配置并直接查看结果。

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

image-20230529213605385

图11.4-由FilmPass提供的胶片效果

下一个效果是泛光效果,您可以在图 11.3 的屏幕左上角看到它

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

您在左上角看到的效果称为泛光效果。应用泛光效果时,场景中的明亮区域将更加突出,并渗入较暗的区域。创建 BloomPass 的代码如下:

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

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

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

  • strength:这是绽放效果的强度。这个值越高,越亮的区域越亮,它们渗入较暗的区域就越多。
  • kernelSize:这是内核的大小。这是在一步中模糊的区域的大小。如果将其设置得更高,将包含更多像素以确定特定点的效果。
  • sigma:使用西格玛属性,您可以控制泛光效果的清晰度。值越高,泛光效果看起来越模糊。
  • resolution:分辨率属性定义创建泛光效果的精确程度。如果将其设置得太低,结果将看起来很块状。

理解这些属性的更好方法是使用前面提到的示例 multi-passes.html 来试验它们。以下屏幕截图显示了具有高西格玛大小和高强度的泛光效果:

image-20230529214156791

图11.5-使用BloomPass的开花效应

我们将要看的下一个简单效果是DotScreenPass效果

将场景输出为一组点

使用DotScreenPass与使用BloomPass非常相似。我们刚刚看到了BloomPass的实际应用。现在让我们看一下 DotScreenPass 的代码:

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

有了这个效果,我们不需要效果复制将结果输出到屏幕上。

DotScreenPass 也可以配置为一些属性,如下所示:

  • center:使用 center 属性,可以微调点的偏移方式。
  • angle:点以某种方式对齐。使用角度属性,可以更改此对齐方式。
  • scale:有了这个,我们可以设置要使用的点的大小。刻度越低,点越大。

适用于其他着色器的内容也适用于此着色器。获得正确的设置要容易得多使用实验,如下图所示:

image-20230529214507207

图 11.6 – 使用 DotScreenPass 的点屏效果

在我们继续使用下一组简单的着色器之前,我们将首先看看我们是如何在同一个屏幕上渲染多个场景的。

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

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

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() 函数。如果我们每次在 composer 上调用 render() 函数时都不这样做,那么屏幕的先前渲染部分将被清除。使用这种方法,我们只在渲染循环开始时清除所有内容。

为了避免所有 composer 在同一空间中渲染,我们将 composer 使用的渲染器的视口函数设置为屏幕的不同部分。此函数采用四个参数:x、y、宽度和高度。正如您在代码示例中所看到的,我们使用此函数将屏幕划分为四个区域,并使作曲家渲染到各自的区域。请注意,如果需要,您还可以将此方法用于多个场景、相机和 WebGLRenderer 实例。通过此设置,渲染循环会将四个 EffectComposer 对象中的每一个渲染到屏幕的各个部分。让我们快速看一下另外几个通行证。

其他简单通道

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

image-20230529215059148

图11.7-另一组四组通道

我们不会在这里详细介绍,因为这些通道的配置方式与前面的部分。在此示例中,您可以看到以下内容:

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

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

要正确查看 OutlinePass,您可以将场景背景设置为黑色并缩小一点:

image-20230529221014035

图11.8-显示场景轮廓的轮廓通道

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

使用蒙版的高级效果器流

在前面的示例中,我们将后处理通道应用于完整的屏幕。但是,Three.js 还能够仅将通道应用于特定区域。在本节中,我们将执行以下步骤:

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

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

image-20230529221444507

图 11.9 – 使用蒙版将效果应用于屏幕的一部分

我们需要做的第一件事是设置我们将要渲染的各种场景:

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

要创建地球和火星球体,我们只需使用正确的材质和纹理创建球体,并将它们添加到其特定场景中。对于背景场景,我们加载一个纹理并将其设置为场景背景。这显示在下面的代码中(addEarth和addMars只是保持代码清晰的辅助函数;它们创建一个简单的 THREE.Mesh 和THREE.SphereGeometry。创建一些灯光,并将它们全部添加到 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)

在此示例中,我们使用场景的背景属性来添加星空背景。还有另一种创建背景的方法。我们可以使用THREE.OrhoGraphicCamera.,渲染对象的大小在离相机更近或更远时不会改变,因此,通过定位一个 THREE.PlaneGeometry对象直接在THREE.rhoGraphicCamera前面,我们也可以创建一个背景。

现在,我们已经有了三个场景,可以开始设置通道和效果编辑器了。让我们从完整的通道链开始,之后我们将查看各个通道:

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)

要使用蒙版,我们需要以稍微不同的方式创建效果编辑器。我们需要将内部使用的呈现目标的 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的代码,接下来的三个过程是marsMask,effectColorify和clearMask。首先,我们将看看这三个通道是如何定义的:

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 步骤。在此示例中,这意味着添加蓝色发光的效果着色通道仅应用于在 sceneTMars 中渲染的对象。

我们使用相同的方法将棕褐色过滤器应用于地球物体。我们首先根据地球场景创建一个蒙版,并在EffectComposer中使用此蒙版。使用MaskPass后,我们添加要应用的效果(在本例中为effectSepia),完成后,我们添加ClearMaskPass以再次删除蒙版。

这个特定的效果编辑器的最后一步是我们已经看到的。我们需要将最终结果复制到屏幕上,然后再次使用效果复制通道。通过此设置,我们可以应用我们希望成为整个屏幕一部分的效果。但请注意,如果火星场景和地球场景重叠,这些效果将应用于渲染图像的一部分。

两者的效果将应用于屏幕的该部分:

image-20230529222751088

图 11.10 – 当蒙版重叠时,将应用两种效果

在使用 MaskPass 时,还有一个有趣的属性,那就是反向属性。如果此属性设置为 true,则蒙版将反转。换句话说,效果应用于除传递到 MaskPass 的场景之外的所有内容。这显示在以下屏幕截图中,我们将 earthMask 的反属性设置为 true:

image-20230529222951873

在我们继续讨论ShaderPass之前,我们将看看两个提供更高级效果的通道:BokehPass和SSAOPass。

高级通道-散景(Bokeh)

使用 BokehPass,您可以为场景添加散景效果。在散景效果中,只有部分场景清晰对焦,其余场景看起来很模糊。要查看此效果的实际效果,您可以打开 bokeh.html示例:

image-20230529223339423

图11.12-一个未聚焦的散景效应

最初,当您打开它时,整个场景看起来会很模糊。使用右侧的散景控件,您可以将焦点值设置为场景中要对焦的部分,并使用光圈属性来确定应对焦的区域的大小。通过滑动焦点,可以使前景中的立方体集成为焦点,如下所示:

image-20230529223552428

图11.13 – 散景聚焦于第一组立方体

或者,如果我们进一步滑动焦点,我们就可以聚焦于红色的立方体:

image-20230529223643119

图11.14 – 散景聚焦于第二组立方体

而且,如果我们进一步滑动焦点,我们就可以关注场景的另一组绿色立方体:

image-20230529223730133

图 11.15 – 散景聚焦在第三组立方体上

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 的结果:

image-20230529224119814

图11.16-应用的AO通道

在没有应用环境光遮蔽滤镜的情况下,类似的场景看起来非常平坦,如下所示:

image-20230529224242110

图 11.17 – 没有 AO 通道的同一场景

请注意,如果您使用这个,您必须密切关注应用程序的整体性能,因为这是一个非常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:将颜色覆盖应用于屏幕。我们已经在掩码示例中看到了这个。
  • ColorCorrectionShader:使用此着色器,可以更改颜色分布。
  • GammaCorrectionShader:将伽马校正应用于渲染场景。这使用固定的伽玛系数2。请注意,您也可以使用gammaFactor、gammaInput和gammaOutput属性直接在THRE.WebGLRenderer上设置伽玛校正。
  • HueSaturationShader:这允许您更改颜色的色调和饱和度。
  • KaleidoShader:这为场景添加了万花筒效果,在场景中心周围提供径向反射。
  • LuminosityShader和LuminostyHighPassShader:这提供了亮度效果,其中显示了场景的亮度。
  • MirrorShader:这会为部分屏幕创建镜像效果。
  • PixelShader:这将创建像素化效果。
  • RGBShiftShader:此着色器分离颜色的红色、绿色和蓝色成分。
  • SepiaShader:这会在屏幕上创建类似深褐色的效果。
  • SobelOperatorShader:这提供了边缘检测。
  • VignetteShader:这会产生渐晕效果。此效果显示图像中心周围的深色边框。

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

  • HorizontalBlurShader 和 VerticalBlurShader:这些操作会将模糊效果应用到整个场景中。
  • HorizontalTiltShiftShader 和 VerticalTiltShiftShader:这些都重现了一种倾斜-位移效应。有了倾斜位移效果,通过确保只有部分图像是清晰的,可以创建看起来很微型的场景。
  • FocusShader:这是一个简单的着色器,它会导致一个急剧渲染的中心区域,沿其边界模糊。

最后,我们不会详细介绍一些着色器;为了完整起见,我们列出了它们。这些着色器主要由另一个着色器或我们在本章开头讨论的着色器过程在内部使用:

  • THRE.FXAShader:此着色器在后处理阶段应用抗锯齿效果。如果在渲染过程中应用抗锯齿过于昂贵,请使用此选项。
  • THREE.ConvolutionShader:BloomPass渲染过程内部使用此着色器。
  • THREE.DepthLimitedBlurShader:SAOPass内部用于环境遮挡。
  • THREE.HalftoneShader:这是HalftonePass内部使用的。
  • THREE.SAOShader:这提供了着色器形式的环境遮挡。
  • THREE.SSAOShader:这为着色器形式的环境遮挡提供了一种替代方法。
  • THREE.SMAAShader:这为渲染场景提供了抗锯齿功能。
  • THREE.ToneMapShader:AdaptiveToneMappingPass内部使用该着色器。
  • UnpackDepthRGBAShader:可用于将RGBA纹理中的编码深度值可视化为视觉颜色。

如果您浏览Three.js分布的“Shaders”目录,您可能会注意到本章中未列出的其他几个着色器。这些着色器——FresnelShader、OceanShader、ParallaxShader和WaterRefractionShader——不是可以用于后处理的着色器,但它们应该与我们在第4章“使用THREE.js材质”中讨论的THRE.ShaderMaterial对象一起使用。

我们将从一些简单的着色器开始

简单着色器

为了尝试基本的着色器,我们创建了一个示例,您可以使用大多数着色器,并直接在场景中查看效果。你可以在shaders.html上找到这个例子。下面的屏幕截图显示了一些效果。

image-20230530212904973

图11.18–BrightnessContractShader效果

SobelOperatorShader效果检测轮廓:

image-20230530213037704

图11.19–SobelOperatorShader效果

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

image-20230530213200837

图11.20–KaleidoShader效果

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

image-20230530213316951

图11.21–MirrorShader效果

RGBShiftShader效果如下所示:

image-20230530213442177

图11.22–RGBShiftShader 效果

您可以使用LuminosityHighPassShader在场景中播放亮度:

image-20230530213735001

图11.23-亮度高通道着色器效果

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

模糊着色器

在本节中,我们将不再深入讨论代码;我们将向您展示各种模糊着色器的结果。您可以使用shaders-blur.html示例进行这些实验。显示的前两个着色器是HorizontalBlurShader和VerticalBlurShader:

image-20230530213941419

图11.24–水平遮光板和垂直遮光板

HorizontalTiltShiftShader和VerticalTiltShiftShader提供了另一种类似模糊的效果。该着色器不会模糊整个场景,只会模糊一小部分区域。这提供了一种称为倾斜偏移的效果。这通常用于从普通照片中创建类似微缩的场景。下面的屏幕截图显示了这种效果:

image-20230530214202600

图11.25- HorizontalTiltShiftShader 和 VerticalTiltShiftShader

最后一个类似模糊的效果由FocusShader提供:

image-20230530214258427

图11.26-聚焦着色器

到目前为止,我们已经使用了Three.js提供的着色器。但是,也可以编写自己的着色器用于Three.EffectComposer。

创建自定义后处理着色器

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

顶点着色器和片段着色器

创建顶点和片段着色器是一个非常广泛的主题。在本节中,我们将只触及这些着色器可以做什么以及它们如何工作的表面。有关更深入的信息,您可以在上找到WebGL规范http://www.khronos.org/webgl/.另一个资源是Shadertoy,上面有很多例子,可在https://www.shadertoy.com,或 Shaders手册: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 }
 },
 // 0.2126 R + 0.7152 G + 0.0722 B
 // 顶点着色器对于后处理步骤总是相同的
 
 vertexShader: [
 'varying vec2 vUv;',
 'void main() {',
 'vUv = uv;',
 'gl_Position = projectionMatrix * modelViewMatrix * 
 vec4( position, 1.0 );',
 '}'
 ].join('\n'),
 fragmentShader: [
 // 传入我们自定义的 uniforms
 'uniform float rPower;',
 'uniform float gPower;',
 'uniform float bPower;',
 // 传入我们将要修改的图像/纹理
 'uniform sampler2D tDiffuse;',
 // 用于确定我们正在处理的正确texel
 'varying vec2 vUv;',
 // 对每个像素并行执行
 'void main() {',
 // 从我们正在处理的纹理中获取像素
 (called a texel)
 '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(将对象的位置映射到世界位置)来确定在屏幕上渲染顶点的位置。对于后处理,这段代码中唯一有趣的事情是,使用可 varying vec2 vUv 变量将指示从纹理读取哪个纹素的uv值传递给片段着色器。

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

在这里,我们可以看到 uniform 的四个实例。uniform属性的实例具有从JavaScript传递到着色器的值,这些值对于处理的每个片段都是相同的。在这种情况下,我们传入三个float,由float类型标识(用于确定颜色在最终灰度图像中的比例),还传入一个纹理(tDiffuse),由tDiffuse类型标识。此纹理包含EffectComposer实例上一个过程中的图像。Three.js确保在使用tDiffuse作为其名称时将该纹理传递给该着色器。我们还可以自己从JavaScript中设置统一属性的其他实例。在使用JavaScript中的这些统一之前,我们必须定义要向JavaScript公开哪些统一属性。此操作如下所示,位于着色器文件的顶部:

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)的纹素(纹理中的像素)。接下来,我们使用该纹素的r、g和b属性来计算灰度值。该灰度值设置为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)

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

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

这个结果可以在custom-shaders-scene.html中看到。下面的屏幕截图显示了此示例:

图11.27-一个自定义灰度滤波器

image-20230530220657693

让我们来创建另一个自定义着色器。这一次,我们将把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在当前屏幕中传递的内容,第二个由我们定义为一个整数(类型:“i”),用作我们想要呈现结果的颜色深度。代码本身非常简单:

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

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

image-20230530221523144

图11.28-一个自定义位滤波器

这就是本章关于后处理的内容。

总结

我们在本章中讨论了许多不同的后处理选项。正如您所看到的,创建EffectComposer和将过程链接在一起实际上非常容易。你只需要记住几件事。并非所有的传球都会在屏幕上显示输出。如果要输出到屏幕,则始终可以将ShaderPass与CopyShader一起使用。向编写器添加过程的顺序很重要。效果按顺序应用。如果要重用特定EffectComposer实例的结果,可以使用TexturePass来执行此操作。当EffectComposer中有多个RenderPass时,请确保将clear属性设置为false。如果没有,您将只看到最后一个RenderPass步骤的输出。如果只想将效果应用于特定对象,则可以使用“遮罩过程”。完成遮罩后,请使用ClearMaskPass清除遮罩。除了Three.js提供的标准过程之外,还有许多标准着色器可用。您可以将这些与ShaderPass一起使用。使用Three.js中的标准方法,创建用于后处理的自定义着色器非常容易

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