后期处理是在最终渲染结果上添加额外的效果。通常在电影制作中使用这种技术,但我们也可以在WebGL中使用。
下面是一些常见的可以

  • 景深(像相机一样,远处的东西看起来模糊)
  • 光晕效果(光源周围的一种明亮的效果)
  • 神光(通过物体的光线形成的效果)
  • 运动模糊(快速移动的物体周围的模糊效果)
  • 故障效果(图像看起来像是出了问题)
  • 轮廓线(强调物体边缘的线条)
  • 颜色变化(改变图像的颜色风格)
  • 抗锯齿(使图像边缘更平滑,减少锯齿状的感觉)
  • 反射和折射(模拟光在物体表面或者通过物体时的行为)

[1]工作原理

对于大多数情况,后处理的工作方式都是类似的。下面针对three.js中后处理的工作原理进行简单理解

渲染目标

渲染目标一词是特定于three.js的,在其他地方通常会称为缓冲区
我们并不是直接在画布上进行渲染,而是在我们称之为"渲染目标"的地方进行渲染。这个渲染目标将会给我们一个非常类似于常规纹理的纹理。说得更简单一点,我们是在一个纹理上进行渲染,而不是在屏幕上的画布上。(也就是webgl中离屏渲染的概念,一个简单的例子是将离屏渲染的东西作为纹理添加到物体上,最后渲染到画布中)
然后,这个纹理被应用到一个面对摄像头并覆盖整个视图的平面上(画布)。这个平面使用了一个特殊的片元着色器的材质,该着色器将进行后期处理效果。如果后期处理效果只是将图像变红,那么它只需在那个片元着色器中将像素的红色值乘以一个值(通常不只只是调整颜色值这么简单)。最后显示到画布上。
在three.js中,这些后处理效果(effects),被称为通道(passes)

乒乓缓冲

我们的后处理可以有多个通道(passes):比如一个做运动模糊,一个做颜色变化,一个做景深等等。
正因为我们可以有多个通道,后处理需要两个渲染目标。原因是我们不能在同一时间获取一个渲染目标的纹理同时在它上面绘制。这个思路就是在第一个渲染目标上绘制,同时从第二个上获取纹理。在下一次通道时,我们交换这两个渲染目标,从第二个上取纹理,再在第一个上绘制。然后在下一个通道再交换,如此反复。这就是我们所说的乒乓缓冲。

在画布上的最后一次通道

最后一次通道不会绘制在一个渲染目标上,因为我们可以直接将其放在画布上,这样用户就可以看到最后的结果了。

[2]EffectComposer

在three.js中EffectComposer类可以帮我们在通过后处理实现一些效果时处理大部分繁重的东西,该类管理一系列后处理通道,以生成最终的视觉效果。后处理通道按其添加/插入的顺序执行。最后一个通道会自动呈现到屏幕。
EffectComposer将处理创建渲染目标,执行乒乓球操作,将上一个通道的纹理发送到当前通道,在画布上绘制最后一个通道等所有过程。
下面为构建一个后处理通道的基本步骤:

/**
 * Renderer(普通的渲染器)—
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    antialias: true
})
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFShadowMap
renderer.physicallyCorrectLights = true
renderer.outputEncoding = THREE.sRGBEncoding
renderer.toneMapping = THREE.ReinhardToneMapping
renderer.toneMappingExposure = 1.5
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

/**
* Post processing
*/
const effectComposer = new EffectComposer(renderer)
effectComposer.setSize(sizes.width, sizes.height)//参数同render
effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2))//参数同render
//实例化第一个通道并添加到effectComposer中
const renderPass = new RenderPass(scene, camera)
effectComposer.addPass(renderPass)

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Update controls
    controls.update()

    // Render
    // renderer.render(scene, camera)把原理的取消掉
    effectComposer.render()

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

在tick中将renderer.render(scene, camera)替换effectComposer.render()effectComposer将使用乒乓球及其渲染目标进行渲染
由于上面代码只创建了一个通道renderPass,所以和原来直接在画布中绘制没什么区别。
下面为一些可用的通道列表
https://threejs.org/docs/index.html#examples/en/postprocessing/EffectComposer
下面将使用其中的一些来进行实验,并创建一些自己的通道

[3]DotScreenPass

DotScreenPass 将应用某种黑白光栅效果,因为这是three.js带的后处理通道,所以使用也比较简单:

import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'

//添加改通道至effectComposer即可
const dotScreenPass = new DotScreenPass()
effectComposer.addPass(dotScreenPass)

image.png
要去除改通道可直接使用其属性enabled

const dotScreenPass = new DotScreenPass()
dotScreenPass.enabled = false
effectComposer.addPass(dotScreenPass)

[4]GlitchPass

GlitchPass 这将添加屏幕故障,例如当相机在电影中被黑客入侵时。
GIF 2023-8-6 12-41-01.gif
导入添加:

import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'

// ...

const glitchPass = new GlitchPass()
effectComposer.addPass(glitchPass)

一些通道还具有一些有意思的属性,具体即可查看相应的文档
例如GlitchPass通道的一个goWild属性也挺有意思的,曝闪效果

[5]RGBShiftPass

有些内置的后处理通道需要做一些处理才能使用比如RGBShiftPass通道,它是由
ShaderPass、RGBShiftShader组成的
RGBShift 不是作为通道提供,而是作为着色器提供。我们需要导入此着色器并将其添加于 effectComposer , ShaderPass

import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
import { RGBShiftShader } from 'three/examples/jsm/shaders/RGBShiftShader.js'
// ...

const rgbShiftPass = new ShaderPass(RGBShiftShader)
effectComposer.addPass(rgbShiftPass)

后处理半色调效果:
image.png

[6]抗锯齿

使用后处理通道实现一些效果后,有些时候你回发现webglrender中配置的抗锯齿效果不起作用了,(通常如果你的屏幕像素比超过1就不会出现这个问题)
还有如果你的后处理只添加了renderPass通道也不会出现抗锯齿问题,因为渲染是在具有抗锯齿支持的画布中完成的。
因为EffectComposer渲染目标默认不支持抗锯齿。不过我们有三个可用的抗锯齿选项:

  • 使用一种可以管理抗锯齿的特定渲染目标,但它不适用于所有现代浏览器(需要支持webgl2)
  • 使用一个通道来实现抗锯齿,但性能会有所下降。
  • 结合上两种方案,先测试浏览器是否支持可管理抗锯齿的特定渲染目标,如果不支持再使用抗锯齿通道

使用 WebGLMultisampleRenderTarget

默认情况下,EffectComposer 使用的是 WebGLRenderTarget,我们可以提供自己的渲染目标作为EffectComposer 第二个参数,为了实验学习,我们先提供一个和原先默认一样的渲染目标。看看能正常运行,如果没问题在构建要给支持抗锯齿的渲染目标。
可以通过查看/node_modules/three/examples/jsm/postprocessing/EffectComposer.js来了解EffectComposer的一些具体参数信息。

const renderTarget = new THREE.WebGLRenderTarget(
  800,//width
  600,
  {
    minFilter: THREE.LinearFilter,
    magFilter: THREE.LinearFilter,
    format: THREE.RGBAFormat
  }
)

前两个参数是 width 和 height 。我们可以使用随机值,因为当调用 setSize(…) 函数时,渲染目标的大小将被调整 effectComposer 。
第三个参数是一个对象

然后将该渲染目标传递给EffectComposer实例化

const effectComposer = new EffectComposer(renderer, renderTarget)

这样就通过使用我们自己创建的渲染目标下渲染出了和之前使用默认渲染目标一样的渲染结果
WebGLMultisampleRenderTarget类似于WebGLRenderTarget,但支持多样本抗锯齿(MSAA
只不过WebGLMultisampleRenderTarget需要支持webgl2的环境下才能使用
WebGLMultisampleRenderTarget替换为WebGLRenderTarget即可发现完成了抗锯齿了

const renderTarget = new THREE.WebGLMultisampleRenderTarget(
// ...
)

使用抗锯齿通道

现在为止一般的浏览器都是支持webgl2了,可以直接使用WebGLMultisampleRenderTarget来实现抗锯齿,不过如果有一些特殊的需求,非要在webgl1支持的浏览器中抗锯齿的话,就只能使用抗锯齿通道了
对于抗锯齿通道,有不同的选择:

  • FXAA:性能消耗一般,但结果也是“一般”,可能会模糊
  • SMAA:通常比 FXAA 更好,但性能较差 — 不要与 MSAA 混淆
  • SSAA:质量最好,但性能最差
  • TAA:性能高但结果有限

对于这些选择可以根据实际的视觉效果和性能需求来进行选择

在这里那SMAA通道进行测试
导入 SMAAPass ,实例化它并将其添加到 effectComposer :

import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'

// ...

const smaaPass = new SMAAPass()
effectComposer.addPass(smaaPass)

抗锯齿应该就没问题了

权衡选择

  • 如果像素比高于 1 ,推荐使用WebGLRenderTarget,不进行抗锯齿处理
  • 如果像素比为 1,并且浏览器支持 WebGL 2,推荐使用 WebGLMultisampleRenderTarget进行抗锯齿处理
  • 如果像素比为1,但浏览器不支持 WebGL 2,我们使用 WebGLRenderTarget 并启用 SMAA渲染通道

对于像素比的获取可以通过renderer.getPixelRatio()方法获取到,而浏览器是否支持webgl2也可以通过擦好看renderer.capabilities中的isWebGL2属性获取到
有了上的API,就可以封装一个判断来使用那种方式来处理抗锯齿了

let RenderTargetClass = null

if(renderer.getPixelRatio() === 1 && renderer.capabilities.isWebGL2){
  RenderTargetClass = THREE.WebGLMultisampleRenderTarget
  console.log('Using WebGLMultisampleRenderTarget')
}
else{
  RenderTargetClass = THREE.WebGLRenderTarget
  console.log('Using WebGLRenderTarget')
}
  const renderTarget = new RenderTargetClass(
  // ...
)


if(renderer.getPixelRatio() === 1 && !renderer.capabilities.isWebGL2){
const smaaPass = new SMAAPass()
effectComposer.addPass(smaaPass)

console.log('Using SMAA')
}

[7]UnrealBloomPass

虚幻的BloomPass,有点炫酷(⊙o⊙)的
这个通道也是比较常用的,可是实现光辉、烈火环境、激光、光剑或放射性物质等
导入与使用

import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'

// ...

const unrealBloomPass = new UnrealBloomPass()
effectComposer.addPass(unrealBloomPass)

image.png
这是默认效果,它主要有3个些参数可供我们调整:

  • strength :光芒有多强
  • radius :亮度可以传播多远
  • threshold :在亮度极限下,事物开始发光
const unrealBloomPass = new UnrealBloomPass()
effectComposer.addPass(unrealBloomPass)
unrealBloomPass.strength = 0.3
unrealBloomPass.radius = 1
unrealBloomPass.threshold = 0.6

gui.add(unrealBloomPass, 'enabled')
gui.add(unrealBloomPass, 'strength').min(0).max(2).step(0.001)
gui.add(unrealBloomPass, 'radius').min(0).max(2).step(0.001)
gui.add(unrealBloomPass, 'threshold').min(0).max(1).step(0.001)

image.png

[8]构建自定义通道

创建自定义通道类似于创建自定义着色器

色调通道

这里从实现一个简单的色彩滤镜通道开始
首先,创建一个着色器:

const TintShader = {
    uniforms:{
      tDiffuse: { value: null }
    },
    vertexShader: `
     varying vec2 vUv;
      void main(){
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      vUv = uv;
      }
    `,
    fragmentShader: `
        
        uniform sampler2D tDiffuse;
        varying vec2 vUv;
     
      void main(){
      vec4 color = texture2D(tDiffuse, vUv);
      color.r += 0.1; //红色滤镜效果
        gl_FragColor = color;
      }
    `
}

需要从上一个通道中获取纹理,这些纹理回自动保存到tDiffuse uniform变量中,不需要手动传递,设定为null即可,uv坐标也是默认的获取即可。
然后将该使用该着色器构建要给通道即可

const tintPass = new ShaderPass(TintShader)
effectComposer.addPass(tintPass)

image.png
还可以通过定义一个uniform动态控制色调:

const TintShader = {
  uniforms:{
    tDiffuse: { value: null },
    uTint: { value: null }
  },
  
  // ...
  
  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform vec3 uTint;
    
    varying vec2 vUv;
    
    void main(){
      vec4 color = texture2D(tDiffuse, vUv);
      color.rgb += uTint;
      
      gl_FragColor = color;
  	}
  `
}
const tintPass = new ShaderPass(TintShader)
tintPass.material.uniforms.uTint.value = new THREE.Vector3()
gui.add(tintPass.material.uniforms.uTint.value, 'x').min(- 1).max(1).step(0.001).name('red')
gui.add(tintPass.material.uniforms.uTint.value, 'y').min(- 1).max(1).step(0.001).name('green')
gui.add(tintPass.material.uniforms.uTint.value, 'z').min(- 1).max(1).step(0.001).name('blue')

image.png

[9]位移通道
下面测试通过自定义通道来实现位移效果,而不是仅像上面的只是调色
创建要一个新的着色器DisplacementShader

const DisplacementShader = {
  uniforms:{
  	tDiffuse: { value: null }
  },
  vertexShader: `
    varying vec2 vUv;
    
    void main(){
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      
      vUv = uv;
    }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    
    varying vec2 vUv;
    
    void main(){
    vec2 newUv = vec2(vUv.x,vUv.y + sin(vUv.x * 10.0) * 0.1);
    vec4 color = texture2D(tDiffuse, newUv);
      
    gl_FragColor = color;
    }
  `
}

const displacementPass = new ShaderPass(DisplacementShader)
effectComposer.addPass(displacementPass)

在y、x轴上应用了sin函数:
image.png
也可以通过传递要给uTime变量过去做个动画:

const DisplacementShader = {
  uniforms:{
    tDiffuse: { value: null },
    uTime: { value: null }
  },

// ...

  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform float uTime;
    
    varying vec2 vUv;
    
    void main(){
    vec2 newUv = vec2(
        vUv.x,
        vUv.y + sin(vUv.x * 10.0 + uTime) * 0.1
    );
    vec4 color = texture2D(tDiffuse, newUv);
    
    gl_FragColor = color;
    }
  `
}

const clock = new THREE.Clock()

const tick = () =>{
  const elapsedTime = clock.getElapsedTime()
  
  // Update passes
  displacementPass.material.uniforms.uTime.value = elapsedTime
  
  // ...
}

[10]添加法线贴图

下面通过在上面自定义通道的基础上添加一个法线贴图来实现屏幕的遮面效果:
这是法线贴图的图:
image.png

片段着色器代码:

fragmentShader: `

  uniform sampler2D tDiffuse;
  uniform sampler2D uNormalMap;
  
  varying vec2 vUv;

void main(){
        vec3 normalColor = texture2D(uNormalMap, vUv).xyz * 2.0 - 1.0;
        vec2 newUv = vUv + normalColor.xy * 0.1;
        vec4 color = texture2D(tDiffuse, newUv);

        vec3 lightDirection = normalize(vec3(- 1.0, 1.0, 0.0));
        float lightness = clamp(dot(normalColor, lightDirection), 0.0, 1.0);
        color.rgb += lightness * 2.0;

        gl_FragColor = color;
      }
`

image.png

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐