十八、Three.js后期处理与自定义着色器

在前边章节中,我们已经讲完了three.js的常用功能,今天我们来讲解一个主要功能——渲染后期处理,接下来我们会对这个功能进行介绍,除此之外,我们也会介绍着色器。

我们将要讲解以下知识:

  1. 配置three.js,用于后期处理。
  2. 介绍three.js的基本后期处理通道。
  3. 使用掩码将效果应用到部分场景。
  4. 模糊效果和高级滤镜。
  5. 简单的色器。

1. 配置three.js以进行后期处理

为了可以使three.js可以进行后期处理,我们将要对以下配置进行修改:

  • 创建EffectComposer(效果组合器)对象,在该对象上我们可以添加后期处理通道。
  • 配置EffectComposer对象,使它可以渲染场景,并应用后期处理。
  • 在渲染循环中,使用 EffectComposer 来渲染场景、应用通道和输出结果。

在开发前,我们需要保证加载以下依赖:

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
  • EffectComposer 文件用于提供 EffectComposer 对象,以便添加后期处理步骤。
  • RenderPass 文件用于在EffectComposer 对象上添加渲染通道,如果没有通道,我们的场景就不会被渲染。

首先,我们通过一个案例 —— 模拟电视屏幕效果,来认识EffectComposer 。

最终效果图

我们先引入文件,该文件是电视效果渲染通道:

import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass'

首先我们需要创建一个EffectComposer 对象,你可以在对象的构造函数里传入THREE.WebGLRenderer。

// 定义渲染器
  const renderer = new THREE.WebGLRenderer({
    'antialias':true,
    'alpha':true
  // 创建 EffectComposer 对象
  const composer = new EffectComposer(renderer)

接下来,我们要在组合器中添加各种通道。

每个通道都会按照加入EffectComposer的顺序来执行,我们将第一个通道RenderPass 加入,下面这个通道会渲染场景,但是不会输出到屏幕。

// 创建 EffectComposer 对象
  const composer = new EffectComposer(renderer)
  // 加入RenderPass通道
  const renderPass = new RenderPass(scene, camera)
  composer.addPass(renderPass)

如上所示,调用addPass就可以把RenderPass添加到EffectComposer上。接下来,添加一个可以把结果输出到屏幕上的通道,当然,并不是所有通道都能实现这个功能,本案例我们将FilmPass通道来将结果输出到屏幕上。我们先创建该对象,然后添加到组合器中。

  // 创建 FilmPass 对象通道,并添加到组合器
  const effectFilm = new FilmPass(0.8, 0.325, 256, false)
  effectFilm.renderToScreen = true
  composer.addPass(effectFilm)

如你所见,我们在创建FilmPass时,设置了renderToScreen 为true,所以,当我们将这个通道添加到组合器中时,场景会被渲染并通过FilmPass将结果输出到屏幕。

接下来,我们创建一个物体,展示到场景中。

// 添加一个地球
  function createSphere (material) {
    const sphereGeometry = new THREE.SphereGeometry(20, 100, 100)
    const mesh = new THREE.Mesh(sphereGeometry, material)
    return mesh
  const textureLoader = new THREE.TextureLoader()
  const cubeMaterial = new THREE.MeshLambertMaterial({
    'map': textureLoader.load('img/textures/earth/Earth.png'),
    'normalMap': textureLoader.load('img/textures/earth/EarthNormal.png')
  const sphere = createSphere(cubeMaterial)
  scene.add(sphere)

最后,我们更新渲染循环。

...
const clock = new THREE.Clock()
// 帧率动画
  function renderScence () {
    const delta = clock.getDelta()
    sphere.rotation.y += 0.01
    trackballControls.update(clock)
    requestAnimationFrame(renderScence)
    // renderer.render(scene, camera)
    composer.render(delta)
  renderScence()

2. 后期处理通道

Three.j提供了许多后期处理通道,这些通道可以直接添加到EffectComposer组合器中。

新版three.js中,通道对象文件都在该目录下:three/examples/jsm/postprocessing/

例如:

import { AdaptiveToneMappingPass } from 'three/examples/jsm/postprocessing/AdaptiveToneMappingPass'

接下来,我会列出three.js提供的所有处理通道:

名称 描述
AdaptiveToneMappingPass 该通道可以根据场景的光照度自动调节场景的亮度。
BloomPass 该通道通过增强场景中明亮的区域来模拟真实世界中的摄像机。
BokehPass 该通道可以实现类似大光圈镜头的景深效果。
ClearPass 该通道清空当前纹理缓存。
CubeTexturePass 用于渲染天空盒。
DotScreenPass 将黑点图层应用于屏幕的原始图片上。
FilmPass 通过扫描线和失真来模拟电视屏幕效果。
GlitchPass 随机在屏幕上显示电脉冲。
HalftonePass 用于模拟传统印刷术的半色调效果。
MaskPass 在图片上显示掩码,后续通道只会影响到掩码区域。
OutlinePass 勾勒出场景中的物体轮廓。
RenderPass 在当前场景和摄像机的基础上渲染出一个新场景。
SAOPass 实现实时环境光遮挡效果。
SMAAPass 全屏反锯齿效果。
SSAARenderPass 使用另一种算法实现全屏反锯齿效果。
SSAOPass 使用另一种算法实现实时环境光遮挡效果
SavePass 该通道执行时会赋值当前渲染结果,在后续步骤中可以使用。实际应用中用处不大。
ShaderPass 自定义着色器通道,可以传入自定义着色器作为参数,以生成一个高级、自定义的后期处理通道。
TAARenderPass 也是一个全屏反锯齿效果。
TexturePass 将合成器的当前状态保存为纹理,然后将其作为参数传入到其他的EffectComposer组合器中。
UnrealBloomPass 该通道与 Bloom 类似,但是它实现的效果更接近于Unreal3D引擎的Bloom效果。

接下来,我们会详细介绍那些常用通道的使用方法。

2.1 简单的后期处理通道

我们先来看看使用 FilmPass、BloomPass、DotScreenPass可以做什么。

在以上案例中,我们展示了四个场景,这四个场景分别使用了四种不同的处理通道。左上角使用的是FilmPass,右上角使用的是DotScreenPass,左下角使用的是BloomPass,右下角使用原始渲染结果。

这个示例中,我们使用了ShaderPass和TexturePass,这两个通道可以将原始渲染结果输出作为其它三个场景的输入,以此来重用原始渲染结果。所以在看各个通道前,我们先来看看这两个通道:

  const renderPass = new RenderPass(scene, camera)
  const effectCopy = new ShaderPass(CopyShader)
  effectCopy.renderToScreen = true
  const composer = new EffectComposer(renderer)
  composer.addPass(renderPass)
  composer.addPass(effectCopy)
  const renderedScene = new TexturePass(composer.renderTarget2)

这段代码我们创建了EffectComposer对象,该对象输出默认场景(右下角原始场景)。这个组合器有两个通道:RenderPass(用来渲染场景)和ShaderPass(使用CopyShader进行配置后,如果renderToScreen 属性设置为true时,渲染结果会直接输出到屏幕上而不会有进一步的后期处理)。

我们创建一个TexturePass,将其传入到composer.renderTarget2中,这样就可以将renderedScene 作为变量传入其它组合器的输入,而不用从零开始渲染场景。

FilmPass创建类似电视的效果

  // 电视效果
  const effectFilm = new FilmPass(0.8, 0.325, 256, false)
  effectFilm.renderToScreen = true
  const effectFilmComposer = new EffectComposer(renderer)
  // 创建新场景
  effectFilmComposer.addPass(renderedScene)
  // 添加FilmPass通道
  effectFilmComposer.addPass(effectFilm)
  // 使用effectCopy输出渲染
  effectFilmComposer.addPass(effectCopy)

FilmPass接受如下参数:

  1. noiseIntensity 控制场景粗糙程度
  2. scanlinesIntensity 扫描线的显著程度
  3. scanLinesCount 扫描线对的数量
  4. grayscale 是否为灰度图

这些参数可以直接通过构造函数进行传递,当然你也可以直接设置它们:

effectFilm.uniforms.grayscale.value = true

BloomPass在场景添加泛光效果

当使用泛光效果时,场景中的明亮区域会变得更加显著,而且还会渗入到较暗的区域。

  // 泛光效果
  const bloomPass = new BloomPass()
  const bloomComposer = new EffectComposer(renderer)
  bloomComposer.addPass(renderedScene)
  bloomComposer.addPass(bloomPass)
  bloomComposer.addPass(effectCopy)

BloomPass所有可设置的属性如下:

  1. Strength 定义泛光强度。
  2. kernelSize 泛光效果偏移量。
  3. sigma 泛光效果锐利程度。
  4. Resolution 泛光效果精确度。

DotScreenPass将场景作为点集输出

  // 将场景作为点集输出
  const dotScreenPass = new DotScreenPass()
  const dotScreeComposer = new EffectComposer(renderer)
  dotScreeComposer.addPass(renderedScene)
  dotScreeComposer.addPass(dotScreenPass)
  dotScreeComposer.addPass(effectCopy)

DotScreenPass可以配置的属性如下:

  1. center 微调偏移量
  2. angle 改变对齐方式。
  3. Scale 设置所用点的大小,值越小,点越大。

在同一场景显示多个渲染器的输出结果

// 帧率动画
  const width = window.innerWidth
  const height = window.innerHeight
  const halfWidth = width / 2
  const halfHeight = height / 2
  renderer.autoClear = false
  function renderScence () {
    const delta = clock.getDelta()
    sphere.rotation.y += 0.01
    trackballControls.update(clock)
    renderer.clear()
    renderer.setViewport(0, 0, halfWidth, halfHeight)
    effectFilmComposer.render(delta)
    renderer.setViewport(0, halfHeight, halfWidth, halfHeight)
    bloomComposer.render(delta)
    renderer.setViewport(halfWidth, 0, halfWidth, halfHeight)
    dotScreeComposer.render(delta)
    renderer.setViewport(halfWidth, halfHeight, halfWidth, halfHeight)
    composer.render(delta)
    requestAnimationFrame(renderScence)
    renderer.render(scene, camera)
  renderScence()

renderer.autoClear设置为false并显示调用clear()方法,将不会每次在循环调用组合器的render方法时,会清空场景,而是只在循环开始时清空所有东西。

为了避免所有效果组合器在同一个地方渲染,我们将效果组合器所用的Renderer的视图区域设置成屏幕的不同部分。

完整代码如下:

  import * as THREE from 'three'
  // 用于鼠标移动镜头
  import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls'
  // 用于后期处理的依赖
  import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
  import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'
  import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
  import { CopyShader } from 'three/examples/jsm/shaders/CopyShader'
  import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass'
  import { TexturePass } from 'three/examples/jsm/postprocessing/TexturePass'
  import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass'
  import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass'
  // 定义场景
  const scene = new THREE.Scene()
  // 创建一个粗细为20的坐标轴
  const axes = new THREE.AxesHelper(100)
  scene.add(axes)
  // 定义摄像机
  const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight)
  camera.position.set(-50, 40, 50)
  camera.lookAt(scene.position)
  // 定义渲染器
  const renderer = new THREE.WebGLRenderer({
    'antialias':true,
    'alpha':true
  // 定义场景的颜色为黑色
  renderer.setClearColor(0x000000)
  // 定义场景大小为整个窗口
  renderer.setSize(window.innerWidth, window.innerHeight)
  // 通道
  const renderPass = new RenderPass(scene, camera)
  const effectCopy = new ShaderPass(CopyShader)
  effectCopy.renderToScreen = true
  const composer = new EffectComposer(renderer)
  composer.addPass(renderPass)
  composer.addPass(effectCopy)
  const renderedScene = new TexturePass(composer.renderTarget2)
  // 电视效果
  const effectFilm = new FilmPass(0.8, 0.325, 256, false)
  effectFilm.renderToScreen = true
  const effectFilmComposer = new EffectComposer(renderer)
  // 创建新场景
  effectFilmComposer.addPass(renderedScene)
  // 添加FilmPass通道
  effectFilmComposer.addPass(effectFilm)
  // 使用effectCopy输出渲染
  effectFilmComposer.addPass(effectCopy)
  // 泛光效果
  const bloomPass = new BloomPass()
  const bloomComposer = new EffectComposer(renderer)
  bloomComposer.addPass(renderedScene)
  bloomComposer.addPass(bloomPass)
  bloomComposer.addPass(effectCopy)
  // 将场景作为点集输出
  const dotScreenPass = new DotScreenPass()
  const dotScreeComposer = new EffectComposer(renderer)
  dotScreeComposer.addPass(renderedScene)
  dotScreeComposer.addPass(dotScreenPass)
  dotScreeComposer.addPass(effectCopy)
  // 将渲染的结果加入到div中
  document.querySelector('#webgl-box').appendChild(renderer.domElement)
  // 实现鼠标移动摄像头
  const trackballControls = new TrackballControls(camera, renderer.domElement)
  const clock = new THREE.Clock()
  // 聚光灯
  const spotLight = new THREE.SpotLight(0xFFFFFF, 1.2, 150, 120)
  spotLight.position.set(-40, 60, -10)
  spotLight.castShadow = true
  scene.add(spotLight)
  const ambientLight = new THREE.AmbientLight(0x3c3c3c)
  scene.add(ambientLight)
  // 添加一个地球
  function createSphere (material) {
    const sphereGeometry = new THREE.SphereGeometry(20, 100, 100)
    const mesh = new THREE.Mesh(sphereGeometry, material)
    return mesh
  const textureLoader = new THREE.TextureLoader()
  const cubeMaterial = new THREE.MeshLambertMaterial({
    'map': textureLoader.load('img/textures/earth/Earth.png'),
    'normalMap': textureLoader.load('img/textures/earth/EarthNormal.png')
  const sphere = createSphere(cubeMaterial)
  scene.add(sphere)
  // 帧率动画
  const width = window.innerWidth
  const height = window.innerHeight
  const halfWidth = width / 2
  const halfHeight = height / 2
  renderer.autoClear = false
  function renderScence () {
    const delta = clock.getDelta()
    sphere.rotation.y += 0.01
    trackballControls.update(clock)
    renderer.clear()
    renderer.setViewport(0, 0, halfWidth, halfHeight)
    effectFilmComposer.render(delta)
    renderer.setViewport(0, halfHeight, halfWidth, halfHeight)
    bloomComposer.render(delta)
    renderer.setViewport(halfWidth, 0, halfWidth, halfHeight)
    dotScreeComposer.render(delta)
    renderer.setViewport(halfWidth, halfHeight, halfWidth, halfHeight)
    composer.render(delta)
    requestAnimationFrame(renderScence)
    renderer.render(scene, camera)
  renderScence()
  // 窗口大小改变后,改变摄像头
  const onResize = () => {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()
    renderer.setSize(window.innerWidth, window.innerHeight)
  window.addEventListener('resize', onResize, false)

更多后期处理通道,请自己探索研究~


2.2 使用掩码的高级效果组合

在前边示例中,我们是对整个屏幕上应用后期处理通道,当然,我们也可以在特定的区域使用通道,我们接下来会采用如下步骤制作一个案例:

  1. 创建一个作为背景图片的场景。
  2. 创建一个场景,里边包含一个“地球”球体。
  3. 创建一个场景, 里边包含一个“火星”球体。
  4. 创建 EffectComposer 对象,用于将这三个场景渲染到一个图片中。
  5. 在渲染火星的球体上应用泛光效果。
  6. 在渲染为地球的球体上应用电视效果。

效果图如下:

我们讲解一下核心代码:

首先,我们需要配置各个需要被渲染的场景。

  // 配置各个需要渲染的场景
  const sceneEarth = new THREE.Scene() // 地球
  const sceneMars = new THREE.Scene() // 火星
  const sceneBG = new THREE.Scene() // 背景

接下来创建两个函数,分别用来生成地球和火星网格图形,并分别为它们添加各自的光源。

// 添加光源
  function addLight (scene) {
    const spotLight = new THREE.SpotLight(0xFFFFFF, 1.2, 150, 120)
    spotLight.position.set(-10, 30, 40)
    scene.add(spotLight)
    const ambientLight = new THREE.AmbientLight(0x3c3c3c)
    scene.add(ambientLight)
  // 创建地球
  function addEarth (scene) {
    const textureLoader = new THREE.TextureLoader()
    const cubeMaterial = new THREE.MeshLambertMaterial({
      'map': textureLoader.load('img/textures/earth/Earth.png'),
      'normalMap': textureLoader.load('img/textures/earth/EarthNormal.png')
    const sphereGeometry = new THREE.SphereGeometry(20, 100, 100)
    const mesh = new THREE.Mesh(sphereGeometry, cubeMaterial)
    scene.add(mesh)
    // 新建一个场景,用来存放可以自由控制的光源
    const pivot = new THREE.Object3D()
    addLight(pivot)
    scene.add(pivot)
    return {'earth': mesh, 'pivot': pivot}
  // 创建火星
  function addMars (scene) {
    const textureLoader = new THREE.TextureLoader()
    const cubeMaterial = new THREE.MeshLambertMaterial({
      'map': textureLoader.load('img/textures/mars/mars_1k_color.jpg'),
      'normalMap': textureLoader.load('img/textures/mars/mars_1k_normal.jpg')
    const sphereGeometry = new THREE.SphereGeometry(20, 100, 100)
    const mesh = new THREE.Mesh(sphereGeometry, cubeMaterial)
    scene.add(mesh)
    // 新建一个场景,用来存放可以自由控制的光源
    const pivot = new THREE.Object3D()
    addLight(pivot)
    scene.add(pivot)
    return {'mars': mesh, 'pivot': pivot}

创建场景。

// 添加背景
  const textureLoader = new THREE.TextureLoader()
  sceneBG.background = textureLoader.load('img/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)

创建通道及掩码通道。

  const bgRenderPass = new RenderPass(sceneBG, camera)
  const earthRenderPass = new RenderPass(sceneEarth, camera)
  earthRenderPass.clear = false
  const marsRenderPass = new RenderPass(sceneMars, camera)
  marsRenderPass.clear = false
  // setup the mask
  const clearMask = new ClearMaskPass()
  const earthMask = new MaskPass(sceneEarth, camera)
  const marsMask = new MaskPass(sceneMars, camera)

创建电视效果和泛光效果渲染通道。

// 电视效果
  const effectFilm = new FilmPass(0.8, 0.325, 256, false)
  effectFilm.renderToScreen = true
  // 泛光效果
  const bloomPass = new BloomPass()
  const effectCopy = new ShaderPass(CopyShader)
  effectCopy.renderToScreen = true

定义EffectComposer对象,注意 stencilBuffer 设置为 true。然后添加渲染通道到EffectComposer对象。

const composer = new EffectComposer(renderer)
  composer.renderTarget1.stencilBuffer = true
  composer.renderTarget2.stencilBuffer = true
  composer.addPass(bgRenderPass)
  composer.addPass(earthRenderPass)
  composer.addPass(marsRenderPass)
  composer.addPass(earthMask) // 创建地球掩码
  composer.addPass(effectFilm) // 创建火星渲染专用处理通道
  composer.addPass(marsRenderPass)
  composer.addPass(marsMask) // 创建火星掩码
  composer.addPass(bloomPass) // 创建火星渲染专用处理通道
  composer.addPass(clearMask)
  composer.addPass(effectCopy)

最后渲染场景。

function render() {
    trackballControls.update(clock)
    const delta = clock.getDelta()
    earthAndLight.earth.rotation.y += 0.01
    earthAndLight.pivot.rotation.y += 0.01
    marsAndLight.mars.rotation.y += 0.01
    marsAndLight.pivot.rotation.y += 0.01
    composer.render(delta)
    requestAnimationFrame(render)
  render()

切记,不要忘了给渲染器设置:

renderer.autoClear = false

下面贴出完整代码:

import * as THREE from 'three'
// 用于鼠标移动镜头
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls'
// 用于后期处理的依赖
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
import { CopyShader } from 'three/examples/jsm/shaders/CopyShader'
import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass'
import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass'
import { ClearMaskPass, MaskPass } from 'three/examples/jsm/postprocessing/MaskPass'
export default () => {
  // 定义摄像机
  const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight)
  camera.position.set(-50, 40, 50)
  // 定义渲染器
  const renderer = new THREE.WebGLRenderer({
    'antialias':true,
    'alpha':true
  renderer.autoClear = false
  // 定义场景的颜色为黑色
  renderer.setClearColor(0x000000)
  // 定义场景大小为整个窗口
  renderer.setSize(window.innerWidth, window.innerHeight)
  // 将渲染的结果加入到div中
  document.querySelector('#webgl-box').appendChild(renderer.domElement)
  // 配置各个需要渲染的场景
  const sceneEarth = new THREE.Scene() // 地球
  const sceneMars = new THREE.Scene() // 火星
  const sceneBG = new THREE.Scene() // 背景
  // 添加光源
  function addLight (scene) {
    const spotLight = new THREE.SpotLight(0xFFFFFF, 1.2, 150, 120)
    spotLight.position.set(-10, 30, 40)
    scene.add(spotLight)
    const ambientLight = new THREE.AmbientLight(0x3c3c3c)
    scene.add(ambientLight)
  // 创建地球
  function addEarth (scene) {
    const textureLoader = new THREE.TextureLoader()
    const cubeMaterial = new THREE.MeshLambertMaterial({
      'map': textureLoader.load('img/textures/earth/Earth.png'),
      'normalMap': textureLoader.load('img/textures/earth/EarthNormal.png')
    const sphereGeometry = new THREE.SphereGeometry(20, 100, 100)
    const mesh = new THREE.Mesh(sphereGeometry, cubeMaterial)
    scene.add(mesh)
    // 新建一个场景,用来存放可以自由控制的光源
    const pivot = new THREE.Object3D()
    addLight(pivot)
    scene.add(pivot)
    return {'earth': mesh, 'pivot': pivot}
  // 创建火星
  function addMars (scene) {
    const textureLoader = new THREE.TextureLoader()
    const cubeMaterial = new THREE.MeshLambertMaterial({
      'map': textureLoader.load('img/textures/mars/mars_1k_color.jpg'),
      'normalMap': textureLoader.load('img/textures/mars/mars_1k_normal.jpg')
    const sphereGeometry = new THREE.SphereGeometry(20, 100, 100)
    const mesh = new THREE.Mesh(sphereGeometry, cubeMaterial)
    scene.add(mesh)
    // 新建一个场景,用来存放可以自由控制的光源
    const pivot = new THREE.Object3D()
    addLight(pivot)
    scene.add(pivot)
    return {'mars': mesh, 'pivot': pivot}
  // 添加背景
  const textureLoader = new THREE.TextureLoader()
  sceneBG.background = textureLoader.load('img/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)
  // 配置通道,融合场景
  const bgRenderPass = new RenderPass(sceneBG, camera)
  const earthRenderPass = new RenderPass(sceneEarth, camera)
  earthRenderPass.clear = false
  const marsRenderPass = new RenderPass(sceneMars, camera)
  marsRenderPass.clear = false
  // setup the mask
  const clearMask = new ClearMaskPass()
  const earthMask = new MaskPass(sceneEarth, camera)
  const marsMask = new MaskPass(sceneMars, camera)
  // 电视效果
  const effectFilm = new FilmPass(0.8, 0.325, 256, false)
  effectFilm.renderToScreen = true
  // 泛光效果
  const bloomPass = new BloomPass()
  const effectCopy = new ShaderPass(CopyShader)
  effectCopy.renderToScreen = true
  const composer = new EffectComposer(renderer)
  composer.renderTarget1.stencilBuffer = true
  composer.renderTarget2.stencilBuffer = true
  composer.addPass(bgRenderPass)
  composer.addPass(earthRenderPass)
  composer.addPass(marsRenderPass)
  composer.addPass(earthMask) // 创建地球掩码
  composer.addPass(effectFilm) // 创建火星渲染专用处理通道
  composer.addPass(marsRenderPass)
  composer.addPass(marsMask) // 创建火星掩码
  composer.addPass(bloomPass) // 创建火星渲染专用处理通道
  composer.addPass(clearMask)
  composer.addPass(effectCopy)
  // 实现鼠标移动摄像头
  const trackballControls = new TrackballControls(camera, renderer.domElement)
  const clock = new THREE.Clock()
  function render() {
    trackballControls.update(clock)
    const delta = clock.getDelta()
    earthAndLight.earth.rotation.y += 0.01
    earthAndLight.pivot.rotation.y += 0.01
    marsAndLight.mars.rotation.y += 0.01
    marsAndLight.pivot.rotation.y += 0.01
    composer.render(delta)
    requestAnimationFrame(render)
  render()

总结:

掩码的用法很简单,其实就是

A的掩码通道 -> 某效果渲染通道 -> clearMask =》这样只有A上会显示这种效果。

我们需要注意的是,通道clear属性必须设置为false,EffectComposer对象stencilBuffer 属性必须设置为true,renderer.autoClear 必须设置为false,这样,才可以正确的使用掩码通道。


其它后期处理通道这里不再一一讲解,感兴趣的可以自己去实验。


3. 使用 ShaderPass 自定义效果

基于ShaderPass 我们可以编写更多着色器程序来为场景添加更多自定义效果,在three.js中自带了一些现成的着色器程序可以直接使用。

它们被维护在example目录下:

three/examples/jsm/shaders/

着色器库中的着色器用法如下:

  var effectCopy = new THREE.ShaderPass(THREE.CopyShader); // 该着色器用作输出效果,因为一般着色器是不能够输出的
  effectCopy.renderToScreen = true;
  var bleachByPassFilter = new THREE.ShaderPass(THREE.BleachBypassShader); // 漂白效果
  var brightnessContrastShader = new THREE.ShaderPass(THREE.BrightnessContrastShader) // 改变亮度与对比度
  var colorifyShader = new THREE.ShaderPass(THREE.ColorifyShader); // 为整个屏幕覆盖颜色
  var colorCorrectionShader = new THREE.ShaderPass(THREE.ColorCorrectionShader); // 调整颜色分布
  var freiChenShader = new THREE.ShaderPass(THREE.FreiChenShader); // 图像边缘检测
  var gammaCorrectionShader = new THREE.ShaderPass(THREE.GammaCorrectionShader);
  var hueSaturationShader = new THREE.ShaderPass(THREE.HueSaturationShader); // 色调饱和度
  var kaleidoShader = new THREE.ShaderPass(THREE.KaleidoShader); // 万花筒效果
  var luminosityHighPassShader = new THREE.ShaderPass(THREE.LuminosityHighPassShader); // 亮度效果
  var luminosityShader = new THREE.ShaderPass(THREE.LuminosityShader); // 亮度效果
  var mirrorShader = new THREE.ShaderPass(THREE.MirrorShader); // 镜面效果
  var pixelShader = new THREE.ShaderPass(THREE.PixelShader); // 像素画
  pixelShader.uniforms.resolution.value = new THREE.Vector2(256, 256);
  var rgbShiftShader = new THREE.ShaderPass(THREE.RGBShiftShader); // 红绿蓝颜色分开
  var sepiaShader = new THREE.ShaderPass(THREE.SepiaShader); // 乌贼墨效果
  var sobelOperatorShader = new THREE.ShaderPass(THREE.SobelOperatorShader); // 边缘检测
  sobelOperatorShader.uniforms.resolution.value = new THREE.Vector2(256, 256);
  var vignetteShader = new THREE.ShaderPass(THREE.VignetteShader); // 晕映效果
   var horBlurShader = new THREE.ShaderPass(THREE.HorizontalBlurShader); // 屏幕模糊
    var verBlurShader = new THREE.ShaderPass(THREE.VerticalBlurShader); // 屏幕模糊
    var horTiltShiftShader = new THREE.ShaderPass(THREE.HorizontalTiltShiftShader); // 移轴摄影
    var verTiltShiftShader = new THREE.ShaderPass(THREE.VerticalTiltShiftShader); // 移轴摄影
    var triangleBlurShader = new THREE.ShaderPass(THREE.TriangleBlurShader, "texture"); // 基于三角形法添加模糊
    var focusShader = new THREE.ShaderPass(THREE.FocusShader); // 中央尖锐边角模糊

4. 自定义后期处理着色器

我们还没有系统的讲解着色器语言,所以本章只讲两个简单的着色器案例。

同时推荐大家到

去寻找你需要的效果,里边有很多现成的,效果很棒的着色器代码。

4.1 自定义灰度着色器

如果想使用自定义着色器,你需要实现两个组件:顶点着色器(vertex shader)和 片段着色器(fragment shader)。顶点着色器用于改变每个顶点的位置,片段着色器可以定义每个像素的颜色。对于后期处理,我们只需要实现片段着色器即可。

这里我们不对源码做讲解,有机会我会单独开一篇文章讲解着色器语言(GLSL)。

自定义灰度着色器:

  // 自定义灰度着色器
  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 is always the same for postprocessing steps
    'vertexShader': [
      'varying vec2 vUv;',
      'void main() {',
      'vUv = uv;',
      'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
    ].join('\n'),
    'fragmentShader': [
      // pass in our custom uniforms
      'uniform float rPower;',
      'uniform float gPower;',
      'uniform float bPower;',
      // pass in the image/texture we'll be modifying
      'uniform sampler2D tDiffuse;',
      // used to determine the correct texel we're working on
      'varying vec2 vUv;',
      // executed, in parallel, for each pixel
      'void main() {',
      // get the pixel from the texture we're working with (called a texel)
      'vec4 texel = texture2D( tDiffuse, vUv );',
      // calculate the new color
      'float gray = texel.r*rPower + texel.g*gPower + texel.b*bPower;',
      // return this new color
      'gl_FragColor = vec4( vec3(gray), texel.w );',
    ].join('\n')

使用该着色器:

  ...
  const renderPass = new RenderPass(scene, camera)
  const effectCopy = new ShaderPass(CopyShader)
  effectCopy.renderToScreen = true
  const effectCustomGrayScale = new ShaderPass(CustomGrayScaleShader)