跳转到内容

OpenGL 编程/现代 OpenGL 教程 07

来自维基教科书,开放世界中的开放书籍
茶壶,每个面片都是不同的蓝色阴影

茶壶是 3D 开发人员中一个著名的模型。

你可以找到各种版本的模型作为顶点,但你知道原始版本实际上是由 (3,3) 贝塞尔曲面组成的吗? 贝塞尔曲面由控制点描述,我们可以用任何精度的级别来评估曲面,以生成一组顶点。

那么我们如何创建一个高清晰度的茶壶版本呢? :)

数学到代码

[编辑 | 编辑源代码]

来自维基百科文章(见下文以了解解释

给定的 (nm) 阶贝塞尔曲面由一组 (n + 1)(m + 1) 个控制点 ki,j 定义。[...]
二维贝塞尔曲面可以定义为参数曲面,其中点的坐标 p 是参数坐标 u, v 的函数,由下式给出:
在单位正方形上进行评估,其中
是伯恩斯坦多项式,并且
是二项式系数。


好的,实际上这很简单。

  • 大写的“E”表示“求和”,从值 a 到值 b,步长为 1(即:它是一个for循环,用于做加法)
  • 我们的数据由 4x4 个点组成,因此我们的贝塞尔曲面阶数为 (3,3) - 这是一个 3D 曲面
  • 我们创建一个顶点网格,并通过 (u,v) 对其进行索引;u 和 v 在 0 到 1 之间(相当于贝塞尔曲线中的t,我们可以说它是沿着轴的完成百分比)

复杂的是围绕数学运算的代码;)

获取控制点

[编辑 | 编辑源代码]
带控制点的茶壶

PDF 文章将数据显示为一组大的控制点顶点,然后是几个 4x4 顶点索引的面片

我们希望我们的控制点以某种方式像这样(为了清晰起见,我们将使用顶点结构)

  struct vertex { GLfloat x, y, z; };
  ...
  #define ORDER 3
  struct vertex control_points_k[ORDER+1][ORDER+1] = {
    { { 1, 2, 3}, { 4, 5, 6}, { 7, 8, 9}, {10,11,12} },
    { {13,14,15}, {16,17,18}, {19,20,21}, {22,23,24} },
    ...
  };

此外,我们希望这个数组适用于茶壶中的 28 个面片。

我们注意到文章中提供的数据不能直接使用

  • 我们没有直接的顶点,而是有顶点的索引
  • 索引从 1 开始,而不是像 C/C++ 中那样从 0 开始

我们将数据存储为 C 数组,然后编写一个函数将其转换为我们想要的格式。 我们首先在单独的步骤中执行此操作,因为一次完成所有操作会使我们的代码看起来很复杂。

顶点

struct vertex teapot_cp_vertices[] = {
  {  1.4   ,   0.0   ,  2.4     },
  {  1.4   ,  -0.784 ,  2.4     },
  {  0.784 ,  -1.4   ,  2.4     },
  ...

索引

#define TEAPOT_NB_PATCHES 28
GLushort teapot_patches[][ORDER+1][ORDER+1] = {
  // rim
  { {   1,   2,   3,   4 }, {   5,   6,   7,   8 }, {   9,  10,  11,  12 }, {  13,  14,  15,  16, } },
  { {   4,  17,  18,  19 }, {   8,  20,  21,  22 }, {  12,  23,  24,  25 }, {  16,  26,  27,  28, } },
  { {  19,  29,  30,  31 }, {  22,  32,  33,  34 }, {  25,  35,  36,  37 }, {  28,  38,  39,  40, } },
  { {  31,  41,  42,   1 }, {  34,  43,  44,   5 }, {  37,  45,  46,   9 }, {  40,  47,  48,  13, } },
  // body
  ...

现在我们的函数获取控制点集

void build_control_points_k(int p, struct vertex control_points_k[][ORDER+1]) {
  for (int i = 0; i <= ORDER; i++)
    for (int j = 0; j <= ORDER; j++)
      control_points_k[i][j] = teapot_cp_vertices[teapot_patches[p][i][j] - 1];
}

计算顶点

[编辑 | 编辑源代码]

现在我们将用 10x10 的分辨率(或您想要的任何精度)来评估贝塞尔曲面。

对于每个 4x4 面片,我们计算 10x10 网格中的每个点(因此 u 和 v 以 1/10 的步长递增)

#define RESU 10
#define RESV 10
struct vertex teapot_vertices[TEAPOT_NB_PATCHES * RESU*RESV];
...
void build_teapot() {
  // Vertices
  for (int p = 0; p < TEAPOT_NB_PATCHES; p++) {
    struct vertex control_points_k[ORDER+1][ORDER+1];
    build_control_points_k(p, control_points_k);
    for (int ru = 0; ru <= RESU-1; ru++) {
      float u = 1.0 * ru / (RESU-1);
      for (int rv = 0; rv <= RESV-1; rv++) {
	float v = 1.0 * rv / (RESV-1);
	teapot_vertices[p*RESU*RESV + ru*RESV + rv] = compute_position(control_points_k, u, v);
      }
    }
  }

  // Elements
  ...
}

对于 (u,v) 处的顶点,我们计算上述方程(“公式”)中的“EE”求和

struct vertex compute_position(struct vertex control_points_k[][ORDER+1], float u, float v) {
  struct vertex result = { 0.0, 0.0, 0.0 };
  for (int i = 0; i <= ORDER; i++) {
    for (int j = 0; j <= ORDER; j++) {
      float poly_i = bernstein_polynomial(i, ORDER, u);
      float poly_j = bernstein_polynomial(j, ORDER, v);
      result.x += poly_i * poly_j * control_points_k[i][j].x;
      result.y += poly_i * poly_j * control_points_k[i][j].y;
      result.z += poly_i * poly_j * control_points_k[i][j].z;
    }
  }
  return result;
}

注意:我们可以通过仅在每个 i 循环中计算一次poly_i来优化代码:将其移到两个for行的开头之间。

bernstein_polynomial 和 binomial_coefficient 函数很繁琐,但很简单

float bernstein_polynomial(int i, int n, float u) {
  return binomial_coefficient(i, n) * powf(u, i) * powf(1-u, n-i);
}
float binomial_coefficient(int i, int n) {
  assert(i >= 0); assert(n >= 0);
  return 1.0f * factorial(n) / (factorial(i) * factorial(n-i));
}
int factorial(int n) {
  assert(n >= 0);
  int result = 1;
  for (int i = n; i > 1; i--)
    result *= i;
  return result;
}

注意:文章展示了 Pascal 代码来完成这项工作。 你可能已经注意到作者硬编码了 n=m=3 的方程。 我们没有使用这种方法,因为它实际上使代码更难与方程比较,并且并没有真正使代码更清晰或更短。

为了能够以任何顺序声明我们的函数,我们需要在文件顶部预声明它们

void build_control_points_k(int p, struct vertex control_points_k[][ORDER+1]);
struct vertex compute_position(struct vertex control_points_k[][ORDER+1], float u, float v);
float bernstein_polynomial(int i, int n, float u);
float binomial_coefficient(int i, int n);
int factorial(int n);

绘制顶点

[编辑 | 编辑源代码]

现在我们有了顶点网格,可以使用元素技术来绘制每个正方形。

GLushort teapot_elements[TEAPOT_NB_PATCHES * (RESU-1)*(RESV-1) * 2*3];
...
void build_teapot() {
  // Vertices
  ...

  // Elements
  int n = 0;
  for (int p = 0; p < TEAPOT_NB_PATCHES; p++)
    for (int ru = 0; ru < RESU-1; ru++)
      for (int rv = 0; rv < RESV-1; rv++) {
	// 1 square ABCD = 2 triangles ABC + CDA
	// ABC
	teapot_elements[n] = p*RESU*RESV +  ru   *RESV +  rv   ; n++;
	teapot_elements[n] = p*RESU*RESV +  ru   *RESV + (rv+1); n++;
	teapot_elements[n] = p*RESU*RESV + (ru+1)*RESV + (rv+1); n++;
	// CDA
	teapot_elements[n] = p*RESU*RESV + (ru+1)*RESV + (rv+1); n++;
	teapot_elements[n] = p*RESU*RESV + (ru+1)*RESV +  rv   ; n++;
	teapot_elements[n] = p*RESU*RESV +  ru   *RESV +  rv   ; n++;
      }
}

我们可以像往常一样绘制它。

  glBindBuffer(GL_ARRAY_BUFFER, vbo_teapot_vertices);
  glVertexAttribPointer(
    attribute_coord3d, // attribute
    3,                 // number of elements per vertex, here (x,y,z)
    GL_FLOAT,          // the type of each element
    GL_FALSE,          // take our values as-is
    0,                 // no extra data between each position
    0                  // offset of first element
  );
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_teapot_elements);
  glDrawElements(GL_TRIANGLES, sizeof(teapot_elements)/sizeof(teapot_elements[0]), GL_UNSIGNED_SHORT, 0);

我们得到了会飞的茶壶!

使精度任意

[编辑 | 编辑源代码]
4x4 分辨率的茶壶

目前,我们可以通过修改 #define 来修改分辨率和伯恩斯坦次数。

如果我们想动态地改变这些值(例如,当用户点击 +/- 按钮时改变分辨率),我们无法再使用静态数组,因为 C/C++ 中存在限制。我们选择使用静态数组,因为它使代码更容易理解。要进行更改,您需要:

  • 创建一个指向浮点数数组的指针数组(多维)
  • 使用一维数组并使用数学运算来计算正确的索引。例如,在 4x4 数组中,array[2][3] 等效于 array[2*4+3] - 这正是我们对 teapot_elements[] 数组所做的操作。

这留给读者作为练习;)

当我们将精度设置得非常高时,例如 49x49(67228 个顶点和 774144 个元素),一些顶点似乎合并了。请记住,我们使用 GL_UNSIGNED_SHORT 来索引顶点?这意味着我们只能寻址最多 65536 个顶点。如果我们想要绘制更多,我们需要将茶壶分成几个顶点数组。

更进一步

[编辑 | 编辑源代码]

茶壶的历史 页面中,您会找到一个指向包含其他茶具元素(特别是勺子和杯子)的贝塞尔曲面的存档的链接。将它们显示在茶壶周围!

注意:GLUT 中确实有一个内置的茶壶,它具有静态分辨率(glutSolidTeapot() - 由顶点而不是贝塞尔曲面组成)。我们没有在本教程中使用它,因为它不有趣,因为它属于旧式/1.x OpenGL,而且我们希望尽可能少地使用 GLUT:例如,在移动开发中可能没有 GLUT 可用。

本教程没有讨论法线。它们是计算光照所必需的(欢迎您贡献一个新部分)。

< OpenGL 编程

浏览并下载 完整代码
华夏公益教科书