烟雾处理图解原理:3步拆解渲染核心,面试不再卡壳
烟雾处理图解原理: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. 合成最终Alphafloat 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”视觉瑕疵,这是美术与程序协作的常见盲区。这个知识点你面试被问过吗?留言说说