shader小白的学习之路。
最近在研究three.js中有关后处理的部分。后处理是指在渲染场景后对图像进行额外处理,以实现各种视觉效果,如模糊、景深、颜色校正等。
这里做了一些总结,包括UnrealBloomPass的使用、描边、闪屏等Pass的基础介绍。
1.UnrealBloomPass辉光特效
在后处理中,有两个Pass可以做到辉光效果,分别为BloomPass和UnrealBloomPass,这里先简单介绍一下二者(主要也是为自己之前的一些疑问做个总结)。
UnrealBloomPass
是一个更高级的泛光效果实现,通常用于模拟类似 Unreal Engine 中的高质量泛光效果。它通过以下方式实现泛光效果:
-
高斯模糊:对场景中的高亮度区域进行高斯模糊处理。
-
屏幕混合:将模糊后的高亮度区域与原始场景混合,从而实现逼真的泛光效果。
-
参数控制:可以通过
strength
(强度)、threshold
(阈值)和radius
(模糊半径)等参数来精细调整效果。
UnrealBloomPass
的优点是效果逼真,适合用于需要高质量泛光的场景,但它可能会对性能产生一定影响。
BloomPass:
BloomPass
是一个更基础的泛光实现,通常用于简单的场景。它通过以下方式实现泛光效果:
-
亮度提取:提取场景中的高亮度区域。
-
模糊处理:对这些高亮度区域进行模糊处理。
-
混合输出:将模糊后的区域与原始场景混合。
BloomPass
的实现相对简单,适合用于对性能要求较高的场景,但效果可能不如 UnrealBloomPass
逼真。
二者如何选择:
-
效果质量:
UnrealBloomPass
提供更高质量和更逼真的泛光效果,而BloomPass
更适合简单场景。 -
性能:
UnrealBloomPass
由于其复杂的实现(如高斯模糊和屏幕混合),可能会对性能产生较大影响,而BloomPass
更轻量。 -
参数控制:
UnrealBloomPass
提供更丰富的参数(如strength
、threshold
和radius
),允许更精细的调整。
相比之下,UnrealBloomPass的效果更好一些,这里使用UnrealBloomPass一个机械手臂的辉光特效。
机械手臂辉光效果
效果图:
看起来也算是有点科技感在里面的哈。
简单说一下实现思路:
首先就是要对后处理有一定基础(场景的基础搭建省略),知道基本的搭建后处理通道的流程:
const composer = new EffectComposer(renderer); // 创建后处理通道
composer.setSize(window.innerWidth, window.innerHeight); // 设置大小
composer.setPixelRatio(window.devicePixelRatio); // 设置像素比
// 渲染通道
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass); // 添加
const v2 = new Three.Vector2(window.innerWidth, window.innerHeight); // 获取当前容器大小,这里我是占满全屏,就用的window
const unrealBloomPass = new UnrealBloomPass(v2, 1.5, 0.5, 0.2); // 创建辉光通道
composer.addPass(unrealBloomPass); // 添加进通道
这里就搭建好了一个基础的后处理器,作用就是让场景里的模型有辉光效果。UnrealBloomPass的参数分别为:
resolution: Three.Vector2, strength: number, radius: number, threshold: number
resolution // 分辨率设置
strength // 定义泛光效果的强度,值越高,明亮的区域越明亮,而且渗入较暗区域的也就越多
radius // 半径设置,越大越大
threshold // 定义阈值,根据场景自定义设置
这里有一些需要注意的点。辉光对场景的背景也会生效(至于原因,我个人愚见是使用GLSL获取了整个屏幕的片元,让后让其中的亮点发光),因此场景最好是黑的,否则:
(renderer.setClearColor(0x00ff00);)
场景也跟着发光很多场合下都不是我们想要的(包括你设置scene.background也是一样,而且设置了还可能会出现一些Bug)。
针对不同的模型(这里是线条,可能不是很明显,底下用原模型进行观察),在使用辉光特效时可能会导致模型变得很模糊,如图:
上图中,左边加入了renderTarget,右边没有加入
可以看到,右边比左边光晕更大,模型看起来也要比左边模糊一些。为了解决这个问题,可以在创建EffectComposer时加入WebGLRenderTargert:
const renderTarget = new Three.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
minFilter: Three.LinearFilter,
magFilter: Three.LinearFilter,
format: Three.RGBAFormat,
colorSpace: Three.SRGBColorSpace, // 重新设定颜色空间
}
);
// 创建effect后处理器
// const composer = new EffectComposer(renderer);
const composer = new EffectComposer(renderer, renderTarget);
加上即可。如果加不加感觉效果不大的可以不加(比如在加一个描边啥的场景里面,加入描边的模型达到预期,但是其他的模型可能会变得比较模糊,看起来像是眼睛花了)。
2.常见的后处理通道介绍
1.EffectComposer
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
// 构造函数
EffectComposer(renderer: WebGLRenderer, renderTarget?: WebGLRenderTarget)
-
renderer: 用于渲染场景的渲染器。
-
renderTarget: (可选)EffectComposer内部使用的预配置渲染目标。
EffectComposer的基本工作流程如下:
-
创建一个EffectComposer实例。
-
添加渲染目标和一系列的Pass。
-
在渲染循环中调用composer的render方法。
基础使用:
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
const renderTarget = new Three.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
minFilter: Three.LinearFilter,
magFilter: Three.LinearFilter,
format: Three.RGBAFormat,
colorSpace: Three.SRGBColorSpace, // 重新设定颜色空间
}
);
const composer = new EffectComposer(renderer, renderTarget); // 后者可选
const renderPass = new RenderPass(scene, camera); // 创建一个渲染通道
composer.addPass(renderPass); // 将渲染通道加到composer中
2.RenderPass
RenderPass
通道的作用是把场景和相机作为参数传入,获得场景的渲染结果,不对渲染结果做特定处理。 简单说就是RenderPass
用来生成第一张原始图,用来传给后面通道使用,所以一般RenderPass
会作为第一个通道
renderToScreen属性:默认值是false
,true
将处理的结果保存到帧缓冲区,false
直接显示在canvas画布上面。
(以为我漏了3?其实是辉光的Pass)
4.OutlinePass实现描边发光效果
import { OutlinePass } from "three/examples/jsm/Addons.js";
// 传入长宽、场景、相机
const v2 = new Three.Vector2(container.clientWidth, container.clientHeight);
const outlinePass = new OutlinePass(v2, scene, camera)
// 将此通道结果渲染到屏幕
outlinePass.renderToScreen = true
// OutlinePass相关属性设置
PaoutlinePassss.visibleEdgeColor = new Three.Color(76, 148, 156) // 可见边缘的颜色
outlinePass.hiddenEdgeColor = new Three.Color(0, 1, 0) // 不可见边缘的颜色
outlinePass.edgeGlow = 1.0 // 发光强度
outlinePass.usePatternTexture = false // 是否使用纹理图案
outlinePass.edgeThickness = 2.0 // 边缘浓度
outlinePass.edgeStrength = 2.0 // 边缘的强度,值越高边框范围越大
outlinePass.pulsePeriod = 0 // 闪烁频率,值越大频率越低
// 一般生产环境中会配合鼠标事件,来改变selectedObjects,使选中的物体描边,也可以初始就指定,作为第四个参数传进去
outlinePass.selectedObjects = [需要添加外边框的网格模型]
// 将通道加入组合器
composer.addPass(outlinePass)
5.GlitchPass闪屏效果
import { GlitchPass } from "three/examples/jsm/Addons.js";
// 初始化通道
const glitchPass = new GlitchPass()
// 将此通道结果渲染到屏幕
glitchPass.renderToScreen = true
// 把通道加入到组合器
composer.addPass(glitchPass)
6.FXAAShader和SMAAPass抗锯齿通道
// FXAA抗锯齿Shader
import { FXAAShader } from "three/addons/shaders/FXAAShader.js";
// SMAA抗锯齿通道
import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js";
const FXAAPass = new ShaderPass(FXAAShader);
const pixelRatio = renderer.getPixelRatio();
FXAAPass.uniforms.resolution.value.x = 1 / (window.innerWidth * pixelRatio);
FXAAPass.uniforms.resolution.value.y = 1 / (window.innerHeight * pixelRatio);
composer.addPass(FXAAPass);
const pixelRatio = renderer.getPixelRatio();
const smaaPass = new SMAAPass(
window.innerWidth * pixelRatio,
window.innerHeight * pixelRatio
);
composer.addPass(smaaPass);
二者使用其中的一个就行,个人推荐FXAAShader通道。
7.GammaCorrectionShader伽马校正通道
import { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.js";
// 创建伽马校正通道
const gammaPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);
目前来看,这个通道可以很好校正因为其他shader导致的颜色变化。
8.ShaderPass
作用是将一个自定义的着色器应用到渲染结果上,接收一个着色器(Shader)作为输入,并将其运用到当前的渲染帧上。写法上应该参照的是CopyShader的写法(本身就是用于拷贝已有的渲染结果的!):
import { CopyShader } from "three/examples/jsm/shaders/CopyShader.js"; // 传入了CopyShader着色器,用于拷贝渲染结果
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 这里展示一些内容,以及自己增加的东西
const shader = {
uniforms: {
tDiffuse: { value: null },
opacity: { value: 1.0 },
uStrength: {value: 0} // 这个是自己添加的
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader: `
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
if(vUv.x < 0.5) gl_FragColor = vec4(gl_FragColor.rgb, 0.8); // 这一行是自己添加的,表示左半边的片元透明度都变成了0.8,右半边不变
}
`,
};
const shaderPass = new ShaderPass(shader);
composer.addPass(shaderPass);
shaderPass.uniforms.uStrength.value = 2.0; // 调整自定义参数
效果就是:
(左边和右边分庭抗礼)
9.OutputPass
主要作用是将最终的处理结果渲染到屏幕上,一般放在composer的最后一个通道上。(不加好像也没啥大问题)
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 添加 OutputPass
const outputPass = new OutputPass();
composer.addPass(outputPass);
3.比较一些Pass的效果
结合这个线条模型,比较在辉光特效下,SMAAPass、Gamma通道、OutputPass之间对结果的影响:
添加了WebGLRenderTarget之后,SMAAPass通道的效果微乎其微(最少这个模型中看不太出来),Gamma通道可以增强效果(而且对于颜色校正也很有帮助),输出Pass也可以增强效果(原因不明)。
总结
以辉光效果通道为引子,整理介绍了一些常见的后处理通道和使用方法,并比较了一些通道对于辉光效果的作用。
别怕懒,多尝试,总归能找到比较合适的!!!
(PS: 有讲的不对的不好的地方,欢迎指正!!)
(PPS: 坚持学习,做大做强)
PPPS:附上源码:
<template>
<div id="bloom" ref="bloomRef"></div>
<div id="btnBox">
<button @click="handleAddPass">增加SMAA</button>
<button @click="handleRemovePass">移除SMAA</button>
<button @click="handleAddGamma">增加Gamma</button>
<button @click="handleRemoveGamma">移除Gamma</button>
<button @click="handleAddOutput">增加Output</button>
<button @click="handleRemoveOutput">移除Output</button>
</div>
</template>
<script lang="ts" setup>
import * as Three from "three";
import { ref, onMounted } from "vue";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/Addons.js";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import {
SMAAPass, // 抗锯齿通道
OutputPass, // 输出通道
UnrealBloomPass, // 泛光通道,感觉这个只能在暗色的场景里面使用,针对明亮的物体,发光
ShaderPass, // 自定义shader通道
RenderPass, // 渲染通道
GammaCorrectionShader, // 伽马颜色校正后处理Shader
} from "three/examples/jsm/Addons.js";
const bloomRef = ref();
const scene = new Three.Scene();
const camera = new Three.PerspectiveCamera(
80,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 80, 100);
camera.lookAt(0, 0, 0);
const ambientLight = new Three.AmbientLight(0xffffff, 2);
scene.add(ambientLight);
const createLine = (model: Three.Mesh) => {
const edges = new Three.EdgesGeometry(model.geometry, 20);
const line = new Three.LineSegments(
edges,
new Three.LineBasicMaterial({
color: new Three.Color(0x00bfff),
})
);
const quar = new Three.Quaternion();
const pos = new Three.Vector3();
const scale = new Three.Vector3();
model.getWorldPosition(pos);
model.getWorldQuaternion(quar);
model.getWorldScale(scale);
line.scale.copy(scale);
line.position.copy(pos);
line.quaternion.copy(quar);
return line;
};
new GLTFLoader().load("/manipulator.glb", (glb) => {
const model = glb.scene.children[0];
const lightGroup = new Three.Group();
model.traverse((child) => {
if (child.isMesh) {
const line = createLine(child);
lightGroup.add(line);
}
});
scene.add(lightGroup);
lightGroup.scale.set(20, 20, 20);
});
const renderer = new Three.WebGLRenderer({
antialias: true,
logarithmicDepthBuffer: true,
});
renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比,避免渲染模糊问题
const renderTarget = new Three.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
minFilter: Three.LinearFilter,
magFilter: Three.LinearFilter,
format: Three.RGBAFormat,
colorSpace: Three.SRGBColorSpace, // 重新设定颜色空间
}
);
// 创建effect后处理器
// const composer = new EffectComposer(renderer);
const composer = new EffectComposer(renderer, renderTarget);
composer.setSize(window.innerWidth, window.innerHeight); // 设置大小
composer.setPixelRatio(window.devicePixelRatio); // 设置像素比
// 渲染通道
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass); // 添加
const v2 = new Three.Vector2(window.innerWidth, window.innerHeight);
const unrealBloomPass = new UnrealBloomPass(v2, 1.5, 0.5, 0);
nrealBloomPass.renderToScreen = true;
composer.addPass(unrealBloomPass);
// 创建伽马颜色矫正通道
const gammaPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);
const shader = {
uniforms: {
tDiffuse: { value: null },
opacity: { value: 1.0 },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader: `
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
if(vUv.x < 0.5) gl_FragColor = vec4(gl_FragColor.rgb, 0.8);
}
`,
};
const shaderPass = new ShaderPass(shader);
// composer.addPass(shaderPass);
// 创建SMAA抗锯齿通道
const pixelRatio = renderer.getPixelRatio();
const smaaPass = new SMAAPass(
window.innerWidth * pixelRatio,
window.innerHeight * pixelRatio
);
composer.addPass(smaaPass);
// 创建输出通道,防止点击发光后场景颜色变暗
const outputPass = new OutputPass();
composer.addPass(outputPass);
const handleAddPass = () => {
composer.addPass(smaaPass);
console.log(composer.passes);
};
const handleRemovePass = () => {
composer.removePass(smaaPass);
console.log(composer.passes);
};
const handleAddGamma = () => {
composer.addPass(gammaPass);
console.log(composer.passes);
};
const handleRemoveGamma = () => {
composer.removePass(gammaPass);
console.log(composer.passes);
};
const handleAddOutput = () => {
composer.addPass(outputPass);
console.log(composer.passes);
};
const handleRemoveOutput = () => {
composer.removePass(outputPass);
console.log(composer.passes);
};
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
const loop = () => {
composer.render();
requestAnimationFrame(loop);
};
onMounted(() => {
renderer.setSize(bloomRef.value.clientWidth, bloomRef.value.clientHeight);
bloomRef.value.appendChild(renderer.domElement);
loop();
});
</script>
<style lang="scss" scoped>
#bloom {
width: 100vw;
height: 100vh;
}
#btnBox {
position: fixed;
top: 20px;
right: 50px;
cursor: pointer;
}
</style>