一、简介
本文介绍了如何使用 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 类
cpp
struct Particle
{
float Type; // 0: launch, 1: shell, 2 : second shell
glm::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
类的变量
cpp
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 id
GLuint m_VBO_TFB[2]; // 两个顶点缓冲区 , 交替作为 update / render buffer
GLuint m_TFO[2]; // 两个 transform feedback 对象 TFO
Shader m_updateShader; // particle update shader
Shader m_renderShader; // particle render shader
Texture m_randomTexture; // 随机数纹理
Texture m_particleTexture; // 粒子的纹理
float m_time; // 系统运行的总时间
...
}
b. InitParticleSystem()
初始化 ParticleSystem
cpp
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, VBO
glGenVertexArrays(2, m_VAO); // 生成 两个 VAO
glGenTransformFeedbacks(2, m_TFO); // 生成 两个 TFO
glGenBuffers(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 shader
const 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 shader
m_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 进行更新粒子、渲染粒子
cpp
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()
更新粒子
cpp
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); // 激活纹理单元 0
glUniform1i(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); // type
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Particle), (const GLvoid *)(sizeof(float))); // position
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Particle),
(const GLvoid *)(sizeof(float) + sizeof(glm::vec3))); // velocity
glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, sizeof(Particle),
(const GLvoid *)((sizeof(float) + sizeof(glm::vec3)) + sizeof(glm::vec3))); // lifetime
glEnableVertexAttribArray(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()
渲染粒子
cpp
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 buffer
glEnable(GL_BLEND); // 启用 blend
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // blend 模式为 D = alpha*S + (1-alpha)*D
glEnable(GL_PROGRAM_POINT_SIZE);
// 2. 设置 render shader 中的 uniform 变量以及纹理变量
m_renderShader.use();
m_renderShader.setVec3("gCameraPos", CameraPos);
m_renderShader.setMat4("gVP", VP);
glActiveTexture(GL_TEXTURE1); // 激活纹理单元 1
glUniform1i(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))); // position
glEnableVertexAttribArray(0);
// 4. 开始使用 render shader 渲染粒子
glDisable(GL_RASTERIZER_DISCARD); // 开启 光栅化 以及之后的阶段
glDrawTransformFeedback(GL_POINTS, m_TFO[m_render_input_id]);
};
...
}
3.3. Update shader
a. Vertex shader
cpp
#version 410
layout(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
cpp
#version 410
layout(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 particle
if (Type0[0] == PARTICLE_TYPE_LAUNCHER) {
// 如果 particle 生存时间过长
// 那么就生成一个 Shell particle, 并且更新 Launcher particle
if (Age >= gLauncherLifetime) {
// 生成 一个 Shell particle
Type1 = PARTICLE_TYPE_SHELL;
// 初始化 position, dir, velocity, age
Position1 = 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 vertex
EmitVertex();
EndPrimitive();
Age = 0.0;
}
// 更新 Launcher particle
Type1 = PARTICLE_TYPE_LAUNCHER;
Position1 = Position0[0];
Velocity1 = Velocity0[0];
Age1 = Age;
EmitVertex();
EndPrimitive();
} else {
// 如果是 Shell or Second Shell particle
float 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 particle
vec3 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, velocity
Position1 = 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 Shell
for (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 particle
if (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
cpp
#version 410 core
void main() {
// do nothing
}
3.4. Render shader
a. Vertex shader
cpp
#version 410
layout(location = 0) in vec3 Position;
void main() { gl_Position = vec4(Position, 1.0); }
b. Geometer shader
cpp
#version 410
layout(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;
// p1
Pos -= right;
gl_Position = gVP * vec4(Pos, 1.0);
TexCoord = vec2(0.0, 0.0);
EmitVertex();
// p2
Pos.y += gBillboardSize;
gl_Position = gVP * vec4(Pos, 1.0);
TexCoord = vec2(0.0, 1.0);
EmitVertex();
// p3
Pos.y -= gBillboardSize;
Pos += right;
gl_Position = gVP * vec4(Pos, 1.0);
TexCoord = vec2(1.0, 0.0);
EmitVertex();
// p4
Pos.y += gBillboardSize;
gl_Position = gVP * vec4(Pos, 1.0);
TexCoord = vec2(1.0, 1.0);
EmitVertex();
EndPrimitive();
}
c. Fragment shader
cpp
#version 410
uniform 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实现粒子效果 中下载。
三、参考引用
[2]. ogldev-tutorial28