介绍
后期处理是指在最终图像(渲染)上添加效果。人们大多在电影制作中使用这种技术,但我们也可以在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'
同时还需要一个叫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)
如果要禁用通道,只要设置通道的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)
一些通道还有可修改属性。像GlitchPass
的goWild
为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)
修复颜色
可以观察到渲染后的颜色变得更暗了,这是因为先前写的下边这个设置渲染器输出编码的代码不再工作了:
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版本中已被移除)
WebGLMultisampleRenderTarget
跟 WebGLRenderTarget
相似,但是它可以支持多重采样抗锯齿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
的值为红色
我们需要从上一个通道中获得贴图纹理,这个纹理自动存储在名为 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;
}
`
}
画面又回来了,但现在我们可以在片元着色器中修改纹理了。
更改下颜色:
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
路径下找到该蜂巢法线贴图。
添加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()