一、简介
本文介绍了如何使用 OpenGL 中的 Transform Feedback 实现粒子效果,最终可以实现下图的效果:
本文的粒子系统实现参考了modern-opengl-tutorial, ogldev-tutorial28 和 粒子系统–喷泉 [OpenGL-Transformfeedback]。
二、使用 TransformFeedback 实现效果
1. Transform Feedback 简介
Transform Feedback 是 OpenGL 中用于获取 vertex shader 和 geometry shader 处理后的顶点数据的一种机制,可以在 GPU 上将 vertex shader, geometry shader 处理后的数据存储到以一个 buffer 中,而不进行接下来的 clipper, Rasterizer 和 Fragment Shader 阶段。
Transform Feedback Buffer 在渲染管线中所处的位置如下图所示:
基于 Transform feedback,我们可以在 GPU 上对多个顶点数据行进并行运算,粒子系统 就是 Transform feedback 的一个典型应用。
2. 粒子系统实现
在实现粒子系统时,使用 update Shader 和 render Shader 两个 着色器:
- update shader 用来更新粒子的状态,包括更新粒子状态、生成新粒子、消灭旧粒子。
- render shader 用来将粒子显示在屏幕上。
粒子系统的实现流程如下:
上图展示了使用 Update shader 和 Render shader 实现粒子系统的流程。图中左侧黄色虚线内为使用 Update shader 更新粒子,右侧蓝色虚线内为使用 Render shader 将粒子渲染到屏幕上,然后再进入下一帧的Update-Render
流程。
在 Update shader 中,输入为 Update input VBO
,输出为 Update output VBO
。在 Render shader 中,Update output VBO
又作为渲染时的输入,Render input VBO
。由于 Transform Feedback 中的在读 一个 VBO
时,不能同时写该 VBO
,及Update input VBO
与 Update output VBO
不能是同一个 buffer object。因此在代码实现使用两个 VBO 交替作为 一个Update-Render
流程中的Update input VBO
和 Update output VBO
。
例如,渲染一个n帧的结果,其 Update input VBO
和 Update output VBO
所代表的 buffer 变换如下所示:
3. 部分代码讲解
3.1. Particle 类
struct Particle
{float Type; // 0: launch, 1: shell, 2 : second shellglm::vec3 Pos;glm::vec3 Velocity;float Life;
};
系统中粒子的类型分为三类, launch, shell 和 second shell。
- launch 类粒子相当于一个发射器,其位置、速度一直保持不变,在 Life 到达一定的数值时生成 shell 类粒子;
- shell 类粒子由 launch 类粒子生成后,获得一个初始的速度,假设只受到重力,根据牛顿第二定律更新自己的 速度、位置。并且 shell 粒子的 Life 在到达一定数值时生成 second shell 类粒子;
- second shell 类粒子初始时于生成该粒子的父粒子具有相同的位置,但是速度不同。 second shell 粒子的 Life 到达一定数值后死亡。
3.2. PaticleSystem 类
a. PaticleSystem
类的变量
class ParticleSystem
{
public:
...
private:bool m_isFirst; // 标记 是否时第一次调用 Render()GLuint m_VAO[2]; // 两个 VAO 分别用于 update 和 render 的输入unsigned int m_update_input_id; // update input id,unsigned int m_render_input_id; // render input id, update output idGLuint m_VBO_TFB[2]; // 两个顶点缓冲区 , 交替作为 update / render bufferGLuint m_TFO[2]; // 两个 transform feedback 对象 TFOShader m_updateShader; // particle update shaderShader m_renderShader; // particle render shaderTexture m_randomTexture; // 随机数纹理Texture m_particleTexture; // 粒子的纹理float m_time; // 系统运行的总时间...
}
b. InitParticleSystem()
初始化 ParticleSystem
class ParticleSystem
{public:...bool InitParticleSystem(const glm::vec3 &Pos){// 1. 生成 初始粒子Particle Particles[MAX_PARTICLES];Particles[0].Type = 0;Particles[0].Pos = Pos;Particles[0].Velocity = glm::vec3(0.0f, 0.01f, 0.0f);Particles[0].Life = 0.0f;// 2. 初始化 VAO, TFO, VBOglGenVertexArrays(2, m_VAO); // 生成 两个 VAOglGenTransformFeedbacks(2, m_TFO); // 生成 两个 TFOglGenBuffers(2, m_VBO_TFB); // 生成 两个 buffer (TFB), 分别绑定到 对应的 VAO 和 TFO 上for (unsigned int i = 0; i < 2; i++){// VAO[i] <- VBO[i]// TFO[i] <- VBO[i]glBindVertexArray(m_VAO[i]);glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_TFO[i]);glBindBuffer(GL_ARRAY_BUFFER, m_VBO_TFB[i]);glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_VBO_TFB[i]);glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(Particles), Particles, GL_DYNAMIC_DRAW);glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_VBO_TFB[i]);}// 3. 初始化 update shader, render shader// update shaderconst char *feedbackVaryings[] = {"Type1", "Position1", "Velocity1", "Age1"};m_updateShader = Shader("../resources/particleUpdate.vert", "../resources/particleUpdate.frag","../resources/particleUpdate.geom", feedbackVaryings);m_updateShader.use();m_updateShader.setFloat("gLauncherLifetime", 100.0f);m_updateShader.setFloat("gShellLifetime", 10000.0f);m_updateShader.setFloat("gSecondaryShellLifetime", 500.f);// 初始化 render shaderm_renderShader = Shader("../resources/particleRender.vert", "../resources/particleRender.frag","../resources/particleRender.geom");m_renderShader.use();m_renderShader.setFloat("gBillboardSize", 0.01f);// 4. 初始化 纹理// 随机数纹理m_randomTexture.id = TextureFromRand();m_randomTexture.path = "random";m_randomTexture.type = "texture_diffuse";// 粒子纹理m_particleTexture.id = TextureFromFile("particle.png", "../resources/textures/");m_particleTexture.path = "../resources/textures/particle.png";m_particleTexture.type = "texture_diffuse";return true;};...
}
c. Render()
调用 update shader 和 Render shader 进行更新粒子、渲染粒子
class ParticleSystem
{
public:
...void Render(float DeltaTimeMillis, const glm::mat4 &VP, const glm::vec3 &CameraPos){m_time += DeltaTimeMillis;// 更新 粒子updateParticles(DeltaTimeMillis);// 渲染 粒子renderParticles(VP, CameraPos);// 交换 update shader 使用的 VAO 和 TFO// 0 -> 1 -> 0 -> 1 -> 0 -> ...m_update_input_id = (m_update_input_id + 1) % 2;// 交换 render shader 使用的 VAO// 1 -> 0 -> 1 -> 0 -> 1 -> ...m_render_input_id = (m_render_input_id + 1) % 2;};...
}
d. updateParticles()
更新粒子
class ParticleSystem
{
public:
...void updateParticles(float DelatTimeMillis){// 1. 设置 update shader 中的 uniform 变量以及纹理变量m_updateShader.use();m_updateShader.setFloat("gTime", m_time);m_updateShader.setFloat("gDeltaTimeMillis", 1.0f * DelatTimeMillis);glActiveTexture(GL_TEXTURE0); // 激活纹理单元 0glUniform1i(glGetUniformLocation(m_updateShader.ID, "gRandomTexture"),0); // 将纹理单元0 与着色器的 sampler 变量 gRandomTexture 关联glBindTexture(GL_TEXTURE_1D, m_randomTexture.id); // 将 纹理对象 绑定到当前的纹理单元 GL_SAMPLER_1D 纹理上// 2. 绑定 VAO, TFB// 绑定VAO, 作为 update shader 的输入glBindVertexArray(m_VAO[m_update_input_id]);// 根据 update shader 设置 VAO 中不同属性的读取方式glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, sizeof(Particle), 0); // typeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Particle), (const GLvoid *)(sizeof(float))); // positionglVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Particle),(const GLvoid *)(sizeof(float) + sizeof(glm::vec3))); // velocityglVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, sizeof(Particle),(const GLvoid *)((sizeof(float) + sizeof(glm::vec3)) + sizeof(glm::vec3))); // lifetimeglEnableVertexAttribArray(0);glEnableVertexAttribArray(1);glEnableVertexAttribArray(2);glEnableVertexAttribArray(3);// 绑定 TFO, 作为 update shader 的输出glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_TFO[m_render_input_id]);// 3. 开始使用 update shader 更新粒子glEnable(GL_RASTERIZER_DISCARD); // 跳过光栅化以及之后的阶段glBeginTransformFeedback(GL_POINTS);if (m_isFirst){ // 第一次 运行 update shader, 只有一个 粒子glDrawArrays(GL_POINTS, 0, 1);m_isFirst = false;}else{ // 之后运行 update shader, 粒子个数不确定, 由 opengl 根据 transform feedback object 自行确定粒子个数glDrawTransformFeedback(GL_POINTS, m_TFO[m_update_input_id]);}glEndTransformFeedback();glDisable(GL_RASTERIZER_DISCARD); // 开启光栅化以及之后的阶段glDisableVertexAttribArray(0);glDisableVertexAttribArray(1);glDisableVertexAttribArray(2);glDisableVertexAttribArray(3);};...
}
e. renderParticles()
渲染粒子
class ParticleSystem
{
public:
...void renderParticles(const glm::mat4 &VP, const glm::vec3 &CameraPos){// 1. 设置渲染状态glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT); // 使用 (0.2,0.3,0.3,1.0) 清空 color texture, 清空 depth bufferglEnable(GL_BLEND); // 启用 blendglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // blend 模式为 D = alpha*S + (1-alpha)*DglEnable(GL_PROGRAM_POINT_SIZE);// 2. 设置 render shader 中的 uniform 变量以及纹理变量m_renderShader.use();m_renderShader.setVec3("gCameraPos", CameraPos);m_renderShader.setMat4("gVP", VP);glActiveTexture(GL_TEXTURE1); // 激活纹理单元 1glUniform1i(glGetUniformLocation(m_renderShader.ID, "gColorMap"), 1);glBindTexture(GL_TEXTURE_2D, m_particleTexture.id); // 将 纹理对象 绑定到当前的纹理单元的 GL_SAMPLER_1D 纹理上// 3. 绑定 VAO// 绑定VAO, 作为 render shader 的输入glBindVertexArray(m_VAO[m_render_input_id]);// 根据 render shader 设置 VAO 中不同属性的读取方式glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Particle), (void *)(sizeof(float))); // positionglEnableVertexAttribArray(0);// 4. 开始使用 render shader 渲染粒子glDisable(GL_RASTERIZER_DISCARD); // 开启 光栅化 以及之后的阶段glDrawTransformFeedback(GL_POINTS, m_TFO[m_render_input_id]);};...
}
3.3. Update shader
a. Vertex shader
#version 410layout(location = 0) in float Type;
layout(location = 1) in vec3 Position;
layout(location = 2) in vec3 Velocity;
layout(location = 3) in float Age;out float Type0;
out vec3 Position0;
out vec3 Velocity0;
out float Age0;void main() {Type0 = Type;Position0 = Position;Velocity0 = Velocity;Age0 = Age;
}
b. Geometer shader
#version 410layout(points) in;
layout(points, max_vertices = 30) out;/* 从 vertex shader 输入的 point 的属性 */
in float Type0[];
in vec3 Position0[];
in vec3 Velocity0[];
in float Age0[];/* 输出到 fragment shader 的 point 的属性*/
out float Type1;
out vec3 Position1;
out vec3 Velocity1;
out float Age1;/* 用于更新 particle 的变量 */
uniform float gDeltaTimeMillis; // 时间间隔
uniform float gTime; // 当前时刻
uniform sampler1D gRandomTexture; // 随机纹理
uniform float gLauncherLifetime; // Launcher 的生存时间
uniform float gShellLifetime; // Shell 的生存时间
uniform float gSecondaryShellLifetime; // Secondary Shell 的生存时间#define PARTICLE_TYPE_LAUNCHER 0.0f
#define PARTICLE_TYPE_SHELL 1.0f
#define PARTICLE_TYPE_SECONDARY_SHELL 2.0f// 使用 random texture 获取一个随机值 (random texture相当于一个随机数池)
vec3 GetRandomDir(float TexCoord) {vec3 Dir = texture(gRandomTexture, TexCoord).xyz;Dir -= vec3(0.5, 0.5, 0.5);return Dir;
}void main() {// 更新 particle 的生存时间float Age = Age0[0] + gDeltaTimeMillis;// 增加随机性float g_Time = (sin(gTime) + 1.0) / 2.0 * 1000.0;g_Time = gTime;// Launcher particleif (Type0[0] == PARTICLE_TYPE_LAUNCHER) {// 如果 particle 生存时间过长// 那么就生成一个 Shell particle, 并且更新 Launcher particleif (Age >= gLauncherLifetime) {// 生成 一个 Shell particleType1 = PARTICLE_TYPE_SHELL;// 初始化 position, dir, velocity, agePosition1 = Position0[0];vec3 Dir = GetRandomDir(g_Time / 1000.0);Dir.y = max(Dir.y, 0.95);Velocity1 = normalize(Dir) / 12.0;// Velocity1 = Velocity0[0];Age1 = 0.0;// emit vertexEmitVertex();EndPrimitive();Age = 0.0;}// 更新 Launcher particleType1 = PARTICLE_TYPE_LAUNCHER;Position1 = Position0[0];Velocity1 = Velocity0[0];Age1 = Age;EmitVertex();EndPrimitive();} else {// 如果是 Shell or Second Shell particlefloat DeltaTimeSecs = gDeltaTimeMillis / 1000.0;float t1 = Age0[0] / 1000.0;float t2 = Age / 1000.0;// position 的改变量vec3 DeltaP = DeltaTimeSecs * Velocity0[0];// velocity 的改变量// vec3 DeltaV = vec3(DeltaTimeSecs) * vec3(0.0, -9.81, 0.0);// 如果是 Shell particlevec3 DeltaV = vec3(0, DeltaTimeSecs / 1000.0 * -9.81, 0);if (Type0[0] == PARTICLE_TYPE_SHELL) {if (Age < gShellLifetime) {// 如果 Shell particle 还在生存时间内Type1 = PARTICLE_TYPE_SHELL;// 更新 position, velocityPosition1 = Position0[0] + DeltaP;Velocity1 = Velocity0[0] + DeltaV;// Velocity1 = Velocity0[0];// Velocity1 = Velocity0[0] + vec3(0.0, DeltaTimeSecs * -9.8, 0.0);Age1 = Age;EmitVertex();EndPrimitive();} else {// 如果 Shell particle 超过生存时间了,那么就 分裂为 10 个 Second Shellfor (int i = 0; i < 10; i++) {Type1 = PARTICLE_TYPE_SECONDARY_SHELL;Position1 = Position0[0];vec3 Dir = GetRandomDir((g_Time + i) / 1000.0);Velocity1 = normalize(Dir) / 20.0;Age1 = 0.0f;EmitVertex();EndPrimitive();}}} else {// 如果是 Second Shell particleif (Age < gSecondaryShellLifetime) {// 如果 Second Shell 还在生存周期内Type1 = PARTICLE_TYPE_SECONDARY_SHELL;Position1 = Position0[0] + DeltaP;Velocity1 = Velocity0[0] + DeltaV;Age1 = Age;EmitVertex();EndPrimitive();}// 如果 Second Shell 超过生存周期, 那么就消灭该 Second Shell particle// (什么也不做)}}
}
c. Fragment shader
#version 410 core
void main() {// do nothing
}
3.4. Render shader
a. Vertex shader
#version 410
layout(location = 0) in vec3 Position;
void main() { gl_Position = vec4(Position, 1.0); }
b. Geometer shader
#version 410layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
uniform mat4 gVP;
uniform vec3 gCameraPos;
uniform float gBillboardSize;out vec2 TexCoord;void main() {// 以 p0 = gl_Position 为右下角,绘制一个矩形 (两个三角形)// p2 --- p4// | \ |// | \ |// p1 --- p3 (p0)vec3 Pos = gl_in[0].gl_Position.xyz;vec3 toCamera = normalize(gCameraPos - Pos);vec3 up = vec3(0.0, 1.0, 0.0);vec3 right = cross(toCamera, up) * gBillboardSize;// p1Pos -= right;gl_Position = gVP * vec4(Pos, 1.0);TexCoord = vec2(0.0, 0.0);EmitVertex();// p2Pos.y += gBillboardSize;gl_Position = gVP * vec4(Pos, 1.0);TexCoord = vec2(0.0, 1.0);EmitVertex();// p3Pos.y -= gBillboardSize;Pos += right;gl_Position = gVP * vec4(Pos, 1.0);TexCoord = vec2(1.0, 0.0);EmitVertex();// p4Pos.y += gBillboardSize;gl_Position = gVP * vec4(Pos, 1.0);TexCoord = vec2(1.0, 1.0);EmitVertex();EndPrimitive();
}
c. Fragment shader
#version 410uniform sampler2D gColorMap;
in vec2 TexCoord;
out vec4 FragColor;
void main() {FragColor = texture(gColorMap, TexCoord);if (FragColor.r >= 0.9 && FragColor.g >= 0.9 && FragColor.b >= 0.9) {discard;}
}
4. 全部代码及模型文件
用于实现粒子效果的全部代码以及模型文件可以在OpenGL使用TransformFeedback实现粒子效果 中下载。
三、参考引用
[1]. modern-opengl-tutorial
[2]. ogldev-tutorial28
[3]. 粒子系统–喷泉 [OpenGL-Transformfeedback]