OpenGL自己实现一个粒子系统

说起这个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();
}

最后放两张运行效果截图(?):
好吧,虽然看起来一般般,但至少粒子系统的效果还是有的。。

至于烟花的粒子系统,很遗憾代码已经丢失了(?),有时间考虑重写一个。。。

最后总结一下,平时课堂上的编程作业和真正的软件项目开发肯定是完全不一样的,在真实的项目开发过程中,需求是不断变化的,所以我们不能够把代码写死,写出来的代码一定要有足够的灵活性和弹性,同时可读、易读、易改,以适应无法预期的需求变更。如何写出这样一些高质量的代码可能就只能靠自己平时的经验和技术沉淀了。总之,要写出好代码还是要多写代码,以一个真正的项目开发人员来要求自己,多总结,多反思。

  • 4
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
OpenGL本身并没有提供粒子系统实现,但是我们可以利用OpenGL的一些基本绘图功能自己实现一个粒子系统。下面是一个简单实现思路: 1. 定义粒子的数据结构,包括位置、速度、生命周期等信息。 2. 在OpenGL中创建一个点精灵(point sprite)的贴图,并使用glEnable(GL_POINT_SPRITE)启用点精灵功能。 3. 在OpenGL中创建一个纹理贴图,并将其与点精灵绑定。 4. 在每一帧渲染时,根据每个粒子的位置和生命周期,更新其位置和生命周期,并将其渲染为一个点精灵。 下面是一个简单粒子系统实现代码,供参考: ```c++ #include <iostream> #include <vector> #include <cstdlib> #include <ctime> #include <cmath> #include <GL/glut.h> using namespace std; const int WIDTH = 800; const int HEIGHT = 600; struct Particle { float x, y; // 粒子在屏幕上的位置 float vx, vy; // 粒子的速度 float life; // 粒子的生命周期 }; vector<Particle> particles; // 存储粒子的向量 GLuint texture; // 纹理贴图 void init() { // 初始化随机数生成器 srand(time(NULL)); // 加载纹理贴图 glEnable(GL_TEXTURE_2D); glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); int width, height; unsigned char* image = SOIL_load_image("particle.png", &width, &height, 0, SOIL_LOAD_RGBA); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image); SOIL_free_image_data(image); // 初始化粒子系统 for (int i = 0; i < 1000; ++i) { Particle p; p.x = WIDTH / 2.0f; p.y = HEIGHT / 2.0f; float angle = rand() % 360; float speed = rand() % 10 + 1; p.vx = speed * cos(angle); p.vy = speed * sin(angle); p.life = rand() % 100 + 50; particles.push_back(p); } } void update() { // 更新粒子系统状态 for (int i = 0; i < particles.size(); ++i) { particles[i].x += particles[i].vx; particles[i].y += particles[i].vy; particles[i].life -= 1.0f; if (particles[i].life <= 0.0f) { particles[i].x = WIDTH / 2.0f; particles[i].y = HEIGHT / 2.0f; float angle = rand() % 360; float speed = rand() % 10 + 1; particles[i].vx = speed * cos(angle); particles[i].vy = speed * sin(angle); particles[i].life = rand() % 100 + 50; } } } void render() { // 渲染粒子系统 glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texture); glBegin(GL_POINTS); for (int i = 0; i < particles.size(); ++i) { glColor4f(1.0f, 1.0f, 1.0f, particles[i].life / 150.0f); glTexCoord2f(0.0f, 0.0f); glVertex2f(particles[i].x, particles[i].y); glTexCoord2f(1.0f, 0.0f); glVertex2f(particles[i].x + 10.0f, particles[i].y); glTexCoord2f(1.0f, 1.0f); glVertex2f(particles[i].x + 10.0f, particles[i].y + 10.0f); glTexCoord2f(0.0f, 1.0f); glVertex2f(particles[i].x, particles[i].y + 10.0f); } glEnd(); glDisable(GL_BLEND); glutSwapBuffers(); } void reshape(int w, int h) { glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0, w, h, 0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void idle() { update(); glutPostRedisplay(); } int main(int argc, char* argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); glutInitWindowSize(WIDTH, HEIGHT); glutCreateWindow("Particle System"); glutDisplayFunc(render); glutReshapeFunc(reshape); glutIdleFunc(idle); init(); glutMainLoop(); return 0; } ``` 在这个例子中,我们使用了SOIL库来加载纹理贴图,你需要在编译时链接该库。另外,我们使用GL_BLEND和glColor4f函数来实现粒子的淡入淡出效果,你可以根据需要调整这些参数。 这只是一个简单粒子系统实现,你可以根据需要添加更多的功能,比如粒子的旋转、大小、颜色等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值