GPU Gems 1 · Chapter 6. Fire in the "Vulcan" Demo

从粒子发射器、视频纹理精灵、体积纹理和分层合成出发,构建可控的实时 Vulcan 火焰。

学习目标

  • 能解释 Vulcan 为什么在程序化火焰、二维扰动和视频纹理精灵之间做取舍
  • 能实现用发射器生成粒子、用体积纹理的 z 坐标播放精灵动画,并用 A/B 混合隐藏重复
  • 能修改 alpha/additive 混合与分层合成代码,说明它们如何影响颜色控制和像素预算
  • 能回答:为什么少量绑定发射器的粒子,反而比大量自由粒子更能保持移动火焰的连续性?

直觉引入:火焰不是每一帧都重新发明

一团真正有说服力的火焰,第一眼看见的是形状、颜色层次和烟雾的节奏,而不是每个火舌背后的物理方程。实时系统的任务,是用有限的像素预算把这些线索稳定地送到屏幕上。

如果只追求物理真实,程序化方案可能需要数千粒子;如果只追求省事,一张固定图片又会暴露重复。Vulcan 的经验是把可控的粒子运动、少量动画素材和后处理组合起来:每一层承担自己擅长的视觉工作,再用性能测量决定分辨率和混合方式。

1. 先选路线:程序化、二维流还是视频精灵

Vulcan 的目标不是蜡烛大小的火苗,而是包围角色、带烟雾、可由用户控制的猛烈火焰。完全程序化的火焰节省纹理内存,却要处理大量粒子和顶点;屏幕空间二维扰动适合小型蜡烛火焰,却容易受相机角度和前后运动限制;视频纹理把真实火焰的形状线索直接带进渲染流程。

这里的 不是偷懒的静态贴图:每个粒子仍有位置、尺寸、寿命、透明度和动画起始帧,可以通过数量、混合和运动规则重组成大规模效果。

三条火焰路线:先选约束,再选算法完全程序化省纹理内存数千粒子压 CPU/GPU适合可控小尺度二维扰动多次 render-to-texture视角和运动受限视频纹理精灵大规模 + 可控烟雾Vulcan 的最终路线目标:猛烈、可控、实时,而非单一指标最优
Vulcan 没有把‘最物理’当成唯一目标,而是用可控的视频纹理精灵换取规模、烟雾和实时性能。

做方案评估时,先写清三个约束:目标视角是否会绕到火焰顶部或底部、是否需要把烟雾和火焰混在一起、像素阶段最多能花多少预算。这样就不会因为“程序化”或“真实素材”这类标签,跳过实际的相机和性能条件。

2. 从发射器生成粒子,再用 z 坐标播放动画

角色模型上可以放置多个 。发射器决定粒子从哪里来,也暴露尺寸、每秒密度、纹理和寿命等调参入口;粒子系统只需要根据这些参数生成自己的 quad 并提交绘制。

动画帧可以存进 。如果纹理深度是 D,第 k 帧的采样坐标可写成:

z=k+0.5D,0k<D.z=\frac{k+0.5}{D},\qquad 0\le k<D.

这个式子在说:把离散帧映射到体积纹理的切片中心,递增 k 就能播放动画;在相邻切片之间采样,还能获得帧间插值。

Vulcan 使用两种火焰动画和一种烟雾动画,每种 64 帧,总共 192 帧;为了满足体积纹理切片数是二的幂,最终使用 256 个切片。压缩的 256×256×256 B8G8R8A8 体积纹理约占 16 MB;低内存平台还可以缩到 64×64×256,保留帧数而降低每帧空间分辨率。

发射器 → 粒子 → 体积纹理帧Vulcanemitter粒子 quadsize · life · flip UVz0z1z2z3256³ volume texturez = frame / 25564 帧 × 3 动画 → 256 个切片,换取硬件插帧
把帧序列装进体积纹理,递增 z 坐标就能播放动画;A/B 混合和 UV 翻转可以延长少量素材。
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,而是自己生成四边形,这样可以写入自定义纹理坐标,在 uv 方向翻转帧。翻转成本很低,却能把同一段素材变成更多姿态;如果资源和带宽允许,还可以进一步加入任意旋转或更多动画族。

注意不要把“每个参数都加随机”误当成真实感。随机尺寸太大,会让火焰像一组互不相干的贴纸;随机透明度太强,会让烟雾断裂。先固定种子和范围,再以源头连续性、重复周期和镜头运动作为验收指标。

4. alpha blending 保留颜色控制

开始实现时,additive blending 很诱人:粒子无需严格排序,叠加后也容易形成发光。但它会把多个颜色不断推向饱和,艺术家无法稳定控制火焰和烟雾的色调。

把颜色控制权交回材质,并把代价变成每帧排序几百个粒子。对 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. 用发射器绑定权重补齐移动拖尾

火焰源头移动时,少量粒子会暴露空洞。解决办法不是立刻把粒子数加到几千,而是引入 。粒子刚出生时权重高,跟随角色运动;随着年龄增加,权重衰减,粒子逐渐交给上升、滚动和风等自由运动。

用一个线性示意式表达:

p(t)=w(t)pemitter(t)+(1w(t))pfree(t),w(0)1,w(T)0.p(t)=w(t)p_{emitter}(t)+(1-w(t))p_{free}(t),\qquad w(0)\approx1,\quad w(T)\approx0.

这个式子在说:新粒子优先补齐源头,老粒子优先形成自然的漂移;权重曲线本身可以按寿命、火焰类型和源头速度调整。

用年龄权重保持火焰连续出生老化10跟随 emitter自由运动角色移动时补齐拖尾避免所有粒子黏成一团p = w(age) · emitter + (1 − w) · free
权重只在粒子早期帮它跟住发射器;随着年龄增长,粒子再逐步交给空气运动。

这个方法的好处是它只增加一次位置混合,却解决了移动角色上的连续性问题。流体模拟仍然可以作为未来扩展,但当粒子很少、尺寸很大时,过细的流体细节也未必能被屏幕采样出来。

6. 分层合成:先减少像素,再谈更多粒子

大型视频纹理 billboard 会消耗大量像素处理。这里的 把火焰先画进四分之一屏幕分辨率的目标,再与场景合成。火焰源图只有 256×256,降低目标分辨率的画质损失有限,却能把混合操作数量大幅压低。

另外,方形精灵常常包含大片透明区域。离线分析每个动画帧,可以为它生成更贴合火焰轮廓的矩形,并记录一个偏移量,使矩形中心仍对应粒子位置。这样减少的是无效 texel,而不是把火焰几何粗暴删掉。

猜一猜:把发射器绑定从 0.2 调到 0.9,角色移动时拖尾会怎样?把分层分辨率降到 25% 后,最先变化的是轮廓、颜色,还是帧时间?

Vulcan Fire Lab

先猜:火焰要更“真”,应该先加粒子还是先改混合?

可交互
粒子火焰预览alpha:可控颜色emitterAA/BAA/BArender target50%帧索引影响高度与形状;绑定影响拖尾连续性

观察: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 调到 01。分别描述亮度饱和、重复轮廓和颜色控制的变化。

问题 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
用热量纹理扰动场景采样坐标,模拟空气折射造成的摇曳。

资料与写作方式声明

本章以GPU Gems 1 · Chapter 6. Fire in the "Vulcan" Demo公开完整正文核定章节范围、事实坐标与时代语境,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…