对象同步变换反馈粒子系统:Transform FeedBack Particle System & Sync - MartinRGB/GLES30_ProgrammingGuide_NDK GitHub Wiki

同步对象

让 GPU 在 某个操作执行完之前先等待,然后将更多命令送入执行队列。

变换反馈

变换反馈允许顶点着色器的输出保存在 VBO 中,可以做到,一个 VBO 准备生成,另外一个 VBO 区域绘制,需要 2 组着色器

粒子系统中的综合运用

这个案例中的构思是:分配两个 VBO 保存粒子数据,算法在两个 VBO 中切换,步骤如下:

1.每一 frame 中,选择某一个 VBO 作为输入,绑定为 GL_ARRAY_BUFFER,输出绑定为 GL_TRANSFORM_FEEDBACK_BUFFER

2.启用 GL_RASTERIZER_DISCARD,不绘制片段

3.顶点着色器生成新粒子到 变换反馈 VBO 中,将现有粒子原封不动复制到其中。

4.禁用 GL_RASTERIZER_DISCARD,开始绘制

5.粒子渲染到 FBO

6.下一帧中,I/O VBO 进行交换。

下面为工程中的流程简单分析

1.粒子的生成的初始化

粒子生成部分,利用传入的 3D 噪音生成随机数值,设置速度、位置、大小、当前时间、寿命,并设置为变换反馈属性,注意:输出变量必须在链接程序对象之前标记为用于变量反馈

   UserData *userData = esContext->userData;
   ...

   userData->emitProgramObject = esLoadProgram ( vShaderStr, fShaderStr );

   {
      // 用于变换反馈的变量
      const char *feedbackVaryings[5] =
      {
         "v_position",
         "v_velocity",
         "v_size",
         "v_curtime",
         "v_lifetime"
      };

      // 将顶点着色器的输出作为变换反馈的变量,GL_INTERLEAVED_ATTRIBS制定了输出变量在缓冲区对象中交叉存取
      glTransformFeedbackVaryings ( userData->emitProgramObject, 
                                    5, 
                                    feedbackVaryings, 
                                    GL_INTERLEAVED_ATTRIBS );

      // 设置完变换反馈后,链接到程序对象
      glLinkProgram ( userData->emitProgramObject );

      // Get the uniform locations - this also needs to happen after glLinkProgram is called again so
      // that the uniforms that output to varyings are active
      ...
   }

2.粒子绘制初始化

完成粒子绘制部分,初始化变换反馈 index,初始化粒子所有属性,读取粒子的材质图,读取 3D 噪音作为随机量,并且创建两块 FBO

Particle particleData[ NUM_PARTICLES ];
   UserData *userData = ( UserData * ) esContext->userData;
   int i;
   
   // Write Shader Code Here

   InitEmitParticles ( esContext );

   // Load the shaders and get a linked program object
   userData->drawProgramObject = esLoadProgram ( vShaderStr, fShaderStr );

   // Get the uniform locations
   ...

   userData->time = 0.0f;
   //初始化当前缓冲区 index 为 0
   userData->curSrcIndex = 0;

   glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );

   userData->textureId = LoadTexture ( esContext->platformData, "smoke.tga" );

   ...

   // 生成一段 3D 噪音图像,以便求随机值
   userData->noiseTextureId = Create3DNoiseTexture ( 128, 50.0 );

   // 粒子属性数据的初始化
   for ( i = 0; i < NUM_PARTICLES; i++ )
   {
      Particle *particle = &particleData[i];
      particle->position[0] = 0.0f;
      particle->position[1] = 0.0f;
      particle->velocity[0] = 0.0f;
      particle->velocity[1] = 0.0f;
      particle->size = 0.0f;
      particle->curtime = 0.0f;
      particle->lifetime = 0.0f;
   }


   // 创建两块帧缓冲区
   glGenBuffers ( 2, &userData->particleVBOs[0] );

   for ( i = 0; i < 2; i++ )
   {
      glBindBuffer ( GL_ARRAY_BUFFER, userData->particleVBOs[i] );
      glBufferData ( GL_ARRAY_BUFFER, sizeof ( Particle ) * NUM_PARTICLES, particleData, GL_DYNAMIC_COPY );
   }

   return TRUE;

3.粒子生成过程

使用 glBindBufferBase 绑定到 GL_TRANSFORM_FEEDBACK_BUFFER 目标。

禁用光栅化来不绘制任何片段,只是执行着色器流程,把 vertex shader 中的属性输出到 VBO,这一过程中。

调用 glBeginTransformFeedback(GL_POINTS) 启动变换反馈渲染,直到 glEndTransformFeedback

为了结果完成后再传入,我们使用了同步对象 glFence()

   ...
   //Source
   GLuint srcVBO = userData->particleVBOs[ userData->curSrcIndex ];
   //Destination
   GLuint dstVBO = userData->particleVBOs[ ( userData->curSrcIndex + 1 ) % 2 ];
   glUseProgram ( userData->emitProgramObject );

   // 源缓冲区设置 Vertex 属性
   SetupVertexAttributes ( esContext, srcVBO );

   // Set transform feedback buffer 设置 变换反馈 的缓冲区
   glBindBuffer ( GL_TRANSFORM_FEEDBACK_BUFFER, dstVBO );
   glBindBufferBase ( GL_TRANSFORM_FEEDBACK_BUFFER, 0, dstVBO );

   // Turn off rasterization - we are not drawing 关闭光栅化
   glEnable ( GL_RASTERIZER_DISCARD );

  // ### 粒子的生成流程,设置 Uniform,绑定 3D噪音Texture
  ...
  
  // Emit particles using transform feedback
   glBeginTransformFeedback ( GL_POINTS );
   glDrawArrays ( GL_POINTS, 0, NUM_PARTICLES );
   glEndTransformFeedback();

   // 创建同步对象,确保上面的生成完成 Create a sync object to ensure transform feedback results are completed before the draw that uses them.
   userData->emitSync = glFenceSync ( GL_SYNC_GPU_COMMANDS_COMPLETE, 0 );
   
   // 清空各种绑定
   ...
   
   // 生成完成后,更替 index
   userData->curSrcIndex = ( userData->curSrcIndex + 1 ) % 2;

然后再 update 方法中进行生成更新

4.粒子绘制过程

等待同步对象生成的完成,然后采用标准绘制流程


   // 阻塞GL服务器端,直到变换反馈完成 Block the GL server until transform feedback results are completed
   glWaitSync ( userData->emitSync, 0, GL_TIMEOUT_IGNORED );
   glDeleteSync ( userData->emitSync );

   // ### 标准绘制流程 
   ...

5.着色器分析

生成部分

vert:

每当粒子寿命到期,着色器将其作为新活跃粒子的潜在候选,此着色器为粒子随机生成属性

//根据传入的 3D noise texture,通过 gl_VertexID 内建变量 为每个粒子选择不同的纹理坐标
float randomValue( inout float seed )                               
{                                                                   
   float vertexId = float( gl_VertexID ) / float( NUM_PARTICLES );  
   vec3 texCoord = vec3( u_time, vertexId, seed );                  
   seed += 0.1;                                                     
   return texture( s_noiseTex, texCoord ).r;                        
}                                                                   
void main()                                                         
{                                                                   
  float seed = u_time;                                              
  float lifetime = a_curtime - u_time;
  //随机生成速度,设置变换反馈的输出变量                              
  if( lifetime <= 0.0 && randomValue(seed) < u_emissionRate )       
  {                                                                 
     v_position = vec2( 0.0, -1.0 );                                
     v_velocity = vec2( randomValue(seed) * 2.0 - 1.00,             
                        randomValue(seed) * 1.4 + 1.0 );            
     v_size = randomValue(seed) * 20.0 + 60.0;                      
     v_curtime = u_time;                                            
     v_lifetime = 2.0;                                              
  }                                                                 
  else                                                              
  {                                                                 
     v_position = a_position;                                       
     v_velocity = a_velocity;                                       
     v_size = a_size;                                               
     v_curtime = a_curtime;                                         
     v_lifetime = a_lifetime;                                       
  }                                                                 
  gl_Position = vec4( v_position, 0.0, 1.0 );                       
}                                                                   

frag:

设置纯色白色

绘制部分

vert:

粒子的年龄根据生成时,属性中保存的时间戳计算,速度和位置据此更新

void main()                                                         
{                                                                   
  float deltaTime = u_time - a_curtime;                             
  if ( deltaTime <= a_lifetime )                                    
  {                                                                 
     vec2 velocity = a_velocity + deltaTime * u_acceleration;       
     vec2 position = a_position + deltaTime * velocity;             
     gl_Position = vec4( position, 0.0, 1.0 );                      
     gl_PointSize = a_size * ( 1.0 - deltaTime / a_lifetime );      
  }                                                                 
  else                                                              
  {                                                                 
     gl_Position = vec4( -1000, -1000, 0, 0 );                      
     gl_PointSize = 0.0;                                            
  }                                                                 
};

frag:

设置texture和颜色