Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FireworkGL - 3D 实时烟花粒子特效模拟器

FireworkGL Banner

项目概述

FireworkGL 是一个高性能的 3D 实时烟花粒子特效模拟器,基于现代 OpenGL 技术构建。它实现了逼真的烟花物理模拟、多种爆炸效果、交互式控制,并通过实例化渲染技术支持数万粒子同时流畅运行。

核心特性

🎆 3D 烟花系统

  • 随机自动发射:系统会自动在随机位置发射烟花
  • 升空物理:烟花火箭遵循真实的物理规律升空
  • 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         # 本文档

核心模块说明

1. 数学模块 (Math.h)

Vector3 - 3D 向量类

  • 基本运算:加法、减法、数乘、数除
  • 几何运算:点积、叉积、归一化
  • 插值:线性插值
  • 距离计算

Matrix4 - 4x4 矩阵类

  • 透视投影矩阵
  • 正交投影矩阵
  • 观察矩阵(LookAt)
  • 平移/缩放矩阵

Color - RGBA 颜色结构体

  • 颜色运算(乘法、加法)
  • 线性插值
  • HSV 到 RGB 转换

2. 随机数模块 (Random.h)

基于高性能的梅森旋转算法(MT19937)实现:

  • 均匀分布:整数、浮点数
  • 正态分布:均值为 0 的高斯分布
  • 球坐标系:单位球面上的随机点
  • 半球分布:指定方向的半球随机点
  • 颜色生成:随机颜色、色相范围、预设烟花颜色

3. 粒子系统模块 (Particle.h)

粒子生命周期算法

初始化阶段:
  ├── 设置位置、速度、颜色
  ├── 初始化尾迹缓存
  └── 标记为活跃

更新阶段(每帧):
  ├── 物理更新
  │   ├── 应用空气阻力: v = v * (1 - r * dt)
  │   ├── 应用重力加速度: v = v0 + a * dt
  │   └── 更新位置: p = p0 + v * dt
  ├── 尾迹更新
  │   ├── 添加新位置到尾迹列表
  │   ├── 移除超过最大长度的旧点
  │   └── 更新尾迹点的生命周期比例
  ├── 生命周期更新
  │   ├── 累计已存活时间
  │   ├── 计算生命周期比例
  │   └── 检查是否超过最大寿命
  └── 视觉属性更新
      ├── 颜色插值(平方插值,后期变化更快)
      ├── 大小插值(线性)
      └── 亮度衰减(指数衰减)

死亡阶段:
  ├── 标记为非活跃
  ├── 放回对象池等待重用
  └── 清空尾迹数据

对象池模式(Object Pool)

为了避免频繁的内存分配和释放,采用对象池设计:

  1. 预分配:程序启动时创建固定数量的粒子对象
  2. 空闲列表:使用队列管理可用的粒子索引
  3. 活跃列表:使用向量跟踪正在使用的粒子
  4. 回收机制:粒子死亡后立即放回空闲列表

性能优势

  • 避免 new/delete 的开销
  • 减少内存碎片
  • 更好的缓存局部性
  • 可预测的内存使用

尾迹系统(Trail System)

每个粒子可以记录历史位置形成尾迹:

  1. 数据结构std::vector<TrailPoint> 存储位置和生命周期比例
  2. 更新策略:每帧添加新位置,移除超出最大长度的旧位置
  3. 渲染方式:使用线段连接连续的尾迹点
  4. 透明度衰减:越旧的尾迹点透明度越低

4. 烟花系统模块 (Firework.h)

烟花类型 (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);
    }
}

5. 渲染系统模块 (Renderer.h)

实例化渲染(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 任务

  1. 打开 VS Code
  2. 打开 main.cpp 文件
  3. Ctrl+Shift+B 执行编译任务
  4. 或者按 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.cppAppConfig 结构体中:

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+

性能瓶颈分析

  1. CPU 瓶颈

    • 粒子物理更新(大量循环)
    • 解决方案:
      • 优化物理计算(使用 SIMD 指令)
      • 多线程更新(将粒子分组并行处理)
  2. GPU 瓶颈

    • 填充率过高(大量半透明像素)
    • 解决方案:
      • 减少粒子大小
      • 减少尾迹长度
      • 使用距离裁剪
  3. CPU-GPU 通信瓶颈

    • 大量数据传输
    • 解决方案:
      • 已经使用实例化渲染(优化过)
      • 考虑使用映射缓冲区(Persistent Mapping)

内存使用

  • 每个粒子约 150 字节(不含尾迹)
  • 20,000 粒子约 3MB
  • 尾迹数据额外占用(每粒子最多 20 个点)

扩展开发指南

添加新的烟花类型

  1. FireworkType 枚举中添加新类型:
enum class FireworkType {
    // ... 现有类型
    MyNewType,  // 新类型
};
  1. FireworkRocket::explode() 中添加处理分支:
void explode(ParticlePool& particlePool) {
    switch (m_config.type) {
        // ... 现有类型
        case FireworkType::MyNewType:
            explodeMyNewType(particlePool);
            break;
    }
}
  1. 实现新的爆炸函数:
void explodeMyNewType(ParticlePool& particlePool) {
    // 实现你的爆炸逻辑
    // 参考现有类型的实现
}
  1. 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) {
        // 自定义物理
        // 例如:正弦运动、螺旋运动等
    }
    
    // ...
}

添加音效支持

  1. 选择音频库(推荐 OpenAL 或 miniaudio)

  2. main.cpp 中添加音频管理器:

class AudioManager {
public:
    void init() { /* 初始化音频系统 */ }
    void playLaunchSound() { /* 发射音效 */ }
    void playExplosionSound() { /* 爆炸音效 */ }
    void playCrackleSound() { /* 爆裂音效 */ }
};
  1. 在适当的位置调用:
// 火箭发射时
g_audioManager->playLaunchSound();

// 爆炸时
g_audioManager->playExplosionSound();

常见问题

Q: 编译时找不到 GLFW/glfw3.h

A: 检查 c_cpp_properties.json 中的 include 路径是否正确:

"includePath": [
    "${workspaceFolder}/**",
    "d:/opengl_libs/glfw-3.4/include",
    "d:/opengl_libs/glad/include",
    "d:/opengl_libs"
]

Q: 链接时找不到 -lglfw3

A: 检查库文件路径:

d:/opengl_libs/glfw-3.4/lib-mingw-w64/libglfw3.a

确保 tasks.json 中的库路径正确:

"-Ld:/opengl_libs/glfw-3.4/lib-mingw-w64"

Q: 程序启动后黑屏

A: 可能的原因:

  1. OpenGL 版本不支持

    • 检查显卡驱动
    • 确保支持 OpenGL 3.3+
  2. 着色器编译失败

    • 检查显卡是否支持 GLSL 330
    • 在控制台查看是否有错误输出

Q: FPS 很低

A: 性能优化建议:

  1. 减少最大粒子数:

    size_t maxParticles = 10000;  // 从 20000 减少
  2. 减少尾迹长度:

    config.trailLength = 8;  // 从 15 减少
  3. 关闭垂直同步:

    bool vsync = false;
  4. 降低窗口分辨率

技术细节

坐标系统

世界空间(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;
}

版本历史

v1.0.0 (2024)

  • 初始版本
  • 实现 10 种烟花效果
  • 实例化渲染系统
  • 鼠标交互控制
  • 对象池粒子管理

许可证

本项目仅供学习和教育用途。

参考文献

  1. OpenGL 编程指南(红宝书)
  2. Learn OpenGL - https://learnopengl.com/
  3. Real-Time Collision Detection - Christer Ericson
  4. Game Physics Engine Development - Ian Millington

联系方式

如有问题或建议,请提交 Issue 或 PR。


享受烟花盛宴! 🎆✨

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages