【OpenGL】 粒子系統實例(三)

自己是這方面的小白,希望大神可以對有問題以及可以優化的地方提出來。也歡迎指出不足和吐槽。希望幫到小白。

opengl 太陽、地球、月亮 酷炫實例(一):
https://blog.csdn.net/qq_40515692/article/details/100778870

opengl 太陽、地球、月亮 酷炫實例(二):
https://blog.csdn.net/qq_40515692/article/details/100802534

自己參考得比較多的網站是這個:
http://www.cppblog.com/doing5552/archive/2009/01/08/71532.html
以及:https://blog.csdn.net/xie_zi/article/details/1911406

所有代碼都可以去github免費下載:
https://github.com/Iamttp/OpenGLTest

好先上這一節的效果圖。這一節是上一節的繼續,在上一節的代碼基礎上添加了粒子系統。在這裏插入圖片描述
是不是有點震撼呢?我也可以嗎?答案是肯定的。而且下面介紹的這個粒子系統通用性較好,很容易修改,弄出屬於自己的特效。

現在直接給出主函數代碼,可以看到和上一節的代碼幾乎沒有區別,就只是引入了頭文件,定義了Waterfall w;,然後在flag爲1時調用了w.Update();,在myDisplay裏面持續渲染(調用了函數w.Render();)

#include <gl/glut.h>
#include <stdio.h>
#include <time.h>
#include <cmath>
#include "Waterfall.h"

static float angle = 0.0, ratio;  // angle繞y軸的旋轉角,ratio窗口高寬比
static float x = 0.0f, y = 0.0f, z = 3.0f;  //相機位置
static float lx = 0.0f, ly = 0.0f, lz = -1.0f;  //視線方向,初始設爲沿着Z軸負方向
static int my_angle = 0; // 表示旋轉的角度
Waterfall w;
int flag = 0;
/**
 * 定義觀察方式
 */
void changeSize(int w, int h) {
	//除以0的情況
	if (h == 0) h = 1;
	ratio = 1.0f * w / h;
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	//設置視口爲整個窗口大小
	glViewport(0, 0, w, h);
	//設置可視空間
	gluPerspective(45, ratio, 1, 1000);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(x, y, z, x + lx, y + ly, z + lz, 0.0f, 1.0f, 0.0f);
}
/**
 * 視野漫遊函數
 */
void orientMe(float ang) {
	lx = sin(ang);
	lz = -cos(ang);
	glLoadIdentity();
	gluLookAt(x, y, z, x + lx, y + ly, z + lz, 0.0f, 1.0f, 0.0f);
}
void moveMeFlat(int direction) {
	x = x + direction * (lx) * 0.1;
	z = z + direction * (lz) * 0.1;
	glLoadIdentity();
	gluLookAt(x, y, z, x + lx, y + ly, z + lz, 0.0f, 1.0f, 0.0f);
}

/**
 * 加入按鍵控制
 */
void processSpecialKeys(int key, int x, int y) {
	switch (key) {
		case GLUT_KEY_F1:
			flag = 1;
			break;
		case GLUT_KEY_LEFT:
			angle -= 0.01f;
			orientMe(angle);
			break;
		case GLUT_KEY_RIGHT:
			angle += 0.01f;
			orientMe(angle);
			break;
		case GLUT_KEY_UP:
			moveMeFlat(1);
			break;
		case GLUT_KEY_DOWN:
			moveMeFlat(-1);
			break;
	}
}

void myDisplay(void) {
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// 太陽
	glPushMatrix();
	glColor3f(1.0, 1.0, 0.0);
	glutSolidSphere(0.15, 200, 200);
	glPopMatrix();

	// 地球
	glPushMatrix();
	glColor3f(0.0, 0.0, 1.0);
	glRotated(my_angle, 1.0, 1.0, 1.0);  //公轉
	glTranslatef(0.5, 0.5, -0.5);        //平移
	glutSolidSphere(0.1, 200, 200);
	glPopMatrix();

	// 月亮
	glPushMatrix();
	glColor3f(1.0, 1.0, 1.0);
	glRotated(my_angle, 1.0, 1.0, 1.0);  //然後移動到地球旁邊旋轉
	glTranslatef(0.5, 0.5, -0.5);        //平移
	glRotated(my_angle, 1.0, 1.0, 1.0);  //先假設原點爲地球旋轉
	glTranslatef(-0.15, -0.15, 0.15);    //平移
	glutSolidSphere(0.05, 200, 200);     //繪製月亮
	glPopMatrix();
	if (flag == 1) {
		flag = 0;
		w.Update();
	}
	glPushMatrix();
	w.Render();
	glPopMatrix();

	glutSwapBuffers();
}
/**
 * 計時增加角度
 */
void myIdle(void) {
	static int mm = 0;
	mm++;
	if (mm % 300000 == 0) {
		++my_angle;
		if (my_angle >= 360) my_angle = 0;
		myDisplay();
	}
}

int main(int argc, char* argv[]) {
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
	glutInitWindowPosition(100, 100);
	glutInitWindowSize(1000, 1000);
	glutCreateWindow("太陽,地球和月亮");  // 改了窗口標題

	glutDisplayFunc(&myDisplay);
	glutIdleFunc(&myIdle);  // 表示在CPU空閒的時間調用某一函數
	glutSpecialFunc(&processSpecialKeys);  // 按鍵
	glutReshapeFunc(&changeSize);

	// 在OpenGL中,默認是沒有開啓深度檢測的,後繪製的物體覆蓋先繪製的物體。
	// GL_DEPTH_TEST 用來開啓更新深度緩衝區的功能
	glEnable(GL_DEPTH_TEST);
	glutMainLoop();
	return 0;
}

那麼這個Waterfall怎麼實現的呢?
第一步,我們需要定義一個結構體,這個結構體描述了每個粒子的屬性,比如:這個粒子的生命(還可以存在多久)、這個粒子的RGB值,這個粒子的位置,這個粒子的速度等等。

typedef struct {
	float life;  // last time
	float r, g, b;     // color
	float x, y, z;     // the position
	float xi, yi, zi;  // what direction to move
} WaterfallParticle;

第二步,考慮類提供給外界的接口,實現自然是定義時調用的構造函數了,定義一些我們全局(相對於上面提到的個體的參數)可能要用到的參數,比如有多少粒子、每個粒子存在時間等等。

然後是Update函數,它主要是初始化創建一些粒子(一般就創建mParticleNumber個)。

然後是Render函數負責渲染粒子,RenderParticle函數負責渲染單個粒子。

那每個粒子存在哪裏呢?我們這裏就使用C++ STL裏面的list來作爲存儲粒子的容器。那爲什麼不用數組呢?因爲粒子數目沒有固定。那爲什麼不用vector?這個比list常用多了,但是因爲我們會有大量的刪除插入操作,所以使用list,具體原因如下:

vector與數組類似,擁有一段連續的內存空間,並且起始地址不變。便於隨機訪問,時間複雜度爲O(1),但因爲內存空間是連續的,所以在進入插入和刪除操作時,會造成內存塊的拷貝,時間複雜度爲O(n)。
 
list底層是由雙向鏈表實現的,因此內存空間不是連續的。根據鏈表的實現原理,List查詢效率較低,時間複雜度爲O(n),但插入和刪除效率較高。只需要在插入的地方更改指針的指向即可,不用移動數據。

#define DEFAULT_PARTICLE_NUMBER 5000
#define DEFAULT_PARTICLE_LIFESPAN 30

class Waterfall {
public:
	Waterfall(GLuint particleNumber = DEFAULT_PARTICLE_NUMBER,
		GLuint particleLifespan = DEFAULT_PARTICLE_LIFESPAN)
		: mParticleNumber(particleNumber),
		mParticleLifespan(particleLifespan) {}
	void Update();
	//渲染,普通的渲染函數就是渲染每一粒存在的粒子
	virtual void Render();
	int getSize() { return ls.size(); }

private:
	GLuint mParticleNumber;
	GLuint mParticleLifespan;
	std::list<WaterfallParticle> ls;
	void RenderParticle(const WaterfallParticle& p);
};

第三步,就是實現這些函數了,我們首先實現Update吧,這裏初始化mParticleNumber個粒子,顏色、速度、生命值很簡單直接賦值爲隨機數就可以了,初始位置怎麼賦值呢?三維中的圓可以自行百度它的方程,我這裏已經計算好了,效果如文章開頭(數學渣表示不敢確定正不正確 / w \)。當然也可以直接二維的圓,使用註釋的代碼。

void Waterfall::Update() {
	//新粒子的創建
	WaterfallParticle particle;
	int newParticleNumber = mParticleNumber;
	for (int i = 0; i < newParticleNumber; ++i) {
		auto rate = randFloat(-0, 1);
		////////////////////////////////////
		double sqrt2 = sqrt(2);
		double sqrt6 = sqrt(6);
		particle.x = cos(2 * PI * rate) / sqrt2 + sin(2 * PI * rate) / sqrt6;
		particle.y = -1.0 * cos(2 * PI * rate) / sqrt2 + sin(2 * PI * rate) / sqrt6;
		particle.z = -2.0 * sin(2 * PI * rate) / sqrt6;
		//////////////////////////////////////
		/*
			particle.x = cos(2 * PI * rate) * 1;
			particle.y = sin(2 * PI * rate) * 1;
			particle.z = 0;
		*/
		//////////////////////////////////////
		particle.r = randFloat(0, 1);
		particle.g = randFloat(0, 1);
		particle.b = randFloat(0, 1);
		particle.xi = randFloat(-0.1f, 0.1f);
		particle.yi = randFloat(-0.1f, 0.1f);
		particle.zi = randFloat(-0.1f, 0.1f);
		particle.life = mParticleLifespan;
		ls.push_back(particle);
	}
}

然後是Render,這個函數就是更新位置和及時清除life爲0的粒子。然後就是對還活着的粒子進行單個渲染,用了一些C++11的寫法,可以自行百度喲。

void Waterfall::Render() {
	auto p = ls.begin();
	while (p != ls.end()) {
		p->x += p->xi * 0.1;
		p->y += p->yi * 0.1;
		p->z += p->zi * 0.1;
		p->life--;
		if (p->life == 0) {
			ls.erase(p++);
		}
		else {
			p++;
		}
	}
	glPointSize(2);//設置點的大小爲2
	glBegin(GL_POINTS);
	for (auto& item : ls) RenderParticle(item);
	glEnd();
}

最後就是單個粒子的渲染,這裏就非常簡單了。

void Waterfall::RenderParticle(const WaterfallParticle& p) {
	glColor4f(p.r, p.g, p.b, p.life);
	glVertex3f(p.x, p.y, p.z);
}

最後附上Waterfall.h的所有代碼,主函數代碼(運行後按F1出現粒子光環)已經在文章開頭給出了。

#ifndef WATERFALL_H
#define WATERFALL_H

#include <gl/glut.h>
#include <list>

const double PI = acos(-1.0);
typedef struct {
	float life;  // last time
	float r, g, b;     // color
	float x, y, z;     // the position
	float xi, yi, zi;  // what direction to move
} WaterfallParticle;

#define DEFAULT_PARTICLE_NUMBER 5000
#define DEFAULT_PARTICLE_LIFESPAN 30

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); }

class Waterfall {
public:
	Waterfall(GLuint particleNumber = DEFAULT_PARTICLE_NUMBER,
		GLuint particleLifespan = DEFAULT_PARTICLE_LIFESPAN)
		: mParticleNumber(particleNumber),
		mParticleLifespan(particleLifespan) {}
	void Update();
	//渲染,普通的渲染函數就是渲染每一粒存在的粒子
	virtual void Render();
	int getSize() { return ls.size(); }

private:
	GLuint mParticleNumber;
	GLuint mParticleLifespan;
	std::list<WaterfallParticle> ls;
	void RenderParticle(const WaterfallParticle& p);
};

//粒子的狀態更新,可以盡情發揮自己的創意編寫代碼
void Waterfall::Render() {
	auto p = ls.begin();
	while (p != ls.end()) {
		p->x += p->xi * 0.1;
		p->y += p->yi * 0.1;
		p->z += p->zi * 0.1;
		p->life--;
		if (p->life == 0) {
			ls.erase(p++);
		}
		else {
			p++;
		}
	}
	glPointSize(2);//設置點的大小爲10
	glBegin(GL_POINTS);
	for (auto& item : ls) RenderParticle(item);
	glEnd();
}

void Waterfall::Update() {
	//新粒子的創建
	WaterfallParticle particle;
	int newParticleNumber = mParticleNumber;
	for (int i = 0; i < newParticleNumber; ++i) {
		auto rate = randFloat(-0, 1);
		double sqrt2 = sqrt(2);
		double sqrt6 = sqrt(6);
		particle.x = cos(2 * PI * rate) / sqrt2 + sin(2 * PI * rate) / sqrt6;
		particle.y = -1.0 * cos(2 * PI * rate) / sqrt2 + sin(2 * PI * rate) / sqrt6;
		particle.z = -2.0 * sin(2 * PI * rate) / sqrt6;
		/*
			particle.x = cos(2 * PI * rate) * 1;
			particle.y = sin(2 * PI * rate) * 1;
			particle.z = 0;
		*/
		particle.r = randFloat(0, 1);
		particle.g = randFloat(0, 1);
		particle.b = randFloat(0, 1);
		particle.xi = randFloat(-0.1f, 0.1f);
		particle.yi = randFloat(-0.1f, 0.1f);
		particle.zi = randFloat(-0.1f, 0.1f);
		particle.life = mParticleLifespan;
		ls.push_back(particle);
	}
}

void Waterfall::RenderParticle(const WaterfallParticle& p) {
	glColor4f(p.r, p.g, p.b, p.life);
	glVertex3f(p.x, p.y, p.z);
}
#endif
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章