【OpenGL ES】顶点缓冲区对象VBO与顶点数组对象VAO

【完整示例代码】https://github.com/geminy/aidear/tree/master/graphics/mu/examples/opengles3

顶点属性即顶点数据,可以通过顶点数组逐顶点指定,也可以为所有顶点指定一个常量,所有OpenGL ES 3.0实现必须支持至少16个顶点属性,应用程序可以通过glGetIntegerv查询GL_MAX_VERTEX_ATTRIBS得到支持的最大定点属性个数。

1、常量顶点属性

常量顶点属性使用glVertexAttrib系列函数指定,有如下几个函数:

void glVertexAttrib1f(GLuint index,
    GLfloat v0);

void glVertexAttrib2f(GLuint index,
    GLfloat v0,
    GLfloat v1);

void glVertexAttrib3f(GLuint index,
    GLfloat v0,
    GLfloat v1,
    GLfloat v2);

void glVertexAttrib4f(GLuint index,
    GLfloat v0,
    GLfloat v1,
    GLfloat v2,
    GLfloat v3);

void glVertexAttribI4i(GLuint index,
    GLint v0,
    GLint v1,
    GLint v2,
    GLint v3);

void glVertexAttribI4ui(GLuint index,
    GLuint v0,
    GLuint v1,
    GLuint v2,
    GLuint v3);

void glVertexAttrib1fv(GLuint index,
    const GLfloat *v);

void glVertexAttrib2fv(GLuint index,
    const GLfloat *v);

void glVertexAttrib3fv(GLuint index,
    const GLfloat *v);

void glVertexAttrib4fv(GLuint index,
    const GLfloat *v);

void glVertexAttribI4iv(GLuint index,
    const GLint *v);

void glVertexAttribI4uiv(GLuint index,
    const GLuint *v);

glVertexAttrib中,index为定点属性索引,顶点属性各分量的默认值为(0, 0, 0, 1),数字1、2、3、4表示设置从第一个开始的几个分量,f、i、ui表示类型float、int、unsigned int,v表示顶点属性分量保存在向量(数组)中,大写I表示类型扩展为完整的int或unsigned int。

2、顶点属性数组

顶点(属性)数组使用glVertexAttribPointer和glVertexAttribIPointer指定,如下:

void glVertexAttribPointer(GLuint index,
    GLint size,
    GLenum type,
    GLboolean normalized,
    GLsizei stride,
    const GLvoid * pointer);

void glVertexAttribIPointer(GLuint index,
    GLint size,
    GLenum type,
    GLsizei stride,
    const GLvoid * pointer);

glVertexAttribPointer和glVertexAttribIPointer中,index为顶点属性索引,从0到最大顶点属性数减1,size为顶点属性分量数量,从1到4,初始值为4也就是有4个属性分量,type为数据格式,两者都包括GL_BYTE、GL_UNSIGNED_BYTE、GL_SHORT、GL_UNSIGNED_SHORT、GL_INT、GL_UNSIGNED_INT ,前者还包括GL_HALF_FLOAT、GL_FLOAT、GL_FIXED、GL_INT_2_10_10_10_REV、GL_UNSIGNED_INT_2_10_10_10_REV,初始值为GL_FLOAT,normalized表示非浮点数据类型格式在转化为浮点值时是否应该规范化,int从-1到1,unsigned int从0到1,type为GL_FIXED时忽略normalized,stride为顶点属性分量之间的跨距,为0时表示顺序存储,初始值为0,pointer为保存顶点属性数据的缓冲区(顶点数组)的指针或顶点缓冲区对象(VBO)内的偏移量,初始值为0,这个pointer参数的用法比较特殊,下面例子有所展示。

glVertexAttribPointer与glVertexAttribIPointer的唯一区别就是后者没有normalized,在用于顶点着色器之前,顶点属性在内部保存为单精度浮点数,如果数据类型表示顶点属性不是浮点数,顶点属性将在用于顶点着色器之前转换为单精度浮点数,normalized控制非浮点顶点属性属性到单精度浮点值的转换,为GL_FALSE时直接转换为浮点值,为GL_TRUE时才进行规范化,根据type类型规范化(映射)为从-1到1或者从0到1,而glVertexAttribIPointer则是用整数存储顶点属性数据。

分配和存储顶点属性数据有两种常用的方法,结构数组和数组结构,前者在一个缓冲区中存储顶点属性,后者在单独的缓冲区中保存每个顶点属性,简单的说就是前者为一个大的缓冲区,后者为几个小的缓冲区,两者的缓冲区总大小相等。那么,对于OpenGL ES 3.0硬件实现,哪种分配方法最高效?在大部分情况下是结构数组,因为每个顶点的属性数据可以顺序读取,对于内存访问来说很可能是高效的,但也有其缺点,在修改特定属性或者顶点属性的一个子集时造成顶点缓冲区的跨距更新,将变得效率低下,当顶点缓冲区以缓冲区对象的形式提供时,需要重新加载整个顶点属性缓冲区,可以通过将动态的顶点属性保存在单独的缓冲区来避免这种效率低下的情况。有时出于性能考虑,顶点属性尽可能使用低精度或低范围的数据类型,如OpenGL ES 3.0提供的GL_HALF_FLOAT,为16位浮点格式,是纹理坐标、法线、副法线、切向量等存储每个分量的候选,颜色可以存储为GL_UNSIGNED_BYTE,每个顶点颜色具有4个颜色分量,顶点位置则存储为GL_FLOAT,这个type参数指定的顶点属性数据格式不仅影响顶点属性数据的图形内存存储需求,而且影响整体性能,数据空间占用越小,需要的内存带宽越小。

最后,应用程序可以让OpenGL ES使用常量数据或者来自顶点数组的数据,glEnableVertexAttribArray和glDisableVertexAttribArray函数分别用于启用和禁用通用顶点属性数组,如果某个通用属性索引的顶点属性数组被禁用,将使用为该索引指定的常量顶点熟悉数据,顶点熟悉数组默认是不可用的。

void glEnableVertexAttribArray(GLuint index);
void glDisableVertexAttribArray(GLuint index);

3、顶点属性声明

在顶点着色器中,变量通过in声明为顶点属性(输入变量),属性变量也可以选择包含一个布局限定符,提供属性索引。in限定符只能用于内建的数据类型,所以,属性变量不能声明为数组或者结构。顶点属性为四分量向量,与编译器自动打包的统一变量及顶点着色器输出与片段着色器输入(插值器)变量不同,属性不进行打包。需要注意GL_MAX_VERTEX_ATTRIBS,是个有限资源。在顶点着色器中声明为顶点属性的变量其实是只读变量,在着色器中不能修改,其值通过客户端API即应用程序通过glVertexAttrib等函数指定,在着色器中使用了属性变量时才被认为是活动变量。

在OpenGL ES 3.0中,可以使用三种方法将通用顶点属性索引映射到顶点着色器中的一个属性变量名称,一是在顶点着色器源代码中用布局限定符layout指定,推荐使用这种方式,二是OpenGL ES 3.0在程序链接阶段为没有指定索引的顶点绑定一个属性索引,三是应用程序使用glBindAttribLocation进行绑定,相关函数如下所示。

void glGetProgramiv(GLuint program,
    GLenum pname, // GL_ACTIVE_ATTRIBUTES
    GLint *params);

void glGetActiveAttrib(GLuint program,
    GLuint index,
    GLsizei bufSize,
    GLsizei *length,
    GLint *size,
    GLenum *type,
    GLchar *name);

void glBindAttribLocation(GLuint program,
    GLuint index,
    const GLchar *name);

GLint glGetAttribLocation(GLuint program,
    const GLchar *name); 

glGetActiveAttrib查询指定program与index的活动属性的信息。glBindAttribLocation将顶点属性索引绑定到顶点着色器的一个属性变量,在下一次程序链接时生效,不会改变当前链接的程序中使用的绑定,如果之前绑定了name,则它所指定的绑定被index代替,可以在顶点着色器链接到程序对象之前调用,绑定任何属性名称,不存在的属性名称或者在链接到程序对象的顶点着色器中不活动的属性将被忽略。glGetAttribLocation查询绑定到属性变量name的顶点属性索引,如果没有绑定,则内部实现自动绑定到一个顶点属性索引,这发生在链接阶段,失败时返回-1。

4、顶点数组示例

下面是使用了常量顶点属性(颜色,索引为1,所以三角形的颜色整个都为红色)和顶点属性数组(位置,索引为0,所以指定了三角形的三个不同的顶点位置)的代码片段,结果为一个白色背景的红色三角形。

// Example_6_3.c
int Init ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   const char vShaderStr[] =
      "#version 300 es                            \n"
      "layout(location = 0) in vec4 a_position;   \n"
      "layout(location = 1) in vec4 a_color;      \n"
      "out vec4 v_color;                          \n"
      "void main()                                \n"
      "{                                          \n"
      "    v_color = a_color;                     \n"
      "    gl_Position = a_position;              \n"
      "}";

   const char fShaderStr[] =
      "#version 300 es            \n"
      "precision mediump float;   \n"
      "in vec4 v_color;           \n"
      "out vec4 o_fragColor;      \n"
      "void main()                \n"
      "{                          \n"
      "    o_fragColor = v_color; \n"
      "}" ;

   GLuint programObject;
   // Create the program object
   programObject = esLoadProgram ( vShaderStr, fShaderStr );
   if ( programObject == 0 )
   {
      return GL_FALSE;
   }

   // Store the program object
   userData->programObject = programObject;

   glClearColor ( 1.0f, 1.0f, 1.0f, 0.0f );
   return GL_TRUE;
}

void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLfloat color[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
   // 3 vertices, with (x,y,z) per-vertex
   GLfloat vertexPos[3 * 3] =
   {
      0.0f,  0.5f, 0.0f, // v0
      -0.5f, -0.5f, 0.0f, // v1
      0.5f, -0.5f, 0.0f  // v2
   };

   glViewport ( 0, 0, esContext->width, esContext->height );
   glClear ( GL_COLOR_BUFFER_BIT );
   glUseProgram ( userData->programObject );

   glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vertexPos );
   glEnableVertexAttribArray ( 0 );
   glVertexAttrib4fv ( 1, color )

   glDrawArrays ( GL_TRIANGLES, 0, 3 );
   glDisableVertexAttribArray ( 0 );
}

【OpenGL ES】顶点缓冲区对象VBO与顶点数组对象VAO_第1张图片

修改上面的代码,将三角形的颜色设置也用和位置设置一样的方法,通过顶点数组指定三个顶点为不同的颜色以绘制颜色渐变三角形。

void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLfloat color[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
   // 3 vertices, with (x,y,z) per-vertex
   GLfloat vertexPos[3 * 3] =
   {
      0.0f,  0.5f, 0.0f, // v0
      -0.5f, -0.5f, 0.0f, // v1
      0.5f, -0.5f, 0.0f  // v2
   };
   // 3 vertices, with (r,g,b,a) per-vertex
   GLfloat vertexColor[3 * 4] =
   {
       1.0f, 0.0f, 0.0f, 1.0f,
       0.0f, 1.0f, 0.0f, 1.0f,
       0.0f, 0.0f, 1.0f, 1.0f
   };

   glViewport ( 0, 0, esContext->width, esContext->height );
   glClear ( GL_COLOR_BUFFER_BIT );
   glUseProgram ( userData->programObject );

   glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vertexPos );
   glEnableVertexAttribArray ( 0 );
//   glVertexAttrib4fv ( 1, color );
   glVertexAttribPointer ( 1, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, vertexColor );
   glEnableVertexAttribArray ( 1 );

   glDrawArrays ( GL_TRIANGLES, 0, 3 );

   glDisableVertexAttribArray ( 0 );
   glDisableVertexAttribArray ( 1 );
}

【OpenGL ES】顶点缓冲区对象VBO与顶点数组对象VAO_第2张图片

5、顶点缓冲区对象

顶点缓冲区对象即Vertex Buffer Object,简称VBO。顶点数组数据保存在客户内存中,使用glDrawArrays或glDrawElements绘图时,这些数据必须从客户内存复制到图形内存,如果数据过大或者频繁绘图时,将带来性能问题,于是引入了VBO,在图形内存中分配和缓冲顶点数据并且进行渲染,这样可以提高渲染性能,降低内存带宽和电力消耗,不仅是顶点数据,描述图元顶点的索引、作为glDrawElements参数传递的元素索引也可以缓存。

OpenGL ES 3.0支持两类缓冲区对象,数组缓冲区对象GL_ARRAY_BUFFER用于顶点数据,元素数组缓冲区对象GL_ELEMENT_ARRAY_BUFFER用于图元索引,使用VBO涉及如下几个函数。

void glGenBuffers(GLsizei n,
    GLuint * buffers);

void glBindBuffer(GLenum target,
    GLuint buffer);

void glBufferData(GLenum target,
    GLsizeiptr size,
    const GLvoid * data,
    GLenum usage);

void glBufferSubData(GLenum target,
    GLintptr offset,
    GLsizeiptr size,
    const GLvoid * data);

void glDeleteBuffers(GLsizei n,
    const GLuint * buffers);

glGenBuffers用于创建缓冲区对象,n为缓冲区对象数量,buffers保存创建的缓冲区对象。glBindBuffer用于绑定缓冲区对象,buffer为缓冲区对象,不存在时将自动创建,之前的绑定将失效,0为保留值,将解绑所有绑定的缓冲区对象,target如下:

GL_ARRAY_BUFFER
GL_ATOMIC_COUNTER_BUFFER // from GL ES 3.1
GL_COPY_READ_BUFFER // from GL ES 3.0
GL_COPY_WRITE_BUFFER // from GL ES 3.0
GL_DISPATCH_INDIRECT_BUFFER // from GL ES 3.1
GL_DRAW_INDIRECT_BUFFER // from GL ES 3.1
GL_ELEMENT_ARRAY_BUFFER
GL_PIXEL_PACK_BUFFER // from GL ES 3.0
GL_PIXEL_UNPACK_BUFFER // from GL ES 3.0
GL_SHADER_STORAGE_BUFFER // from GL ES 3.1
GL_TEXTURE_BUFFER // from GL ES 3.2
GL_TRANSFORM_FEEDBACK_BUFFER // from GL ES 3.0
GL_UNIFORM_BUFFER // from GL ES 3.0

glBindBuffer之后,可以通过glGet查看GL_ARRAY_BUFFER_BINDING、GL_ELEMENT_ARRAY_BUFFER_BINDING等绑定状态。glBufferData用于创建并初始化缓冲区对象的数据,之前的数据将被清除,target同上,size为缓冲区对象的数据长度,data为缓冲区对象的数据,可以为NULL,usage如下:

GL_STREAM_DRAW // 修改一次,较少使用,应用程序修改数据,用于GL绘图和图片相关的命令
GL_STREAM_READ // 修改一次,较少使用,GL中读取的数据修改这个数据,用于返回到应用程序
GL_STREAM_COPY // 修改一次,较少使用,GL中读取的数据修改这个数据,用于GL绘图和图片相关的命令
GL_STATIC_DRAW // 修改一次,多次使用,应用程序修改数据,用于GL绘图和图片相关的命令
GL_STATIC_READ // 修改一次,多次使用,GL中读取的数据修改这个数据,用于返回到应用程序
GL_STATIC_COPY // 修改一次,多次使用,GL中读取的数据修改这个数据,用于GL绘图和图片相关的命令
GL_DYNAMIC_DRAW // 修改多次,多次使用,应用程序修改数据,用于GL绘图和图片相关的命令
GL_DYNAMIC_READ // 修改多次,多次使用,GL中读取的数据修改这个数据,用于返回到应用程序
GL_DYNAMIC_COPY // 修改多次,多次使用,GL中读取的数据修改这个数据,用于GL绘图和图片相关的命令

glBufferData之后,可以通过glGetBufferParameter查看GL_BUFFER_SIZE、GL_BUFFER_USAGE等缓冲区对象相关信息。glBufferSubData用于更新缓冲区对象的数据,offset为偏移量。glBufferData或glBufferSubData之后,客户缓冲区中的数据已经缓冲到缓冲区对象,可以释放。最后,glDeleteBuffers用于删除缓冲区对象。

6、顶点缓冲区对象示例

下面的例子画了两个相同的三角形,顶点属性的缓冲区都是使用了上面提到的结构数组的形式,即位置和颜色数据都在一个数组中,左边的使用的是同前面例子中一样的顶点数组,右边的使用了两个VBO,一个数组缓冲区和一个元素数组缓冲区,两个三角形的偏移量通过uniform变量进行设置,代码如下:

// VertexBufferObjects.c
typedef struct
{
   // Handle to a program object
   GLuint programObject;
   // VertexBufferObject Ids
   GLuint vboIds[2];
   // x-offset uniform location
   GLuint offsetLoc;
} UserData;

#define VERTEX_POS_SIZE       3 // x, y and z
#define VERTEX_COLOR_SIZE     4 // r, g, b, and a
#define VERTEX_POS_INDX       0
#define VERTEX_COLOR_INDX     1

int Init ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   const char vShaderStr[] =
      "#version 300 es                            \n"
      "layout(location = 0) in vec4 a_position;   \n"
      "layout(location = 1) in vec4 a_color;      \n"
      "uniform float u_offset;                    \n"
      "out vec4 v_color;                          \n"
      "void main()                                \n"
      "{                                          \n"
      "    v_color = a_color;                     \n"
      "    gl_Position = a_position;              \n"
      "    gl_Position.x += u_offset;             \n"
      "}";

   const char fShaderStr[] =
      "#version 300 es            \n"
      "precision mediump float;   \n"
      "in vec4 v_color;           \n"
      "out vec4 o_fragColor;      \n"
      "void main()                \n"
      "{                          \n"
      "    o_fragColor = v_color; \n"
      "}" ;

   GLuint programObject;

   // Create the program object
   programObject = esLoadProgram ( vShaderStr, fShaderStr );
   userData->offsetLoc = glGetUniformLocation ( programObject, "u_offset" );
   if ( programObject == 0 )
   {
      return GL_FALSE;
   }

   // Store the program object
   userData->programObject = programObject;
   userData->vboIds[0] = 0;
   userData->vboIds[1] = 0;

   glClearColor ( 1.0f, 1.0f, 1.0f, 0.0f );
   return GL_TRUE;
}

// vertices   - pointer to a buffer that contains vertex
//              attribute data
// vtxStride  - stride of attribute data / vertex in bytes
// numIndices - number of indices that make up primitive
//              drawn as triangles
// indices    - pointer to element index buffer.
void DrawPrimitiveWithoutVBOs ( GLfloat *vertices,
                                GLint vtxStride,
                                GLint numIndices,
                                GLushort *indices )
{
   GLfloat   *vtxBuf = vertices;

   // 解除绑定
   glBindBuffer ( GL_ARRAY_BUFFER, 0 );
   glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, 0 );

   glEnableVertexAttribArray ( VERTEX_POS_INDX );
   glEnableVertexAttribArray ( VERTEX_COLOR_INDX );

   glVertexAttribPointer ( VERTEX_POS_INDX, VERTEX_POS_SIZE,
                           GL_FLOAT, GL_FALSE, vtxStride,
                           vtxBuf );
   vtxBuf += VERTEX_POS_SIZE;
   glVertexAttribPointer ( VERTEX_COLOR_INDX,
                           VERTEX_COLOR_SIZE, GL_FLOAT,
                           GL_FALSE, vtxStride, vtxBuf );

   glDrawElements ( GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT,
                    indices );

   glDisableVertexAttribArray ( VERTEX_POS_INDX );
   glDisableVertexAttribArray ( VERTEX_COLOR_INDX );

}

void DrawPrimitiveWithVBOs ( ESContext *esContext,
                             GLint numVertices, GLfloat *vtxBuf,
                             GLint vtxStride, GLint numIndices,
                             GLushort *indices )
{
   UserData *userData = esContext->userData;
   GLuint   offset = 0;

   // vboIds[0] - used to store vertex attribute data
   // vboIds[l] - used to store element indices
   if ( userData->vboIds[0] == 0 && userData->vboIds[1] == 0 )
   {
      // Only allocate on the first draw
      glGenBuffers ( 2, userData->vboIds );

      glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] );
      glBufferData ( GL_ARRAY_BUFFER, vtxStride * numVertices,
                     vtxBuf, GL_STATIC_DRAW );
      glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1] );
      glBufferData ( GL_ELEMENT_ARRAY_BUFFER,
                     sizeof ( GLushort ) * numIndices,
                     indices, GL_STATIC_DRAW );
   }

   glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] );
   glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1] );

   glEnableVertexAttribArray ( VERTEX_POS_INDX );
   glEnableVertexAttribArray ( VERTEX_COLOR_INDX );

   glVertexAttribPointer ( VERTEX_POS_INDX, VERTEX_POS_SIZE,
                           GL_FLOAT, GL_FALSE, vtxStride,
                           ( const void * ) offset );
   offset += VERTEX_POS_SIZE * sizeof ( GLfloat );
   glVertexAttribPointer ( VERTEX_COLOR_INDX,
                           VERTEX_COLOR_SIZE,
                           GL_FLOAT, GL_FALSE, vtxStride,
                           ( const void * ) offset );

   glDrawElements ( GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT,
                    0 );

   glDisableVertexAttribArray ( VERTEX_POS_INDX );
   glDisableVertexAttribArray ( VERTEX_COLOR_INDX );

   glBindBuffer ( GL_ARRAY_BUFFER, 0 );
   glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, 0 );
}

void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;

   // 3 vertices, with (x,y,z) ,(r, g, b, a) per-vertex
   GLfloat vertices[3 * ( VERTEX_POS_SIZE + VERTEX_COLOR_SIZE )] =
   {
      -0.5f,  0.5f, 0.0f,        // v0
      1.0f,  0.0f, 0.0f, 1.0f,  // c0
      -1.0f, -0.5f, 0.0f,        // v1
      0.0f,  1.0f, 0.0f, 1.0f,  // c1
      0.0f, -0.5f, 0.0f,        // v2
      0.0f,  0.0f, 1.0f, 1.0f,  // c2
   };
   // Index buffer data
   GLushort indices[3] = { 0, 1, 2 };

   glViewport ( 0, 0, esContext->width, esContext->height );
   glClear ( GL_COLOR_BUFFER_BIT );
   glUseProgram ( userData->programObject );
   glUniform1f ( userData->offsetLoc, 0.0f );

   DrawPrimitiveWithoutVBOs ( vertices,
                              sizeof ( GLfloat ) * ( VERTEX_POS_SIZE + VERTEX_COLOR_SIZE ),
                              3, indices );

   // Offset the vertex positions so both can be seen
   glUniform1f ( userData->offsetLoc, 1.0f );

   DrawPrimitiveWithVBOs ( esContext, 3, vertices,
                           sizeof ( GLfloat ) * ( VERTEX_POS_SIZE + VERTEX_COLOR_SIZE ),
                           3, indices );
}

void Shutdown ( ESContext *esContext )
{
   UserData *userData = esContext->userData;

   glDeleteProgram ( userData->programObject );
   glDeleteBuffers ( 2, userData->vboIds );
}

int esMain ( ESContext *esContext )
{
   esContext->userData = malloc ( sizeof ( UserData ) );

   esCreateWindow ( esContext, "VertexBufferObjects", 320, 240, ES_WINDOW_RGB );

   if ( !Init ( esContext ) )
   {
      return GL_FALSE;
   }

   esRegisterShutdownFunc ( esContext, Shutdown );
   esRegisterDrawFunc ( esContext, Draw );

   return GL_TRUE;
}

【OpenGL ES】顶点缓冲区对象VBO与顶点数组对象VAO_第3张图片

7、顶点数组对象

前面介绍了加载顶点属性的两种方法,顶点数组和VBO,VBO性能更好,因为VBO减少了CPU与GPU间复制的数据量,不过使用VBO每次也都要调用glBindBuffer、glVertexAttribPointer、glEnableVertexAttribArray等,OpenGL ES 3.0引入了一个新的概念,VAO,即顶点数组对象,可以更快地在顶点数组配置之间切换,使顶点数组的使用更加高效。VAO提供包含在顶点数组和VBO配置之间切换所需要的所有状态的单一对象,实际上,OpenGL ES 3.0中总是有一个活动的VAO,默认值为0,管理VAO涉及如下几个函数。

void glGenVertexArrays(GLsizei n,
    GLuint *arrays);

void glBindVertexArray(GLuint array);

GLboolean glIsVertexArray(GLuint array);

void glDeleteVertexArrays(GLsizei n,
    const GLuint *arrays);

每个VAO都包含一个完整的状态向量,描述所有顶点缓冲区绑定和启用的顶点客户状态,绑定VAO时,它的状态向量提供顶点缓冲区状态的当前设置,用glBindVertexArray绑定VAO后,更改顶点数组状态的后续调用如glBindBuffer、glEnableVertexAttribArray等将影响新的VAO。然后,应用程序可以通过绑定一个已经设置状态的VAO快速地在顶点数组配置之间切换,所有变化可以在一个函数调用中完成,直接看下面的例子,结果同上面示例的彩色三角形。

// VertexArrayObjects.c
typedef struct
{
   // Handle to a program object
   GLuint programObject;
   // VertexBufferObject Ids
   GLuint vboIds[2];
   // VertexArrayObject Id
   GLuint vaoId;
} UserData;

#define VERTEX_POS_SIZE       3 // x, y and z
#define VERTEX_COLOR_SIZE     4 // r, g, b, and a

#define VERTEX_POS_INDX       0
#define VERTEX_COLOR_INDX     1

#define VERTEX_STRIDE         ( sizeof(GLfloat) *     \
                                ( VERTEX_POS_SIZE +    \
                                  VERTEX_COLOR_SIZE ) )

int Init ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   const char vShaderStr[] =
      "#version 300 es                            \n"
      "layout(location = 0) in vec4 a_position;   \n"
      "layout(location = 1) in vec4 a_color;      \n"
      "out vec4 v_color;                          \n"
      "void main()                                \n"
      "{                                          \n"
      "    v_color = a_color;                     \n"
      "    gl_Position = a_position;              \n"
      "}";

   const char fShaderStr[] =
      "#version 300 es            \n"
      "precision mediump float;   \n"
      "in vec4 v_color;           \n"
      "out vec4 o_fragColor;      \n"
      "void main()                \n"
      "{                          \n"
      "    o_fragColor = v_color; \n"
      "}" ;

   GLuint programObject;

   // 3 vertices, with (x,y,z) ,(r, g, b, a) per-vertex
   GLfloat vertices[3 * ( VERTEX_POS_SIZE + VERTEX_COLOR_SIZE )] =
   {
      0.0f,  0.5f, 0.0f,        // v0
      1.0f,  0.0f, 0.0f, 1.0f,  // c0
      -0.5f, -0.5f, 0.0f,        // v1
      0.0f,  1.0f, 0.0f, 1.0f,  // c1
      0.5f, -0.5f, 0.0f,        // v2
      0.0f,  0.0f, 1.0f, 1.0f,  // c2
   };
   // Index buffer data
   GLushort indices[3] = { 0, 1, 2 };

   // Create the program object
   programObject = esLoadProgram ( vShaderStr, fShaderStr );
   if ( programObject == 0 )
   {
      return GL_FALSE;
   }

   // Store the program object
   userData->programObject = programObject;

   // Generate VBO Ids and load the VBOs with data
   glGenBuffers ( 2, userData->vboIds );

   glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] );
   glBufferData ( GL_ARRAY_BUFFER, sizeof ( vertices ),
                  vertices, GL_STATIC_DRAW );
   glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1] );
   glBufferData ( GL_ELEMENT_ARRAY_BUFFER, sizeof ( indices ),
                  indices, GL_STATIC_DRAW );

   // Generate VAO Id
   glGenVertexArrays ( 1, &userData->vaoId );

   // Bind the VAO and then setup the vertex
   // attributes
   glBindVertexArray ( userData->vaoId );

   glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] );
   glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1] );

   glEnableVertexAttribArray ( VERTEX_POS_INDX );
   glEnableVertexAttribArray ( VERTEX_COLOR_INDX );

   glVertexAttribPointer ( VERTEX_POS_INDX, VERTEX_POS_SIZE,
                           GL_FLOAT, GL_FALSE, VERTEX_STRIDE, ( const void * ) 0 );

   glVertexAttribPointer ( VERTEX_COLOR_INDX, VERTEX_COLOR_SIZE,
                           GL_FLOAT, GL_FALSE, VERTEX_STRIDE,
                           ( const void * ) ( VERTEX_POS_SIZE * sizeof ( GLfloat ) ) );

   // Reset to the default VAO
   glBindVertexArray ( 0 );

   glClearColor ( 1.0f, 1.0f, 1.0f, 0.0f );
   return GL_TRUE;
}

void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;

   glViewport ( 0, 0, esContext->width, esContext->height );
   glClear ( GL_COLOR_BUFFER_BIT );
   glUseProgram ( userData->programObject );

   // Bind the VAO
   glBindVertexArray ( userData->vaoId );

   // Draw with the VAO settings
   glDrawElements ( GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, ( const void * ) 0 );

   // Return to the default VAO
   glBindVertexArray ( 0 );
}

void Shutdown ( ESContext *esContext )
{
   UserData *userData = esContext->userData;

   glDeleteProgram ( userData->programObject );
   glDeleteBuffers ( 2, userData->vboIds );
   glDeleteVertexArrays ( 1, &userData->vaoId );
}

int esMain ( ESContext *esContext )
{
   esContext->userData = malloc ( sizeof ( UserData ) );

   esCreateWindow ( esContext, "VertexArrayObjects", 320, 240, ES_WINDOW_RGB );

   if ( !Init ( esContext ) )
   {
      return GL_FALSE;
   }

   esRegisterShutdownFunc ( esContext, Shutdown );
   esRegisterDrawFunc ( esContext, Draw );

   return GL_TRUE;
}

8、映射缓冲区对象

前面介绍了使用glBufferData将数据加载到缓冲区对象,在OpenGL ES 3.0中,应用程序也可以将缓冲区对象的数据映射到应用程序的地址空间,这种方式可以减少应用程序的内存空间,返回GPU存储缓冲区的地址空间的直接指针,避免复制步骤,从而实现更好的更新性能,涉及如下几个函数。

void *glMapBufferRange(GLenum target,
    GLintptr offset,
    GLsizeiptr length,
    GLbitfield access);

GLboolean glUnmapBuffer(GLenum target);

void glFlushMappedBufferRange(GLenum target,
    GLintptr offset,
    GLsizeiptr length);

glMapBufferRange返回指向缓冲区对象的指定范围的数据的指针,供应用程序使用,以读取或者更新缓冲区对象的内容,出错时返回NULL。glUnmapBuffer指示更新已经完成并解除映射,成功时返回GL_TRUE,之前映射的指针不再可以使用,失败时很有可能是数据已被破坏,如在运行时屏幕分辨率变为更大的宽度、高度和像素位数,则映射的内存可能不得不释放。glUnmapBuffer会刷新整个映射范围,如果想只刷新映射范围的一个子区域,使用glFlushMappedBufferRange,此时glMapBufferRange的access可以是GL_MAP_WRITE_BIT和GL_MAP_FLUSH_EXPLICIT_BIT的组合,如果使用了GL_MAP_FLUSH_EXPLICIT_BIT,而又没有明确地通过glFlushMappedBufferRange刷新修改后的区域,它的内容将是未定义的。target同上面提到的glBufferData的target,如GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER等,offset和length一起指定映射范围,access为访问标记,必须至少是GL_MAP_READ_BIT和GL_MAP_WRITE_BIT中的一个,还可以添加其它的标记,具体用法如下:

GL_MAP_READ_BIT // 从缓冲区对象读取
GL_MAP_WRITE_BIT // 修改缓冲区对象
GL_MAP_INVALIDATE_RANGE_BIT // 指定范围的缓冲区内容可能被释
GL_MAP_INVALIDATE_BUFFER_BIT // 整个缓冲区内容可能被释放
GL_MAP_FLUSH_EXPLICIT_BIT // 应用程序需使用glFlushMappedBufferRange刷新对映射范围的操作
GL_MAP_UNSYNCHRONIZED_BIT // 返回缓冲区范围的指针之前不需要等到缓冲区对象上的未决操作

下面是映射缓冲区对象的一个例子,使用了VBO,但缓冲区对象的数据不是通过glBufferData指定,而是先使用glMapBufferRange进行映射,再使用memcpy拷贝数据,结果同上面示例中的彩色三角形。

// MapBuffer.c
typedef struct
{
   // Handle to a program object
   GLuint programObject;

   // VertexBufferObject Ids
   GLuint vboIds[2];
} UserData;

#define VERTEX_POS_SIZE       3 // x, y and z
#define VERTEX_COLOR_SIZE     4 // r, g, b, and a

#define VERTEX_POS_INDX       0
#define VERTEX_COLOR_INDX     1

int Init ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   const char vShaderStr[] =
      "#version 300 es                            \n"
      "layout(location = 0) in vec4 a_position;   \n"
      "layout(location = 1) in vec4 a_color;      \n"
      "out vec4 v_color;                          \n"
      "void main()                                \n"
      "{                                          \n"
      "    v_color = a_color;                     \n"
      "    gl_Position = a_position;              \n"
      "}";

   const char fShaderStr[] =
      "#version 300 es            \n"
      "precision mediump float;   \n"
      "in vec4 v_color;           \n"
      "out vec4 o_fragColor;      \n"
      "void main()                \n"
      "{                          \n"
      "    o_fragColor = v_color; \n"
      "}" ;

   GLuint programObject;

   // Create the program object
   programObject = esLoadProgram ( vShaderStr, fShaderStr );
   if ( programObject == 0 )
   {
      return GL_FALSE;
   }

   // Store the program object
   userData->programObject = programObject;
   userData->vboIds[0] = 0;
   userData->vboIds[1] = 0;

   glClearColor ( 1.0f, 1.0f, 1.0f, 0.0f );
   return GL_TRUE;
}

void DrawPrimitiveWithVBOsMapBuffers ( ESContext *esContext,
                                       GLint numVertices, GLfloat *vtxBuf,
                                       GLint vtxStride, GLint numIndices,
                                       GLushort *indices )
{
   UserData *userData = esContext->userData;
   GLuint   offset = 0;

   // vboIds[0] - used to store vertex attribute data
   // vboIds[l] - used to store element indices
   if ( userData->vboIds[0] == 0 && userData->vboIds[1] == 0 )
   {
      GLfloat *vtxMappedBuf;
      GLushort *idxMappedBuf;

      // Only allocate on the first draw
      glGenBuffers ( 2, userData->vboIds );

      glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] );
      glBufferData ( GL_ARRAY_BUFFER, vtxStride * numVertices,
                     NULL, GL_STATIC_DRAW );

      vtxMappedBuf = ( GLfloat * )
                     glMapBufferRange ( GL_ARRAY_BUFFER, 0, vtxStride * numVertices,
                                        GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT );

      if ( vtxMappedBuf == NULL )
      {
         esLogMessage ( "Error mapping vertex buffer object." );
         return;
      }

      // Copy the data into the mapped buffer
      memcpy ( vtxMappedBuf, vtxBuf, vtxStride * numVertices );

      // Unmap the buffer
      if ( glUnmapBuffer ( GL_ARRAY_BUFFER ) == GL_FALSE )
      {
         esLogMessage ( "Error unmapping array buffer object." );
         return;
      }

      // Map the index buffer
      glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1] );
      glBufferData ( GL_ELEMENT_ARRAY_BUFFER,
                     sizeof ( GLushort ) * numIndices,
                     NULL, GL_STATIC_DRAW );
      idxMappedBuf = ( GLushort * )
                     glMapBufferRange ( GL_ELEMENT_ARRAY_BUFFER, 0, sizeof ( GLushort ) * numIndices,
                                        GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT );

      if ( idxMappedBuf == NULL )
      {
         esLogMessage ( "Error mapping element array buffer object." );
         return;
      }

      // Copy the data into the mapped buffer
      memcpy ( idxMappedBuf, indices, sizeof ( GLushort ) * numIndices );

      // Unmap the buffer
      if ( glUnmapBuffer ( GL_ELEMENT_ARRAY_BUFFER ) == GL_FALSE )
      {
         esLogMessage ( "Error unmapping element array buffer object." );
         return;
      }
   }

   glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] );
   glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1] );

   glEnableVertexAttribArray ( VERTEX_POS_INDX );
   glEnableVertexAttribArray ( VERTEX_COLOR_INDX );

   glVertexAttribPointer ( VERTEX_POS_INDX, VERTEX_POS_SIZE,
                           GL_FLOAT, GL_FALSE, vtxStride,
                           ( const void * ) offset );

   offset += VERTEX_POS_SIZE * sizeof ( GLfloat );
   glVertexAttribPointer ( VERTEX_COLOR_INDX,
                           VERTEX_COLOR_SIZE,
                           GL_FLOAT, GL_FALSE, vtxStride,
                           ( const void * ) offset );

   glDrawElements ( GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT,
                    0 );

   glDisableVertexAttribArray ( VERTEX_POS_INDX );
   glDisableVertexAttribArray ( VERTEX_COLOR_INDX );

   glBindBuffer ( GL_ARRAY_BUFFER, 0 );
   glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, 0 );
}

void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;

   // 3 vertices, with (x,y,z) ,(r, g, b, a) per-vertex
   GLfloat vertices[3 * ( VERTEX_POS_SIZE + VERTEX_COLOR_SIZE )] =
   {
      0.0f,  0.5f, 0.0f,        // v0
      1.0f,  0.0f, 0.0f, 1.0f,  // c0
      -0.5f, -0.5f, 0.0f,        // v1
      0.0f,  1.0f, 0.0f, 1.0f,  // c1
      0.5f, -0.5f, 0.0f,        // v2
      0.0f,  0.0f, 1.0f, 1.0f,  // c2
   };
   // Index buffer data
   GLushort indices[3] = { 0, 1, 2 };

   glViewport ( 0, 0, esContext->width, esContext->height );
   glClear ( GL_COLOR_BUFFER_BIT );
   glUseProgram ( userData->programObject );

   DrawPrimitiveWithVBOsMapBuffers ( esContext, 3, vertices,
                                     sizeof ( GLfloat ) * ( VERTEX_POS_SIZE + VERTEX_COLOR_SIZE ),
                                     3, indices );
}

void Shutdown ( ESContext *esContext )
{
   UserData *userData = esContext->userData;

   glDeleteProgram ( userData->programObject );
   glDeleteBuffers ( 2, userData->vboIds );
}

int esMain ( ESContext *esContext )
{
   esContext->userData = malloc ( sizeof ( UserData ) );

   esCreateWindow ( esContext, "MapBuffers", 320, 240, ES_WINDOW_RGB );

   if ( !Init ( esContext ) )
   {
      return GL_FALSE;
   }

   esRegisterShutdownFunc ( esContext, Shutdown );
   esRegisterDrawFunc ( esContext, Draw );

   return GL_TRUE;
}

9、复制缓冲区对象

最后介绍一个直接复制缓冲区对象的技术,是OpenGL ES 3.0提供的,直接从一个缓冲取对象的内容复制到另一个缓冲区对象,函数如下:

void glCopyBufferSubData(GLenum readtarget,
    GLenum writetarget,
    GLintptr readoffset,
    GLintptr writeoffset,
    GLsizeiptr size);

你可能感兴趣的:(图形图像)