使用OpenGL的变换反馈(transform feedback)构造粒子系统
粒子系统是三维计算机图形学中用来模拟火,爆炸,雾,雪,流行尾迹或者发光轨迹等视觉效果的技术。粒子系统模拟这些现象的步奏简单的分为以下两步:
1. 更新每个粒子的属性,如粒子的颜色、速度,位置等。
2. 渲染粒子。采用billboard和纹理渲染粒子对象。
粒子系统负责管理粒子的个数和属性,当粒子结束生命周期时,如粒子的位置变换到特定的位置后,粒子系统负责将粒子设置为初始状态并开始新的旅程。但粒子系统有如下两个问题:
1. 粒子系统是在CPU中更新粒子的状态。每次更新状态后都需要将数据从cpu端拷贝到GPU端,假设粒子状态每帧都更新,那么数据的传递将对渲染的效率产生很大的影响。
2. 粒子系统中对粒子状态的更新都是用的同一个方法,GPU正好适合处理这样的运算。
我们可以只往GPU中传递一次粒子的初始属性,然后在GPU中维护粒子的属性并进行渲染。过程如下:
1. 使用两个shaderprogram,其中一个shader负责更新粒子的属性,另一个shader负责渲染渲染。
2. 利用变换反馈(transform feedback)将两组顶点缓冲区轮流作为渲染用的顶点数据,和保存更新后的顶点数据。变换反馈位于几何图形着色器和顶点装配阶段之间。因为几何着色器是可选的,如果不使用几何着色器,这些数据实际上是来自顶点着色器。变换反馈允许将一个顶点找色器或者几何着色器的结果保存到一个缓冲区中,这个缓冲区中保存的更新后的数据作为下一步的渲染使用。OPenGL中使用变化反馈的示例如下:
const char *tf_varying[]= {"out_position","out_color"}; glTransformFeedbackVaryings(update_program,2,tf_varying,GL_SEPARATE_ATTRIBS);
这两句话指明负责更新粒子状态的shader中的输出变量"out_position"和"out_color"将被被保存到变换反馈缓冲区中,需要在shader链接之前指定。然而变换反馈并没有真正的缓冲区,它只是一个绑定点,需要将真正的缓冲区绑定到该绑定点:
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,0,render_positionBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,1,render_colorBuffer);
第二个参数0和1就代表了变换反馈的绑定点,这个值可以通过glGenTransformFeedbacks(2,tid)函数来获取,但变换反馈的绑定点个数有限制。
程序的主渲染函数为render函数,首先使用更新粒子状态的shader,该shader轮流使用两组缓冲区,最开始粒子的初始状态是保存在A组缓冲区中的,B组缓冲区中的数据没有初始化。第一次进入render函数的时候,frame_index & 1为真,变换反馈绑定到B组,所以A组中的数据变换更新后被保存到了B组,渲染着色器使用的是B组的数据;第二次进入render函数时,使用B组的数据进行更新,并将新的粒子属性保存到A组,渲染的时候使用A组数据,如此反复进行。
void render(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(update_program);
if (frame_index & 1)
{
glBindVertexArray(vaoA);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,0,positionBufferB);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,1,colorBufferB);
}
else
{
glBindVertexArray(vaoB);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,0,positionBufferA);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,1,colorBufferA);
}
//查询变换反馈的结果
GLuint q;
GLint vertices_to_render;
glGenQueries(1,&q);
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN,q);
glEnable(GL_RASTERIZER_DISCARD);//禁止栅格化,几何着色器之后的阶段被删掉
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 3);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
glGetQueryObjectiv(q,GL_QUERY_RESULT,&vertices_to_render);
glUseProgram(render_program);
if (frame_index & 1)
{
glBindVertexArray(vaoB);
}
else
{
glBindVertexArray(vaoA);
}
glDrawArrays(GL_POINTS, 0, 3);
++frame_index;
glutSwapBuffers();
}
以上程序渲染的时候使用的是glDrawArrays(GL_POINTS, 0, 3)命令,还需要使用几何着色器利用单个顶点生成四边形进行纹理贴图,以达到BillBoard的效果。一个简单几何着色器如下:
#version 400
layout(points) in;
layout(triangle_strip,max_vertices = 4) out;
uniform float size; //矩形的半径
out vec2 texcoord;
out vec4 color;
//const float size =0.5;
void main()
{
gl_Position = gl_in[0].gl_Position +vec4(-size, -size, 0.0, 0.0);
texcoord = vec2(0.0, 0.0);
color = vec4(1.0, 0.0, 0.0, 1.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position +vec4(size, -size, 0.0, 0.0);
texcoord = vec2(1.0, 0.0);
color = vec4(1.0, 0.0, 0.0, 1.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position +vec4(size, size, 0.0, 0.0);
texcoord = vec2(1.0, 1.0);
color = vec4(0.0, 0.0, 1.0, 1.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4(0.0,size, 0.0, 0.0);
texcoord = vec2(0.0, 1.0);
color = vec4(0.0, 0.0, 1.0, 1.0);
EmitVertex();
EndPrimitive();
}
该几何着色器生成三角形的方法很粗陋,在实际使用中为达到billboard的效果,需要传入相机参数,并根据相机的参数来生成三角形顶点的位置,使三角形始终面向观察方向。此外,为达到更好的效果,纹理坐标需要动态更改。