OpenGL ES 框架详细解析(十) —— 使用顶点数据的最佳做法

版本记录

版本号 时间
V1.0 2017.10.02

前言

OpenGL ES是一个强大的图形库,是跨平台的图形API,属于OpenGL的一个简化版本。iOS系统可以利用OpenGL ES将图像数据直接送入到GPU进行渲染,这样避免了从CPU进行计算再送到显卡渲染带来的性能的高消耗,能带来来更好的视频效果和用户体验。接下来几篇就介绍下iOS 系统的 OpenGL ES框架。感兴趣的可以看上面几篇。
1. OpenGL ES 框架详细解析(一) —— 基本概览
2. OpenGL ES 框架详细解析(二) —— 关于OpenGL ES
3. OpenGL ES 框架详细解析(三) —— 构建用于iOS的OpenGL ES应用程序的清单
4. OpenGL ES 框架详细解析(四) —— 配置OpenGL ES的上下文
5. OpenGL ES 框架详细解析(五) —— 使用OpenGL ES和GLKit进行绘制
6. OpenGL ES 框架详细解析(六) —— 绘制到其他渲染目的地
7. OpenGL ES 框架详细解析(七) —— 多任务,高分辨率和其他iOS功能
8. OpenGL ES 框架详细解析(八) —— OpenGL ES 设计指南
9. OpenGL ES 框架详细解析(九) —— 调整您的OpenGL ES应用程序

Best Practices for Working with Vertex Data - 使用顶点数据的最佳做法

要使用OpenGL ES渲染帧,您的应用程序会配置图形管道并提交要绘制的图形基元。 在某些应用程序中,所有原语都使用相同的流水线配置绘制; 其他应用程序可以使用不同的技术来渲染框架的不同元素。 但是,无论您在应用程序中使用哪些基元,或者如何配置管道,您的应用程序都会为OpenGL ES提供顶点。 本章提供了顶点数据的刷新,并针对如何有效地处理顶点数据的目标建议进行跟踪。

顶点由一个或多个属性组成,如位置,颜色,正常或纹理坐标。 OpenGL ES 2.0或3.0应用程序可以自由定义自己的属性; 顶点数据中的每个属性对应于作为顶点着色器的输入的属性变量。 OpenGL 1.1应用程序使用固定功能管道定义的属性。

您将属性定义为由一到四个组件组成的向量。 属性中的所有组件共享一个公共数据类型。 例如,颜色可以被定义为四个GLubyte组件(红色,绿色,蓝色,alpha)。 当属性加载到着色器变量中时,应用程序数据中未提供的任何组件都将使用OpenGL ES的默认值填充。 最后一个组件填充1,其他未指定的组件填充0,如图8-1所示。

Figure 8-1 Conversion of attribute data to shader variables

您的应用程序可以将属性配置为常量,这意味着对于作为draw命令的一部分而提交的所有顶点使用相同的值,也可以是数组,这意味着每个顶点都是该属性的值。 当您的应用程序在OpenGL ES中调用一个函数来绘制一组顶点时,顶点数据将从应用程序复制到图形硬件。 图形硬件比作用于顶点数据,处理着色器中的每个顶点,组装原语并将其光栅化到帧缓冲区中。 OpenGL ES的一个优点在于,它将一组功能标准化,将顶点数据提交给OpenGL ES,从而消除OpenGL提供的更老且更低效的机制。

必须提交大量图元以呈现帧的应用程序需要仔细管理其顶点数据以及如何将其提供给OpenGL ES。 本章中描述的做法可以归纳为几个基本原则:

  • 减小顶点数据的大小。
  • 减少在OpenGL ES可以将顶点数据传输到图形硬件之前必须进行的预处理。
  • 减少将顶点数据复制到图形硬件的时间。
  • 减少对每个顶点执行的计算。

Simplify Your Models - 简化您的模型

基于iOS设备的图形硬件非常强大,但它显示的图像通常非常小。 您不需要非常复杂的模型来在iOS上展示令人信服的图形。 减少用于绘制模型的顶点数量直接减少顶点数据的大小和对顶点数据执行的计算。

您可以使用以下一些技术来降低模型的复杂性:

  • 以不同级别的细节提供模型的多个版本,并根据对象与相机的距离和显示尺寸在运行时选择适当的模型。
  • 使用纹理消除对某些顶点信息的需求。 例如,凹凸贴图可以用于在不添加更多顶点数据的情况下向模型添加细节。
  • 一些模型添加顶点以改善照明细节或渲染质量。 通常在光栅化阶段对每个顶点计算值并在三角形内插插值时完成此操作。 例如,如果您将聚光灯指向三角形的中心,则其效果可能不会被忽视,因为聚光灯中最亮的部分不会指向顶点。 通过添加顶点,您可以提供额外的插值点,代价是增加顶点数据的大小和对模型执行的计算。 不添加额外的顶点,而是考虑将计算移动到管道的片段阶段中:
    • 如果您的应用程序使用OpenGL ES 2.0或更高版本,则应用程序会在顶点着色器中执行计算,并将其分配给变量。 变化值由图形硬件插值,并作为输入传递给片段着色器。 相反,将计算的输入分配给变量,并在片段着色器中执行计算。 这样做会将从每顶点成本执行该计算的成本更改为每个片段成本,从而降低顶点阶段的压力,并减少管道片段阶段上的压力。 当您的应用程序在顶点处理中被阻塞时,执行此操作,计算价格便宜,并且可以通过更改显着减少顶点计数。
    • 如果您的应用程序使用OpenGL ES 1.1,您可以使用DOT3照明来执行每个片段的照明。 您可以通过添加凹凸贴图纹理来保存正常信息,并使用GL_DOT3_RGB模式的纹理组合操作应用凹凸贴图。

Avoid Storing Constants in Attribute Arrays - 避免在属性数组中存储常量

如果您的模型包含使用在整个模型中保持不变的数据的属性,则不要为每个顶点复制该数据。 OpenGL ES 2.0和3.0应用程序可以设置不变的顶点属性,也可以使用均匀的着色器值来保存该值。 OpenGL ES 1.1应用程序应该使用诸如glColor4ubglTexCoord2f的每顶点属性函数。


Use the Smallest Acceptable Types for Attributes - 使用最小可接受类型的属性

指定每个属性组件的大小时,请选择提供可接受结果的最小数据类型。 以下是一些准则:

  • 使用四个无符号字节组件(GL_UNSIGNED_BYTE)指定顶点颜色。
  • 使用2或4个无符号字节(GL_UNSIGNED_BYTE)或无符号短(GL_UNSIGNED_SHORT)指定纹理坐标。 不要将多组纹理坐标包装到单个属性中。
  • 避免使用OpenGL ES GL_FIXED数据类型。 它需要与GL_FLOAT相同的内存量,但提供较小的值范围。 所有iOS设备都支持硬件浮点数,因此可以更快地处理浮点值。
  • OpenGL ES 3.0上下文支持更广泛的小数据类型,例如GL_HALF_FLOATGL_INT_2_10_10_10_REV。 这些通常为诸如法线等属性提供足够的精度,占用的空间小于GL_FLOAT。

如果指定较小的组件,请确保重新排列顶点格式,以避免顶点数据不对齐。 请参阅Avoid Misaligned Vertex Data


Use Interleaved Vertex Data - 使用交错顶点数据

您可以将顶点数据指定为一系列数组(也称为数组结构),也可以将数组指定为每个元素包含多个属性(结构数组)的数组。 iOS上的首选格式是具有单个交错顶点格式的结构数组。 交错数据为每个顶点提供更好的内存位置。

Figure 8-2 Interleaved memory structures place all data for a vertex together in memory

此规则的一个例外是当您的应用程序需要以不同于其余顶点数据的速率更新某些顶点数据时,或者如果某些数据可以在两个或多个模型之间共享。 在任一情况下,您可能需要将属性数据分成两个或多个结构。

Figure 8-3 Use multiple vertex structures when some data is used differently

Avoid Misaligned Vertex Data - 避免不对齐的顶点数据

当您设计顶点结构时,将每个属性的开始对齐为一个偏移量,该偏移量是其组件大小或4个字节的倍数,以较大者为准。 当属性不对齐时,iOS必须在将数据传递到图形硬件之前执行其他处理。

在图8-4中,位置和正常数据分别定义为三个短整数,总共六个字节。 正常数据从偏移量6开始,这是本机大小(2字节)的倍数,但不是4字节的倍数。 如果这个顶点数据被提交到iOS,iOS将不得不花费更多的时间在将数据传递到硬件之前进行复制和对齐。 要解决这个问题,在每个属性之后明确添加两个字节的填充。

Figure 8-4 Align Vertex Data to avoid additional processing

Use Triangle Strips to Batch Vertex Data - 使用三角条绑定顶点数据

使用三角形条可以显着减少OpenGL ES必须在模型上执行的顶点计算次数。 在图8-5的左侧,使用总共九个顶点指定三个三角形。 C,E和G实际上指定相同的顶点! 通过将数据指定为三角形条,可以将顶点数从9个减少到5个。

Figure 8-5 Triangle strip

有时,您的应用程序可以将多个三角形条组合成一个更大的三角形条。 所有条带必须共享相同的渲染要求。 意即:

  • 您必须使用相同的着色器来绘制所有的三角形条。
  • 您必须能够渲染所有的三角形条,而不会改变任何OpenGL状态。
  • 三角形条必须共享相同的顶点属性。

要合并两个三角形条,请复制第一个条带的最后一个顶点和第二个条带的第一个顶点,如图8-6所示。 当该条提交到OpenGL ES时,三角形DEE,EEF,EFF和FFG被认为是退化的,不进行处理或光栅化。

Figure 8-6 Use degenerate triangles to merge triangle strips

为获得最佳性能,您的模型应作为单个索引三角形条提交。 为了避免在顶点缓冲区中多次指定相同顶点的数据,请使用单独的索引缓冲区,并使用glDrawElements函数绘制三角形条(如果适用,则使用glDrawElementsInstanceglDrawRangeElements函数)。

在OpenGL ES 3.0中,您可以使用原始重新启动功能来合并三角形条,而不使用简并三角形。 当启用此功能时,OpenGL ES将索引缓冲区中最大可能的值视为完成一个三角形条并启动另一个三角形条的命令。 List 8-1显示了这种方法。

// Using primitive restart in OpenGL ES 3.0

// Prepare index buffer data (not shown: vertex buffer data, loading vertex and index buffers)
GLushort indexData[11] = {
    0, 1, 2, 3, 4,    // triangle strip ABCDE
    0xFFFF,           // primitive restart index (largest possible GLushort value)
    5, 6, 7, 8, 9,    // triangle strip FGHIJ
};
 
// Draw triangle strips
glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
glDrawElements(GL_TRIANGLE_STRIP, 11, GL_UNSIGNED_SHORT, 0);

在可能的情况下,对顶点和索引数据进行排序,因此共享共同顶点的三角形在三角形条中相互接近。 图形硬件通常会缓存最近的顶点计算,以避免重新计算顶点。


Use Vertex Buffer Objects to Manage Copying Vertex Data - 使用顶点缓冲区对象来管理复制顶点数据

Listing 8-2提供了一个简单应用程序可用于向顶点着色器提供位置和颜色数据的函数。 它启用两个属性并配置每个属性以指向交错顶点结构。 最后,它调用glDrawElements函数将模型渲染为单个三角形条。

// Listing 8-2  Submitting vertex data to a shader program

typedef struct _vertexStruct
{
    GLfloat position[2];
    GLubyte color[4];
} vertexStruct;
 
void DrawModel()
{
    const vertexStruct vertices[] = {...};
    const GLubyte indices[] = {...};
 
    glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE,
        sizeof(vertexStruct), &vertices[0].position);
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE,
        sizeof(vertexStruct), &vertices[0].color);
    glEnableVertexAttribArray(GLKVertexAttribColor);
 
    glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, indices);
}

此代码有效,但效率低下。 每次调用DrawModel时,将索引和顶点数据复制到OpenGL ES中,并传输到图形硬件。 如果顶点数据在调用之间没有变化,这些不必要的副本可能会影响性能。 为避免不必要的副本,您的应用程序应将其顶点数据存储在顶点缓冲对象(VBO)中。 由于OpenGL ES拥有顶点缓冲区对象的内存,因此可将缓冲区存储在图形硬件更易于访问的内存中,或将数据预处理为图形硬件的首选格式。

注意:在OpenGL ES 3.0中使用顶点数组对象时,还必须使用顶点缓冲对象。

Listing 8-3创建了一对顶点缓冲对象,一个用于保存顶点数据,另一个用于条带的索引。 在每种情况下,代码生成一个新对象,将其绑定为当前缓冲区,并填充缓冲区。 当应用程序初始化时,将调用CreateVertexBuffers

// Listing 8-3  Creating a vertex buffer object

GLuint    vertexBuffer;
GLuint    indexBuffer;
 
void CreateVertexBuffers()
{
 
    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 
    glGenBuffers(1, &indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
}

Listing 8-4Listing8-2修改为使用顶点缓冲对象。 Listing 8-4的主要区别在于,glVertexAttribPointer函数的参数不再指向顶点数组。 相反,每个都是顶点缓冲区对象的偏移量。

// Listing 8-4 Drawing with a vertex buffer object

void DrawModelUsingVertexBuffers()

{

glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);

glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE,

sizeof(vertexStruct), (void *)offsetof(vertexStruct, position));

glEnableVertexAttribArray(GLKVertexAttribPosition);

glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE,

sizeof(vertexStruct), (void *)offsetof(vertexStruct, color));

glEnableVertexAttribArray(GLKVertexAttribColor);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);

glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, (void*)0);

}

1. Buffer Usage Hints - 缓冲区使用提示

上一个例子初始化了顶点缓冲区一次,之后从不改变它的内容。 您可以更改顶点缓冲区的内容。 顶点缓冲对象设计的关键部分是应用程序可以通知OpenGL ES如何使用缓冲区中存储的数据。 OpenGL ES实现可以使用这个提示来改变它用于存储顶点数据的策略。 在Listing 8-3中,对glBufferData函数的每次调用都提供了一个使用提示作为最后一个参数。 将GL_STATIC_DRAW传递给glBufferData告诉OpenGL ES,这两个缓冲区的内容从来不会改变,这给了OpenGL ES更多机会来优化数据的存储方式和位置。

OpenGL ES规范定义了以下用例:

  • GL_STATIC_DRAW用于渲染多次的顶点缓冲区,其内容被指定一次,从不改变。
  • GL_DYNAMIC_DRAW用于渲染多次的顶点缓冲区,其内容在渲染循环期间发生变化。
  • GL_STREAM_DRAW用于渲染少量次数然后被丢弃的顶点缓冲区。

在iOS中,GL_DYNAMIC_DRAWGL_STREAM_DRAW是等效的。 您可以使用glBufferSubData函数更新缓冲区内容,但这样做会导致性能损失,因为它会刷新命令缓冲区并等待所有命令完成。 双重或三重缓冲可以降低这种性能成本。 (请参阅Use Double Buffering to Avoid Resource Conflicts。)为获得更好的性能,请使用OpenGL ES 3.0中的glMapBufferRange函数或OpenGL ES 2.0或1.1中的EXT_map_buffer_range扩展提供的相应函数。

如果您的顶点格式中的不同属性需要不同的使用模式,则将顶点数据分割成多个结构,并为共享常见使用特征的每个属性集分配一个单独的顶点缓冲对象。 Listing 8-5修改了上一个示例以使用单独的缓冲区来保存颜色数据。 通过使用GL_DYNAMIC_DRAW提示分配颜色缓冲区,OpenGL ES可以分配该缓冲区,使您的应用程序保持合理的性能。

// **Listing 8-5**  Drawing a model with multiple vertex buffer objects

typedef struct _vertexStatic

{

GLfloat position[2];

} vertexStatic;

typedef struct _vertexDynamic

{

GLubyte color[4];

} vertexDynamic;

// Separate buffers for static and dynamic data.

GLuint staticBuffer;

GLuint dynamicBuffer;

GLuint indexBuffer;

const vertexStatic staticVertexData[] = {...};

vertexDynamic dynamicVertexData[] = {...};

const GLubyte indices[] = {...};

void CreateBuffers()

{

// Static position data

glGenBuffers(1, &staticBuffer);

glBindBuffer(GL_ARRAY_BUFFER, staticBuffer);

glBufferData(GL_ARRAY_BUFFER, sizeof(staticVertexData), staticVertexData, GL_STATIC_DRAW);

// Dynamic color data

// While not shown here, the expectation is that the data in this buffer changes between frames.

glGenBuffers(1, &dynamicBuffer);

glBindBuffer(GL_ARRAY_BUFFER, dynamicBuffer);

glBufferData(GL_ARRAY_BUFFER, sizeof(dynamicVertexData), dynamicVertexData, GL_DYNAMIC_DRAW);

// Static index data

glGenBuffers(1, &indexBuffer);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);

glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

}

void DrawModelUsingMultipleVertexBuffers()

{

glBindBuffer(GL_ARRAY_BUFFER, staticBuffer);

glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE,

sizeof(vertexStruct), (void *)offsetof(vertexStruct, position));

glEnableVertexAttribArray(GLKVertexAttribPosition);

glBindBuffer(GL_ARRAY_BUFFER, dynamicBuffer);

glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE,

sizeof(vertexStruct), (void *)offsetof(vertexStruct, color));

glEnableVertexAttribArray(GLKVertexAttribColor);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);

glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, (void*)0);

}

Consolidate Vertex Array State Changes Using Vertex Array Objects - 使用顶点数组对象合并顶点数组状态更改

仔细看看Listing 8-5中的DrawModelUsingMultipleVertexBuffers函数。 它支持许多属性,绑定多个顶点缓冲对象,并配置属性以指向缓冲区。 所有这些初始化代码基本上是静态的; 没有一个参数从帧到帧变化。 如果每次应用程序渲染帧时都调用此函数,则会重新配置图形管道,导致了很多不必要的开销。 如果应用程序绘制了许多不同类型的模型,则重新配置管道可能会成为瓶颈。 而是使用顶点数组对象来存储一个完整的属性配置。 顶点数组对象是OpenGL ES 3.0核心规范的一部分,可通过OES_vertex_array_object扩展在OpenGL ES 2.0和1.1中提供。

Figure 8-7显示了具有两个顶点数组对象的示例配置。 每个配置独立于另一个; 每个顶点数组对象可以引用一组不同的顶点属性,它们可以存储在同一个顶点缓冲区对象中,也可以跨越多个顶点缓冲对象。

Figure 8-7 Vertex array object configuration

Listing 8-6提供了用于配置上面显示的第一个顶点数组对象的代码。 它为新的顶点数组对象生成一个标识符,然后将顶点数组对象绑定到上下文。 之后,它会调用配置顶点属性,如果代码没有使用顶点数组对象。 配置存储到绑定的顶点数组对象而不是上下文。

// Listing 8-6  Configuring a vertex array object

void ConfigureVertexArrayObject()
{
    // Create and bind the vertex array object.
    glGenVertexArrays(1,&vao1);
    glBindVertexArray(vao1);
         // Configure the attributes in the VAO.
    glBindBuffer(GL_ARRAY_BUFFER, vbo1);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE,
        sizeof(staticFmt), (void*)offsetof(staticFmt,position));
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_UNSIGNED_SHORT, GL_TRUE,
        sizeof(staticFmt), (void*)offsetof(staticFmt,texcoord));
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
    glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE,
        sizeof(staticFmt), (void*)offsetof(staticFmt,normal));
    glEnableVertexAttribArray(GLKVertexAttribNormal);
 
    glBindBuffer(GL_ARRAY_BUFFER, vbo2);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE,
        sizeof(dynamicFmt), (void*)offsetof(dynamicFmt,color));
    glEnableVertexAttribArray(GLKVertexAttribColor);
 
    // Bind back to the default state.
    glBindBuffer(GL_ARRAY_BUFFER,0);
    glBindVertexArray(0); 
}

要绘制,代码绑定顶点数组对象,然后像以前一样提交绘图命令。

注意:在OpenGL ES 3.0中,不允许顶点数组数据的客户端存储 - 顶点数组对象必须使用顶点缓冲对象。

为了获得最佳性能,您的应用程序应该配置每个顶点数组对象一次,而不要在运行时更改它。 如果您需要在每个帧中更改顶点数组对象,则可以创建多个顶点数组对象。 例如,使用双缓冲的应用程序可能为奇数帧配置一组顶点数组对象,而为偶数帧配置第二组。 每组顶点数组对象将指向用于呈现该帧的顶点缓冲区对象。 当顶点数组对象的配置不会改变时,OpenGL ES可以缓存有关顶点格式的信息,并改进如何处理这些顶点属性。


Map Buffers into Client Memory for Fast Updates - 将缓冲区映射到客户端内存中,实现快速更新

OpenGL ES应用程序设计中更具挑战性的问题之一是使用动态资源,特别是如果您的顶点数据需要更改每个帧。 高效地平衡CPU和GPU之间的并行性需要仔细地管理应用程序内存空间和OpenGL ES内存之间的数据传输。 传统技术,例如使用glBufferSubData功能,可以降低性能,因为它们会迫使GPU在传输数据时等待,即使可能会从同一缓冲区其他位置的数据渲染。

例如,您可能需要修改顶点缓冲区并在每次通过高帧率渲染循环时绘制其内容。 渲染的最后一帧的绘图命令可能仍在使用GPU,而CPU正在尝试访问缓冲存储器以准备绘制下一帧,导致缓冲区更新调用阻止进一步的CPU工作,直到GPU完成。 您可以通过手动将CPU和GPU访问同步到缓冲区来提高这种情况下的性能。

glMapBufferRange函数提供了一种更有效的方式来动态更新顶点缓冲区。 (此函数可在OpenGL ES 3.0中使用,并通过OpenGL ES 1.1和2.0中的EXT_map_buffer_range扩展名使用)。使用此函数可以获取指向OpenGL ES内存区域的指针,然后可以使用该指针来写入新数据。glMapBufferRange函数允许将缓冲区数据存储的任何子范围映射到客户端内存中。 它还支持使用该功能与OpenGL同步对象时进行异步缓冲区修改的提示,如Listing 8-7所示。

// Listing 8-7  Dynamically updating a vertex buffer with manual synchronization

GLsync fence;
GLboolean UpdateAndDraw(GLuint vbo, GLuint offset, GLuint length, void *data) {
    GLboolean success;
 
    // Bind and map buffer.
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    void *old_data = glMapBufferRange(GL_ARRAY_BUFFER, offset, length,
        GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT |
        GL_MAP_UNSYNCHRONIZED_BIT );
 
    // Wait for fence (set below) before modifying buffer.
    glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT,
        GL_TIMEOUT_IGNORED);
 
    // Modify buffer, flush, and unmap.
    memcpy(old_data, data, length);
    glFlushMappedBufferRange(GL_ARRAY_BUFFER, offset, length);
    success = glUnmapBuffer(GL_ARRAY_BUFFER);
 
    // Issue other OpenGL ES commands that use other ranges of the VBO's data.
 
    // Issue draw commands that use this range of the VBO's data.
    DrawMyVBO(vbo);
 
    // Create a fence that the next frame will wait for.
    fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
    return success;
}

此示例中的UpdateAndDraw函数使用glFenceSync函数在提交使用特定缓冲区对象的绘图命令之后立即建立同步点或栅栏。 然后使用glClientWaitSync函数(在下一遍遍历渲染循环中)来检查该同步点,然后再修改缓冲区对象。 如果这些绘图命令在渲染循环回来之前在GPU上完成执行,则CPU执行不会阻塞,UpdateAndDraw函数继续修改缓冲区并绘制下一个帧。 如果GPU尚未完成这些命令,则glClientWaitSync函数将阻止进一步的CPU执行,直到GPU到达栅栏。 通过手动将同步点放置在代码周围的潜在资源冲突周围,可以最大限度地减少CPU等待GPU的时间。

后记

未完,待续~~~

推荐阅读更多精彩内容