前一阵子把“Unity火灾搭建模型”这件事从零到一完整做完,回头翻材质参数和脚本的时候发现,真正决定作品层级的,从来不是那个火焰模型有多精细,而是整套由粒子、Shader、灯光和交互脚本组成的“系统模型”有没有闭环。很多人搜“unity火灾搭建模型”,以为拖一个火焰预制体扔进场景就够了,结果要么火看起来像一张会飘动的贴图,要么场景里温度、蔓延、昏暗度全都对不上,程序一跑就穿帮。这篇就把我实打实拆解、调参、排坑的经验写出来,重点覆盖:火焰粒子怎么调、烟和热浪怎么补、动态光照怎么搭、单堆火怎么扩展成场景级火情,以及WebGL、移动端、VR这类实际发布环境里最容易翻车的地方。无论是做数字孪生、消防演示,还是游戏Boss战场景,这套方法都能让你省下大量反复试错的时间。
1. 先把“火灾模型”拆成三层,而不是去建一个模型
1.1 你以为要建一个物体,实际要建的是“整套燃烧状态”
Unity里处理火灾,我吃过最大的亏就是把精力全花在找模型上:火焰材质、黑烟贴图、火星粒子,一通下载之后发现它们根本不像同一个火场里的东西。后来才想明白,所谓“火灾搭建模型”,本质上不是建模软件的Mesh,而是“燃烧状态的可视化系统”:同一个物体要经历初始引燃、火苗渐大、旺盛燃烧、伴随黑烟、逐步熄灭这五个阶段。如果只是把火焰特效挂在物体身上,它就只是一个“灯光秀”,没有火场的节奏。
我现在的做法是先做需求拆解,不管什么项目,先把火灾抽象成三层:
- 逻辑层:负责“什么被点燃、热量怎么传播、燃烧多久、何时熄灭”,通常由C#脚本驱动。
- 表现层:粒子系统负责火焰、烟雾、余烬,Shader负责材质变化,灯光负责照亮周围。
- 反馈层:声音、热浪扰动、UI状态、地面焦黑等,让玩家或观众从更多感官维度相信“这里确实着火了”。
这套拆法对代码结构和后续性能优化都很有帮助。比如在数字孪生项目里,一个工厂车间可能同时出现多个起火点,逻辑层需要控制火源蔓延,表现层则需要做LOD和粒子数量控制。如果一开始不分层,后期扩展时每一处火源都要复制一堆零散组件,维护成本会直接失控。
1.2 先划清项目边界:高保真还是可交互
做火灾模型前,我一定会反问一句:这个火是玩家只能看,还是要真实参与交互?这两者的技术选型完全不同。
如果只是场景氛围,比如远处的建筑燃烧,可以用序列帧贴图或轻量粒子,尽量不占性能;如果玩家必须靠近、灭火、穿越火区,那火焰就不能只是一个视觉元素,它要有范围和伤害判定,要有被灭火器喷到后逐渐熄灭的逻辑。通常这种交互我更倾向把“火焰的碰撞范围”做成Trigger区域,而不是让每个粒子都做碰撞,因为粒子碰撞在移动端的开销非常大。
另外一个容易忽略的问题是:你要做的是“室内局部火灾”还是“室外大范围蔓延”。室内场景需要重点处理烟雾扩散和氧气不足的视觉效果,室外则要处理风对火焰方向的干扰。Unity的粒子系统自带Velocity over Lifetime和Noise模块,这两种动态效果极其关键,但很多新手都放着不用。后面我会细讲怎么靠Noise把火焰从“纸片抖动”变成“真实湍流”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 火焰主体怎么调:从参数到双层粒子的完整方案
2.1 火焰粒子系统的“面包和黄油”参数
先上干货,一个在PC和移动端都能跑、又比较接近真实火焰的主体粒子配置,我一般不会分开太远。以Unity的Particle System为例,最常见的火焰错觉来自三个地方:
- Emission发射速率。速率不是越高越好,尺寸大、数量少的火焰更容易有体积感,数量多、尺寸小的火焰更像火花。一个半径0.3到0.5米的火焰源,Rate over Time我会设在30到80之间,有时候还要用Burst做初始爆燃效果。
- Shape形状。火焰发射器用Cone锥形最自然,角度控制在5到12度,半径0.2到0.4米。圆锥发射让粒子向上散开,比默认的半球形更像火舌。
- Lifetime生命周期。火焰粒子存活时间不要太长,0.5到1.2秒比较合适。寿命太长会让火焰像一座持续喷发的火山,寿命太短又闪烁得像迪斯科灯。
我在项目里反复微调以后的参数表如下,适合那种普通室内垃圾桶着火的大小,如果火势更大,可以整体放大粒子的Simulation Space和Start Size。
| 参数 | 我的常用值 | 备注 |
|---|---|---|
| Duration | 1到2秒循环 | 配合Looping |
| Start Speed | 1.2到2.5 | 上升速度,数值太大会拉出长丝状 |
| Start Size | 1到2.5(用随机双曲线) | 大小不一才有层次 |
| Start Lifetime | 0.5到1.2 | 随机范围0.4到1.0 |
| Rate over Time | 40到80 | 火堆越大可以适当降低,靠尺寸补 |
| Shape Angle | 8左右 | 锥角太大会像倒扣的漏斗 |
| Gravity Modifier | -0.2到-0.5 | 负重力模拟热气流上升 |
| Simulation Space | World | 火源移动时才建议World |
把重力调成负值这一点很多教程不强调。真实火焰因为受热空气浮力驱动,粒子自始至终都在向上加速,如果单纯给粒子一个向上的初速度,它会匀速直线升起,毫无生气的关键就是这个“加速”丢失了。粒子系统里的Gravity Modifier填负数,等效于给粒子施加持续的向上加速度,火舌才能有那种抖动的上升感。
2.2 用Noise让火焰摆脱“纸片摆动”
粒子系统自带的Noise模块是目前性价比最高也最容易出效果的功能。在火焰粒子系统的Noise模块里,把Strength设为0.3到0.6,Frequency调在0.6到1.2之间,Scroll Speed给个0.5到1.0,你会肉眼看到火焰边缘从平滑圆弧变成类似真实火苗的扭曲轮廓。
为什么颜色板调了半天还是假?因为火焰的边缘运动不是简单正弦波,而是高频扰动叠加低频膨胀。Noise模块的Octaves可以理解成扰动的精细程度,1层是大规模摆动,3层会增加很多小锯齿,让边缘变得更碎。我通常把Octaves开到3到4,同时让Strenght随机化。如果发现火舌太碎、像抖动的塑料布,就把Frequency降到0.5到0.8;如果火苗硬得像铁棍,就调高Frequency,让摆动周期更短。
Noise基础上还要配合Velocity over Lifetime。真实火场中,每个火舌的运动方向不完全垂直,而是在上升的同时左右摇摆。这时候可以加一个范围很小、方向偏向Z轴或X轴的扰动速度。另一种做法是使用Curl Noise纹理,对粒子位置做向量偏移,这个在VFX Graph里更顺手。如果你的项目还在用Built-in管线,直接用粒子Noise模块就够用了。
2.3 一定要做两层火:内焰加外焰
只用一个粒子发射器做火,很难同时表现出“内部高温发白、边缘橙色偏暗”的层次。我建议所有火焰模型都至少拆成两个粒子系统:内焰和外壳。
内焰负责颜色最亮的一部分,尺寸大约是外壳的60%到70%,颜色从亮黄色到白色渐变,渲染模式用Additive叠加。亮度要开高,最好材质勾选HDR,这样命中Bloom时才能产生炽热发光感。内焰的粒子数量可以少一点,Rate大概15到30,寿命短一点。
外焰负责包裹内焰的暖橙红色,占比更大,Rate可以到40到60。外焰材质用Alpha混合,而不是纯Additive。Alpha混合能让外焰边缘逐渐虚化,和背景融合得更自然;若也用Additive,火焰边缘会因为叠加太多而白成一片。
两个粒子系统在层级里是父子关系,子粒子跟随父物体。我通常把内焰的Order in Layer调小一点,让外焰先渲染,内焰再叠上去。如果你用URP,注意Shader选“Universal Render Pipeline/Particles/Unlit”,如果还想让火焰接受阴影和深度雾,可以切到Lit,但移动端成本会高一些,需要实测。
3. 决定真实感的另一半:烟、余烬和热浪
3.1 烟雾层:面积最大也最容易穿帮
很多新手做火灾只盯着火苗,结果大火烧得很旺,烟雾却几乎没有或只是直直往上飘的圆柱,看起来非常假。真实火灾里烟雾有两个视觉作用:一是遮盖火焰边缘的硬边,让发光区域自然过渡;二是填充空间,让人感受到“灾情严重”。
我的烟雾粒子用的贴图不是那些对比强烈的黑烟纹理,而是一张边缘柔和的软圆点,使用Alpha混合模式。粒子初始颜色可能是暗灰色带一点透明度,颜色曲线最后慢慢变成“黑灰色但透明度接近零”,这样烟雾从半透明到淡出不会出现一个明显圆形在场景里打转。
烟雾粒子的参数和火焰差异很大:Start Lifetime要长,通常2到4秒;Start Speed很小,0.3到0.8;Start Size继续向上增长,可以靠Size over Lifetime曲线让它出炉后逐渐扩大到2到3倍。烟雾的重力不要设成负值,否则会一路升到天上去,应设成-0.15到-0.3,让它有上升趋势但逐渐衰减。
如果场景里有一台风扇或者窗户的风向,给烟雾添加一个全局方向的Velocity over Lifetime。这样烟雾会飘向通风方向,整个场景的火灾压迫感会立刻增一截。如果你发现烟雾粒子前半段看得太清楚、像灰团,可以把颜色曲线在粒子刚出生后设置成非常低的Alpha,然后快速升到峰值,再用长时间缓慢衰减。
3.2 余烬和火花:细节点睛但不能喧宾夺主
火堆周围的余烬粒子是我最喜欢的部分,因为成本低、效果明显。余烬不像主火焰那样密集,其实只要有少量火星在热气流里翻滚飘散,观众就会下意识觉得“这火非常热”。
余烬粒子用很小的Quad或者Point渲染,粒子数量控制在10到30,尺寸0.05到0.15,颜色用柠檬黄或橙红。重力不要设成完全向下,最好在-0.1到0,让它受热气流影响飘忽。速度范围比火焰更猛一些,0.5到3,部分粒子会突然蹿高,再慢慢落下。
在Particle System里加一个简单的Collision模块,选择Planes或者World,可以让余烬落在地面后消失。但如果场景中有大量碰撞体,粒子碰撞的物理开销会上升。我的经验是余烬粒子一般不加物理碰撞,只需把它的Start Lifetime设短一点,让火星在寿命末期透明度归零,这样就像“熄灭在空中”,观众通常不会察觉。非要落到地上熄灭,可以加简单的Damping参数而不是完整碰撞。
同样需要小心的是,余烬不能太密。一个0.5米的火焰源,30颗余烬在慢镜头下已经很丰富,再加到100颗就没有火花零星的浪漫感,反而让画面变脏。
3.3 热浪扭曲:一个小技巧让火焰“热”起来
热浪是最容易被忽略但效果极好的一层。很多人在PC上看到火焰视频,觉得“空气都在发抖”,但没意识到这是后处理或Shader做的视觉欺骗。Unity里实现热浪有几种常见方案:
- Built-in管线里,一个比较老但有效的办法是用GrabPass抓屏,然后做UV扰动。
- URP下GrabPass不再方便,更推荐的做法是额外建一个RenderTexture相机,把背景场景灌到RenderTexture,再在火焰区域放一个透明的折射Shader采样这张图。这个办法不依赖GrabPass,在URP项目里更稳。
- 如果你的Unity版本支持VFX Graph,可以用Screen Space Distortion模板,效率高很多。
但你并不一定要上Shader。如果只想低成本模拟热浪,可以在火焰中心摆几个接近透明的半球Mesh,材质用噪声纹理做扭曲,配合很弱的半透明混合。这只对单机小范围有用,因为它本质上是一个假的扰动纹理,不适合大面积火场。
我建议优先使用RenderTexture方案放在场景里,因为大多数UPR项目后期要做移动端或WebGL发布,GrabPass在WebGL上容易出兼容性问题。具体做法是:把主相机后面跟一个不渲染UI的Secondary Camera,Target Texture指向一张低分辨率RenderTexture,再把火焰周围地面或区域的特效材质用这张纹理做偏移采样;偏移强度用一张噪声贴图控制。采样方向和强度随时间变化,就能模拟热空气折射。低分辨率256到512就够,没必要开4K,否则发热量不容小觑。
4. Flames里的光与影:火光照明的动态控制
4.1 点光源闪烁不能用AudioSource那种随机
火光和普通恒定光最大区别是亮度和颜色都在快速变化。网上很多“火光闪烁”脚本只是用Random.Range每帧乱跳,结果是灯光像接触不良的灯泡,一闪一闪,非常不自然。真实火光强度曲线介于Perlin噪声和随机低频波动之间,应包含一个较大的慢波动(周期0.2到0.5秒)和一个较小的快抖动。
我一般写一个自定义脚本:主光源强度是基于Perlin噪声生成的,频率慢一点;叠加一个AnimationCurve控制的次级变化。Unity自带Mathf.PerlinNoise非常直观,用时间乘以频率再采样,输出0到1,映射到光的强度范围即可。
csharp复制using UnityEngine;
public class FireLightFlicker : MonoBehaviour
{
public Light targetLight;
public float baseIntensity = 2f;
public float intensityVariation = 0.8f;
public float frequency = 2.5f;
private float seed;
void Start()
{
seed = Random.Range(0f, 10f);
if (targetLight == null) targetLight = GetComponent<Light>();
}
void Update()
{
if (targetLight == null) return;
float t = Time.time * frequency + seed;
float noise = Mathf.PerlinNoise(t, 0.5f);
float targetIntensity = baseIntensity + (noise - 0.5f) * intensityVariation;
targetLight.intensity = Mathf.Lerp(targetLight.intensity, targetIntensity, Time.deltaTime * 8f);
}
}
注意到这里做了Lerp平滑,避免光强跳变太生硬。这种方法比Random.Range好在连续性和自相关性:第0.1帧的光亮值不会和第0.2帧毫无关联,人能感受到一种“火在摇曳”的惯性。
4.2 点光灯数量要节制:移动端和VR是硬约束
火焰光照如果每个火堆都放实时光源,性能会瞬间爆炸。移动端实时光照数量通常不建议超过2到4个,VR里特殊情况下可能更紧张。一套火灾场景里“每堆火都有动态点光”的浪漫想法,在真机上一测帧率立刻打回原形。
务实的思路是“几堆主火放实时灯,远处火堆只保留粒子系统,或者用Lightmap/反射探针做亮度模拟”。在数字孪生项目里,我甚至会让真正影响角色安全感的“主火源”有动态光,其他辅助火源共用同一点光源。方法是把火源加入一个管理器,管理器定期判断哪个火源离玩家最近、影响最大,然后动态调整点光源位置和强度,而不是为每个火焰源都建一个Light组件。
另外,火焰照射到角色和地面上时,如果阴影太重也会出戏。我通常把每盏火焰点光源的Shadow Type设为No Shadow,火灾场景本身混乱,硬阴影反而帮倒忙。若必须有阴影,也只在Showcase级PC项目里给主光源开软阴影,Light Range不超过8米。
4.3 Bloom和HDR配合,但也别把场景烧白
想让火焰看起来“亮得在发光”,核心是要让火焰材质输出的颜色大于1。Unity标准粒子材质如果颜色在HDR模式下调到2、3甚至更高,配合相机的Bloom后处理,就会产生令人信服的发光感。如果不开HDR,再怎么提高颜色值也只是变成纯白,画面没有“辉光外溢”。
不过Bloom强度要克制。火焰在夜晚场景里尤其容易过曝,一下就把周围环境烧成一片白色。我的调试顺序是:先调火焰粒子本身颜色,让画面最亮处接近“白色但还能看到结构”,再加Bloom。Bloom阈值设在0.8到1.2左右,强度先给0.3再微调。如果开了Bloom后背景都被照亮到看不清,八成不是Bloom太强,而是灯光范围和粒子HDR值都太高,应优先降低光源强度和粒子亮度,而不是削Bloom。
5. 从“一堆火”到“一场火”:蔓延逻辑与性能取舍
5.1 蔓延不要用复杂热传导,绝大多数场景用“热值脚本”就够了
如果有交互需求,例如用灭火器灭火、隔离带要防止火势蔓延,就需要给普通物体加可点燃属性。为了真实而做完整的热传导偏微分方程非常不划算,实际项目中用“热值当量”简化就够:每个可燃物有一个Heat属性,当外部火源持续给它增加Heat,Heat达到阈值后就点燃。
我常用的组件结构分两类:FireSource和BurnableObject。FireSource周期性向以自身为中心、半径R的球形范围发出热量;BurnableObject收到热量值后累加,如果超过IgnitionPoint则触发点燃。点燃后,这个对象自身也会变成FireSource,继续影响周围物体,实现蔓延。
csharp复制using System.Collections;
using UnityEngine;
public class BurnableObject : MonoBehaviour
{
public float heat = 0f;
public float ignitionPoint = 100f;
public float coolingRate = 1f;
public GameObject fireEffectPrefab;
public bool isBurning { get; private set; }
public void ApplyHeat(float amount)
{
if (isBurning) return;
heat += amount;
if (heat >= ignitionPoint)
{
Ignite();
}
}
void Update()
{
if (!isBurning && heat > 0f)
{
heat = Mathf.Max(0f, heat - coolingRate * Time.deltaTime);
}
}
void Ignite()
{
isBurning = true;
if (fireEffectPrefab != null)
{
Instantiate(fireEffectPrefab, transform.position, Quaternion.identity, transform);
}
// 通知周围,让邻近物体也能被点燃
var src = gameObject.AddComponent<FireSource>();
src.radius = 1.2f;
src.heatPerSecond = 30f;
}
}
这里烧起来后还把自身改成FireSource,能让火沿着布料、纸箱、树木逐级蔓延下去。你可以不用在每帧Update里去OverlapSphere检测,那样场景物体一多就卡。更好的是把FireSource的检测做成协程,每0.2秒检测一次,每次一个火源只在周围几个半径内做一次Physics.OverlapSphere,结果缓存在List里然后逐个ApplyHeat。
5.2 蔓延形态别太圆:用方向、噪声和随机性控制
如果蔓延只按球形检测扩散,最后火势会变成一个非常均匀的圆环,看起来像程序生成的病毒扩散,不像真实火灾。真实火灾蔓延受风向、可燃物分布和材质湿度共同影响。处理办法有三个:一是让每个BurnableObject的点火阈值有一定随机范围,比如100到130之间;二是给FireSource的热量分布加方向权重,例如风向强的地方heatPerSecond乘1.5,逆风方向乘0.3;三是为粒子特效的Noise种子设定随机值,让每个火焰源的形态不一样。
我还会在FireSource的检测逻辑里保持一个“最近一次传播时间”,避免同一帧内多个FireSource反复点燃同一个物体。还有一个细节是,物体被点燃后要把Collider保留,这样Trigger检测还可以继续命中,但点火时不要对已经燃烧完毕的对象继续施加热量,否则会出现“灰烬一直受到热量却无法熄灭”的Bug。
5.3 粒子泛滥是最大性能杀手,池化和LOD必须安排
当场景出现20个火源时,如果每个火源都实例化完整的三层粒子加动态灯,性能一定崩。这里给出一个很实用的规则:全场景同时最多只做3盏动态灯光;超过3个火源后,远距离火源只保留最外层的火焰粒子并砍掉内焰与烟雾;再远一点就彻底不播放粒子,只保留一个Billboard贴图或用Lightmap模拟红外热点。Unity的LOD Group并不直接支持Particle System切换,所以通常我写一个FireLOD脚本:
- 距离玩家小于10米:完整特效层,内焰、外焰、烟、余烬、动态光全开。
- 距离10到25米:只用外焰粒子加固定光,去掉余烬和光闪烁脚本。
- 距离大于25米:只显示贴图或让粒子系统直接Pause,并保留冒烟的小发射器。
还要做好粒子对象池。如果Unity版本支持,可以直接在场景里预先放几个不同强度火源预制体并禁用,需要点燃时SetActive(true),不需要时SetActive(false)再移回池子。不推荐频繁Instantiate和Destroy粒子物体,因为Instantiate时粒子系统的预热耗时和GC压力很容易让移动端出现明显卡顿。
5.4 移动端、WebGL和VR的实际环境注意点
这部分是从Unity项目发布角度总结的几个容易踩坑的点。
如果发布WebGL或者微信小游戏,Shader兼容性要提前排查。GrabPass在这种环境不一定可用,热浪如果非要用GrabPass,很容易报错或没有效果。粒子Bloom在WebGL平台要去看URP Asset里是否启用了HDR,若没开启HDR,材质颜色再高也会被压缩到1以内。还有纹理格式,粒子柔边贴图尽量用工程内置的ETC2或ASTC,WebGL2环境下对纹理尺寸对齐要求比较严格,某些Atlas纹理会在运行时出现紫边。
如果目标是Pico4这类VR设备,双重实时渲染会让粒子系统开销放大两倍。半透明粒子是深度排序和填充率的噩梦,VR中尤其明显。我的经验是:烟雾粒子数量下调50%,粒子贴图分辨率用128或256就够;动态点光源尽量不开,改用一个由Stereo摄像机可见的微弱探照灯或面光模拟火光亮度。粒子系统要开启Per Particle SystemInfo,避免双眼渲染不同步导致闪烁。
如果用Unity做数字孪生,还会遇到一个特殊需求:UI层要同步显示火情数据和传感器读数。火焰蔓延的Heat值和真实传感器传来的温度值对应起来,用UI文本控件动态更新就行。不要每帧直接SetText,会触发频繁的Rebuild;可以0.5秒更新一次,或者只在热值变化超过阈值时刷新。这个优化在VR或WebGL的UI里收益很明显。
6. 实战复盘:我在这套火灾模型里翻过的几个车
6.1 火焰粒子在Android上“发黑”,罪魁祸首是贴图压缩
某个项目发到Android真机测试,火焰原本在PC上是亮黄橙色,结果手机上变成中间发灰的暗色,像烧焦的纸片。排查后发现是贴图导入设置里Alpha Source不一致:我用了一张灰度图当Alpha mask,但压缩格式选择了RGBA Compressed ETC2,把原本2.0以上的HDR颜色信息压掉了。解决办法是单独建立一张“火焰专用”纹理,NoSRGB关闭,Wrap Mode改成Clamp,Alpha Is Transparency勾上,必要时用ASTC 6x6而不是ETC2,这样颜色保留会好很多。
每次做粒子特效我都建议在手机设置里单独验证一遍颜色值,不要只看Game视图。很多材质在编辑器里看着亮,真机因为没有HDR或压缩导致Gamma空间变化,视觉差别极大。
6.2 透明粒子前后穿插导致火焰像“硬纸板”
火焰本身是半透明的,粒子之间需要由引擎根据深度排序。但把所有火焰粒子放同一个Particle System Renderer里,批量排序往往会产生很生硬的交错边缘。火焰中心的白热内焰一旦被外焰遮挡,看起来就像一块块前后不连续的纸板。
我的应对办法是:把火焰拆成两个ParticleSystem,内焰用较高Priority和Renderer Order,外焰放低优先级。若还是穿插严重,可以给内焰和外焰各设置一个不同的MaterialQueue,例如内焰在Queue 3000,外焰在Queue 2995,让外焰永远先画。再不行就调整粒子渲染模式,把内焰的Render Alignment设为Facing,强制它始终面向相机,减少自遮挡。这些排序问题没有一个万能解,只能结合场景里的其它透明物体逐层排查。
6.3 火焰热点检不到人,角色走进火区掉血无效
火灾场景如果要做伤害判定,我第一版犯过很低级的错误:把FireSource的OverlapSphere结果过滤条件设成了“必须是Player标签”,结果角色换成NPC后,伤害就无效了。改用LayerMask而不是字符串标签更可靠,因为任何物体只要登上“可燃物”或者“受击方”Layer,就会被火源捕获。检测半径也要适当放大一点,因为火焰粒子视觉边缘比Collider范围大,玩家会和火焰“贴脸”却不受伤害,体验很怪。通常我把每次ApplyHeat的半径在视觉半径基础上乘1.3到1.5。
另外,如果火源是动态火源(例如车辆在移动过程中燃烧),OverlapSphere的检测中心位置要每帧从粒子系统的世界位置获取,而不能从根物体的旧坐标取,否则火焰视觉飘到半空,伤害判定还留在地面。
6.4 光照开销优化后,忘了恢复Lightmap导致场景一片黑
场景最初烘焙好Lightmap,一切正常。之后加入动态灯光优化,切换了LightingMode到Subtractive或者关闭了Baked GI,结果发现整个环境变得奇黑无比。排查到最后是LPPV和Lightmap的设置没有正确关联。部分光照模式在混合照明下若没有放Light Probe Group,动态特效盒子和角色会接收不到间接光。
解决方案比较简单:不要在运行时去切换Mixed Lighting Mode,而是从一开始就决定是否用Mixed照明。如果项目需要白天与夜晚切换,建议把环境光用LightingSettingsAsset分别配置好,再写一个编辑器工具批量切换。这种问题一旦出现往往要耗费大半天,而源头只是工程光照设置不够规范。
6.5 UI上做热力图和火灾实时状态更新的坑
这个只有做过数字孪生的兄弟会懂。一开始我用Unity UGUI的Image做热力区域,发现每个火灾热点状态变化都要实时更新纹理颜色,结果Texture2D.Apply每帧调用直接拖垮CPU,Unity Profiler里Scripts和GC Alloc都异常高。优化方式有两种:要么用RawImage配合RenderTexture,把热力数据编码进RenderTexture,通过材质Shader读取;要么创建一个低分辨率(128x128)的Texture2D,每0.5秒在Update之外刷新一次。
还有做消防CCTV的UI叠加时,UI上那根动态画线如果用来标记火焰边缘,纯用LineRenderer组件在World空间合适,但若需要UI里的世界坐标映射,要在Canvas上写Screen坐标转UI坐标的逻辑。这个问题和火焰模型本身无关,但实际项目里很容易被各种边缘case打断节奏,所以提前规划好坐标空间很重要。
另一个细节是,当UI和世界空间里同时有火灾效果,文字提示会跟粒子特效抢视觉焦点。通常我会让UI上的报错提示只在玩家正视火灾源时才弹出,否则让UI保持最小化,真实项目比游戏更需要“少打扰”。
最后再分享一个调试小技巧
如果你拿到一套网上找的火焰预制体,别急着往场景里砸。可以先建一个纯黑背景(或一个封闭盒子),把火放进去,只留一盏微弱环境光,这样你能快速看清粒子每层颜色和透明度是否合理。调完主体火势后,再把火放进实际场景里对比,此时你大概率会发现自己需要重新调亮度和Bloom值,因为环境反射会造成巨大差异。
后面的蔓延、光照和性能优化都不是一次到位的事。每做一次改动,最好就用Unity Profiler和Frame Debugger看一遍DrawCall、Particle数量、SetPass Call和临时显存分配。尤其是用SimplePerf去真机采帧时,你会看到火焰粒子特效在发热降频里占很重的一笔账。只要是长期项目,这套“分层搭建+动态裁减+真机Profile”的思路绝对值得固化到你们团队的开发文档里。
