返回

OpenGL 简介:通过示例代码探索图形绘制

IOS

在初步了解 OpenGL 的基本概念后,我们自然会产生疑问:OpenGL 究竟是如何绘制图形的?它内部的流程是什么样的?为了解答这些疑问,我们不妨通过两个简单的例子来深入理解 OpenGL 的 API 和图形绘制流程。

首先,我们来绘制一个最基础的图形——三角形。在 OpenGL 中,所有的图形都是由一系列的顶点构成的,三角形自然也不例外。我们需要先定义三角形的三个顶点坐标,然后告诉 OpenGL 如何利用这些顶点数据绘制出三角形。

void drawTriangle() {
  // 首先,定义三角形的三个顶点坐标,每个顶点由 x、y、z 三个坐标值组成
  float vertices[] = {
    -0.5f, -0.5f, 0.0f,  // 左下角顶点
    0.5f, -0.5f, 0.0f,   // 右下角顶点
    0.0f,  0.5f, 0.0f    // 顶部顶点
  };

  // 创建一个顶点数组对象 (VAO),VAO 可以理解为一个容器,用于存储与顶点相关的所有信息
  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  // 创建一个顶点缓冲对象 (VBO),VBO 用于存储顶点数据
  GLuint vbo;
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);

  // 将顶点数据复制到 VBO 中
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  // 告诉 OpenGL 如何解释 VBO 中的顶点数据
  // 0 表示顶点属性的位置,3 表示每个顶点有 3 个 float 值,GL_FLOAT 表示数据类型为 float,
  // GL_FALSE 表示不需要归一化,3 * sizeof(float) 表示每个顶点的步长,(void*)0 表示偏移量为 0
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
  glEnableVertexAttribArray(0);

  // 最后,调用 glDrawArrays 函数绘制三角形
  // GL_TRIANGLES 表示绘制模式为三角形,0 表示从第 0 个顶点开始绘制,3 表示绘制 3 个顶点
  glDrawArrays(GL_TRIANGLES, 0, 3);
}

通过这段代码,我们成功地绘制了一个三角形。接下来,我们尝试绘制一个稍微复杂一点的图形——立方体。立方体由 6 个面组成,每个面都是一个正方形,而每个正方形又可以由两个三角形组成。因此,我们可以将立方体看作是由 12 个三角形组成的。

void drawCube() {
  // 定义立方体的顶点数据,每个顶点仍然由 x、y、z 三个坐标值组成
  float vertices[] = {
    // 前面
    -0.5f, -0.5f,  0.5f, 
    0.5f, -0.5f,  0.5f,  
    0.5f,  0.5f,  0.5f,  
    -0.5f,  0.5f,  0.5f, 

    // 后面
    -0.5f, -0.5f, -0.5f, 
    0.5f, -0.5f, -0.5f,  
    0.5f,  0.5f, -0.5f,  
    -0.5f,  0.5f, -0.5f, 

    // ... 其他面的顶点数据 ...
  };

  // 定义索引数据,用于告诉 OpenGL 如何使用顶点数据绘制三角形
  GLuint indices[] = {
    0, 1, 2, 2, 3, 0,  // 前面
    4, 5, 6, 6, 7, 4,  // 后面
    // ... 其他面的索引数据 ...
  };

  // 创建 VAO 和 VBO,并将顶点数据复制到 VBO 中
  GLuint vao, vbo;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  // 设置顶点属性指针
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
  glEnableVertexAttribArray(0);

  // 创建一个索引缓冲对象 (EBO),EBO 用于存储索引数据
  GLuint ebo;
  glGenBuffers(1, &ebo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

  // 调用 glDrawElements 函数绘制立方体
  // GL_TRIANGLES 表示绘制模式为三角形,36 表示绘制 36 个索引,GL_UNSIGNED_INT 表示索引数据类型为 unsigned int,(void*)0 表示偏移量为 0
  glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, (void*)0);
}

通过这两个例子,我们可以总结出 OpenGL 的图形绘制流程:

  1. 定义顶点数据 : 包括顶点位置、颜色、法线、纹理坐标等信息。
  2. 创建 VAO : VAO 用于存储与顶点相关的所有信息,例如 VBO、EBO 和顶点属性指针等。
  3. 创建 VBO : VBO 用于存储顶点数据。
  4. 将顶点数据复制到 VBO : 使用 glBufferData 函数将顶点数据从 CPU 内存复制到 GPU 内存。
  5. 设置顶点属性指针 : 告诉 OpenGL 如何解释 VBO 中的顶点数据。
  6. 创建 EBO (可选) : 如果使用索引绘制,则需要创建 EBO 并将索引数据复制到 EBO 中。
  7. 调用 glDrawArraysglDrawElements 函数渲染几何体 : glDrawArrays 用于非索引绘制,glDrawElements 用于索引绘制。

常见问题解答

1. VAO、VBO 和 EBO 分别是什么?

VAO (Vertex Array Object) 可以理解为一个容器,用于存储与顶点相关的所有信息,例如 VBO、EBO 和顶点属性指针等。VBO (Vertex Buffer Object) 用于存储顶点数据,例如顶点位置、颜色、法线、纹理坐标等。EBO (Element Buffer Object) 用于存储索引数据,索引数据用于告诉 OpenGL 如何使用顶点数据绘制三角形。

2. 为什么要使用 VAO?

使用 VAO 可以将与顶点相关的所有信息存储在一个对象中,方便管理和切换不同的顶点配置。例如,如果需要绘制多个不同的物体,每个物体都有不同的顶点数据和顶点属性指针,那么可以使用多个 VAO,每个 VAO 存储一个物体的顶点信息。

3. 为什么要使用 EBO?

使用 EBO 可以减少顶点数据的冗余,提高渲染效率。例如,一个立方体有 8 个顶点,但如果使用索引绘制,只需要存储 8 个顶点数据和 36 个索引数据,而如果使用非索引绘制,则需要存储 24 个顶点数据。

4. glVertexAttribPointer 函数的参数分别是什么含义?

glVertexAttribPointer 函数用于设置顶点属性指针,告诉 OpenGL 如何解释 VBO 中的顶点数据。它的参数含义如下:

  • index: 顶点属性的位置。
  • size: 每个顶点属性的组成部分数量,例如顶点位置由 x、y、z 三个组成部分,因此 size 为 3。
  • type: 数据类型,例如 GL_FLOATGL_INT 等。
  • normalized: 是否需要归一化。
  • stride: 每个顶点的步长,单位为字节。
  • pointer: 偏移量,单位为字节。

5. glDrawArraysglDrawElements 函数的区别是什么?

glDrawArrays 函数用于非索引绘制,glDrawElements 函数用于索引绘制。非索引绘制需要为每个三角形指定三个顶点,而索引绘制可以使用索引数据来复用顶点数据,减少顶点数据的冗余。