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 可用。
本教程没有讨论法线。它们是计算光照所必需的(欢迎您贡献一个新部分)。