《OGL dev》Etay Meiri Tutorial 28 - Particle System using Transform Feedback 笔记

粒子系统是模拟由大量粒子构成的现象的技术,粒子以某种规律一起运动。

粒子系统每一帧执行两个步骤:

  1. 更新每个粒子的属性。
  2. 渲染粒子。

更新粒子属性的步骤,在DirectX10引入Stream Output之前在CPU完成,引入之后在GPU完成。

  • OpenGL在3.0版本引入相同的功能,命名为Transform Feedback。

在CPU更新粒子属性存在两点劣势:

  1. CPU每帧都需要更新vertex buffer,写入到GPU内存(即显存)。而粒子的数量通常非常多,1万以上很常见。如果1秒60帧,那么每秒在CPU与GPU之间拷贝的数据量将非常大。
  2. 单核CPU,需要逐个更新粒子属性。多核CPU需要额外的编码,才能并行执行。而并行执行是GPU最擅长的,不需要额外编码。

渲染粒子的步骤,在GPU完成。一个粒子渲染结果,通常为一个像素或一个billboard的quad。

Transform Feedback提供在GPU更新粒子属性的支持:

  1. 提供一个Transform Feedback Buffer位于GS之后,裁剪之前,接收GS的结果,如果GS不存在,则为VS的结果。
  2. Transform Feedback Buffer可以作为draw call的vertex buffer。
  3. 由于GS输出的顶点数是未知的,提供一个不接收顶点数的draw call,glDrawTransformFeedback()。它接收一个Transform Feedback Buffer。系统为每个Transform Feedback Buffer维护顶点数,由系统跟踪GS进行更新。Transform Feedback Buffer的顶点数支持手动重置。
  4. 当管线到达Transform Feedback Buffer时,可以选择,图元以常规路线进入光栅器,或丢弃图元。

OpenGL规定,draw call的输入输出,不能使用相同的buffer。因此,实际需要两个transform feedback buffer,每帧互相交换。

glGenTransformFeedbacks(2, m_transformFeedback);,创建两个transform feedback对象。其中m_transformFeedback,是GLuint类型的数组,用于接收创建的对象的handle。

glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_transformFeedback[i]);,将transform feedback对象m_transformFeedback[i],绑定到GL_TRANSFORM_FEEDBACK,后续对transform feedback的操作都应用于m_transformFeedback[i]。

  • 每次调用glBindTransformFeedback,作为参数的transform feedback对象的顶点数被置0。

glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_particleBuffer[i]);,将vertex buffer对象m_particleBuffer[i]绑定到GL_TRANSFORM_FEEDBACK_BUFFER,使得m_particleBuffer[i]成为当前transform feedback对象的transform feedback buffer。

  • 其中第二个参数0,表示m_particleBuffer[i]成为当前transform feedback对象的索引为0的transform feedback buffer。之所以存在索引,是因为GS输出的顶点属性,其中哪些属性输出到哪个buffer是可以通过glTransformFeedbackVaryings()函数设置的。索引最大值,即buffer上限,为GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS。
const GLchar* Varyings[4];
Varyings[0] = "Type1";
Varyings[1] = "Position1";
Varyings[2] = "Velocity1";
Varyings[3] = "Age1";

glTransformFeedbackVaryings(m_shaderProg, 4, Varyings, GL_INTERLEAVED_ATTRIBS);

glTransformFeedbackVaryings必须在shader链接之前调用。

  1. 第一个参数为shader程序。
  2. 第三个参数为输出到transform feedback buffer的属性名数组。
  3. 第二个参数为属性名数组的元数个数。
  4. 最后一个参数GL_INTERLEAVED_ATTRIBS表示,将属性名数组中的所有属性作为一个structure写入到transform feedback buffer中。这个参数还可以使用GL_SEPARATE_ATTRIBS,表示属性名数组中每一个属性写入到一个transform feedback buffer中。其中属性在属性名数组中的索引,对应glBindBufferBase的第二个参数。
glBeginTransformFeedback(GL_POINTS);

if (m_isFirst) {
    glDrawArrays(GL_POINTS, 0, 1);

    m_isFirst = false;
}
else {
    glDrawTransformFeedback(GL_POINTS, m_transformFeedback[m_currVB]);
}            

glEndTransformFeedback();

glBeginTransformFeedback()与glEndTransformFeedback()之间调用的draw call,输出被重定向到transform feedback buffer。

  • glBeginTransformFeedback()与glEndTransformFeedback()必须成对出现,否则导致崩溃。
  • 不论draw call的拓扑类型如何,输出到transform feedback buffer的拓扑类型一定是点列表。比如参数为triangle strip的draw call绘制4个顶点,和参数为三角形列表的draw call绘制6个顶点,输出到transform feedback buffer的都是6个顶点(两个三角形)。

glBeginTransformFeedback()接收一个拓扑类型作为参数,它限制draw call的拓扑类型。

  • GL_POINTS,draw call的拓扑必须是GL_POINTS。
  • GL_LINES,draw call的拓扑必须是GL_LINES、GL_LINE_LOOP或GL_LINE_STRIP。
  • GL_TRIANGLES,draw call的拓扑必须GL_TRIANGLES、GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN。

glDrawTransformFeedback(GL_POINTS, m_transformFeedback[m_currVB]);,glDrawTransformFeedback是不需要在顶点数的draw call。它的第一个参数为拓扑类型,它的顶点数从transform feedback对象参数m_transformFeedback[m_currVB]中获得。因为transform feedback对象的初始顶点数为0,所以第一次draw call这里使用glDrawArrays(GL_POINTS, 0, 1);提供1个顶点。

glDisable(GL_RASTERIZER_DISCARD);,禁用光栅化,transform feedback buffer之后的图元被丢弃。因为光栅化被禁用,所以不需要FS。

bool RandomTexture::InitRandomTexture(unsigned int Size)
{
    Vector3f* pRandomData = new Vector3f[Size];

    for (unsigned int i = 0 ; i < Size ; i++) {
        pRandomData[i].x = RandomFloat();
        pRandomData[i].y = RandomFloat();
        pRandomData[i].z = RandomFloat();
    }

    glGenTextures(1, &m_textureObj);
    glBindTexture(GL_TEXTURE_1D, m_textureObj);
    glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB, Size, 0.0f, GL_RGB, GL_FLOAT, pRandomData);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT);

    delete [] pRandomData;

    return GLCheckError();
}

随机纹理,用于在shader内提供随机数。随机纹理是一个1D纹理。

  • 使用GL_RGB参数调用glTexImage1D,生成三个通道的1D纹理。
  • 使用GL_TEXTURE_WRAP_S和GL_REPEAT调用glTexParameterf,使得纹理采样以循环的方式支持超过[0, 1]区间的纹理坐标。
  • 在shader中通过texture(gRandomTexture, gTime/1000.0).xyz的方式获取随机数,其中gRandomTexture是随机纹理,gTime是接收应用传递的全局时间的uniform variable。