火焰、爆炸、烟雾这类实时特效看起来不真实,往往不是着色器精度不够,而是粒子在运动过程中缺少一致的物理约束。单独调整颜色、大小或贴图虽然能让单帧画面更有冲击力,但连续播放时会出现漂移、闪烁、重量感缺失。要解决这个问题,需要把物理属性描述、运动轨迹计算、粒子密度参数拆成三个独立模块分别验证。物理属性描述定义粒子为什么这样运动,运动轨迹负责让位移过程符合力学规律,粒子密度参数决定半透明叠加后的体积层次。本文围绕这三个方面展开,给出可落地的参数调整方法。

一、物理属性描述:从视觉参数中拆出受力数据
物理属性描述的核心是把粒子当作一个带有质量、受力、阻尼和温度的小体积,而不是屏幕上的一个贴图。很多特效预设把粒子初始速度、生命周期和大小曲线混在一起,导致调参时一个参数影响多个表现。比如火焰上升速度同时受初速度、阻力和浮力影响,如果只改速度曲线,火焰要么像火箭一样窜上去,要么在半路停住。正确的做法是在粒子结构中单独存放物理量,让视觉参数只负责呈现。
在实时特效系统中,可以把粒子当前位置、速度、质量、阻力系数、浮力系数和温度分开存储。质量决定惯性,质量越小越容易受外力和噪声影响;阻力系数用来模拟空气摩擦,通常与速度成正比;浮力系数只在火焰和热烟中启用,普通烟雾可能为负值表示下沉;温度则驱动颜色变化和浮力衰减,避免火焰上升一段后亮度仍然不变。这样设计以后,调整火焰的上升速度就不用再去改大小曲线,而是直接改浮力和质量。
struct Particle
{
float3 position;
float3 velocity;
float mass; // 质量,越小越易受外力
float life; // 剩余寿命
float maxLife;
float drag; // 空气阻力系数
float buoyancy; // 浮力或热升力,可正可负
float temperature; // 用于颜色和浮力衰减
};
// 初始化一个火焰粒子
Particle p;
p.position = emitterPosition + randomOffset;
p.velocity = normalize(randomDirection) * initialSpeed;
p.mass = 0.02f;
p.drag = 0.8f;
p.buoyancy = 0.6f;
p.temperature = 1.0f;
p.life = p.maxLife = 2.5f;
把物理属性独立出来后,材质层只负责根据温度、寿命等变量去采样颜色和透明度。这样单粒子运动是否合理、颜色是否准确可以分开验证。如果发现火焰速度曲线奇怪,先检查质量和浮力,而不是在颜色曲线里加关键帧。这个拆分方式也方便在不同特效之间复用同一套粒子更新逻辑,只是初始化参数不同。
二、运动轨迹:用积分器和噪声替换线性插值
运动轨迹的失真通常来源于线性插值。如果粒子位置在每一帧按固定速度前进,轨迹会呈现均匀直线,即便加一点随机扰动,也只是让直线变成折线。真实世界中,粒子受重力和空气阻力影响,速度是持续变化的,表现为先加速后减速。要获得自然的轨迹,需要使用积分器更新速度与位置,常见做法是半隐式欧拉。
半隐式欧拉先用当前加速度更新速度,再用更新后的速度更新位置。比起显式欧拉,它对阻力较大的粒子更稳定,不容易出现速度震荡。对于爆炸冲击波,可以在径向方向增加一个随时间快速衰减的加速度;对于烟雾,可以使用低频大尺度噪声让运动路径缓慢弯曲;对于火焰,则使用高频小尺度噪声模拟热空气的扰动。
half3 CurlNoise(float3 p); // 外部噪声函数,返回旋度噪声
void UpdateParticle(inout Particle p, float dt, float3 wind, float turbulence)
{
// 重力 + 浮力/热升力,浮力除以质量体现质量对升力的影响
float3 acceleration = float3(0.0f, -9.8f, 0.0f);
acceleration += float3(0.0f, p.buoyancy * p.temperature / p.mass, 0.0f);
// 风力除以质量,轻粒子受风影响更大
acceleration += wind / p.mass;
// 空气阻力与速度反向
acceleration -= p.velocity * p.drag;
// 加入涡旋噪声,让轨迹产生自然弯曲
float time = p.maxLife - p.life;
float3 noise = CurlNoise(p.position * 0.5f + time * 0.3f);
acceleration += noise * turbulence;
// 半隐式欧拉积分
p.velocity += acceleration * dt;
p.position += p.velocity * dt;
p.life -= dt;
}
噪声频率和强度必须与粒子生命周期匹配,否则会出现粒子突然拐弯或不自然抖动。对于持续时间较长的烟雾,噪声频率可以低一些,让路径呈现大尺度弯曲;对于短促的爆炸碎片,噪声强度可以高一些,但频率不能太高,否则会破坏冲击波的径向趋势。调参时建议先关闭噪声,只检查重力和阻力是否让运动曲线自然,再逐步加入噪声。
三、粒子密度参数:体积感来自密度而非数量
粒子密度参数经常被误解为粒子数量。粒子数增加可以填补空洞,但如果每个粒子的透明度和大小不变,重叠区域的能量会线性累积,导致中心过曝或颜色发黑。真实体积感来源于单位体积内粒子的密度和单个粒子在屏幕空间的覆盖面积。对Alpha混合粒子,实际透明度应该按照密度重新分配,而不是简单累加。
在发射器端,目标密度决定了发射率。如果已知发射体积和平均生命周期,可以估算出需要每秒发射多少粒子才能维持目标密度。计算公式可以理解为:发射率等于目标密度乘以发射体积再除以平均生命周期。实际调参时,先固定粒子大小,再根据画面中的空洞或过曝情况调整目标密度,最后再微调粒子半径。不要在粒子半径上反复放大来弥补密度不足,那样只会让贴图边缘模糊。
float ComputeDensityAlpha(float particleRadius, float density, float softness)
{
// 粒子在屏幕空间的近似覆盖面积
float area = particleRadius * particleRadius * 3.1415926f;
// 光学深度近似:密度越大,透射率越低
float opticalDepth = density * area;
// 使用指数衰减计算透明度权重
float alpha = 1.0f - exp(-opticalDepth * softness);
return saturate(alpha);
}
这个函数可以放在粒子着色阶段,密度值由外部统计或采样得到。需要注意的是,叠加混合模式对密度表现影响很大。Additive模式适合火焰和爆炸,密度过高时中心容易过曝到白色,这时要降低密度或减小粒子半径;Alpha模式适合烟雾,密度过高时中心会发灰发黑,要适当提高密度上限或使用软粒子过渡。两类特效不要用同一套密度参数。
四、联调顺序与参数取值范围参考
三个模块单独调好后,还要按顺序联调。先固定物理属性取值范围,确保单粒子运动符合预期;再调整轨迹噪声强度和频率,避免粒子轨迹出现突变;最后根据画面密度反馈调整发射率和粒子大小。可以建立一个调试面板,把质量、阻力、浮力、噪声频率、目标密度、粒子半径暴露出来,避免在材质里反复翻找参数。
不同特效类型适合的物理属性差异很大。火焰粒子质量小、浮力明显,爆炸粒子质量中等、阻力很大,烟雾粒子质量极小、阻力极高且浮力为负。目标密度也完全不同,火焰需要较高密度形成连续燃烧感,烟雾密度过高反而会显得厚重,爆炸只在极短时间内维持高密度。下表给出常见特效的参考范围,实际项目中需要根据引擎单位和镜头距离进行缩放。
| 特效类型 | 质量范围 | 阻力系数 | 浮力/热升力 | 目标密度(个/立方米) |
|---|---|---|---|---|
| 火焰 | 0.01-0.05 | 0.8-1.5 | 0.4-1.2 | 200-500 |
| 爆炸冲击 | 0.1-0.3 | 1.5-3.0 | 0 | 2000-5000(瞬时) |
| 烟雾 | 0.001-0.005 | 2.0-4.0 | -0.3到-0.1 | 30-80 |
| 扬尘 | 0.02-0.08 | 1.0-2.0 | -0.05到0.1 | 50-120 |
这些数值不是绝对标准,但可以作为初始基线。调参时不要让一个模块的参数掩盖另一个模块的问题。例如发现中心过曝,先降低目标密度而不是直接减小粒子亮度;发现轨迹发飘,先检查质量和阻力而不是增加噪声。把物理属性描述、运动轨迹计算和粒子密度参数当作三条独立可调的通道,特效的真实感会随着每一轮联调稳定提升。