1. 火焰不是物体:它为什么天生抗拒静态建模
1.1 火焰的物理特征如何转译成视觉元素
一说火焰,大家第一反应是橙色、红色、黄色。可真正坐在篝火边看半小时,你会发现自己很难用一个固定形状描述它:火舌冲向空中,边缘不断碎裂、卷曲、回缩,顶部变成几乎看不见的淡蓝和透明,再往上是一缕灰烟。三维项目里第一次做火焰的人,往往拿一个圆滚滚的模型加上自发光材质,看起来自然像一只发光的橘子。
火焰本质上是可燃气体在高温下燃烧产生的高温发光区域。它没有固体表面,也没有固定体积,完全由流动的气体组成。气体的密度、温度、成分每一帧都在变,所以火焰天然是一个“动态流体现象”,而不是一个“物体”。想在三维模型里表现它,第一步就是要接受这个前提——不要建模,要模拟。
从视觉层面拆解,火焰必须呈现的元素有四块:
- 形状刻度和湍流:火舌的根部窄、顶部宽,边缘被湍流撕碎,不是平滑的圆锥。
- 颜色随温度分层:内焰温度最高,偏白甚至偏蓝;中焰是黄色;外焰和烟尘区域是橙红到暗红。能量从内往外扩散,颜色和明度跟着下降。
- 自发光与受光的关系:火焰本身是光源,几乎不接受环境光,所以不能像普通材质那样被法线光照拍黑。
- 环境影响链:火加热空气产生扭曲,点燃周围烟雾和余烬,并影响附近物体的明暗。少了任一条,火焰就会像贴纸。
我当时在模拟项目X里做营地篝火,把这些要素拆成一页纸贴在显示器边上:动态剪影、温度色阶、抽吸气流、光与热。后面每一个参数,都在试图回答这四个问题中的一个。
1.2 实时渲染里“假火焰”的三个视觉支柱
离线渲染可以靠流体方程把火算得几乎和自然一样,可游戏、三维可视化里面绝大多数火焰都是实时的,性能颗粒度很重要。做了几年项目之后,我发现所有实时火焰本质上都建立在三个视觉支柱上。
第一个支柱是“动态的剪影”。观众判断一个东西像不像火,首先看轮廓是否在不停变化。哪怕颜色是简单橙色渐变,只要轮廓每帧在摆动、碎裂、上升,人脑就会自动补完“这是火”。反过来,一个完美建模但静止不动的火焰模型,看起来只会像琥珀或者树脂摆件。我见过不少新手希望用高精度模型表现火,结果模型越精致越像现代装置艺术,动态剪影才是欺骗人眼最关键的第一层。
第二个支柱是“颜色梯度与明度曲线”。真实火焰不是均匀半透明的,从中心亮白到外缘暗红,亮部和暗部之间还有噪点、烟尘。实现时通常用多层粒子叠加,内层小而亮,外层大而透,加一个从亮到暗的渐变曲线。这个曲线不是线性下降,而是带着明显的生命感:诞生时瞬间冲亮,上升过程中保持高亮,接近消亡时突然暗淡并转成烟色。
第三个支柱是“环境反馈”。火焰发光,地面要跟着闪;空气要扭曲;灰烬要飘。哪怕是简单场景,把点光源的强度和粒子周期绑定,火焰的“温度感”会立刻上一个台阶。第二次做项目时,我把光源闪烁从粒子速度里拆出来,单独用一条噪声曲线驱动,画面立刻活了。
这三个支柱后来成了我在三维项目里判断所有火焰方案的通用标准。任何新方案拿过来,我先问三个问题:它的动态剪影够不够丰富,颜色梯度能不能还原温度层次,它有没有能力与场景里的光和热做联动。只要能解决其中两个问题,就值得试;如果只带来性能而没有视觉语言上的支撑,那通常只是给手机发热添砖加瓦。
2. 从序列帧到体积渲染:六条主路线的取舍
2.1 六种实现方案的核心逻辑一览
三维火焰的实现路线,翻来覆去逃不出六种。我先把它们放在一张表里对比,后面再逐个说。
| 方案 | 核心逻辑 | 实时性 | 资源开销 | 视觉上限 | 常见适用 |
|---|---|---|---|---|---|
| 序列帧广告牌 | 面片始终面向相机,播放预渲染的火焰动画帧 | 高 | 低 | 高,但近看易穿帮 | 游戏内中近距离火焰、移动端主力 |
| 交叉面片组装 | 几个大面片交叉搭成立体剪影,各自播放动画 | 高 | 极低 | 中低,适合风格化 | 低配场景、方向性火焰 |
| 程序化着色器火焰 | 用噪声在顶点或像素上扭曲形状和颜色,UV沿高度流动 | 高 | 极低 | 中,适合小火焰 | 蜡烛、火把、远处点缀 |
| GPU粒子系统 | 成百上千个粒子由GPU着色器计算位置,模拟气流和湍流 | 较高 | 中 | 高,可近景可远景 | 大篝火、火焰柱、质量要求高 |
| 体积光与体积烟雾 | 用光线步进在三维噪声场中累积发光和吸收 | 低 | 高 | 很高 | 高配桌面与主机、影视预演 |
| 流体燃烧模拟 | 求解流体方程,逐帧计算温度、密度、速度再渲染 | 极低 | 极高 | 最高 | 离线渲染、可视化项目 |
序列帧广告牌是大部分项目的主力。美术在外部工具里把一段火焰渲染成序列图,放在一张大图集里,运行时就一个矩形面片照着时间播放。性能便宜、效果稳定,唯一的代价是贴图资源占内存,以及摄像机凑太近时能看到平面翻转。交叉面片是老办法,把两三个面片十字形交叉,每片用一套火焰纹理,特别适合旧式风格,转动的时候会有“跳轴”感,现在用得少了。
程序化着色器火焰是最被低估的方向。蜡烛这种小火焰,完全不需要粒子,一个平面上做两层噪声扰动,让顶点沿Y轴随机变形,像素颜色按高度走温度渐变,效果已经很足。一碗水都烧不出来的成本,就能做出还算体面的火苗。GPU粒子是粒子系统的进阶版,CPU只负责发射和回收,每个粒子的位置、旋转、大小全在GPU侧算,一个火堆烧两三千粒子还能保持几十帧,这是我做中大型火焰的常用方案。
体积光和体积烟雾是“真正的三维火焰”路线,真实感天花板很高,但每帧都要在一堆体积里做多次采样。流体燃烧模拟则是从物理出发,算完燃烧再算渲染,一帧需要很久,只适合影视和特殊可视化。除了最后两种,前四种在成熟项目里经常是混着用的,没有哪一条路线能单独覆盖所有距离和场景。
2.2 我的选型习惯:先定“视角距离”再定方案
我一直认为,方案不是越高级越好,而是要看火焰在这个项目里的实际处境。我选型时只问三个问题,顺序很固定。
第一个问题是视角距离。如果火焰离摄像机最近只有半米,那序列帧广告牌很可能撑不住,需要GPU粒子加多层粒子加体积感雾。如果最近距离在三米以上,序列帧已经足够。如果火焰只是背景,远处晃一眼,程序化着色器或者静态贴花都行。距离决定细节需求,细节需求直接锁死方案上限,这是最硬的一条约束。
第二个问题是同屏数量。一个场景里只有一堆篝火,可以放心上体积烟雾;如果同时有三十堆篝火、四十根火把,那再高级的方案也要往后退,把钱花在“远近分层”上。远处火焰用低配,近处火焰保留最多细节,而不是在一棵树上吊死。
第三个问题是平台和帧率预算。同一套效果,在桌面电脑上可能只占2毫秒,在中端手机上可能要占15毫秒。如果项目同时上桌面和移动端,我一般先按中端机预算设计主体,然后在桌面端加后处理和粒子数量。反过来设计通常是灾难,因为降级之后脸面很难看。
我做过一次完整的选型错误:某模拟项目X里,地面上只有一堆主篝火,我一开始上了体积烟雾,效果确实惊人,结果场景里加入十几个火把后,帧率掉到不能看。后来为了迁就火把把篝火也降级,等于白折腾。教训是:选型一开始就要把整张场景地图铺开看,而不是盯着一个主角发光。
3. 一个篝火特效的完整制作复盘:三层粒子与光
3.1 粒子分层的设计与参数参考
模拟项目X里有一个中世纪营地场景,主视觉是一堆篝火,旁边还有若干火把和蜡烛。平台目标是桌面电脑和中端手机都要跑,所以最终方案确定为“序列帧广告牌主体 + GPU粒子余烬 + 半透明烟粒子 + 点光源闪烁 + 热扭曲后处理”。这套组合的好处是:主体视觉由贴图保证,性能开销集中在粒子数量和后处理上,而这两者可以按平台灵活压缩。
我把火焰拆成三层粒子,每一层负责一类视觉任务。
第一层是火焰主体。发射器用锥形朝上,发射速率在每秒200到350个,粒子寿命随机分布在0.6到1.4秒,初速0.8到1.6米每秒。粒子大小从0.06米长到0.15米,颜色从(255, 240, 210)的亮白暖黄慢慢过渡到(255, 120, 30)的橙红。透明度曲线要“升得快、降得慢”,一出生很快变成实质火体,在最后阶段慢慢萎缩成烟。混合方式用Additive,保证火体叠加会越来越亮,这是火焰核心区域高亮的关键。
第二层是烟粒子。发射速率大约每秒30到60个,寿命3到5秒,初速0.5到1.0米每秒,粒子尺寸随时间膨胀到原来的1.5倍到2倍。颜色从深灰逐渐变浅黑,透明度从0.15降到0.05,混合方式用Alpha Blend而不是Additive,因为烟是遮挡性的,会把背景压暗,而不是越叠越亮。
第三层是余烬,也就是火星和飞灰。发射频率低一些,大约每秒8到15个,寿命1到4秒,初速1.5到3.5米每秒,尺寸只有0.02到0.05米,颜色从亮黄变红再变暗。这一层的意义是让火焰看起来“活”且有“物质感”,仿佛真的有碎屑被气流带走。
在具体引擎里,这些参数藏在发射器、初始化模块和更新模块三处。我的实现思路大致是这样:
// 火焰粒子更新逻辑(伪代码,不依赖具体引擎API) for each fireParticle: age01 = particle.age / particle.lifetime color = fireGradient.Evaluate(age01) // 温度色阶曲线 alpha = riseFastFadeSlow(age01) // 亮得快、灭得慢 size = lerp(startSize, endSize, age01) // 焰体膨胀 pos.y += riseSpeed * dt // 向上运动 pos.xz += curlNoise(pos + time) * wind * dt // 湍流扰动这里的核心不是照抄数字,而是理解每条曲线负责什么。火焰主体的数字决定体积感,烟的数字决定燃烧后的残留感,余烬的数字决定生命力。
3.2 材质与渲染设置里不能漏的开关
粒子层搭好后,材质设置是最容易翻车的环节。我梳理了几处每回都必须检查的开关。
首先是混合模式。火焰主体用Additive,烟用Alpha Blend。Additive会把火焰放在最亮的一片区域里,但也要小心叠加过多导致整块烧成纯白,丢失层次。如果你发现火焰变成一坨白板,多半是把所有粒子的混合都加到了一起,缺少Alpha层的遮挡,这时要降低Additive粒子的密度或把一半粒子切成截面较弱的方式。
其次是深度写入。所有火焰粒子都要关闭深度写入,让火焰的位置尽量像一层半透明的光与雾,而不是一个实心物体。写深度只会让后面的粒子被莫名其妙挡住,出现闪光和不规则黑块,这在排障时非常容易误判成贴图问题。
第三个容易漏的是“受光开关”。火焰粒子使用自发光材质,不接受场景光照和法线影响。很多引擎默认材质是带光照的,如果不切换到Unlit或类似模式,粒子会跟着光源变化而发灰发暗,这也是做火焰发灰的最常见原因。关闭受光后,颜色梯度才不会被场景里的方向光扭曲。
第四是渲染队列和双面设置。火焰本身不需要双面渲染,关了双面可以省一半填充率。队列顺序上,我习惯让烟粒子优先渲染、火焰主体后渲染、余烬最后。烟是远背景,火焰是发光主体,最后画余烬是因为它亮且小,偶尔出现在前排也不会突兀。如果你把所有粒子都放在同一个透明队列里,帧与帧之间的排序抖动就会非常明显。
3.3 光源、热扭曲与后处理的联动
粒子只是火焰的身体,光源和后处理是火焰的“体温”。我在篝火里放了一个点光源,位置在火焰轴心略上方0.3米,范围控制在半径2米内。点光源的强度不做恒定值,而是用一条与粒子种子同源的噪声曲线驱动,让它轻微闪烁。闪烁范围我通常控制在0.7到1.2倍之间,太稳了像灯泡,太剧烈了像故障灯。
为了让火焰和灯光看起来是同一个存在,我给光源和粒子共用同一个时间相位,这样火舌蹿高时,光照也会同步提亮一小截。实现上并不复杂,只需要在每次更新粒子速度时顺手返回一个当前的湍流强度值,再用这个值去驱动光源强度,几句话的事情就能让画面整体联动。
热扭曲是很多项目里被忽略的一环。真实火焰上方像是空气在“抖动”,这个效果在三维里可以用后处理实现:对屏幕UV加一个随时间变化的噪声偏移,偏移强度由一张火焰亮度Mask控制,只有火焰附近的区域才做扰动。我第一次做的时候没加Mask,全屏热扭曲,看久了眼睛酸,而且边缘闪得厉害。后来把扰动范围限制在火焰周围,强度随距离衰减,效果立刻自然了太多。
Bloom这类辉光后处理视平台而定。桌面端可以开一点,让火焰核心有一种“发光过曝”的感觉;移动端常常禁用全屏Bloom,改用火焰贴图自带的过曝渐变来模拟。总之,光、热、辉光这三个效果必须和粒子运动联动,否则就是“贴了张发光贴纸在地面”。
4. 翻车实录:火焰特效最常见的坑与排查链路
4.1 火焰边缘发灰:一次完整的材质排查
我第一次做三维火焰时,遇到的现象非常典型:粒子已经按橙色配好了,但火焰整体像盖了一层灰棕色雾,远处的火星尤其明显,像脏点一样。当时我第一个怀疑是贴图问题,把火焰贴图换成纯白渐变后,问题依旧,才意识到是材质环节出了漏子。
完整的排查链路可以分成四步。第一步是把火焰粒子材质改成纯自发光,并把漫反射颜色拉满。如果火焰立刻变成鲜艳的橙色,说明问题出在材质被光照系统“拍暗”了,这是最常见的情况。第二步是关闭粒子对光照的响应,把材质切换到Unlit,再测红色和白色渐变区域,看颜色通道是不是被贴图里带的灰色信息污染了。第三步是检查贴图Alpha和RGB是否分离,很多火焰序列图除了Alpha通道的信息之外,RGB里也带了一层灰色,直接用会把灰底也照亮,所以要把颜色信息重新映射到暖色通道。第四步是检查面片法线方向。面片的法线如果和摄像机之间夹角太大,又被默认光照响应,边缘就会被压暗,形成“灰边”。
那次修完之后火焰整体亮了一倍,但看起来仍然偏“塑”。后来我发现原因是粒子数量太少,透明叠加密度不足,火焰外圈显得太稀疏。把发射速率从120提到260,火灾感才真正出来。这个从“材质”到“数量”的排查顺序,我后来用了很多次,每次都有效。
4.2 排序与混合:半透明火焰闪烁的根因
半透明火焰的闪烁和黑斑,比任何其他问题都让人崩溃,因为它不是每帧固定出现的,而是时有时无,很难稳定复现。这个问题的根因是半透明物体的渲染顺序。大多数实时引擎会先从远到近排序半透明物体,再逐个绘制。当两个火焰广告牌交叉时,它们之间的距离逐帧变化,谁先画谁后画也跟着变化,于是透明混合的结果就出现“闪”。
面对排序问题,我尝试了多种对策,按有效性排序如下:
- 把火焰主体切成Additive模式。Additive混合是加法,前后顺序的影响远小于Alpha Blend,闪烁会被压到视觉上几乎察觉不到。
- 把大面片拆成许多小粒子。小粒子的排序错误产生的视觉影响更小,因为每个粒子的透明区域都不大,叠加起来即使顺序错乱,观感也不会撕裂。
- 尽量避免多块大面积广告牌在同一空间交叉。如果场景允许,让火焰区域错开,或者用多个小发射器代替一个大发射器。
- 为烟雾和火焰设置不同的渲染队列。烟作为“背景”先渲染,火焰作为“光源”后渲染,余烬作为“高光”最后渲染,队列顺序固定下来,闪烁自然减少。
有一些朋友会选择直接关闭深度写入来解决,这能消除黑块,但也可能让火焰被远处的模型硬生生盖住,在某些角度穿帮。遇到排序问题,要先分清是“半透明排序抖动”还是“深度误写”,前者调整队列和混合模式,后者检查深度写入设置。
4.3 序列帧、热扭曲和灯光不同步的细节问题
序列帧火焰最容易出现的两个问题,一个是所有火焰同步呼吸,一个是播放速度像“抽筋”。同步呼吸的元凶是火焰动画都在同一个时间轴上从头播放,所有篝火、火把长得一模一样,像复制粘贴出来的克隆人。解决方法是给每个发射源一个随机相位偏移,让它们错开几帧。
播放速度“抽筋”多半是粒子寿命和序列帧总帧数没有对齐。火焰序列帧通常只有几十帧,如果粒子生命周期是0.6秒,而序列帧设计时是每帧约0.04秒,那么总播放时长和寿命必须匹配。我看到不少人只调寿命不调播放速率,结果火焰在粒子中途“断电”,后半段变成透明空壳,非常出戏。
灯光不同步也是高频问题。光源闪烁和火焰粒子是两条独立曲线,即使都点在“1秒周期”,相位对不上,看起来也会像“灯归灯、火归火”。我的习惯是给光源的强度曲线绑定一个全局时间噪声种子,并让这个种子参与粒子速度抽样。听起来很小,效果却立竿见影。
热扭曲的强度我再叮嘱一次:一定要加Mask。第一次加全屏热扭曲时,远处地平线都在晃,我刚看两分钟就头昏。用火焰亮度经过模糊后的贴图作为扰动权重,可以让扭曲只出现在火苗周围,并随着距离快速衰减。
5. 不同平台的火力差距:性能预算与远处火焰处理
5.1 按平台分配各级火焰的预算
同样是火焰,桌面电脑和中端手机完全是两种打法。如果项目要做多平台,性能预算最好一开始就按最弱的平台定。我的习惯是给火焰特效立一份性能预算表,按平台分层,每一项都能明确回落到哪个数字。
| 平台 | 同屏粒子上限 | 火焰主体方案 | 香烟比例 | 后处理 | 动态阴影 |
|---|---|---|---|---|---|
| 桌面高配 | 8000–12000 | GPU粒子或序列帧混合 | 主体60%、烟25%、余烬15% | Bloom + 热扭曲,全屏可用 | 可开短距离阴影 |
| 中端手机 | 2000–3000 | 序列帧广告牌为主 | 主体50%、烟30%、余烬20% | 热扭曲只限火焰区域,Bloom关闭或极轻 | 不开 |
| 低端设备/网页 | 300–800 | 交叉面片或程序着色器 | 主体80%、烟15%、余烬5% | 不启后处理,用贴图自发光模拟 | 不开 |
这三行不是死规则,是一个思考框架。核心思想是:质量高不高不看“能不能烧一万个粒子”,而是看同等预算下,视觉信息有没有被尽量压缩到观众注意的地方。中端手机上,我会把粒子数量省下来,保证主角篝火的密度足够,背景火把降到很低的粒子数,甚至只用点光源加动画序列帧。
移动端还有两个成本大头不能忽略:一个是动态阴影,每开一盏带阴影的光,场景里其他物体都要多一个阴影贴图采样,火焰这种高频动态光源尤其贵,通常直接关掉。另一个是全屏后处理,Bloom和热扭曲都是像素密集操作,移动端宁可让火焰贴图自带一点过曝渐变,也不要轻易开大范围后处理。
5.2 远处火焰的“假”要假得聪明
场景里同时出现几十堆火焰时,真正决定性能的往往不是近处那一堆篝火,而是背景里那些“沉默的大多数”。处理远处火焰,我从不追求让它“和近处一样真”,而是追求让它“在视觉上被识别为一堆火”。
远处的火焰可以降到很离谱:一个简单的强调色光晕,加上一个缓慢闪动的点光源就够了。因为距离远,观众看不到火舌的细节,只能看到“那里有一坨发光的橙色”。如果连光晕都不想要,可以直接用一张火焰形状的发光贴花贴在模型表面,再配一个动态UV扰动,成本低到可以忽略。
中距离的火焰,我用序列帧广告牌并减半粒子数,同时关闭余烬和烟,或者把烟粒子的寿命大幅缩短。这样做的好处是,即使粒子数降低,序列帧仍然保留了火焰剪影的动态,观感上至少有一半细节。LOD切换的过渡也要平滑,我一般会在粒子系统上预留一个“预热阶段”,切换低LOD时先透明渐入,避免突然出现或者消失,否则玩家转个头就会看到一堆火凭空变了样,比性能卡顿更出戏。
如果同屏有几十个火把,我还会把它们打成一个共享的序列帧图集,用一个粒子发射器按不同位置发射多组火焰,一次绘制调用解决所有背景火焰。这些优化做完,帧率曲线会平稳非常多。
6. 最后想说的:建立自己的“火焰直觉”
6.1 多看真实素材:颜色梯度的第一手资料
很多人调火焰,是打开软件凭感觉调。我也经历过这个阶段,但后来发现效率低。真实火焰的颜色层次远比大多数人印象丰富:根部有一小段几乎透明到发蓝,主焰是黄色加白芯,外焰从橙跳到暗红,烟是灰黑色,余烬有黄色、橘色、甚至有淡紫色的过渡。这些层次的排列顺序和比例,才是最需要背下来的。
建议找一个慢放篝火的视频,一帧一帧截图,把火舌的轮廓描出来,把每一块区域的亮度级记录下来。你会发现一个规律:火焰从内到外是一个“明度高到低、色相从白蓝转向黄红”的渐变,而且亮度绝对不是均匀过渡,中间经常有断裂和抖动。把这些规律变成一张颜色曲线,再放进粒子系统里,效果会比随手拉渐变靠谱十倍。
6.2 一个小练习:从蜡烛火苗到篝火
如果让我给新手一个最有效的练习路径,那就是先做一支蜡烛,再做篝火。蜡烛只需要一个点发射器、一个圆形渐变贴图、一个横向卷动的噪声扰动,目标只有两个:形状要像蜡烛的火苗,颜色要呈现温度分层。这个阶段不要碰烟、不要碰余烬、不要碰Bloom,专注把“一个火苗的动态剪影”做对。
等蜡烛火苗在静止镜头里不会穿帮,再加一层烟,加一个点光源闪烁,再慢慢把它放大成一个篝火主体,补上余烬和热扭曲。每次只加一个变量,你就能清楚地知道哪个参数是火焰感的关键。我做模拟项目X时就是这么复盘的,最后发现篝火的灵魂其实不在火焰主体,而在那层若有若无的烟和几个在风里飘的余烬,它们让火有了“方向”和“呼吸”。
最后分享我个人一直在用的习惯:做火焰,永远不要闷头调参。调十分钟,截三张不同时机的图,盯三分钟,和视频素材比对一下,再继续调。这个“素材对照”的笨办法,我现在还在用。火焰不是一个需要被精确建模的对象,而是一个需要被“感觉”出来的现象,你越肯花时间去看它,它就越愿意在你的三维世界里活起来。