FireworkGL 是一个高性能的 3D 实时烟花粒子特效模拟器,基于现代 OpenGL 技术构建。它实现了逼真的烟花物理模拟、多种爆炸效果、交互式控制,并通过实例化渲染技术支持数万粒子同时流畅运行。
- 随机自动发射:系统会自动在随机位置发射烟花
- 升空物理:烟花火箭遵循真实的物理规律升空
- 3D 空间爆炸:在三维空间中球形/定向爆炸
- 动态颜色渐变:粒子从高亮到暗淡的平滑过渡
- 发光效果:基于点精灵的中心发光渲染
- 尾迹拖影:记录历史位置,实现流畅的运动轨迹
- 生命周期衰减:粒子大小、透明度随时间自然衰减
- 鼠标左键点击:在屏幕任意位置触发烟花爆炸
- 空格键:快速发射随机烟花
- 数字键 1-5:发射特定类型的烟花
- A 键:切换自动发射开关
- 物理模拟:重力、空气阻力、速度衰减
- 音效支持:代码结构预留音频播放接口
- 实例化渲染:一次绘制调用渲染所有粒子
- 对象池模式:避免频繁的内存分配和释放
- 数据批处理:一次性上传所有顶点数据到 GPU
- 状态缓存:最小化 OpenGL 状态切换开销
FireworkGL/
├── main.cpp # 主程序入口,包含主循环和事件处理
├── Math.h # 数学工具类(Vector3, Matrix4, Color)
├── Random.h # 随机数生成器(梅森旋转算法)
├── Particle.h # 粒子系统核心(粒子、尾迹、对象池)
├── Firework.h # 烟花系统(火箭、爆炸类型、管理器)
├── Renderer.h # 渲染系统(着色器、VAO/VBO、实例化渲染)
├── stb_image.h # 图像加载库(第三方)
├── .vscode/
│ ├── c_cpp_properties.json # VS Code C++ 配置
│ ├── launch.json # 调试配置
│ └── tasks.json # 编译任务配置
└── README.md # 本文档
Vector3 - 3D 向量类
- 基本运算:加法、减法、数乘、数除
- 几何运算:点积、叉积、归一化
- 插值:线性插值
- 距离计算
Matrix4 - 4x4 矩阵类
- 透视投影矩阵
- 正交投影矩阵
- 观察矩阵(LookAt)
- 平移/缩放矩阵
Color - RGBA 颜色结构体
- 颜色运算(乘法、加法)
- 线性插值
- HSV 到 RGB 转换
基于高性能的梅森旋转算法(MT19937)实现:
- 均匀分布:整数、浮点数
- 正态分布:均值为 0 的高斯分布
- 球坐标系:单位球面上的随机点
- 半球分布:指定方向的半球随机点
- 颜色生成:随机颜色、色相范围、预设烟花颜色
粒子生命周期算法
初始化阶段:
├── 设置位置、速度、颜色
├── 初始化尾迹缓存
└── 标记为活跃
更新阶段(每帧):
├── 物理更新
│ ├── 应用空气阻力: v = v * (1 - r * dt)
│ ├── 应用重力加速度: v = v0 + a * dt
│ └── 更新位置: p = p0 + v * dt
├── 尾迹更新
│ ├── 添加新位置到尾迹列表
│ ├── 移除超过最大长度的旧点
│ └── 更新尾迹点的生命周期比例
├── 生命周期更新
│ ├── 累计已存活时间
│ ├── 计算生命周期比例
│ └── 检查是否超过最大寿命
└── 视觉属性更新
├── 颜色插值(平方插值,后期变化更快)
├── 大小插值(线性)
└── 亮度衰减(指数衰减)
死亡阶段:
├── 标记为非活跃
├── 放回对象池等待重用
└── 清空尾迹数据
对象池模式(Object Pool)
为了避免频繁的内存分配和释放,采用对象池设计:
- 预分配:程序启动时创建固定数量的粒子对象
- 空闲列表:使用队列管理可用的粒子索引
- 活跃列表:使用向量跟踪正在使用的粒子
- 回收机制:粒子死亡后立即放回空闲列表
性能优势:
- 避免 new/delete 的开销
- 减少内存碎片
- 更好的缓存局部性
- 可预测的内存使用
尾迹系统(Trail System)
每个粒子可以记录历史位置形成尾迹:
- 数据结构:
std::vector<TrailPoint>存储位置和生命周期比例 - 更新策略:每帧添加新位置,移除超出最大长度的旧位置
- 渲染方式:使用线段连接连续的尾迹点
- 透明度衰减:越旧的尾迹点透明度越低
烟花类型 (FireworkType)
| 类型 | 描述 | 特点 |
|---|---|---|
| Standard | 标准烟花 | 球形爆炸,速度适中 |
| Chrysanthemum | 菊花型 | 粒子密集,停留时间长 |
| Peony | 牡丹型 | 大粒子,长尾迹,多色混合 |
| Willow | 垂柳型 | 初始向上,重力影响大,极长尾迹 |
| Ring | 环形 | 水平面上圆环爆炸 |
| Palm | 棕榈型 | 向上伸展,形成树枝状 |
| Double | 双层 | 内爆+外爆两次爆炸 |
| Strobe | 闪烁型 | 多批次,短生命周期 |
| GoldenRain | 金色雨 | 金色粒子,大重力,下落效果 |
| Crackle | 爆裂型 | 主爆炸后粒子二次分裂 |
火箭物理模拟
发射阶段:
位置: 屏幕下方随机位置
目标: 随机或指定的 3D 位置
方向: 从发射位置指向目标位置归一化
初始速度计算:
v = sqrt(2 * g * h) (h = 目标高度 - 发射高度)
乘以随机因子 (0.9 ~ 1.1) 增加变化
上升阶段:
每帧更新:
v = v * (1 - airResistance * dt) (空气阻力)
v = v + gravity * dt (重力加速度)
p = p + v * dt (位置更新)
发射尾迹:
每 2 帧创建一个小火花粒子
方向与火箭运动方向相反
短生命周期 (0.5s)
爆炸条件:
1. 达到或超过目标高度
2. 垂直速度变为负数(开始下落)
3. 超过最大生命周期 (3s)
爆炸阶段:
根据烟花类型创建不同分布的粒子:
- 球形:单位球面均匀分布
- 半球:指定方向的半球分布
- 环形:水平面上的圆环分布
- 分支:多个定向的粒子群
爆炸算法详解
以标准球形爆炸为例:
void explodeStandard(ParticlePool& particlePool) {
for (int i = 0; i < particleCount; ++i) {
ParticleConfig config;
config.position = explosionPosition;
// 1. 随机方向(单位球面上均匀分布)
Vector3 direction = g_random.onUnitSphere();
// 2. 速度随机变化 (±20%)
float speedVariation = g_random.range(0.8f, 1.2f);
config.velocity = direction * particleSpeed * speedVariation;
// 3. 颜色插值
float colorT = g_random.nextFloat();
config.startColor = Color::lerp(primaryColor, secondaryColor, colorT);
config.endColor = config.startColor * 0.3f;
config.endColor.a = 0.0f;
// 4. 大小随机变化
config.size = particleSize * g_random.range(0.8f, 1.2f);
particlePool.createParticle(config);
}
}实例化渲染(Instanced Rendering)
传统的粒子渲染方式是逐个绘制:
for (每个粒子) {
设置 uniforms (位置, 颜色, 大小)
glDrawArrays(...) // 一次绘制调用
}问题:大量绘制调用导致 CPU/GPU 通信瓶颈。
实例化渲染解决方案:
// 准备实例数据(CPU 端)
std::vector<ParticleInstanceData> instances;
for (每个粒子) {
instances.push_back({位置, 颜色, 大小, 亮度});
}
// 上传到 GPU
glBufferSubData(GL_ARRAY_BUFFER, 0, size, instances.data());
// 一次绘制所有实例
glDrawArraysInstanced(GL_POINTS, 0, 1, instanceCount);实例化属性配置:
// 位置属性(每个实例不同)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, offsetof(position));
glEnableVertexAttribArray(1);
glVertexAttribDivisor(1, 1); // 关键:每 1 个实例更新一次
// 颜色属性
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, stride, offsetof(color));
glEnableVertexAttribArray(2);
glVertexAttribDivisor(2, 1);
// 大小属性
glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, stride, offsetof(size));
glEnableVertexAttribArray(3);
glVertexAttribDivisor(3, 1);着色器设计
粒子顶点着色器:
#version 330 core
layout(location = 0) in vec3 aPosition; // 基础顶点(单个点)
layout(location = 1) in vec3 aInstancePos; // 实例位置
layout(location = 2) in vec4 aInstanceColor; // 实例颜色
layout(location = 3) in float aInstanceSize; // 实例大小
layout(location = 4) in float aInstanceBrightness; // 实例亮度
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform int screenWidth;
uniform int screenHeight;
out vec4 vColor;
out float vBrightness;
void main() {
// 世界空间 -> 视图空间 -> 裁剪空间
vec4 worldPos = vec4(aInstancePos, 1.0);
vec4 viewPos = viewMatrix * worldPos;
vec4 clipPos = projMatrix * viewPos;
gl_Position = clipPos;
// 基于距离的点大小计算
// 远处的粒子看起来更小
float dist = -viewPos.z;
float pointSize = aInstanceSize * 100.0 / max(dist, 0.1);
gl_PointSize = clamp(pointSize, 1.0, 100.0);
// 应用亮度到颜色
vColor = aInstanceColor;
vColor.rgb *= aInstanceBrightness;
vBrightness = aInstanceBrightness;
}粒子片段着色器:
#version 330 core
in vec4 vColor;
in float vBrightness;
out vec4 fragColor;
void main() {
// 将点精灵坐标从 [0,1] 转换到 [-1,1]
vec2 coord = gl_PointCoord * 2.0 - 1.0;
// 计算到中心的距离平方
float dist = dot(coord, coord);
// 丢弃圆形外的像素,形成圆形粒子
if (dist > 1.0) {
discard;
}
// 平滑边缘:距离 0.8~1.0 的区域淡出
float alpha = 1.0 - smoothstep(0.8, 1.0, dist);
// 中心发光:距离中心越近越亮
float centerGlow = 1.0 - smoothstep(0.0, 0.5, dist);
vec3 finalColor = vColor.rgb * (1.0 + centerGlow * vBrightness);
fragColor = vec4(finalColor, vColor.a * alpha);
}混合模式:
// 加法混合(Additive Blending)
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
// 这种混合模式:
// - 多个粒子叠加会变得更亮
// - 非常适合发光效果
// - 无需精确的深度排序- 操作系统:Windows 10/11
- 编译器:MinGW-w64 GCC 8.0+(支持 C++17)
- 库文件:
- GLFW 3.4
- GLAD (OpenGL 3.3 Core Profile)
确保你的库文件按以下结构放置:
D:/opengl_libs/
├── glfw-3.4/
│ ├── include/
│ │ └── GLFW/
│ │ └── glfw3.h
│ └── lib-mingw-w64/
│ └── libglfw3.a
└── glad/
├── include/
│ └── glad/
│ └── glad.h
└── src/
└── glad.c
- 打开 VS Code
- 打开
main.cpp文件 - 按
Ctrl+Shift+B执行编译任务 - 或者按
F5启动调试
# 使用 g++ 编译
g++ -fdiagnostics-color=always -g -std=c++17 main.cpp -o main.exe ^
-I. ^
-Id:/opengl_libs/glfw-3.4/include ^
-Id:/opengl_libs/glad/include ^
-Id:/opengl_libs ^
-Ld:/opengl_libs/glfw-3.4/lib-mingw-w64 ^
-lglfw3 -lopengl32 -lgdi32 -luser32 -lkernel32 ^
d:/opengl_libs/glad/src/glad.c
# 运行
./main.exe程序目前没有命令行参数,所有配置在 main.cpp 的 AppConfig 结构体中:
struct AppConfig {
int windowWidth = 1280; // 窗口宽度
int windowHeight = 720; // 窗口高度
bool fullscreen = false; // 全屏模式
bool vsync = true; // 垂直同步
size_t maxParticles = 20000; // 最大粒子数
size_t maxRockets = 50; // 最大同时烟花数
float cameraDistance = 20.0f; // 相机距离
bool enableAutoLaunch = true; // 自动发射
// ...
};| 配置 | 推荐粒子数 | 预期 FPS |
|---|---|---|
| 低端显卡 | 5,000 ~ 10,000 | 30+ |
| 中端显卡 | 10,000 ~ 20,000 | 60 |
| 高端显卡 | 20,000 ~ 50,000 | 60+ |
-
CPU 瓶颈
- 粒子物理更新(大量循环)
- 解决方案:
- 优化物理计算(使用 SIMD 指令)
- 多线程更新(将粒子分组并行处理)
-
GPU 瓶颈
- 填充率过高(大量半透明像素)
- 解决方案:
- 减少粒子大小
- 减少尾迹长度
- 使用距离裁剪
-
CPU-GPU 通信瓶颈
- 大量数据传输
- 解决方案:
- 已经使用实例化渲染(优化过)
- 考虑使用映射缓冲区(Persistent Mapping)
- 每个粒子约 150 字节(不含尾迹)
- 20,000 粒子约 3MB
- 尾迹数据额外占用(每粒子最多 20 个点)
- 在
FireworkType枚举中添加新类型:
enum class FireworkType {
// ... 现有类型
MyNewType, // 新类型
};- 在
FireworkRocket::explode()中添加处理分支:
void explode(ParticlePool& particlePool) {
switch (m_config.type) {
// ... 现有类型
case FireworkType::MyNewType:
explodeMyNewType(particlePool);
break;
}
}- 实现新的爆炸函数:
void explodeMyNewType(ParticlePool& particlePool) {
// 实现你的爆炸逻辑
// 参考现有类型的实现
}- 在
FireworkManager::createRandomFirework()中添加:
int typeIndex = g_random.range(0, 10); // 增加范围
switch (typeIndex) {
// ... 现有类型
case 10: config.type = FireworkType::MyNewType; break;
}修改 Particle::update() 方法:
bool update(float deltaTime) {
// ... 现有物理更新
// 添加自定义行为
if (type == ParticleType::MyCustomType) {
// 自定义物理
// 例如:正弦运动、螺旋运动等
}
// ...
}-
选择音频库(推荐 OpenAL 或 miniaudio)
-
在
main.cpp中添加音频管理器:
class AudioManager {
public:
void init() { /* 初始化音频系统 */ }
void playLaunchSound() { /* 发射音效 */ }
void playExplosionSound() { /* 爆炸音效 */ }
void playCrackleSound() { /* 爆裂音效 */ }
};- 在适当的位置调用:
// 火箭发射时
g_audioManager->playLaunchSound();
// 爆炸时
g_audioManager->playExplosionSound();A: 检查 c_cpp_properties.json 中的 include 路径是否正确:
"includePath": [
"${workspaceFolder}/**",
"d:/opengl_libs/glfw-3.4/include",
"d:/opengl_libs/glad/include",
"d:/opengl_libs"
]A: 检查库文件路径:
d:/opengl_libs/glfw-3.4/lib-mingw-w64/libglfw3.a
确保 tasks.json 中的库路径正确:
"-Ld:/opengl_libs/glfw-3.4/lib-mingw-w64"A: 可能的原因:
-
OpenGL 版本不支持
- 检查显卡驱动
- 确保支持 OpenGL 3.3+
-
着色器编译失败
- 检查显卡是否支持 GLSL 330
- 在控制台查看是否有错误输出
A: 性能优化建议:
-
减少最大粒子数:
size_t maxParticles = 10000; // 从 20000 减少
-
减少尾迹长度:
config.trailLength = 8; // 从 15 减少
-
关闭垂直同步:
bool vsync = false;
-
降低窗口分辨率
世界空间(World Space):
Y 轴向上
X 轴向右
Z 轴向屏幕内(右手坐标系)
相机位置:(0, 2, 20)
看向目标:(0, 0, 0)
上方向:(0, 1, 0)
视锥体:
FOV: 60 度
近裁剪面: 0.1
远裁剪面: 100
使用线性 RGB 颜色空间,值范围 [0, 1]。
烟花预设颜色:
- 红色: (1.0, 0.2, 0.2)
- 橙色: (1.0, 0.6, 0.2)
- 黄色: (1.0, 1.0, 0.2)
- 绿色: (0.2, 1.0, 0.2)
- 蓝色: (0.2, 0.6, 1.0)
- 紫色: (0.8, 0.2, 1.0)
- 青色: (0.2, 1.0, 1.0)
- 品红: (1.0, 0.2, 0.8)
- 金色: (1.0, 0.8, 0.3)
- 银色: (0.9, 0.9, 1.0)
使用可变时间步长,基于 glfwGetTime():
float deltaTime = currentTime - lastFrameTime;
// 限制最大时间步长(防止调试暂停时的异常)
if (deltaTime > 0.1f) {
deltaTime = 0.1f;
}- 初始版本
- 实现 10 种烟花效果
- 实例化渲染系统
- 鼠标交互控制
- 对象池粒子管理
本项目仅供学习和教育用途。
- OpenGL 编程指南(红宝书)
- Learn OpenGL - https://learnopengl.com/
- Real-Time Collision Detection - Christer Ericson
- Game Physics Engine Development - Ian Millington
如有问题或建议,请提交 Issue 或 PR。
享受烟花盛宴! 🎆✨
