开头先提一下本例子用的opengl库是比较老的glut(听说在90年代就停止更新了),可能对一些用glew的朋友不是太友好。不过我们老师大概也是觉得重点在于学习原理和绘制方法,就没有在意太多细节吧。
今天在做粒子系统实验的时候,想起来前阵子在AE里做过星空拖尾的效果。于是感慨这次实验的内容就做星轨了。
然后自己在opengl里试着做了做,又丑又卡(。
画到这里就已经花了很长时间(不过是完整分辨率)。而且有的粒子太大了;有的地方有锯齿。而且现在就这么慢,后期的效果就别想了(我开始佩服起ae的stardust插件了)。
总体思路和stardust插件是一样的。由于本次只是一次实验性编码,目的只在于实现星轨和简单后期效果(不考虑UI,不考虑交互和输入输出事件),而且很多地方会比stardust本身拙劣得多(而且我也没有看过stardust插件的源码)。
明显这是一个具有很大提升空间的实验例子。先说一下实现这个例子的基本思路:
其实和在AE里面用stardust插件做的步骤差不多。
1、先画出来一个基本的粒子系统
2、把基本粒子系统的每一个粒子当做成父粒子,自成一个子粒子系统
3、通过对子粒子系统的位置更新和大小更新实现拖尾效果。
4、子粒子装在一个环形队列里进行管理。
5、子粒子的位置通过记录父粒子每帧走过的位置来实现;子粒子的大小通过它在队列里的位置关系来实现。
6、在4,5两点看的懵很正常,我觉得自己讲的也不是很清楚。
(这里建议读者已经可以开始看代码了,改进后得到的最终的代码我放在最后,没改进的我就不放了)
我将列出以下几个可以改进的地方:
1、改进各个参数(宏和全局变量)的设置,使整体更好看
2、优化绘制方式,不绘制累赘的粒子
3、改变纹理贴合的方式。opengl里绘制三角形比正方形快(具体原因不明)
4、尽可能消除锯齿
5、消除轨迹上的一些断续现象
6、一些粒子大又丑
针对第3点:
改为绘制在三角形上。
一样是指明4个顶点与材质绑定。Opengl会用三角形来帮我们画(其实笔者以前用这种三角形拼合的方法画其他正方形的时候出现了不少奇怪的问题,比如材质不显示、三角形颠倒等,但是本例子里使用的是中心对称的贴图,所以无伤大雅)。
我们会明显感觉绘制速度快了不少。
通过不断修改参数,针对1、2、6做了一些优化(大概就是把粒子远离摄像机,使更多的粒子进入了视野,然后再减少父粒子生成数目)。上面一些大丑的情况就是某些粒子离摄像机太近造成的。剩下的就是修改粒子数、子粒子数、粒子尺寸、粒子生命之类的东西了。
针对第4点:由于星轨是有一颗一颗的子粒子有序排列生成的,而且每颗子粒子使用的是我自己贴上去的材质,所以不采用glHint的方式。网上找了集中抗锯齿的方法都是针对glew与glfw库的,这里就不去实现了。
这里需要注意一点,材质没有处理好的时候有很大的几率导致锯齿,比如这种情况:
仔细看会发现,发光和图片边缘撞上了(当然我们采用的是png带有透明通道的图片,记得在代码中打开混合)。
经过一点修改,我们得到了如下效果:
锯齿依然是挺糟心的,整体也不是特别好看;但是有一点值得一提,那就是快了很多。
接下来的一个改进办法是可选的。如果读者仅仅只是为了视觉效果,便可以尝试。我们发现中间的粒子轨迹很短,而边缘的粒子轨迹很长。但他们所包含的子粒子数目是一样的。也就是说,离中心比较近的父粒子,我们没有绘制那么多子粒子的必要。我们可以根据父粒子距离中心点的距离(我们在这里比较平方和,不涉及开方运算。因为开方运算带来的损失很大),动态改变他的子粒子数目。
需要一提的是,我们并不是直接改变子粒子的数目。这样你只会发现你的轨迹变得更短了,而并没有减少你的粒子重叠程度。原因就在于角速度和粒子数目的关系。
所以在绘制每一帧的时候,我们可以对子粒子数组作插值,只绘制其中的一部分,减少重叠程度。
这种做法看上去不是那么稳健,若只用于视觉效果,则值得一试。
我们只需要这样:
在对子系统进行装入的时候就可以这么处理了。值得一提的是,我们不仅仅可以指定偶数,还能指定其他数(通过取模运算自由设定)。我们还需要注意限制距离的大小,如果太大的话,你会发现边缘的轨道出现了一些断续现象,并不是那么好看。
我们还需要在绘制的地方补上一句:
这样就不会绘制我们所不需要的粒子了。
最后的效果。你可能看不出什么差别,但我们少绘制了不少东西。
以上便是所有内容。其实还有很多改进的地方,因为实在是太、太、太丑了。
可以加上一些后期处理或者一些发光的效果,就像AE里面做的一样。但这也是学习opengl的shader之后的事儿了。而且到那时,我将抛弃glut,采用glew和glfw来实现一些效果。
本博客一气呵成写的,没有检查错字。若有,还请各位海涵。
最后上代码,挺乱的其实,不过思路都在上面提到了。各位大佬有改进建议之类的请提出来,在下感激不尽!
// lesson1test.cpp: 定义控制台应用程序的入口点。
// stardust
#include "stdafx.h"
#include
#include
#include
#include
#include "GL/SOIL.h"
#define MAX_PARTICLES 800 // Number of particles to create
#define MAX_PARTICLESONS 500
#define MAX_FILTER 4
#define PI 3.14159f
#define RHO 50
#define LIFE 60.0f
#define TIME 0.01f
GLfloat windowHeight, windowWidth;
GLuint texture[MAX_FILTER];
GLint range_x = 2000;
GLint range_y = 1000;
GLint range_z = 1000;
GLfloat t_speed = 0.002f;
//GLfloat rtt = 0;
GLfloat rtt_speed = 0.001f;
GLfloat z_t = 0;
GLfloat radius = 3.5f;
GLfloat limited_distance = 1000.0f;
GLfloat scale;
typedef struct
{
GLfloat life;
GLfloat init_life;
GLfloat speed_aging;
GLfloat r;
GLfloat g;
GLfloat b;
GLfloat x;
GLfloat y;
GLfloat z;
GLfloat distance;
GLfloat rtt_angle;
GLfloat alpha;
GLint filter;
typedef struct
{ //子粒子系统
GLfloat x;
GLfloat y;
GLfloat z;
GLfloat r;
GLfloat g;
GLfloat b;
GLfloat radius;
}particleSon;
particleSon particleSons[MAX_PARTICLESONS]; //环形队列记录子粒子信息
int psons_head = 0; //队首
int psons_last = 0; //队尾
}particle;
particle particles[MAX_PARTICLES];
//FUNCTION
int loadTextures();
//当窗口改变大小时由GLUT函数库调用
void ChangeSize(int w, int h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
glLoadIdentity(); // Reset The Projection Matrix
// Calculate The Aspect Ratio And Set The Clipping Volume
if (h == 0) h = 1;
gluPerspective(80, (float)w / (float)h, 1.0, 5000.0);
glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix
glLoadIdentity(); // Reset The Modelview Matrix
}
int InitPaticleSystem(void)
{
for (int i = 0; i < MAX_PARTICLES;i++)
{
particles[i].init_life = LIFE + rand() % 10 / 10.0;
particles[i].life = particles[i].init_life;
particles[i].speed_aging = TIME;
particles[i].r = (float)(rand() % 35 + 150) / 255.0f;
particles[i].g = (float)(rand() % 55 + 200) / 255.0f;
particles[i].b = 1.0f;
particles[i].x = rand() % range_x - range_x / 2;
particles[i].y = rand() % range_y - range_y / 2;
particles[i].z = rand() % range_z - range_z / 2;
particles[i].distance = sqrt(particles[i].x*particles[i].x + particles[i].y*particles[i].y);
particles[i].rtt_angle = atan(particles[i].x / particles[i].y)*180.0f / PI;
particles[i].filter = rand() % MAX_FILTER;
//初始化子系统
for (int j = 0;j < MAX_PARTICLESONS;j++)
{
particles[i].particleSons[j].x = 0;
particles[i].particleSons[j].y = 0;
particles[i].particleSons[j].z = 0;
particles[i].particleSons[j].r = particles[i].r;
particles[i].particleSons[j].g = particles[i].g;
particles[i].particleSons[j].b = particles[i].b;
particles[i].particleSons[j].radius = radius;
}
}
return true;
}
void RenderScene(void)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear screen and depth buffer
glLoadIdentity();
glTranslated(0, 0, -1000);
for (int i = 0;i < MAX_PARTICLES; i++) // All particles
{
GLfloat x = particles[i].x; // Position of particle
GLfloat y = particles[i].y;
GLfloat z = particles[i].z;
//glColor3f(particles[i].r, particles[i].g, particles[i].b);
int j = particles[i].psons_head%MAX_PARTICLESONS;
while (true)
{
if (j == particles[i].psons_last)
break;
if (particles[i].particleSons[j].radius < radius && particles[i].particleSons[j].radius!=0) //只绘制半径比父粒子小且尺寸不为0的子粒子
{
glBindTexture(GL_TEXTURE_2D, texture[particles[i].filter]);
glBegin(GL_TRIANGLE_STRIP);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(particles[i].particleSons[j].x - particles[i].particleSons[j].radius, particles[i].particleSons[j].y - particles[i].particleSons[j].radius, particles[i].particleSons[j].z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(particles[i].particleSons[j].x + particles[i].particleSons[j].radius, particles[i].particleSons[j].y - particles[i].particleSons[j].radius, particles[i].particleSons[j].z);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(particles[i].particleSons[j].x + particles[i].particleSons[j].radius, particles[i].particleSons[j].y + particles[i].particleSons[j].radius, particles[i].particleSons[j].z);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(particles[i].particleSons[j].x - particles[i].particleSons[j].radius, particles[i].particleSons[j].y + particles[i].particleSons[j].radius, particles[i].particleSons[j].z);
glEnd();
}
j = (++j) % MAX_PARTICLESONS;
}
}
glutSwapBuffers();
}
void Update() {
for (int i = 0;i < MAX_PARTICLES; i++) // All The Particles
{
particles[i].x = particles[i].distance * cos(particles[i].rtt_angle);
particles[i].y = particles[i].distance * sin(particles[i].rtt_angle);
particles[i].life -= particles[i].speed_aging; // reduce particles life
//更新子系统
particles[i].particleSons[particles[i].psons_last].x = particles[i].x;
particles[i].particleSons[particles[i].psons_last].y = particles[i].y;
particles[i].particleSons[particles[i].psons_last].z = particles[i].z;
particles[i].psons_last = (++particles[i].psons_last) % MAX_PARTICLESONS;
/*printf("draw last++ %d %d %d %d %d", particles[i].psons_head, particles[i].psons_last,
particles[i].particleSons[particles[i].psons_last].x,
particles[i].particleSons[particles[i].psons_last].y,
particles[i].particleSons[particles[i].psons_last].z);*/
if (particles[i].psons_last - particles[i].psons_head == -1) //若队列满了
{
particles[i].psons_head = (++particles[i].psons_head) % MAX_PARTICLESONS;
printf("drink");
}
else
{ //队列没有满
//这里计算的是粒子逐渐衰减的半径。0.98是衰减倍数
particles[i].particleSons[particles[i].psons_last].radius -=
particles[i].particleSons[particles[i].psons_last].radius*pow(0.98f, (particles[i].psons_last - particles[i].psons_head + MAX_PARTICLESONS) % MAX_PARTICLESONS);
//作出优化。如果父粒子距离小于限定距离,则把一部分子粒子半径归零(我们把数组中双数粒子半径归0)
GLfloat tempx = particles[i].particleSons[particles[i].psons_last].x;
GLfloat tempy = particles[i].particleSons[particles[i].psons_last].y;
if (tempx*tempx + tempy * tempy < limited_distance*limited_distance)
if (particles[i].psons_last % 2 == 0)
{
particles[i].particleSons[particles[i].psons_last].radius = 0;
}
printf("psons_last is %d ", particles[i].psons_last);
printf(" radius is %f", particles[i].particleSons[particles[i].psons_last].radius); //调试用
}
printf(" %d %d %d\n", particles[i].psons_head, particles[i].psons_last, (particles[i].psons_last - particles[i].psons_head + MAX_PARTICLESONS) % MAX_PARTICLESONS);
}
}
void TimerFunction(int value) {
Update();
for (int i = 0;i