
烟雾处理图解原理:3步拆解渲染核心,面试不再卡壳
面试被问“烟雾怎么画出来的”,你只能支支吾吾说“调API”吗?这种回答在资深面试官眼里等于零分。真正拉开差距的,是你能否用图解原理的方式,把底层逻辑讲得清清楚楚。今天我们就把烟雾处理从像素级到工程化落地,掰开了揉碎了讲透。
一句话原理:噪声+透明度衰减的视觉魔术
烟雾处理的本质,不是真的模拟气体分子运动,而是利用Perlin Noise生成不规则纹理,叠加Alpha衰减与深度混合,欺骗人眼产生体积感。
很多人误以为烟雾是粒子系统堆出来的,其实粒子只负责“载体”,视觉质感全靠片元着色器里的噪声函数。就像老法师修图,底片是粒子位置,调色是噪声与透明度曲线。面试时若只答“粒子+Alpha”,等于只说了“用油漆刷墙”,没讲“怎么刷出肌理”。
类比解释:像看清晨山谷的晨雾
想象你站在山腰看谷底晨雾。雾不是“一团团”飘来的,而是近处浓、远处淡,边缘模糊、中心朦胧。这种“近浓远淡”对应渲染中的距离衰减函数;“边缘模糊”对应噪声纹理的低频特性;“整体半透明”对应Alpha混合模式。
关键差异在于:真实晨雾是三维体积,而游戏/影视中的烟雾多为2.5D billboard(双面片元)或3D SDF体积。前者成本低、适合实时渲染,后者质感强但算力开销大。面试若被追问“为什么不用纯3D体积”,你能答出“实时帧率限制+移动端GPU带宽瓶颈”,才算真正理解工程权衡。
源码片段:GLSL片元着色器核心逻辑
下面这段GLSL代码来自某开源引擎的烟雾实现,展示噪声采样与衰减的核心逻辑:
// 烟雾片元着色器核心片段
uniform float uTime;
uniform vec3 uCameraPos;
uniform float uDensity;
// 经典2D Perlin噪声函数(简化版)
float perlin2(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = dot(vec2(0.0), vec2(0.0));
float b = dot(vec2(1.0), vec2(0.0));
float c = dot(vec2(0.0), vec2(1.0));
float d = dot(vec2(1.0), vec2(1.0));
return mix(
mix(a, b, u.x),
mix(c, d, u.x),
u.y
);
}
void main() {
// 1. 计算世界坐标到相机的距离
vec3 worldPos = (inverse(uViewProj) * vec4(vLocalPos, 1.0)).xyz;
float dist = length(worldPos - uCameraPos);
// 2. 距离衰减:近处浓,远处淡
float attenuation = exp(-dist * 0.02);
// 3. 噪声采样:用时间偏移制造流动感
vec2 noiseCoord = vUv * 4.0 + uTime * 0.3;
float noise = perlin2(noiseCoord);
// 4. 合成最终Alpha
float alpha = noise * uDensity * attenuation;
// 5. 颜色微调:烟雾偏灰白
vec3 smokeColor = vec3(0.8, 0.8, 0.85);
gl_FragColor = vec4(smokeColor, alpha);
}
逐行拆解:
第18-20行:exp(-dist * 0.02) 是指数衰减函数。系数0.02控制“消散速度”,调大则烟雾更快变透明。面试可追问“为什么用exp而非linear”,答:指数衰减更符合人眼对距离的感知非线性。
第23行:vUv * 4.0 放大噪声频率,uTime * 0.3 让纹理随时间流动。若时间系数过大,烟雾会“抖动”而非“飘动”,这是常见调参坑。
第28行:Alpha直接相乘,未做sRGB校正。在移动端若忽略色彩空间转换,烟雾会偏暗,需配合gl_FragColor.rgb = pow(rgb, vec3(1.0/2.2))。
流程描述:从顶点到片元的完整链路
烟雾渲染的完整数据流如下:
CPU侧:生成粒子位置数组(x,y,z,life,seed),上传至VBO。
顶点着色器:根据life与seed计算billboard朝向,输出vUv与vLocalPos。
片元着色器:执行上述噪声采样+衰减计算,输出RGBA。
混合阶段:启用BLEND,混合函数设为SRC_ALPHA, ONE_MINUS_SRC_ALPHA,深度测试开启但深度写入关闭。
这里有个致命细节:深度写入必须关闭。若开启,烟雾会遮挡后续粒子,导致“切片”假象。某开源引擎官方源码仓库中,烟雾材质预设明确标注_DEPTH_WRITE_OFF,印证了这一工程共识。
实战验证:在Three.js中复现基础烟雾
// Three.js烟雾实现片段
const smokeGeometry = new THREE.PlaneGeometry(1, 1, 32, 32);
const smokeMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uDensity: { value: 0.6 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform float uDensity;
varying vec2 vUv;
float perlin2(vec2 p) { /* 同上GLSL */ }
void main() {
vec2 coord = vUv * 4.0 + uTime * 0.3;
float noise = perlin2(coord);
float alpha = noise * uDensity;
gl_FragColor = vec4(0.8, 0.8, 0.85, alpha);
}
`,
transparent: true,
depthWrite: false, // 关键:关闭深度写入
blending: THREE.NormalBlending
});
const smokeMesh = new THREE.Mesh(smokeGeometry, smokeMaterial);
scene.add(smokeMesh);
// 动画循环中更新时间
function animate() {
smokeMaterial.uniforms.uTime.value += 0.016;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
运行后你会看到:平面片元呈现柔和流动的灰白雾团,靠近相机时更浓,远离时渐隐。若调大uDensity至1.2,烟雾会过曝发白;若开启depthWrite,多片元叠加时会出现明显断层。
避坑指南:三个高频面试陷阱
“烟雾为什么不用粒子系统?”
答:粒子系统负责“运动学”(位置、速度、生命周期),噪声着色器负责“视觉质感”。二者是分工关系,非替代关系。纯粒子无噪声,看起来像“灰点”而非“雾”。
“移动端如何优化烟雾性能?”
答:降低噪声采样频率(如vUv * 2.0而非* 4.0)、减少粒子数量、使用LOW精度浮点、关闭MSAA。某移动端引擎官方文档指出,烟雾粒子数超过2000时,中端机型帧率下降超15%。
“如何避免烟雾闪烁?”
答:噪声时间偏移系数需小于0.5,且粒子生命周期结束时Alpha应线性归零而非突变。突变会导致“pop-in”视觉瑕疵,这是美术与程序协作的常见盲区。
这个知识点你面试被问过吗?留言说说