6.2、动画化顶点数据

例子OpenGLES_Ch6_2会随着时间修改网格顶点属性来模拟一个随风飘扬的旗帜。图6-2是显示结果。网格动画产生的动态效果有时是必需的,但是在3D图形中网格动画并不常见,因为它需要在GPU控制的内存和CPU控制的内存之间频繁地复制顶点数据。坐标系的变化可以产生很多与顶点属性变化相同的效果,但是却不用复制所有的数据。例如,动画3D人物常常是由定义人物外形的复杂非矩形网格构建而成。当人物走、跑、跳 时,网格内的顶点会保持不变,但是变换矩阵会让其看起来就像是人物的四肢通过关节和肌肉在移动,并且皮肤会相应拉伸。第7章会详细讲解模型。

图 6-2

当使用网格动画时,通过优化来减少需要复制的数据量通常意义非凡。为GPU提供三角形几何图形数据的最简单方法是指定所有三角形的三个顶点。但是,当绘制共边三角形时可以使用一个叫做三角形带(triangle strip)的优化方法。一个三角形带结合了两个或者更多个相互连接的三角形。在这个带中的第一个三角形是由前三个顶点定义的,接下来的每个三角形会与带中上一个三角形共用两个顶点。图6-3显示了怎么用标识为A和B的两个三角形绘制一个正方形,但却只用标识为0、1、2和3的四个顶点。在图6-3中的两个三角形共用编号为1、2的顶点。

图 6-3

共享顶点减少了保存几何图形信息所需要的内存容量,减少了GPU必须处理的顶点的总数量。为一个场景中的所有三角形分别指定3个顶点需要(3XN)个顶点(N是三角形的数量)。而使用三角形带绘制相同的场景只需要(2XN)个顶点。

三角形和三角形带中的顶点顺序是不同的。OpenGL ES使用顶点顺序来决定每个三角形的哪一面是前面。在一个3D场景中,每个三角形都有一个前面和一个后面。大部分情况下,你能够看到哪一面是没有关系的,但是OpenGLES可以通过使用背面剔除(backface culling)模式来设置跳过一面或者另一面的渲染,背面剔除会在第7章介绍。按照惯例,首先根据第4章提到的右手原则,法向量指向前面。对于图6-3中的三角形A来 说,法向量是使用顶点(0, 1, 2)并按照逆时针顺序计算出来的。如果顶点是按相反的顺序(0,2,1)处理的,那么结果法向量会指向相反的方向。

如果图6-3中的正方形是由两个不同的三角形绘制的,那么顶点的正确顺序是三角形(0,1,2)、 三角形(2,1,3)。 这样,两个三角形都是按照逆时针顺序处理的。但是,如果在图6-3中的正方形是用一个带有4个顶点的三角形带绘制的,即三角形带(0, 1,2,3),那么OpenGL ES会以一个非常有意义的顺序解释三角形带的顶点:第一个三角形是按(0,1, 2)的顺序渲染的,但是第二个三角形并不是按照(1, 2,3)的顺序,而是按照(2,1, 3)的顺序渲染的。图6-4 显示了一个用来绘制五个相连的三角形的更加复杂的三角形带。在图6-4中的顶点是按照三角带(0,1,2,3,4,5,6)的顺序提交给 OpenGL ES的,但是这些顶点是按照(0, 1, 2),(2,1,3),(2,3,4),(4,3,5),(4,5,6)的顺序处理的,这会赋予所有的五个三角形一个逆时针的顶点顺序。

图6-4中的编号指出了顶点在一个顶点图6-4 一个三角形带形成了一个复杂的图形缓存中的存储顺序。绘制图6-4中显示的三角形带是非常简单的,只需要绑定顶点数组缓存到当前OpenGL ES上下文并调用glDrawArrays(GL_TRIANGLE_STRIP, 0, 7)函数即可。

图 6-4

6.2.1、使用索引顶点

绘制类似在图6-5中显示的网格有一个技巧。当一个网格有多个列时,单独调用glDrawArrays(GL_TRIANGLE_STRIP, ...)而不复制顶点是不可能的。有必要用单独三角形带来绘制每一列。但是在这个网格中的每列与上一列共用一半的顶点数据。例如,在图6-5中的标识为5的顶点是在第一个列中标识为A、B和C的三角形的一 部分,也是在第二列中标识为D、E和F的三角形的一部分。用一个单独的三角形带绘制每个列需要复制在内存中的两个列之间的共享顶点。换句话说,同样的顶点数据既要保存在用来绘制第一个列的顶点缓存中,又要保存在绘制第二列的顶点缓存中。

索引顶点提供了一个优化,这个优化可以消除奢侈的顶点数据复制。当使用索引顶点时,每个顶点只需要在内存中保存一次,无论有多少个三角形使用了这个顶点。

为了应用索引顶点,需要创建一个叫做元素数组缓存(element array buffer)的新缓存来保存顶点索引。在元素数组缓存中的每个值都通过顶点的位置确定了在一个单独的顶点属性数组缓存中的一个顶点。索引0标识在顶点属性数组缓存中的第一个顶点。索引5标识在顶点属性数组缓存中的第六个顶点(从0开始计数),以此类推。元素数组缓存的创建与其他缓存非常类似,参见下面的伪代码。

glGenBuffers(1, &indexBuffer);
glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, <size>, <address>, <usage_ hint>);

按照用来绘制三角形带的参考顶点的顺序保存元素数组缓存内的顶点索引值。相同的索引值可以通过在元素数组缓存中重复这个值来使用任意次。

使用一个元素数组缓存绘制与使用glDrawArrays()函数绘制是相似的,不过使用的是glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices) 函数作为替代。glDrawElements()函数的mode参数所支持的值包括:GLPOINTS、GL_LINE_STRIP、GL_LINE_LOOP、GL_LINES、GL_TRIANGLE_STRIP、 GL_TRIANGLE_FAN和GL_TRIANGLES。例子OpenGLES_Ch6_2使用的是GL_RTIANGLE_STRIP模式。网址http://www.khronos.org/opengles/documentation/opengles1_0/html/glDrawElements.html上有所有模式的帮助文档。glDrawElements() 的第二个参数指定了要使用的索引的数量。对于三角形带来说,索引的数量等于要绘制的三角形的数量加上2。第三个参数指定了索引值的类型,必须是GL_UNSIGNED_BYTE 或者GL_UNSIGNED_SHORT。注意,当使用GL_UNSIGNED_SHORT类型时最多可以引用65536个独一无二的索引,当使用GL_UNSIGNED_BYTE时只能引用256个独一无二的索引。glDrawElements()的最后一个 参数是一个指向包含这些索引的内存的指针。当使用一个元素数组缓存时,第四个参数实际上是一个元素数组缓存的字节偏移量,并且当起始于第一个索引时,其值应该等于NULL.

使用glDrawElements()函数绘制网格有一个技巧:为了在一次glDrawElements()函数调用中绘制多个网格行,需要在每一列的末尾插人空三角形。图6-5显示了带有粗线的空三角形。下 面的列表包含了绘制网格所需要的顶点索引,这些索引是按照它们在元素数组缓存中的保存顺序排列的。这个列表使用粗体突出了产生空三角形的索引。

{04,1,5,2,6,3,7,11,6,105,9,4,8,12,913101411,15,
19141813171216}

顶点(3,7,11)、(4,8,12)和(11,15,19)所组成的三角形都没有面积并且在渲染时都不会产生片元。这个使用空三角形的网格绘制方式可以在一次glDrawElements()函数调用中绘制最多带有65536个不同顶点的网格。每列的那个空三角形的处理所浪费的GPU处理时间要远远低于为每列的绘制单独调用glDrawElements()函数所需要的额外处理时间。

6.2.2 OpenGLES_Ch6_2示例

例子OpenGLES_Ch6_2创建了OpenGLES_Ch6_1 中的SceneMesh类的子类Scene-AnimatedMesh。下面的代码摘录自SceneMesh并且与前面例子的视图控制器中的“-glkView:drawInRect:” 方法的实现代码相似。如果有必要保存网格顶点属性,则会创建一个AGLKVertexAttribArrayBuffer实例。一个新的元素数组缓存被创建,并用索引初始化,同时被绑定到OpenGL ES的上下文,参见粗体代码。


- (void)prepareToDraw;
{
        if(ni1 == self. vertexAttr ibuteBuffer && 0 < [self .vertexData length]) {

            self.vertexAttributeBuffer =  [ [AGLKVertexAttribArrayBuffer alloc ] initWithAttribStride :sizeof(SceneMeshVertex) 
                                                    numberOfVertices:[self.vertexData length] / sizeof(SceneMeshVertex)
                                                                data:[self.vertexData bytes]
                                                                usage:GL_STATIC_DRAW];

            self.vertexData = nil;
        }


        if(O == indexBufferID && 0 < [self. indexData length])  {

        glGenBuffers(1, &indexBufferID);
        NSAssert(O != self. indexBufferID, @"Failed to generate element array buffer" );

        glBindBuffer(GL_ELEMENT_ARRAY_BUPFER, self.indexBufferID);
        glBufferData (GL_ELEMENT_ARAY_BUFFER, [se1f.indexData length] , [self.indexData bytes],GL_STATIC_DRAW);

        self.indexData = nil;

        [self.vertexAttributeBuffer prepareToDrawWithAttrib:GLKVertexAttribPosition
                                        number0fCoordinates:3
                                                attrib0ffset:offsetof (SceneMeshVertex, position)
                                                shouldEnable:YES];

        [self.vertexAttributeBuffer prepareToDrawWithAttrib:GLKVertexAttribNormal
                                        number0fCoordinates: 3
                                                attribOffset:offsetof(SceneMeshVertex, normal)
                                                shouldEnable:YES];


        [self.vertexAttributeBuffer    prepareToDrawwithAttrib:GLKVertexAttribTexCoord0
                                        number0fCoordinates:2
                                                attribOffset:offsetof (SceneMeshVertex, texCoords0)
                                                shouldEnable:YES];

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferID) ;

    }

}

SceneAnimatedMesh类提供了下面的“-drawEntireMesh"方法,这个方法会凋用glDrawElements()函数,而不是前面的例子中使用的glDrawArrays()函数。



- (void )drawEntireMesh {
    glDrawBlements (GL_TRIANGLB_STRIP,NUM_MESE_INDICES ,GL_UNSIGNBD_ HORT,NULL);
}

这个例子通过随着时间改変网格頂点的Y坐示来产生网格动画。碧波蕩漾的水波效果是通过在视图控制器的“-update"方法中以一个固定的頻率调用下面的SceneAnimateMesh方法c产生的。



- (void)updateMeshwithelapsedrime: (NSTimeInterval) anInterval {

    int currentRow;
    int currentColumn;


    for (currentColumn = 0; currentColumn < NUM_MESH_COLUMNS; currentColumn++ ) {
        const GLfloat phaseOffset = 2.0f * anInterval;
        const GLfloat phase = 4.0 * currentColumn / (float )NUM_MESH_COLUMNS;
        const GLfloat yoffset = 2.0 ★ sinf(M_PI * (phase + phaseOffset));


        for(currentRow = 0; currentRow < NUM_MESH_ROWS; currentRow+) {

            mesh[currentColumn] [currentRowj.position.y = yOffset;
        }

    }
    SceneMeshUpdateNormals(mesh) ;
    [self makeDynamicAndUpdatewithVertices:&mesh[0][0]
    numberOfVertices:sizeof(mesh) 1 sizeof (SceneMeshVextex);

}

在前面方法中的粗体代码会设置每个顶点的Y坐标的值为一个正弦三角函数的值,这个正弦三角函数的值基于被修改的顶点的当前列和一个时间间隔。每次重新计算值时,时间间隔就会变化,这产生了一个移动的波纹效果。使用正弦只是因为它能产生一个形象有趣的重复样式。使用正弦之外的其他函数也是可以的。试试不同的方程式,比如yOffset = 2.0 cosf(phase phase + phaseOffset)或者yOffset= 2.0 sinf(M_ PI (sinf(phase) + phaseOffset)),来产生不同的波。

每次顶点位置发生变化时,顶点法向量也必须被重新计算。对于SceneMeshUpdate Normals()的调用会使用一个与第4章介绍的相似的算法实现这一点。计算出网格中的每个三角形的面法线,同时设置每个顶点的法向量为包含这个顶点的所有三角形的面法线向量的一个平均值。平均面法线向量会产生一个光滑的而不是多面的灯光效果。

最后,调用“makeDynamicAndUpdateWithVertices: numberOfVertices:” 方法以向GPU控制的内存发送更新的顶点属性,并设置网格的使用提示为GL_DYNAMIC_DRAW来表明顶点属性可能会频繁更新。


注意

网格动 画需要消耗内存带宽来从CPU控制的内存向GPU控制的内存复制更新的顶点数据。最大限度地减少复制发生的总量是非常重要的,因为内存带宽是嵌入式系统的头号瓶颈。避免复制的一个方法是直接在GPU控制的内存中使用运行在GPU上的一个OpenGL ES 2.0 Shading Language程序来计算新的顶点位置。7.4节将详细讲解这个程序。


results matching ""

    No results matching ""