[OpenGL]使用TransformFeedback实现粒子效果

一、简介

本文介绍了如何使用 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实现粒子效果 中下载。

三、参考引用

1. modern-opengl-tutorial

2. ogldev-tutorial28

3. 粒子系统--喷泉 OpenGL-Transformfeedback

相关推荐
蒸蒸yyyyzwd6 小时前
秋招学习笔记 day46
c++·八股
码匠许师傅10 小时前
【C++三方组件】cpp-httplib:一个头文件起 HTTP 服务
开发语言·c++·http
无名猿10 小时前
new/delete 与 malloc/free:为什么绝对不能混用
c++·内存管理·现代c++·踩坑记录
2601_9622186112 小时前
C++中decltype关键字的实现
开发语言·c++
繁华的地方不一定留下你的脚印13 小时前
C++ std::variant 与 std::visit:安全保存多种类型,写清每个处理分支
开发语言·c++
程序员老陆13 小时前
WIN32_LEAN_AND_MEAN:Windows 头文件里的“精简编译开关”
c++·windows
hold?fish:palm14 小时前
45 二叉树的右视图
开发语言·c++·算法
ShineWinsu14 小时前
对于Redis:Redis的认识以及分布式系统的解析
linux·数据库·c++·redis·缓存·消息队列·分布式架构
布莱克60514 小时前
在C++中,构造函数能不能是虚函数?析构函数为什么要虚?
开发语言·c++·构造函数·析构函数·虚函数
旖旎夜光15 小时前
LeetCode 137 : 只出现一次的数字 II(位运算) —— 题解
c++·笔记·学习·算法·leetcode·力控