说起这个OpenGL粒子系统的实现其实只是之前游戏引擎架构课程中的一次作业,当时的作业要求是分别实现是个瀑布的粒子系统和一个烟花的粒子系统,看起来工作量是比较大的,当时我就想,能不能实现一个粒子系统的框架,能够同时用在这两个不同的粒子系统的实现中,从而起到减少重复工作量的效果?事实证明这是完全可行的,当我搭建好这个框架做完瀑布的粒子系统时很快就把另一个烟花的粒子系统也完成了。不过这个粒子系统说是框架其实只是一个模板类而已,但是后
来也对代码进行了一系列的改进,以下附上最终的代码:
//ParticleSystem.h #pragma once #ifndef PARTICLE_SYSTEM_H #define PARTICLE_SYSTEM_H #include <glad/glad.h> #include <GLFW/glfw3.h> #include <glm\glm.hpp> #define DEFAULT_PARTICLE_NUMBER 100000 #define DEFAULT_PARTICLE_LIFESPAN 10000 template<typename ParticleType> class ParticleSystem { public: //用默认或自定义的粒子数量和粒子生命周期进行初始化 ParticleSystem(GLuint particleNumber= DEFAULT_PARTICLE_NUMBER, GLuint particleLifespan= DEFAULT_PARTICLE_LIFESPAN); //析构 virtual ~ParticleSystem(); //渲染,普通的渲染函数就是渲染每一粒存在的粒子 virtual void Render(); //更新粒子的信息,纯虚函数,不同的粒子系统肯定有不同的实现 virtual void Update(GLfloat deltaTime) = 0; protected: //对一颗粒子进行渲染的函数,纯虚函数,由Render函数调用,不同的粒子系统也有不同的实现 virtual void RenderParticle(const ParticleType& p) = 0; //创建和销毁粒子,由Update函数调用 void CreateParticle(const ParticleType& p); void DestroyParticle(GLint index); protected: //池分配器的分配单元,可以用来保存粒子信息或freelist信息 union PoolAllocUnit { ParticleType particle; struct Link { GLint mark;//判断是否为link的标志,设为1 GLint nextIdx;//使用“栈”的数据结构存储freelist,所以使用单向链表即可 }link; PoolAllocUnit() {} }; //池分配器的地址 PoolAllocUnit* mParticlePool; GLuint mParticleNumber; GLuint mParticleLifespan; private: //表示一个在freelist中的粒子数组的索引(freelist栈的栈顶元素) GLint mFreeIndex; }; template<typename ParticleType> inline ParticleSystem<ParticleType>::ParticleSystem(GLuint particleNumber, GLuint particleLifespan) :mParticleNumber(particleNumber),mParticleLifespan(particleLifespan) { //初始化时,根据粒子的数量进行动态内存分配 mParticlePool = new PoolAllocUnit[mParticleNumber]; //初始化freelist memset(mParticlePool, 0, sizeof(PoolAllocUnit)*mParticleNumber); mFreeIndex = 0; for (GLint i = 0; i < mParticleNumber; ++i) { mParticlePool[i].link.mark = 1; mParticlePool[i].link.nextIdx = i + 1; } mParticlePool[mParticleNumber - 1].link.nextIdx = -1;//-1标记当前freelist只剩最后这一个元素 } template<typename ParticleType> inline ParticleSystem<ParticleType>::~ParticleSystem() { //释放动态内存 delete[] mParticlePool; } template<typename ParticleType> void ParticleSystem<ParticleType>::Render() { //渲染每一个“存在”的粒子 for (GLint i = 0; i < mParticleNumber; ++i) { if (mParticlePool[i].link.mark != 1) { RenderParticle(mParticlePool[i].particle); } } } template<typename ParticleType> void ParticleSystem<ParticleType>::CreateParticle(const ParticleType& particle) { GLint index = mParticlePool[mFreeIndex].link.nextIdx; if (index == -1)return;//如果当前粒子数量已超过设定的最大粒子数量,则函数直接返回 mParticlePool[mFreeIndex].particle = particle; mFreeIndex = index; } template<typename ParticleType> void ParticleSystem<ParticleType>::DestroyParticle(GLint index) { if (index < 0 || index >= mParticleNumber)return;//索引不合法 if (mParticlePool[index].link.mark == 1)return;//当前索引在freelist中 mParticlePool[index].link.mark = 1;//当前索引添加到freelist mParticlePool[index].link.nextIdx = mFreeIndex; mFreeIndex = index; } #endif
本次的粒子系统类的代码编写也算是我的一次编写“高质量代码”的尝试,在代码的可读性、性能、可扩展性等方面都做了一些改进。例如,尽可能的使用命名规则和注释;使用union联合体保存粒子和自由链表的元素,最大化内存的使用率;类成员的声明尽可能的控制成员变量和函数的访问权限,增加系统的健壮性(?);使用GLuint,GLint等代替unsigned int ,int则是考虑到程序的可移植性(是叫这个吧),等等。不过实际上可能还是会有一些小问题,例如,ParticleSystem类的粒子的池分配器联合体为protected型,可以直接被其派生类访问,这实际上还是有点不安全的。
然后如果要用这个框架创建粒子系统的话,只需要自己定义一个粒子结构体并重写ParticleSystem类中的两个纯虚函数Update和RenderParticle即可。
ParticleSystem类写好后,就是使用这个类来创建不同的粒子系统了,例如,用ParticleSystem类创建一个瀑布:
//Waterfall.h #pragma once #ifndef WATERFALL_H #define WATERFALL_H #include"ParticleSystem.h" struct WaterfallParticle { glm::vec3 position; glm::vec3 speed; GLuint lifespan; bool bounced; }; class Waterfall :public ParticleSystem<WaterfallParticle> { public: Waterfall(); virtual void Update(GLfloat deltaTime); private: virtual void RenderParticle(const WaterfallParticle& p); }; #endif
//Waterfall.cpp #include "Waterfall.h" #include"Shader.h" #include "Cube.h" #include <random> extern Shader* shader; extern Cube* cube; static const float gravity = 20.0f; const float pi = 3.1416; float randFloat01() { return 1.0*rand() / RAND_MAX; } float randFloat(float from, float to) { return from + (to - from)*randFloat01(); } int randInt(int from, int to) { return from + rand() % (to - from); } Waterfall::Waterfall() { } //粒子的状态更新,可以尽情发挥自己的创意编写代码 void Waterfall::Update(GLfloat deltaTime) { //新粒子的创建 WaterfallParticle particle; int newParticleNumber = randInt(2, 4); for (int i = 0; i < newParticleNumber; ++i) { particle.position = glm::vec3(-10.0f, 5.0f, -10.0f); particle.speed = glm::vec3(randFloat(9.0f, 12.0f), randFloat(-1.0f, 1.0f), randFloat(-1.0f, 1.0f)); particle.lifespan = mParticleLifespan; particle.bounced = false; CreateParticle(particle); } //已有粒子的更新, for (int i = 0; i < mParticleNumber; ++i) { if (mParticlePool[i].link.mark != 1) { WaterfallParticle* p = &(mParticlePool[i].particle);//p表示当前循环要更新的粒子 p->position += p->speed*deltaTime; if (p->position.x > -5.0f) { p->speed.y -= gravity * deltaTime; } if (p->position.y<0&&!p->bounced) { p->bounced = true; p->speed.y *= -randFloat(0.4f, 0.7f); p->speed.x *= randFloat(0.6f, 0.9f); p->speed.z *= randFloat(0.6f, 0.9f); } if (p->lifespan<=0||p->position.y < -10.0f) { DestroyParticle(i); } p->lifespan--; } } } void Waterfall::RenderParticle(const WaterfallParticle & p) { //设置模型至世界的变换矩阵 glm::mat4 model; model = glm::translate(model, p.position); model = glm::rotate(model, randFloat(0.0f, 180), glm::vec3(1.0f, 0.0f, 0.0f)); model = glm::rotate(model, randFloat(0.0f, 180), glm::vec3(0.0f, 1.0f, 0.0f)); model = glm::scale(model, glm::vec3(0.2f, 0.2f, 0.2f)); //使用shader在屏幕上渲染一个蓝色的小立方体 shader->setMatrix4x4("uModel", model); shader->setVector3("uColor", glm::vec3(randFloat(0.1f, 0.3f), randFloat(0.2f, 0.4f), randFloat(0.8f, 1.0f))); cube->Render(); }
最后放两张运行效果截图(?):
好吧,虽然看起来一般般,但至少粒子系统的效果还是有的。。
至于烟花的粒子系统,很遗憾代码已经丢失了(?),有时间考虑重写一个。。。
最后总结一下,平时课堂上的编程作业和真正的软件项目开发肯定是完全不一样的,在真实的项目开发过程中,需求是不断变化的,所以我们不能够把代码写死,写出来的代码一定要有足够的灵活性和弹性,同时可读、易读、易改,以适应无法预期的需求变更。如何写出这样一些高质量的代码可能就只能靠自己平时的经验和技术沉淀了。总之,要写出好代码还是要多写代码,以一个真正的项目开发人员来要求自己,多总结,多反思。