three.js学习笔记(十九)——后期处理

介绍

后期处理是指在最终图像(渲染)上添加效果。人们大多在电影制作中使用这种技术,但我们也可以在WebGL中使用。
后期处理可以是略微改善图像或者产生巨大影响效果。
下面链接是Three.js官方文档中一些关于后期处理的示例:
https://threejs.org/docs/index.html?q=po#examples/en/postprocessing/EffectComposer

初设

使用与真实渲染一课相同的设置,但是模型换了。这是一款受欢迎的模型,具有许多细节和良好的纹理,与我们的后期处理非常匹配。
three.js学习笔记(十九)——后期处理_第1张图片

如何工作

大多数情况下,后期处理Post-processing的工作方式是相同的。

Render target渲染目标

我们要在称之为Render target渲染目标的地方进行渲染而不是在画布canvas中渲染,这个Render target会给我们一个与寻常纹理非常相似的纹理,简而言之,我们是在屏幕上在纹理中进行渲染而不是在画布上。
术语Render target为Three.js特定用词,其他地方大多是使用buffer一词。
之后该纹理会应用到面向摄影机并覆盖整个视图的平面,该平面使用具有特殊片元着色器的材质,该材质将实现后期处理效果。如果后处理效果包括使图像变红,则它将仅乘以该片元着色器中像素的红色值。
大多数的后期处理效果只要你有灵感,不仅仅只是调整颜色值而已。
在Three.js中这些效果称为passes通道

Ping-pong buffering乒乓缓冲

在后期处理中,我们可以有多个通道。一个用于运动模糊,一个用于颜色变化,另一个执行景深,等等。正因为我们有多个通道,后期处理需要两个Render target,原因在于我们无法在绘制渲染目标的同时获取其贴图纹理。因此,需要在从第二个渲染目标获取纹理的同时绘制第一个渲染目标,然后在下一个通道,交换这俩个渲染目标,在第二步获取纹理,第一步绘制,然后又到下一个通道,再次交换渲染目标,如此反复。这便是称为乒乓缓冲,两者交替地被读和被写。

画布上的最终效果通道

最终效果通道pass不会位于渲染目标中,因为我们可以将其直接放在画布上,以便用户可以看到最终结果。

结语

所有这些对于初学者来说都是非常复杂的,但很幸运我们不必自己去做。我们所要做的就是使用EffectComposer类,它将为我们处理大部分繁重的工作。

EffectComposer效果合成器

效果合成器EffectComposer会处理创建渲染目标,进行乒乓缓冲将上个通道的纹理发送到当前通道,在画布上绘制最终效果等全部过程。
引入效果合成器:

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

同时还需要一个叫RenderPass的通道类,这个通道负责场景的第一次渲染,它会在EffectComposer内部创建的渲染目标中进行渲染,而非画布上:

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

现在可以实例化EffectComposer,将渲染器作为参数传进去。
与WebGLRenderer一样,我们需要使用setPixelRatio(…)提供像素比率,并使用setSize(…)调整其大小,下边将使用与渲染器相同的参数:

/**
 * Post processing
 */
const effectComposer = new EffectComposer(renderer)
effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
effectComposer.setSize(sizes.width, sizes.height)

然后将场景和相机作为参数传给RenderPass,实例化第一个通道,并使用addPass()方法将通道加到效果合成器中:

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

在tick函数中,使用效果合成器effectComposer来进行渲染,而非以往的渲染器渲染:

const tick = () =>
{
    // ...

    // Render
    // renderer.render(scene, camera)
    effectComposer.render()

    // ...
}

通道

因为现在我们只有一个renderPass通道,因此它会跟以往一样直接在画布上进行渲染。
下边会加一些简单的后期处理通道。
(可以在这个文档中找到一系列可用的通道:https://threejs.org/docs/index.html#examples/en/postprocessing/EffectComposer)

DotScreenPass

DotScreenPass将应用某种黑白光栅效果,引入并实例化,添加到效果合成器中,记得要在renderPass后面添加:

import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'
// ...
const renderPass = new RenderPass(scene, camera)
effectComposer.addPass(renderPass)

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

three.js学习笔记(十九)——后期处理_第2张图片
如果要禁用通道,只要设置通道的enabled属性为false:

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

GlitchPass

GlitchPass会添加一种屏幕故障效果,像是在电影中看到的屏幕被入侵时的效果。

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

// ...

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


一些通道还有可修改属性。像GlitchPassgoWild为true,那么将不间断的持续故障闪烁:

glitchPass.goWild = true

RGBShiftPass

有一些通道需要额外的步骤,像RGBShift通道。
RGBShift没法用作通道但是可以用作着色器,因此我们要引入该着色器RGBShiftShader并将其应用于ShaderPass,然后将着色器通道ShaderPass添加到效果合成器中。
传入RGBShiftShader并实例化ShaderPass,添加到effectComposer:

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)

three.js学习笔记(十九)——后期处理_第3张图片

修复颜色

可以观察到渲染后的颜色变得更暗了,这是因为先前写的下边这个设置渲染器输出编码的代码不再工作了:

 renderer.outputEncoding = THREE.sRGBEncoding

注释掉上边这行代码后程序并没有什么变化。
通道是在渲染目标上进行渲染,而它们用的不是相同的配置。好在效果合成器可以提供我们自己的renderTarget作为第二个参数。
到该路径下查看效果合成器的源码后/node_modules/three/examples/jsm/postprocessing/EffectComposer.js可以发现,渲染目标renderTarget是由带有特定参数的WebGLRenderTarget创建而成的。
前俩个参数是宽和高,这里可以随便给值,因为效果合成器会在调用 setSize(...)方法时重新调整renderTarget的尺寸。
第三个参数是个对象,可以从Three.js效果合成器源码中复制那个对象,然后我们再自己添加一个encoding属性,值为THREE.sRGBEncoding

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

之后将 renderTarget添加到效果合成器中:

const effectComposer = new EffectComposer(renderer, renderTarget)

调整尺寸

修复抗锯齿

如果你使用的是像素比大于1的屏幕,你可能看不到现在头盔边缘的锯齿又出现了。
注意,如果只用renderPass,则不会出现锯齿问题,因为渲染是在支持抗锯齿的画布中进行的,所以至少启用一个通道,以便观察锯齿。
之所以出现这种情况是因为WebGLRenderTarget不支持默认的抗锯齿,我们有四种选择:

  • 直接不用抗锯齿
  • 使用一种特定类型的渲染目标renderTarget来管理抗锯齿,但这并不适用于所有现代浏览器。
  • 使用一个通道来做抗锯齿,但是性能较差
  • 结合前两种选择,看浏览器是否支持那个特定类型的渲染目标,不支持则使用通道去做抗锯齿

使用WebGLMultisampleRenderTarget

(注意:教程一直使用的Three.js都为r124版本,而随着Three.js版本更新,WebGLMultisampleRenderTarget在r138版本中已被移除)
在这里插入图片描述
WebGLMultisampleRenderTargetWebGLRenderTarget 相似,但是它可以支持多重采样抗锯齿Multi Sample Antialias (MSAA)
我们可以使用WebGLMultisampleRenderTarget来替换 WebGLRenderTarget ,然后可以看到锯齿立即就被消除了:

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

但是很遗憾,这对现代浏览器不起作用,这是WebGL2的支持问题,开发者们在几年前更新了WebGL,浏览器也慢慢增加了对不同功能的支持。
点击该链接查看支持情况:https://caniuse.com/webgl2
在Bruno Simon大佬编写课程时,像Safari和iOS Safari等主流浏览器仍然不支持它,如果你在上面这些浏览器测试网站,会得到一个黑屏结果。

使用抗锯齿通道

将渲染目标改回WebGLRenderTarget ,并试着用通道来完成抗锯齿。

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

有不同抗锯齿通道可以选择:

  • FXAA:性能良好,但结果也只是良好,可能会导致模糊
  • SMAA:效果比FXAA好,但同时性能也消耗大(不要与MSAA搞混了)
  • SSAA:质量最好,但性能最差
  • TAA:性能良好但结果有限
  • 其他

这节课程会使用SMAA,引入并实例化SMAAPass,添加到效果合成器中:

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并且采用 SMAAPass

要获取像素比,可以调用渲染器的getPixelRatio() 方法。
要知道浏览器是否支持WebGL 2,可以使用渲染器的capabilities属性,一个包含当前渲染环境(RenderingContext)的功能细节的对象,我们需要的是里边的一个属性isWebGL2

下边先处理渲染目标:

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')
}

UnrealBloomPass

这个通道会添加Bloom敷霜辉光效果到渲染中,它对重现光热、激光、光剑或放射性物质非常有用。

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

// ...

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


画面太亮了,需要调整下参数:

  • strength:光的强度
  • radius:亮度的发散半径
  • threshold:限制物体开始发光的亮度值

添加下列代码用以观察:

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)

创建自己的通道

着色通道

我们会创建一个最简单的控制颜色的通道。
首先创建一个着色器,有三个属性:

  • uniforms:里边的内容跟以往一样
  • vertexShader:这个顶点着色器几乎总是有相同的代码,会把平面放在视图的前面
  • fragmentShader:片元着色器会处理后期效果
const TintShader = {
    uniforms:
    {
    },
    vertexShader: `
        void main()
        {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `
}

然后创建这个着色器通道,添加到效果合成器中:

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

屏幕会变成红色,因为片元着色器设置了 gl_FragColor的值为红色
three.js学习笔记(十九)——后期处理_第4张图片
我们需要从上一个通道中获得贴图纹理,这个纹理自动存储在名为 tDiffuse的unifom中。
我们必须将这个unifom的值设为null,效果合成器会更新它,然后在片元着色器检索该uniform:

const TintShader = {
    uniforms:
    {
        tDiffuse: { value: null }
    },

    // ...

    fragmentShader: `
        uniform sampler2D tDiffuse;

        void main()
        {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `
}

现在有了上一个通道的贴图纹理,接下去需要检索像素。
要从sampler2D(一个贴图纹理)中获取像素,需要用 texture2D(...)方法,它接收贴图纹理作为第一个参数,UV坐标作为第二个参数。
像以往一样创建并在片元着色器接收来自顶点着色器的UV坐标变量vUv:

const TintShader = {

    // ...

    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);
            gl_FragColor = color;
        }
    `
}

画面又回来了,但现在我们可以在片元着色器中修改纹理了。
three.js学习笔记(十九)——后期处理_第5张图片
更改下颜色:

const TintShader = {

    // ...

    fragmentShader: `
        uniform sampler2D tDiffuse;

        varying vec2 vUv;

        void main()
        {
            vec4 color = texture2D(tDiffuse, vUv);
            color.r += 0.1;

            gl_FragColor = color;
        }
    `
}


同理,可以创建一个名为uTint的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;
        }
    `
}

注意,我们将该值设为null。
不要直接在着色器对象中设置值,必须在创建完通道后,再去材质中修改值,因为着色器会被多次使用,即便没使用到也一样。

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')

位移通道

这次不处理颜色,而是利用UV来产生位移效果。
跟创建着色通道一样,创建一个名为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()
        {
            vec4 color = texture2D(tDiffuse, vUv);

            gl_FragColor = color;
        }
    `
}

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

现在,让我们基于vUv创建一个带扭曲的newUv,在基于x轴的y轴上用了sin函数:

const DisplacementShader = {

    // ...

    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;
        }
    `
}


下面添加动画效果,为此需要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;
        }
    `
}

记得在材质中设置uTime值:

const displacementPass = new ShaderPass(DisplacementShader)
displacementPass.material.uniforms.uTime.value = 0
effectComposer.addPass(displacementPass)

回到动画函数中更新通道:

const clock = new THREE.Clock()

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

    // Update passes
    displacementPass.material.uniforms.uTime.value = elapsedTime

    // ...
}

蜂巢

我们还可以使用贴图,在/static/textures/interfaceNormalMap.png路径下找到该蜂巢法线贴图。
three.js学习笔记(十九)——后期处理_第6张图片
添加uNormalMap

const DisplacementShader = {
    uniforms:
    {
        // ...
        uNormalMap: { value: null }
    },

    // ...
}

加载完贴图后更新uNormalMap值:

displacementPass.material.uniforms.uNormalMap.value = textureLoader.load('/textures/interfaceNormalMap.png')

更新位移着色器的片元着色器代码:

const DisplacementShader = {
    // ...

    fragmentShader: `
        uniform sampler2D tDiffuse;
        uniform float uTime;
        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;
        }
    `
}

在这里我们不会探讨这段代码做了什么事,因为这不是实现这种效果的正确方式,但依然能看到画面被法线贴图影响后的位移效果。

结尾

可以在官网的例子中搜索后期处理的示例,自己再多尝试其他的后期处理。
https://threejs.org/examples/?q=postprocessing
然后需要记住的是,每次添加通道后将不得不在每一帧上进行渲染,太多通道会导致性能缺陷。

源码

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import * as dat from 'dat.gui'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
import { RGBShiftShader } from 'three/examples/jsm/shaders/RGBShiftShader.js'
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass'
/**
 * Base
 */
// Debug
const gui = new dat.GUI()

// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

/**
 * Loaders
 */
const gltfLoader = new GLTFLoader()
const cubeTextureLoader = new THREE.CubeTextureLoader()
const textureLoader = new THREE.TextureLoader()

/**
 * Update all materials
 */
const updateAllMaterials = () =>
{
    scene.traverse((child) =>
    {
        if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
        {
            child.material.envMapIntensity = 5
            child.material.needsUpdate = true
            child.castShadow = true
            child.receiveShadow = true
        }
    })
}

/**
 * Environment map
 */
const environmentMap = cubeTextureLoader.load([
    '/textures/environmentMaps/0/px.jpg',
    '/textures/environmentMaps/0/nx.jpg',
    '/textures/environmentMaps/0/py.jpg',
    '/textures/environmentMaps/0/ny.jpg',
    '/textures/environmentMaps/0/pz.jpg',
    '/textures/environmentMaps/0/nz.jpg'
])
environmentMap.encoding = THREE.sRGBEncoding

scene.background = environmentMap
scene.environment = environmentMap

/**
 * Models
 */
gltfLoader.load(
    '/models/DamagedHelmet/glTF/DamagedHelmet.gltf',
    (gltf) =>
    {
        gltf.scene.scale.set(2, 2, 2)
        gltf.scene.rotation.y = Math.PI * 0.5
        scene.add(gltf.scene)

        updateAllMaterials()
    }
)

/**
 * Lights
 */
const directionalLight = new THREE.DirectionalLight('#ffffff', 3)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.set(1024, 1024)
directionalLight.shadow.camera.far = 15
directionalLight.shadow.normalBias = 0.05
directionalLight.position.set(0.25, 3, - 2.25)
scene.add(directionalLight)

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

    // Update effect composer
    effectComposer.setSize(sizes.width, sizes.height)
})

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.set(4, 1, - 4)
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

/**
 * 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))


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(
    800,
    600,
    {
        minFilter: THREE.LinearFilter,
        magFilter: THREE.LinearFilter,
        format: THREE.RGBAFormat,
        encoding: THREE.sRGBEncoding
    }
)

const TintShader = {
    uniforms:
    {
        tDiffuse: { value: null },
        uTint: { value: null }
    },
    vertexShader: `
        varying vec2 vUv;

        void main()
        {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

            vUv = uv;
        }
    `,
    fragmentShader: `
        uniform sampler2D tDiffuse;
        uniform vec3 uTint;

        varying vec2 vUv;

        void main()
        {
            vec4 color = texture2D(tDiffuse, vUv);
            color.rgb += uTint;
            gl_FragColor = color;
        }
    `
}

const DisplacementShader = {
    uniforms:
    {
        tDiffuse: { value: null },
        uTime: { value: null },
        uNormalMap: { value: null }
    },
    vertexShader: `
        varying vec2 vUv;

        void main()
        {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

            vUv = uv;
        }
    `,
    fragmentShader: `
        uniform sampler2D tDiffuse;
        uniform float uTime;
        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;
        }
    `
}

/**
 * Post processing
 */
// 效果合成器
 const effectComposer = new EffectComposer(renderer,renderTarget)
 effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
 effectComposer.setSize(sizes.width, sizes.height)

 if(renderer.getPixelRatio() === 1 && !renderer.capabilities.isWebGL2)
 {
     const smaaPass = new SMAAPass()
     effectComposer.addPass(smaaPass)
 
     console.log('Using SMAA')
 }

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

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

const glitchPass = new GlitchPass()
// glitchPass.goWild = true  // 开启持续不间断的故障闪烁
glitchPass.enabled = false
effectComposer.addPass(glitchPass)

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

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

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)

// 自定义着色通道
const tintPass = new ShaderPass(TintShader)
tintPass.material.uniforms.uTint.value = new THREE.Vector3()
effectComposer.addPass(tintPass)
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')

// 自定义位移通道
const displacementPass = new ShaderPass(DisplacementShader)
displacementPass.material.uniforms.uTime.value = 0
displacementPass.material.uniforms.uNormalMap.value = textureLoader.load('/textures/interfaceNormalMap.png')
effectComposer.addPass(displacementPass)
/**
 * Animate
 */
const clock = new THREE.Clock()

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

    // Update passes
    displacementPass.material.uniforms.uTime.value = elapsedTime

    // Update controls
    controls.update()

    // Render
    // renderer.render(scene, camera)
    effectComposer.render()
    

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

tick()

你可能感兴趣的:(three.js学习笔记,javascript,three.js)