OpenGL ES for iOS - 8

作者: 孙健会员 | 来源:发表于2017-05-18 18:53 被阅读255次

    使用顶点数据的最佳做法

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

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

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

    8-1.png

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

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

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

    简化你的模型

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

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

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

    避免在属性数组中存储常量

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

    使用最小可接受类型的属性

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

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

    使用交错顶点数据

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

    8-2.png

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

    8-3.png

    避免不对齐的顶点数据

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

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

    8-4.png

    使用三角条批量顶点数据

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

    8-5.png

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

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

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

    8-6.png

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

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

    // 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);
    
    Where possible, sort vertex and index data so triangles that share common vertices are drawn reasonably close to each other in the triangle strip. Graphics hardware often caches recent vertex calculations to avoid recalculating a vertex.
    

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

    使用顶点缓冲区对象来管理复制顶点数据

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

    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拥有顶点缓冲区对象的内存,因此可将缓冲区存储在图形硬件更易于访问的内存中,或将数据预处理为图形硬件的首选格式

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

    清单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);
     
    }
    

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

    Listing 8-4用顶点缓冲区对象绘制

    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);
    }
    

    缓冲区使用提示

    上一个例子初始化了顶点缓冲区一次,之后从不改变它的内容。您可以更改顶点缓冲区的内容。顶点缓冲对象设计的关键部分是应用程序可以通知OpenGL ES如何使用缓冲区中存储的数据。 OpenGL ES实现可以使用这个提示来改变它用于存储顶点数据的策略。在清单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_DRAW和GL_STREAM_DRAW是等效的。您可以使用glBufferSubData函数更新缓冲区内容,但这样做会导致性能损失,因为它会刷新命令缓冲区并等待所有命令完成。双重或三重缓冲可以降低这种性能成本。 (请参阅使用双缓冲来避免资源冲突。)为获得更好的性能,请在OpenGL ES 3.0中使用glMapBufferRange函数或OpenGL ES 2.0或1.1中由EXT_map_buffer_range扩展提供的相应函数。

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

    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);
    }
    

    使用顶点数组对象合并顶点数组状态更改

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

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

    8-7.png

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

    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可以缓存有关顶点格式的信息,并改进如何处理这些顶点属性

    将缓冲区映射到客户端内存以实现快速更新

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

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

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

    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的时间

    相关文章

      网友评论

        本文标题:OpenGL ES for iOS - 8

        本文链接:https://www.haomeiwen.com/subject/baocxxtx.html