GPU Gems 1 · Chapter 6. Fire in the "Vulcan" Demo
从粒子发射器、视频纹理精灵、体积纹理和分层合成出发,构建可控的实时 Vulcan 火焰。
学习目标
- 能解释 Vulcan 为什么在程序化火焰、二维扰动和视频纹理精灵之间做取舍
- 能实现用发射器生成粒子、用体积纹理的 z 坐标播放精灵动画,并用 A/B 混合隐藏重复
- 能修改 alpha/additive 混合与分层合成代码,说明它们如何影响颜色控制和像素预算
- 能回答:为什么少量绑定发射器的粒子,反而比大量自由粒子更能保持移动火焰的连续性?
直觉引入:火焰不是每一帧都重新发明
一团真正有说服力的火焰,第一眼看见的是形状、颜色层次和烟雾的节奏,而不是每个火舌背后的物理方程。实时系统的任务,是用有限的像素预算把这些线索稳定地送到屏幕上。
如果只追求物理真实,程序化方案可能需要数千粒子;如果只追求省事,一张固定图片又会暴露重复。Vulcan 的经验是把可控的粒子运动、少量动画素材和后处理组合起来:每一层承担自己擅长的视觉工作,再用性能测量决定分辨率和混合方式。
1. 先选路线:程序化、二维流还是视频精灵
Vulcan 的目标不是蜡烛大小的火苗,而是包围角色、带烟雾、可由用户控制的猛烈火焰。完全程序化的火焰节省纹理内存,却要处理大量粒子和顶点;屏幕空间二维扰动适合小型蜡烛火焰,却容易受相机角度和前后运动限制;视频纹理把真实火焰的形状线索直接带进渲染流程。
这里的 ↡把预先拍摄或绘制的火焰帧贴到面向相机的粒子四边形上 不是偷懒的静态贴图:每个粒子仍有位置、尺寸、寿命、透明度和动画起始帧,可以通过数量、混合和运动规则重组成大规模效果。
做方案评估时,先写清三个约束:目标视角是否会绕到火焰顶部或底部、是否需要把烟雾和火焰混在一起、像素阶段最多能花多少预算。这样就不会因为“程序化”或“真实素材”这类标签,跳过实际的相机和性能条件。
2. 从发射器生成粒子,再用 z 坐标播放动画
角色模型上可以放置多个 ↡定义火焰来源、尺寸、密度和寿命等参数的粒子生成位置。发射器决定粒子从哪里来,也暴露尺寸、每秒密度、纹理和寿命等调参入口;粒子系统只需要根据这些参数生成自己的 quad 并提交绘制。
动画帧可以存进 ↡把多张二维帧沿 z 方向堆叠成一个采样对象的三维纹理。如果纹理深度是 D,第 k 帧的采样坐标可写成:
这个式子在说:把离散帧映射到体积纹理的切片中心,递增 k 就能播放动画;在相邻切片之间采样,还能获得帧间插值。
Vulcan 使用两种火焰动画和一种烟雾动画,每种 64 帧,总共 192 帧;为了满足体积纹理切片数是二的幂,最终使用 256 个切片。压缩的 256×256×256 B8G8R8A8 体积纹理约占 16 MB;低内存平台还可以缩到 64×64×256,保留帧数而降低每帧空间分辨率。
struct Particle {
vec3 position;
vec2 size;
float age;
float lifetime;
float frameOffset;
float variant;
};
float frameCoord(Particle p, float frameCount) {
float progress = clamp(p.age / p.lifetime, 0.0, 1.0);
return fract((progress * frameCount + p.frameOffset) / frameCount);
}frameOffset 让同一批粒子不要在同一时刻闪出相同帧;variant 还可以选择 A 或 B 动画。这里的随机性应该是可重放的伪随机值,调试时才能复现某个粒子群的问题。
3. 用 A/B、翻转和属性差异隐藏重复
动画帧有限时,重复是最大的视觉风险。可以为粒子随机分配尺寸、位置偏移、衰减速度、起始帧和整体透明度;再把两种火焰动画按比例混合,让相邻粒子不再共享完全相同的轮廓。
Vulcan 没有依赖硬件 point sprite,而是自己生成四边形,这样可以写入自定义纹理坐标,在 u 或 v 方向翻转帧。翻转成本很低,却能把同一段素材变成更多姿态;如果资源和带宽允许,还可以进一步加入任意旋转或更多动画族。
注意不要把“每个参数都加随机”误当成真实感。随机尺寸太大,会让火焰像一组互不相干的贴纸;随机透明度太强,会让烟雾断裂。先固定种子和范围,再以源头连续性、重复周期和镜头运动作为验收指标。
4. alpha blending 保留颜色控制
开始实现时,additive blending 很诱人:粒子无需严格排序,叠加后也容易形成发光。但它会把多个颜色不断推向饱和,艺术家无法稳定控制火焰和烟雾的色调。
↡按源 alpha 在已有颜色与粒子颜色之间插值的混合方式 把颜色控制权交回材质,并把代价变成每帧排序几百个粒子。对 Vulcan 这种粒子数量已经由视频精灵压低的方案,排序成本可以接受;真正要测量的是排序、过度绘制和目标格式精度的总和。
vec4 composite(vec4 scene, vec4 particle, bool useAlpha) {
if (useAlpha) {
return mix(scene, particle, particle.a);
}
// additive:顺序简单,但亮度会快速饱和。
return scene + particle * particle.a;
}实验中的混合切换只模拟颜色和透明度的决策,不替代真实目标格式测试。A8R8G8B8 等有限精度目标尤其要关注亮部是否被截断,以及烟雾是否被火焰的累加亮度吞掉。
5. 用发射器绑定权重补齐移动拖尾
火焰源头移动时,少量粒子会暴露空洞。解决办法不是立刻把粒子数加到几千,而是引入 ↡表示粒子当前位置还应跟随其发射器多少的随年龄变化权重。粒子刚出生时权重高,跟随角色运动;随着年龄增加,权重衰减,粒子逐渐交给上升、滚动和风等自由运动。
用一个线性示意式表达:
这个式子在说:新粒子优先补齐源头,老粒子优先形成自然的漂移;权重曲线本身可以按寿命、火焰类型和源头速度调整。
这个方法的好处是它只增加一次位置混合,却解决了移动角色上的连续性问题。流体模拟仍然可以作为未来扩展,但当粒子很少、尺寸很大时,过细的流体细节也未必能被屏幕采样出来。
6. 分层合成:先减少像素,再谈更多粒子
大型视频纹理 billboard 会消耗大量像素处理。这里的 ↡把高成本效果先渲染到较小目标,再统一放大合成回主画面的优化策略 把火焰先画进四分之一屏幕分辨率的目标,再与场景合成。火焰源图只有 256×256,降低目标分辨率的画质损失有限,却能把混合操作数量大幅压低。
另外,方形精灵常常包含大片透明区域。离线分析每个动画帧,可以为它生成更贴合火焰轮廓的矩形,并记录一个偏移量,使矩形中心仍对应粒子位置。这样减少的是无效 texel,而不是把火焰几何粗暴删掉。
猜一猜:把发射器绑定从 0.2 调到 0.9,角色移动时拖尾会怎样?把分层分辨率降到 25% 后,最先变化的是轮廓、颜色,还是帧时间?
Vulcan Fire Lab
先猜:火焰要更“真”,应该先加粒子还是先改混合?
观察:alpha 需要排序却保留颜色控制;降低分层分辨率可以换取像素预算。
实验里的“分层分辨率”越低,代表像素预算越小;如果火焰看起来仍然稳定,再考虑增加烟雾层或 glow。先降采样、后加细节,通常比先堆粒子再追帧率更容易定位瓶颈。
7. 后处理:glow 与 heat shimmer 各自承担什么
实时合成把火焰放进 render target,也给了后处理一个明确入口。glow 可以把选中的画面复制出来,用 x、y 两次 box blur,再依据 alpha 把模糊亮部叠回原画面。它适合表达火焰照亮周围空间,但不应该代替火焰自身的轮廓。
↡用热量纹理中的二维偏移去扰动场景采样坐标,模拟空气折射的效果 则可以先渲染一层 heat particles,在最终合成时读取其红、绿通道,作为场景纹理的 (u,v) 偏移。它成本可控,却受暗场对比度影响;Vulcan 最终把像素预算优先给 glow,而不是把每个写过的效果都塞进成品。
vec2 heatOffset = texture(heatTarget, uv).rg * 2.0 - 1.0;
vec4 scene = texture(sceneTarget, uv + heatOffset * 0.02);
vec4 blurred = texture(blurTarget, uv);
vec4 flames = texture(flameTarget, uv);
vec4 glow = blurred * blurred.a;
return mix(scene, flames, flames.a) + glow;由于火焰使用低分辨率目标,深度缓冲也可能无法直接共享。一个低分辨率角色模型写入火焰目标的深度,可以作为便宜的遮挡近似;它牺牲少量顶点成本,换取大量像素节省。
小结:把素材、粒子和像素预算分开验收
- 视频纹理精灵提供形状线索,粒子系统提供位置、寿命和可控变化。
- 体积纹理用 z 坐标播放帧,A/B、翻转和属性差异隐藏有限素材的重复。
- alpha blending 保留艺术家的颜色控制;additive 只在明确接受饱和时使用。
- emitter weight 让新粒子补齐移动源头,老粒子再释放给自由运动。
- layer composition、紧致矩形和后处理共同决定火焰能否进入实时预算。
练习
问题 1 改 Demo 代码:把实验的 blend mode 切到 additive,再把 A/B 混合从 0.45 调到 0 和 1。分别描述亮度饱和、重复轮廓和颜色控制的变化。
问题 2 推导题:粒子寿命为 T,采用 w(t)=1-t/T,当 t=T/4 时,发射器位置和自由位置各占多少?这种权重对移动火焰有什么作用?
问题 3 独立实现题:为一个 256×256 的火焰帧生成紧致矩形和中心偏移,并把火焰先合成到四分之一分辨率目标。你会记录哪些指标来判断画质和性能是否值得?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- video-textured sprite
- 把动画帧贴到粒子四边形上的火焰图像单元,粒子负责位置和变化。
- emitter
- 生成粒子的来源位置,控制密度、尺寸、纹理和寿命等参数。
- volume texture
- 沿 z 方向堆叠多张二维图像的三维纹理,可用 z 坐标读取动画帧。
- alpha blending
- 按源 alpha 在背景与粒子颜色之间插值的混合方式,能保留颜色控制。
- emitter weight
- 粒子对发射器位置的跟随程度,通常随粒子年龄逐渐衰减。
- layer composition
- 先用较小分辨率渲染高成本效果,再放大合成回主画面的策略。
- heat shimmer
- 用热量纹理扰动场景采样坐标,模拟空气折射造成的摇曳。