跳轉到內容

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 程式設計

瀏覽並下載 完整程式碼
華夏公益教科書