顶点索引

获取示例代码,本文代码在分支chapter14中。


本文主要讲解OpenGL中另一个优化技巧,使用顶点索引渲染物体。上一篇文章的例子中渲染正方体需要一个包含36个顶点的数组。

本文修改的顶点数据都在Cube.m中。

- (GLfloat *)cubeData {
    static GLfloat cubeData[] = {
        // X轴0.5处的平面
        0.5,  -0.5,    0.5f, 1,  0,  0, 0, 0,   // VertexA
        0.5,  -0.5f,  -0.5f, 1,  0,  0, 0, 1,   // VertexB
        0.5,  0.5f,   -0.5f, 1,  0,  0, 1, 1,   // VertexC
        0.5,  0.5,    -0.5f, 1,  0,  0, 1, 1,   // VertexC
        0.5,  0.5f,    0.5f, 1,  0,  0, 1, 0,   // VertexD
        0.5,  -0.5f,   0.5f, 1,  0,  0, 0, 0,   // VertexA
        // X轴-0.5处的平面
        -0.5,  -0.5,    0.5f, -1,  0,  0, 0, 0, // VertexE
        -0.5,  -0.5f,  -0.5f, -1,  0,  0, 0, 1, // VertexF
        -0.5,  0.5f,   -0.5f, -1,  0,  0, 1, 1, // VertexG
        -0.5,  0.5,    -0.5f, -1,  0,  0, 1, 1, // VertexG
        -0.5,  0.5f,    0.5f, -1,  0,  0, 1, 0, // VertexH
        -0.5,  -0.5f,   0.5f, -1,  0,  0, 0, 0, // VertexE
        
        -0.5,  0.5,  0.5f, 0,  1,  0, 0, 0,     // VertexH
        -0.5f, 0.5, -0.5f, 0,  1,  0, 0, 1,     // VertexG
        0.5f, 0.5,  -0.5f, 0,  1,  0, 1, 1,     // VertexC
        0.5,  0.5,  -0.5f, 0,  1,  0, 1, 1,     // VertexC
        0.5f, 0.5,   0.5f, 0,  1,  0, 1, 0,     // VertexD
        -0.5f, 0.5,  0.5f, 0,  1,  0, 0, 0,     // VertexH
        -0.5, -0.5,   0.5f, 0,  -1,  0, 0, 0,   // VertexE
        -0.5f, -0.5, -0.5f, 0,  -1,  0, 0, 1,   // VertexF
        0.5f, -0.5,  -0.5f, 0,  -1,  0, 1, 1,   // VertexB
        0.5,  -0.5,  -0.5f, 0,  -1,  0, 1, 1,   // VertexB
        0.5f, -0.5,   0.5f, 0,  -1,  0, 1, 0,   // VertexA
        -0.5f, -0.5,  0.5f, 0,  -1,  0, 0, 0,   // VertexE
        
        -0.5,   0.5f,  0.5,   0,  0,  1, 0, 0,  // VertexH
        -0.5f,  -0.5f,  0.5,  0,  0,  1, 0, 1,  // VertexE
        0.5f,   -0.5f,  0.5,  0,  0,  1, 1, 1,  // VertexA
        0.5,    -0.5f, 0.5,   0,  0,  1, 1, 1,  // VertexA
        0.5f,  0.5f,  0.5,    0,  0,  1, 1, 0,  // VertexD
        -0.5f,   0.5f,  0.5,  0,  0,  1, 0, 0,  // VertexH
        -0.5,   0.5f,  -0.5,   0,  0,  -1, 0, 0,    // VertexG
        -0.5f,  -0.5f,  -0.5,  0,  0,  -1, 0, 1,    // VertexF
        0.5f,   -0.5f,  -0.5,  0,  0,  -1, 1, 1,    // VertexB
        0.5,    -0.5f, -0.5,   0,  0,  -1, 1, 1,    // VertexB
        0.5f,  0.5f,  -0.5,    0,  0,  -1, 1, 0,    // VertexC
        -0.5f,   0.5f,  -0.5,  0,  0,  -1, 0, 0,    // VertexG
    };
    return cubeData;
}

我在每个顶点后做了注释,大家可以发现,其实一共就8个位置不一样的顶点,其他都是重复的。完全可以替换成下面的表现形式。cubeVertex是正方体的8个顶点,cubeVertexIndicecubeData中的顶点对应的索引数组,使用索引即可在cubeVertex中取到对应的顶点数据。这样一来大大减少了需要传递给GPU的数据量。

- (GLfloat *)cubeVertex {
    static GLfloat cubeData[] = {
        0.5,  -0.5,    0.5f, 0.5773502691896258, -0.5773502691896258, 0.5773502691896258, 0, 0,   // VertexA
        0.5,  -0.5f,  -0.5f, 0.5773502691896258, -0.5773502691896258, -0.5773502691896258, 0, 1,   // VertexB
        0.5,  0.5f,   -0.5f, 0.5773502691896258, 0.5773502691896258, -0.5773502691896258, 1, 1,   // VertexC
        0.5,  0.5f,    0.5f, 0.5773502691896258, 0.5773502691896258, 0.5773502691896258, 1, 0,   // VertexD
        -0.5,  -0.5,    0.5f, -0.5773502691896258, -0.5773502691896258, 0.5773502691896258, 0, 0, // VertexE
        -0.5,  -0.5f,  -0.5f, -0.5773502691896258, -0.5773502691896258, -0.5773502691896258, 0, 1, // VertexF
        -0.5,  0.5f,   -0.5f, -0.5773502691896258, 0.5773502691896258, -0.5773502691896258, 1, 1, // VertexG
        -0.5,  0.5f,    0.5f, -0.5773502691896258, 0.5773502691896258, 0.5773502691896258, 1, 0, // VertexH
    };
    return cubeData;
}

- (GLushort *)cubeVertexIndice {
    static GLushort cubeDataIndice[] = {
        0,      // VertexA
        1,      // VertexB
        2,      // VertexC
        2,      // VertexC
        3,      // VertexD
        0,      // VertexA
        
        4,      // VertexE
        5,      // VertexF
        6,      // VertexG
        6,      // VertexG
        7,      // VertexH
        4,      // VertexE
        
        7,      // VertexH
        6,      // VertexG
        2,      // VertexC
        2,      // VertexC
        3,      // VertexD
        7,      // VertexH
        4,      // VertexE
        5,      // VertexF
        1,      // VertexB
        1,      // VertexB
        0,      // VertexA
        4,      // VertexE
        
        7,      // VertexH
        4,      // VertexE
        0,      // VertexA
        0,      // VertexA
        3,      // VertexD
        7,      // VertexH
        6,      // VertexG
        5,      // VertexF
        1,      // VertexB
        1,      // VertexB
        2,      // VertexC
        6,      // VertexG
    };
    return cubeDataIndice;
}

那么,现在问题来了,怎么渲染这种形式的数据呢?这就是下面要介绍的内容。

创建索引数组IBO

在前文中我们为顶点数据创建了VBO,同样我们也可以为索引数据创建IBO(Index Buffer Object)。

- (void)genIndiceVBO {
    glGenBuffers(1, &indiceVbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indiceVbo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 36 * sizeof(GLushort), [self cubeVertexIndice], GL_STATIC_DRAW);
}

和创建VBO唯一不同的就是GL_ELEMENT_ARRAY_BUFFER,创建VBO是这里的值是GL_ARRAY_BUFFER。然后我们在生成VAO的地方增加绑定IBO的代码。

- (void)genVAO {
    glGenVertexArraysOES(1, &vao);
    glBindVertexArrayOES(vao);
    
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indiceVbo);
    [self.context bindAttribs:NULL];
    
    glBindVertexArrayOES(0);
}

这里的vbo绑定的数据已经修改为cubeVertex里的数据了。

- (void)genVBO {
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, 8 * 8 * sizeof(GLfloat), [self cubeVertex], GL_STATIC_DRAW);
}

最后就是绘制部分的修改了,我在GLContext中增加了一个方法。

- (void)drawTrianglesWithIndicedVAO:(GLuint)vao vertexCount:(GLint)vertexCount {
    glBindVertexArrayOES(vao);
    glDrawElements(GL_TRIANGLES, vertexCount, GL_UNSIGNED_SHORT, (void *)0);
}

主要就是使用了glDrawElements绘制带索引的顶点数组。其中GL_UNSIGNED_SHORT指的是索引数组中每个元素的类型。这里索引数组用的是static GLushort cubeDataIndice[],所以使用GL_UNSIGNED_SHORT。除了这个值外,还能用的值为GL_UNSIGNED_BYTEGL_UNSIGNED_INT。下面是glDrawElements的man文档。

到此就可以渲染出正方体了。

看到这奇怪的渲染效果,很明显,事情还没有结束。如果你有看基本光照这一篇文章,应该会知道法线可以有下面两种表现方式。 前者是每个三角形的每个顶点都有一个法线数据,对应不使用索引时的情况。后者是每个顶点有一个法线数据,共享的点的法线是这个点在各个三角形上的法线之和归一化后的结果。

- (GLfloat *)cubeVertex {
    static GLfloat cubeData[] = {
        0.5,  -0.5,    0.5f, 0.5773502691896258, -0.5773502691896258, 0.5773502691896258, 0, 0,   // VertexA
        0.5,  -0.5f,  -0.5f, 0.5773502691896258, -0.5773502691896258, -0.5773502691896258, 0, 1,   // VertexB
        0.5,  0.5f,   -0.5f, 0.5773502691896258, 0.5773502691896258, -0.5773502691896258, 1, 1,   // VertexC
        0.5,  0.5f,    0.5f, 0.5773502691896258, 0.5773502691896258, 0.5773502691896258, 1, 0,   // VertexD
        -0.5,  -0.5,    0.5f, -0.5773502691896258, -0.5773502691896258, 0.5773502691896258, 0, 0, // VertexE
        -0.5,  -0.5f,  -0.5f, -0.5773502691896258, -0.5773502691896258, -0.5773502691896258, 0, 1, // VertexF
        -0.5,  0.5f,   -0.5f, -0.5773502691896258, 0.5773502691896258, -0.5773502691896258, 1, 1, // VertexG
        -0.5,  0.5f,    0.5f, -0.5773502691896258, 0.5773502691896258, 0.5773502691896258, 1, 0, // VertexH
    };
    return cubeData;
}

cubeVertex里面的法线数据就是我手动计算的结果。这样产生的法线会让表面过渡平滑。除了法线,UV也会面临同样的问题,所以渲染出来的贴图才会不正确。

如果为每个顶点属性指定不同的VBO和IBO是可以解决这个问题的,这个方案将会在后面介绍加载wavefront 3D模型时详细介绍。

Updated: