CPU粒子系统 - 火焰与烟雾模拟

粒子系统是游戏引擎和实时渲染的核心技术之一。今天从零实现一个完整的 CPU 粒子系统,模拟真实的火焰、烟雾和飞散火星效果。

效果预览

火焰稳定状态(3秒预热后)

火焰效果

时序演变(四帧对比)

时序演变

从左到右:粒子系统从少到多,逐渐形成稳定火焰形态

项目目标

实现一个功能完整的 CPU 粒子系统,具有:

  • 三种粒子类型:火焰、烟雾、火星
  • 物理模拟:浮力、重力、湍流、阻尼
  • 软粒子渲染:高斯衰减 + 加法/Alpha 混合
  • 颜色生命周期:从热核心到消散的完整颜色渐变

系统架构

粒子结构

每个粒子持有以下状态:

1
2
3
4
5
6
7
8
9
10
11
12
struct Particle {
Vec2 pos; // 位置
Vec2 vel; // 速度
float life; // 当前生命值 [0,1]
float maxLife; // 生命持续时间(秒)
float size; // 当前大小(像素)
float maxSize; // 最大尺寸
float turbSeed; // 湍流种子(保证每个粒子的湍流独立)
ParticleType type; // FIRE / SMOKE / EMBER
bool alive;
float age; // 当前年龄
};

三类粒子的差异

属性 火焰 FIRE 烟雾 SMOKE 火星 EMBER
混合模式 加法 Alpha 加法
浮力 强(正比于 life)
重力
寿命 1.5-3 秒 3-5 秒 1-3 秒
大小变化 先增后减(sin 曲线) 持续增大 不变

核心技术

1. 颜色生命周期渐变

火焰的颜色随生命值(life: 1→0)变化,模拟真实火焰从热核心到消散的过程:

1
2
3
4
5
6
life=1.0 → 蓝白 (1.0, 1.0, 1.0)    ← 刚出生(最热)
life=0.85 → 黄白 (1.0, 0.95, 0.6)
life=0.65 → 橙色 (1.0, 0.55, 0.1)
life=0.40 → 橙红 (0.8, 0.15, 0.02)
life=0.15 → 暗红 (0.3, 0.0, 0.0)
life=0.0 → 消失 (0.0, 0.0, 0.0) ← 死亡
1
2
3
4
5
6
7
8
9
10
Color getFireColor(float life) {
if (life > 0.85f) {
return lerpColor(
Color(1.0f, 0.95f, 0.6f, 0.9f), // 黄白
Color(1.0f, 1.0f, 1.0f, 1.0f), // 蓝白热核
(life - 0.85f) / 0.15f
);
}
// ... 其他区间
}

2. 软粒子渲染(高斯衰减)

避免粒子边缘的硬切割,使用高斯函数产生平滑的边缘衰减:

1
2
3
float t = dist / radius;
float falloff = std::exp(-t * t * 3.0f); // 高斯衰减
float alpha = color.a * falloff;
  • t=0(中心):falloff = 1.0(最亮)
  • t=0.5(半径):falloff ≈ 0.47
  • t=1.0(边缘):falloff ≈ 0.05(几乎透明)

3. 加法混合(火焰叠加)

多个粒子叠加时,亮度相加而不是覆盖,自然形成明亮的火焰核心:

1
2
3
4
5
6
if (additive) {
// 颜色直接叠加,自然产生亮核心
nr = std::min(1.0f, sr + color.r * alpha);
ng = std::min(1.0f, sg + color.g * alpha);
nb = std::min(1.0f, sb + color.b * alpha);
}

这是为什么很多粒子重叠区域会变得极亮(接近白色),就像真实火焰核心一样。

4. 湍流噪声

使用简单的哈希噪声制造自然的湍流效果,让火焰自然摇曳:

1
2
3
4
5
6
7
8
9
10
// 哈希噪声 [-1, 1]
float hash2(float x, float y) {
float n = std::sin(x * 127.1f + y * 311.7f) * 43758.5453f;
return n - std::floor(n);
}

// 施加湍流力
float tx = noise2(p.pos.x * 0.01f + p.turbSeed,
p.pos.y * 0.01f + time * 0.5f);
Vec2 turbForce(tx * 30.0f, ty * 15.0f);

关键:每个粒子有独立的 turbSeed,使得同一位置的不同粒子受到不同的湍流影响,避免整体同步摇摆。

5. 粒子大小的生命周期变化

火焰粒子的大小使用 sin 曲线,模拟”先膨胀后缩小”的效果:

1
2
3
// life 从 1→0,sin(π×life) 在 life=0.5 时最大
float lifeFactor = std::sin(p.life * 3.14159f);
p.size = 3.0f + lifeFactor * p.maxSize;

物理模拟细节

力的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
case ParticleType::FIRE: {
Vec2 buoyancy(0, -40.0f * p.life); // 浮力(正比于温度/生命)
Vec2 wind(sin(time*1.5f + ...) * 10.0f, 0); // 周期性风力
p.vel += (buoyancy + turbForce + wind) * dt;
p.vel = p.vel * 0.97f; // 阻尼
break;
}

case ParticleType::EMBER: {
Vec2 gravity(0, 80.0f); // 重力(向下)
p.vel += (gravity + turbForce * 2.0f) * dt; // 强湍流+重力
break;
}

分层渲染策略

SMOKE → FIRE → EMBER 顺序渲染:

  1. 烟雾:在最底层,Alpha 混合
  2. 火焰:覆盖在烟雾之上,加法混合发光
  3. 火星:在最顶层,加法混合,最小但最亮

量化验证结果

1
2
3
4
5
高亮像素(亮度>200): 23,604 个 ✅
非黑像素占比: 11.2% (>3% 阈值) ✅
火焰区域 R > B(橙红色调正确) ✅
时序帧亮度递增: 26.8 → 36.5 → 38.0 → 41.4 ✅
发射器区域最大亮度: 255 ✅

性能表现

1
2
3
4
5
6
粒子总数(稳定后): 202
火焰粒子: 139
烟雾粒子: 53
火星粒子: 10
预热帧数: 179 帧(3秒 × 60fps)
总运行时间: 0.10秒 ✅

仅 0.1 秒完成 3 秒的模拟 + 渲染,CPU 粒子系统在不需要实时性时非常高效。

经验总结

  1. 加法混合是火焰渲染的核心:不需要复杂的体积渲染,简单的粒子加法叠加就能产生令人信服的发光效果
  2. 独立的湍流种子很重要:如果所有粒子共享同一湍流相位,火焰会整体同步摆动,看起来很假
  3. 分层渲染:先渲染低层的 Alpha 混合烟雾,再渲染加法混合的火焰,视觉效果正确
  4. 高斯衰减软边界:粒子边缘的平滑过渡是”软粒子”外观的关键,没有它粒子看起来像圆饼干
  5. 噪声的时间维度noise(x, y, time) 让湍流随时间变化,产生流动感

下一步扩展

  • GPU 粒子系统:使用 Compute Shader 将模拟移至 GPU,支持百万级粒子
  • 3D 粒子:当前是 2D 投影,可以扩展为真正的 3D 空间粒子
  • 碰撞响应:粒子与场景几何体的碰撞和反弹
  • 粒子图集:使用精灵图(Sprite Sheet)替代程序化圆形,更丰富的视觉效果

代码仓库

GitHub: daily-coding-practice/2026/03/03-04-Particle-System-Fire-Smoke


完成时间: 2026-03-04 05:32
迭代次数: 1次(一次编译通过)
代码行数: ~600行 C++
编译器: g++ (C++17, -O2)