OpenGL 程式設計/現代 OpenGL 教程 07

茶壺是 3D 開發人員中一個著名的模型。
你可以找到各種版本的模型作為頂點,但你知道原始版本實際上是由 (3,3) 貝塞爾曲面組成的嗎? 貝塞爾曲面由控制點描述,我們可以用任何精度的級別來評估曲面,以生成一組頂點。
見
- 茶壺的起源 (PDF) - 計算機圖形與應用 - 1987 年 1 月 (第 7 卷第 1 號) - 一篇古老的文章,包含貝塞爾資料(“面片”)!
- 茶壺的歷史 - 更多歷史細節
那麼我們如何建立一個高畫質晰度的茶壺版本呢? :)
來自維基百科文章(見下文以瞭解解釋)
- 給定的 (n, m) 階貝塞爾曲面由一組 (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);
我們得到了會飛的茶壺!

目前,我們可以透過修改 #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 可用。
本教程沒有討論法線。它們是計算光照所必需的(歡迎您貢獻一個新部分)。