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


    介绍

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

    初设

    使用与真实渲染一课相同的设置,但是模型换了。这是一款受欢迎的模型,具有许多细节和良好的纹理,与我们的后期处理非常匹配。
    在这里插入图片描述

    如何工作

    大多数情况下,后期处理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'
    
    • 1

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

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

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

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

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

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

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

    const tick = () =>
    {
        // ...
    
        // Render
        // renderer.render(scene, camera)
        effectComposer.render()
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    通道

    因为现在我们只有一个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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    如果要禁用通道,只要设置通道的enabled属性为false:

    const dotScreenPass = new DotScreenPass()
    dotScreenPass.enabled = false
    effectComposer.addPass(dotScreenPass)
    
    • 1
    • 2
    • 3

    GlitchPass

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

    import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
    
    // ...
    
    const glitchPass = new GlitchPass()
    effectComposer.addPass(glitchPass)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    一些通道还有可修改属性。像GlitchPassgoWild为true,那么将不间断的持续故障闪烁:

    glitchPass.goWild = true
    
    • 1

    在这里插入图片描述

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    修复颜色

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

     renderer.outputEncoding = THREE.sRGBEncoding
    
    • 1

    注释掉上边这行代码后程序并没有什么变化。
    通道是在渲染目标上进行渲染,而它们用的不是相同的配置。好在效果合成器可以提供我们自己的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
        }
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    const effectComposer = new EffectComposer(renderer, renderTarget)
    
    • 1

    在这里插入图片描述

    调整尺寸

    修复抗锯齿

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

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

    使用WebGLMultisampleRenderTarget

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

    const renderTarget = new THREE.WebGLMultisampleRenderTarget(
        // ...
    )
    
    • 1
    • 2
    • 3

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

    使用抗锯齿通道

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

    const renderTarget = new THREE.WebGLRenderTarget(
        // ...
    )
    
    • 1
    • 2
    • 3

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

    • 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
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到抗锯齿消失

    结合两种解决方案

    • 如果屏幕像素比大于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(
        // ...
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    处理通道:

    if(renderer.getPixelRatio() === 1 && !renderer.capabilities.isWebGL2)
    {
        const smaaPass = new SMAAPass()
        effectComposer.addPass(smaaPass)
    
        console.log('Using SMAA')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    UnrealBloomPass

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

    import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass'
    
    // ...
    
    const unrealBloomPass = new UnrealBloomPass()
    effectComposer.addPass(unrealBloomPass)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    画面太亮了,需要调整下参数:

    • 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    创建自己的通道

    着色通道

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

    • 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);
            }
        `
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    const tintPass = new ShaderPass(TintShader)
    effectComposer.addPass(tintPass)
    
    • 1
    • 2

    屏幕会变成红色,因为片元着色器设置了 gl_FragColor的值为红色
    在这里插入图片描述
    我们需要从上一个通道中获得贴图纹理,这个纹理自动存储在名为 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);
            }
        `
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    现在有了上一个通道的贴图纹理,接下去需要检索像素。
    要从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;
            }
        `
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    画面又回来了,但现在我们可以在片元着色器中修改纹理了。
    在这里插入图片描述
    更改下颜色:

    const TintShader = {
    
        // ...
    
        fragmentShader: `
            uniform sampler2D tDiffuse;
    
            varying vec2 vUv;
    
            void main()
            {
                vec4 color = texture2D(tDiffuse, vUv);
                color.r += 0.1;
    
                gl_FragColor = color;
            }
        `
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述
    同理,可以创建一个名为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;
            }
        `
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    const tintPass = new ShaderPass(TintShader)
    tintPass.material.uniforms.uTint.value = new THREE.Vector3()
    
    • 1
    • 2

    将该值加到调试面板中:

    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')
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    位移通道

    这次不处理颜色,而是利用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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    现在,让我们基于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;
            }
        `
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    下面添加动画效果,为此需要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;
            }
        `
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    记得在材质中设置uTime值:

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

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

    const clock = new THREE.Clock()
    
    const tick = () =>
    {
        const elapsedTime = clock.getElapsedTime()
    
        // Update passes
        displacementPass.material.uniforms.uTime.value = elapsedTime
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    蜂巢

    我们还可以使用贴图,在/static/textures/interfaceNormalMap.png路径下找到该蜂巢法线贴图。
    在这里插入图片描述
    添加uNormalMap

    const DisplacementShader = {
        uniforms:
        {
            // ...
            uNormalMap: { value: null }
        },
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    加载完贴图后更新uNormalMap值:

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

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

    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;
            }
        `
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    结尾

    可以在官网的例子中搜索后期处理的示例,自己再多尝试其他的后期处理。
    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
  • 相关阅读:
    电脑组装教程分享!
    vasp频率计算出错Intel MKL error
    Java中如何计算一个HashSet对象的大小呢?
    【图像分类】【深度学习】【Pytorch版本】 GoogLeNet(InceptionV3)模型算法详解
    Java自定义注解
    蓝桥杯官网练习题(上三角方阵)
    vue-pdf在vue框架中的使用
    洛谷P1202 黑色星期五Friday the Thirteenth
    QT day5
    java版工程管理系统Spring Cloud+Spring Boot+Mybatis实现工程管理系统源码
  • 原文地址:https://blog.csdn.net/weixin_43990650/article/details/126249530