一个 OpenGL 工具箱

顶点数组及其绘制命令

//
// squareAnnulus1.cpp
//
// 该程序绘制一个方形环作为三角形条带
// 顶点使用 glVertex3f() 指定,颜色使用 glColor3f()。
//
// 相互作用:
// 按空格键可在线框和填充之间切换。
//
//#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h>static int isWire = 0; // 是线框?// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);// 绘制方形环。glBegin(GL_TRIANGLE_STRIP);glColor3f(0.0, 0.0, 0.0);glVertex3f(30.0, 30.0, 0.0); // Vertex 0glColor3f(1.0, 0.0, 0.0);glVertex3f(10.0, 10.0, 0.0); // Vertex 1glColor3f(0.0, 1.0, 0.0);glVertex3f(70.0, 30.0, 0.0); // Vertex 2glColor3f(0.0, 0.0, 1.0);glVertex3f(90.0, 10.0, 0.0); // Vertex 3glColor3f(1.0, 1.0, 0.0);glVertex3f(70.0, 70.0, 0.0); // Vertex 4glColor3f(1.0, 0.0, 1.0);glVertex3f(90.0, 90.0, 0.0); // Vertex 5glColor3f(0.0, 1.0, 1.0);glVertex3f(30.0, 70.0, 0.0); // Vertex 6glColor3f(1.0, 0.0, 0.0);glVertex3f(10.0, 90.0, 0.0); // Vertex 7glColor3f(0.0, 0.0, 0.0);glVertex3f(30.0, 30.0, 0.0); // Vertex 8 = Vertex 0glColor3f(1.0, 0.0, 0.0);glVertex3f(10.0, 10.0, 0.0); // Vertex 9 = Vertex 1glEnd();glFlush();
}// 初始化例程。
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);
}// OpenGL 窗口重塑例程。
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.0);glMatrixMode(GL_MODELVIEW);glLoadIdentity();
}// 键盘输入处理例程。
void keyInput(unsigned char key, int x, int y)
{switch (key){case ' ':if (isWire == 0) isWire = 1;else isWire = 0;glutPostRedisplay();break;case 27:exit(0);break;default:break;}
}
// 将交互指令输出到 C++ 窗口的例程。
void printInteraction(void)
{std::cout << "Interaction:" << std::endl;std::cout << "Press the space bar to toggle between wireframe and filled." << std::endl;
}
// 主要例程。
int main(int argc, char **argv)
{printInteraction();glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("squareAnnulus1.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}
// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);// 绘制方形环。glBegin(GL_TRIANGLE_STRIP);glColor3f(0.0, 0.0, 0.0);glVertex3f(30.0, 30.0, 0.0); // Vertex 0glColor3f(1.0, 0.0, 0.0);glVertex3f(10.0, 10.0, 0.0); // Vertex 1glColor3f(0.0, 1.0, 0.0);glVertex3f(70.0, 30.0, 0.0); // Vertex 2glColor3f(0.0, 0.0, 1.0);glVertex3f(90.0, 10.0, 0.0); // Vertex 3glColor3f(1.0, 1.0, 0.0);glVertex3f(70.0, 70.0, 0.0); // Vertex 4glColor3f(1.0, 0.0, 1.0);glVertex3f(90.0, 90.0, 0.0); // Vertex 5glColor3f(0.0, 1.0, 1.0);glVertex3f(30.0, 70.0, 0.0); // Vertex 6glColor3f(1.0, 0.0, 0.0);glVertex3f(10.0, 90.0, 0.0); // Vertex 7glColor3f(0.0, 0.0, 0.0);glVertex3f(30.0, 30.0, 0.0); // Vertex 8 = Vertex 0glColor3f(1.0, 0.0, 0.0);glVertex3f(10.0, 10.0, 0.0); // Vertex 9 = Vertex 1glEnd();glFlush();
}

修改

drawScene

square-Annulus2.cpp

// 顶点坐标向量。
static float vertices[8][3] =
{{ 30.0, 30.0, 0.0 },{ 10.0, 10.0, 0.0 },{ 70.0, 30.0, 0.0 },{ 90.0, 10.0, 0.0 },{ 70.0, 70.0, 0.0 },{ 90.0, 90.0, 0.0 },{ 30.0, 70.0, 0.0 },{ 10.0, 90.0, 0.0 }
};// 顶点颜色向量。
static float colors[8][3] =
{{ 0.0, 0.0, 0.0 },{ 1.0, 0.0, 0.0 },{ 0.0, 1.0, 0.0 },{ 0.0, 0.0, 1.0 },{ 1.0, 1.0, 0.0 },{ 1.0, 0.0, 1.0 },{ 0.0, 1.0, 1.0 },{ 1.0, 0.0, 0.0 }
};// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);glBegin(GL_TRIANGLE_STRIP);for (int i = 0; i < 10; ++i){glColor3fv(colors[i % 8]);glVertex3fv(vertices[i % 8]);}glEnd();glFlush();
}

它绘制与 上面 相同的环,除了顶点坐标和颜色数据现在分别存储在二维全局数组、顶点和颜色中。 此外,在每次迭代中,循环通过顶点声明的指针形式(也称为向量形式)检索坐标值向量,即 glVertex3fv(*pointer),以及具有指针形式 glColor3fv(*pointer) 的颜色值向量。 End 与squareAnnulus1.cpp 相比,square-Annulus2.cpp 获得的一个明显效率是将顶点和颜色数据放在代码中的一个地方,以便能够从其他地方简单地指向它们。这允许将三角形条块(尽管仍包含 10 个顶点及其颜色)编码为一个短循环。将程序的数据收集和放置在与实际访问数据的过程分开的单个位置始终是一种很好的做法。冗余和随之而来的错误趋于消除,内存使用效率更高,并且更容易模块化和调试访问数据的过程。有用的是,OpenGL 提供了特定的设备——顶点数组数据结构——这使得用户可以轻松高效地集中和共享数据。让我们从实时代码中学习它们。

squareAnnulus3.cpp

// 顶点坐标向量。
static float vertices[8][3] =
{{ 30.0, 30.0, 0.0 },{ 10.0, 10.0, 0.0 },{ 70.0, 30.0, 0.0 },{ 90.0, 10.0, 0.0 },{ 70.0, 70.0, 0.0 },{ 90.0, 90.0, 0.0 },{ 30.0, 70.0, 0.0 },{ 10.0, 90.0, 0.0 }
};// 顶点颜色向量。
static float colors[8][3] =
{{ 0.0, 0.0, 0.0 },{ 1.0, 0.0, 0.0 },{ 0.0, 1.0, 0.0 },{ 0.0, 0.0, 1.0 },{ 1.0, 1.0, 0.0 },{ 1.0, 0.0, 1.0 },{ 0.0, 1.0, 1.0 },{ 1.0, 0.0, 0.0 }
};// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);glBegin(GL_TRIANGLE_STRIP);for (int i = 0; i < 10; ++i){// vertices[]中的第i个顶点和colors[]中的第i个颜色由glArrayElement(i)一起调用。for (int i = 0; i < 10; ++i) glArrayElement(i % 8);}glEnd();glFlush();
}// 初始化例程。
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);// 启用两个顶点数组:坐标和颜色。glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_COLOR_ARRAY);// 指定坐标和颜色数组的位置。glVertexPointer(3, GL_FLOAT, 0, vertices);glColorPointer(3, GL_FLOAT, 0, colors);
}

它再次绘制了与以前相同的彩色环。 顶点的坐标和颜色数据分别存储在全局顶点数组、顶点和颜色中,就像在 squareAnnulus2.cpp 中一样,除了现在数组是平面而不是 2D(OpenGL 假定为 1D 数组,但由于 C++ 的方式 存储数组,事实上,我们可以将顶点和颜色指定为 2D 数组,如果我们选择了,就像在 squareAnnulus2.cpp 中一样)。

现在,神奇的是:在三角形带环内

glBegin(GL_TRIANGLE_STRIP);for(int i = 0; i < 10; ++i) glArrayElement(i%8);
glEnd();

来自坐标和颜色数组的第 i 个值向量通过单个 glArrayElement(i) 调用同时检索。
请注意在初始化例程中设置顶点数组的步骤:

  1. 通过调用glEnableClientState(array)启用两个顶点数组,其中array依次为GL VERTEX ARRAY和GL COLOR ARRAY,分别为顶点坐标和颜色值。 参数数组还有其他可能的值来存储其他类型的顶点数据,例如法线值和纹理坐标。
  2. 两个顶点数组的数据通过调用 glVertexPointer(size, type, stride, *pointer) 和 glColorPointer(size, type, stride, *pointer) 来指定。 参数指针是数据数组的起始地址,type 声明数据类型,size 是每个顶点的值数(例如,我们的坐标和颜色数组都为每个顶点存储 3 个值),stride 是字节偏移量 在连续顶点的值的开始之间(0 特别表示连续顶点的值没有分开,就像我们的情况一样)。
// 三角形按顺序剥离顶点索引。
static unsigned int stripIndices[] = { 0, 1, 2, 3, 4, 5, 6, 7, 0, 1 };// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);//glDrawElements() 在一个命令中“拉起”10 个顶点的数据——比调用 glArrayElement() 10 次更有效。glDrawElements(GL_TRIANGLE_STRIP, 10, GL_UNSIGNED_INT, stripIndices);glEnd();glFlush();
}

使用单个绘制调用,代码更加简洁

glDrawElements(GL_TRIANGLE_STRIP, 10, GL_UNSIGNED_INT, stripIndices);

替换整个 glBegin(GL TRIANGLE STRIP)-glEnd() 块。

这个调用的一般形式是

glDrawElements(primitive, countIndices, type, *indices)

其中参数基元是几何基元,索引是索引数组的起始地址,类型是索引数组的数据类型,countIndices 是要使用的索引数。 此调用的作用是从索引指定的序列中启用的顶点数组中为基元选择 countIndices 顶点数,因此等效于循环

glBegin(primitive);for(i = 0; i < countIndices; i++) glArrayElement(indices[i]);
glEnd();

squareAnnulusAndTriangle.cpp

它在 squareAnnulus*.cpp 程序的环内添加了一个三角形。

// 三角形的顶点坐标和颜色向量数组。
static float vertices2AndColors2Interleaved[] =
{40.0, 40.0, 0.0, 0.0, 1.0, 1.0,60.0, 40.0, 0.0, 1.0, 0.0, 0.0,60.0, 60.0, 0.0, 0.0, 1.0, 0.0
};void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);//glDrawElements() 在一个命令中“拉起”10 个顶点的数据——比调用 glArrayElement() 10 次更有效。glDrawElements(GL_TRIANGLE_STRIP, 10, GL_UNSIGNED_INT, stripIndices);// 顶点和颜色数组指针指向三角形的数据。 //stride 参数的值是指定的,因为在连续坐标或颜色向量的开始之间的数组中有六个浮点值。glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), &vertices2AndColors2Interleaved[0]);glColorPointer(3, GL_FLOAT, 6 * sizeof(float), &vertices2AndColors2Interleaved[3]);//画三角形。glDrawArrays(GL_TRIANGLES, 0, 3);glEnd();glFlush();
}

该程序演示了多个顶点数组的使用。 顶点数组vertices1 和colors1 分别包含环的坐标和颜色数据,与squareAnnulus4.cpp 中的完全一样。 另一方面,三角形的单个顶点数组 vertices2AndColors2Interleaved 是交错的,因为它同时包含坐标和颜色数据。 当指向三角形的数据时, glVertexPointer() 和 glColorPointer() 调用的 stride 参数设置为浮点数据项中字节数的 6 倍,因为有 6 个浮点数(特别是 3 个坐标值和 3 个颜色值)在交错数组中连续坐标或颜色向量的开始之间。

该声明

glDrawArrays(GL_TRIANGLES, 0, 3)

绘制三角形还引入了一个新的绘图命令以用于顶点数组。

glDrawArrays(primitive, first, countVertices)

使用顶点数组中的 countVertices 元素绘制几何图元图元,首先从位置处的元素开始。 当绘图只需要线性地(即按代码顺序)处理顶点数组中的元素时,这是首选命令,而无需使用诸如提供中间间接级别的索引数组之类的东西来回跳动。 图 3.4 显示了有序图和索引图之间的区别。

有序与索引绘制的示例:有序绘制处理序列 v0 v1 v2 v3 v4 v5 中的顶点,而索引绘制序列 v1 v3 v5 v2 v0 v4 。

顶点数组可实现高效、逻辑和概念上干净的 OpenGL 代码。 图 3.5 说明了这一点(它显示了也存储在顶点数组中的额外顶点属性——我们稍后会讨论这些)。 此外,它们在最新版本的 OpenGL 中是强制性的,例如 4.x,我们将在稍后介绍。 从现在开始养成使用顶点数组的习惯! 警告:从今往后,我们将始终如一地实现顶点数组,除非程序的开销可能会影响程序的重点。

使用顶点数组的数据逻辑表示:顶点数据在一个区域中,每个图元定义为指向顶点的指针列表。 例如,在 glDrawElements() 调用中,指针值位于索引数组中。

请记住,如果有动画,将重复调用显示例程。 因此,在此例程中存储静态数据或在其中执行实际上可以完成一次并保存结果的计算是特别低效的,而且不幸的是,这是初学者常见的错误。 规则是将顶点属性存储在顶点数组中,而初始化例程是一次性计算的地方。

glMultiDrawElements(primitive, *countIndices, type, **indices, countPrimitives)

实际上,glMultiDrawElements() 的参数与您期望的一样多,为了组合许多

glDrawElements(primitive, countIndices, type, *indices)

调用,每个调用都绘制相同的几何图元:而不是一个 countIndices 值,现在有 一个数组 *countIndices 值; 现在有一个数组 **indices,而不是一个数组 *indices 索引; 最后,countPrimitives 当然是被绘制的图元的数量,即被合并的 glDrawElements() 的数量。 glMultiDrawElements() 调用等价于

for (int i = 0; i < countPrimitives; i++)glDrawElements(primitive, countIndices[i], type, indices[i]);

使用一个(或几个) glMultiDrawElements() 调用绘制由同一几何图元的多个实例组成的对象比使用多个 glDrawElements() 更有效。 让我们使用单个 glMultiDrawElements() 命令重做上一章中的 hemisphere.cpp。

hemisphereMultidraw.cpp

#include <cmath>
#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h>#define PI 3.14159265static float R = 5.0; // 半球的半径。
static int p = 6; // 纵向切片数。
static int q = 4; // 纬度切片的数量。
static float Xangle = 0.0, Yangle = 0.0, Zangle = 0.0; // 旋转半球的角度。
static float *vertices = NULL; // 半球上采样点的顶点数组。
static unsigned int **indices = NULL; // 索引数组数组。
static int *countIndices = NULL; // countIndices 数组。// 用样本点的坐标填充顶点数组。
void fillVertexArray(void)
{int i, j, k;k = 0;for (j = 0; j <= q; j++)for (i = 0; i <= p; i++){vertices[k++] = R * cos((float)j / q * PI / 2.0) * cos(2.0 * (float)i / p * PI);vertices[k++] = R * sin((float)j / q * PI / 2.0);vertices[k++] = -R * cos((float)j / q * PI / 2.0) * sin(2.0 * (float)i / p * PI);}
}// 填充索引数组。
void fillIndices(void)
{int i, j;for (j = 0; j < q; j++)// 注意:第 j 个索引数组的索引(在下面的循环中给出)完全对应// 到 hemisphere.cpp 的绘制循环中第 j 个三角形条上的点。{for (i = 0; i <= p; i++){indices[j][2 * i] = (j + 1) * (p + 1) + i;indices[j][2 * i + 1] = j * (p + 1) + i;}}
}// 填充数组countIndices。
void fillCountIndices(void)
{int j;for (j = 0; j < q; j++) countIndices[j] = 2 * (p + 1);
}// 初始化例程。
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);glEnableClientState(GL_VERTEX_ARRAY); // 启用顶点数组。
}// 绘图程序。
void drawScene(void)
{int j;glClear(GL_COLOR_BUFFER_BIT);glLoadIdentity();// 创建全局数组。vertices = new float[3 * (p + 1) * (q + 1)];indices = new unsigned int *[q];for (j = 0; j < q; j++) indices[j] = new unsigned int[2 * (p + 1)];countIndices = new int[q];// 初始化全局数组。fillVertexArray();fillIndices();fillCountIndices();glVertexPointer(3, GL_FLOAT, 0, vertices);// 平移以原点为中心绘制的半球的命令,// 进入视锥体。glTranslatef(0.0, 0.0, -10.0);// 命令转动半球。glRotatef(Zangle, 0.0, 0.0, 1.0);glRotatef(Yangle, 0.0, 1.0, 0.0);glRotatef(Xangle, 1.0, 0.0, 0.0);// 半球属性。glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);glColor3f(0.0, 0.0, 0.0);// Multidraw 命令等效于 hemisphere.cpp 中的绘图循环 for(j = 0; j < q; j++){...}。glMultiDrawElements(GL_TRIANGLE_STRIP, countIndices, GL_UNSIGNED_INT, (const void **)indices, q);glFlush();
}// OpenGL 窗口重塑例程。
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);glMatrixMode(GL_MODELVIEW);
}// 键盘输入处理例程
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;case 'P':p += 1;glutPostRedisplay();break;case 'p':if (p > 3) p -= 1;glutPostRedisplay();break;case 'Q':q += 1;glutPostRedisplay();break;case 'q':if (q > 3) q -= 1;glutPostRedisplay();break;case 'x':Xangle += 5.0;if (Xangle > 360.0) Xangle -= 360.0;glutPostRedisplay();break;case 'X':Xangle -= 5.0;if (Xangle < 0.0) Xangle += 360.0;glutPostRedisplay();break;case 'y':Yangle += 5.0;if (Yangle > 360.0) Yangle -= 360.0;glutPostRedisplay();break;case 'Y':Yangle -= 5.0;if (Yangle < 0.0) Yangle += 360.0;glutPostRedisplay();break;case 'z':Zangle += 5.0;if (Zangle > 360.0) Zangle -= 360.0;glutPostRedisplay();break;case 'Z':Zangle -= 5.0;if (Zangle < 0.0) Zangle += 360.0;glutPostRedisplay();break;default:break;}
}// 将交互指令输出到 C++ 窗口的例程。
void printInteraction(void)
{std::cout << "Interaction:" << std::endl;std::cout << "Press P/p to increase/decrease the number of longitudinal slices." << std::endl<< "Press Q/q to increase/decrease the number of latitudinal slices." << std::endl<< "Press x, X, y, Y, z, Z to turn the hemisphere." << std::endl;
}// Main routine.
int main(int argc, char **argv)
{printInteraction();glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("hemisphereMultidraw.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

运行 hemisphereMultidraw.cpp,其唯一目的是绘制循环

for(j = 0; j < q; j++){    // 一个纬度三角形带。    glBegin(GL_TRIANGLE_STRIP);    ...}

使用 hemisphere.cpp 的三角形条代替单个

glMultiDrawElements(GL_TRIANGLE_STRIP, countIndices,GL_UNSIGNED_BYTE, (const void **)indices, q)

我们将让读者继续阅读 hemisphere-Multidraw.cpp 的其余代码,这些代码几乎全部用于为 glMultiDrawElements() 调用设置数组。 特别是,fillIndices() 完成了正确填充二维数组索引的工作(这是从 glDrawElements() 转换到 glMultiDrawElements() 时通常需要最小心的部分)。

备注 3.2。重要的! glBegin()-glEnd() 类型的绘图命令用于所谓的即时模式渲染,而 glDrawElements()、glDrawArrays()、glMultiDrawElements()、glMultiDrawArrays() 和更多它们的relatives(参见红皮书list) 用于保留模式渲染。在立即模式下,客户端(运行程序的机器)强制服务器(GPU)渲染,而在保留模式下,客户端只向服务器提供执行指令和使用数据,允许后者进行优化在渲染之前。从 OpenGL 3.1 开始,立即模式绘图已被删除(尽管它仍然可以通过兼容性配置文件访问);只有保留模式调用在更高版本中可用。因此,我们敦促读者从现在开始尽可能使用保留模式命令。停止使用 glBegin()-glEnd()!然而,我们偶尔会为了简单的代码而自己违反这个建议,因为设置索引数组或在 glMultiDraw* 命令的情况下设置索引数组可能有点棘手和分散注意力。

备注 3.3。继续前面评论的主题,当向 GPU 发出保留模式绘图调用时,游戏名称越少越好,因为每次调用的初始化成本。因此,尝试将尽可能多的几何图形打包到每个绘制调用中,更喜欢三角形条带而不是单个三角形——这意味着尽可能多地“条带化”三角形网格——并且更喜欢多次绘制调用而不是单个三角形。

顶点缓冲对象

​ OpenGL 的客户端-服务器模型意味着每次服务器需要顶点数据(例如坐标、颜色等)来执行 glDrawElements() 调用时,都必须从客户端获取。 例如,在 PC 上,这会转化为通过将 CPU(保存应用程序和数据的客户端)连接到 GPU(图形处理单元,作为执行绘图的服务器)的总线传输。 现在,通过总线访问数据通常比本地访问慢很多倍。 此外,如果之前检索过相同的数据,访问甚至可能是多余的命令,随后未更改。 为了避免这种低效率,缓冲区对象允许程序员明确要求某些特定的数据集,通常是与顶点相关的,例如顶点数组,从客户端传送到服务器并存储在那里以备将来使用。 图 3.7 是一个概念化。 我们现在将关注存储顶点数据的缓冲区对象,例如称为顶点缓冲区对象或 VBO。 让我们直接进入显示如何创建、初始化和更新 VBO 的代码。

启动 squareAnnulusVBO.cpp,它修改 squareAnnulus4.- cpp 以在 VBO 中存储与顶点相关的数据。 通过定期更改 VBO 中的颜色值,也有一个简单的动画。 图 3.8 是截图,颜色已经改变。

///
// squareAnnulusVBO.cpp
//
// 这个程序修改squareAnnulus4.cpp来存储顶点和颜色
// 一个顶点缓冲区对象 (VBO) 中的数组和另一个中的索引数组。
// 简单的彩色动画是通过映射在计时器功能的帮助下制作的
// 到顶点缓冲区并改变颜色值。
//
///#include <cmath>
#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h> #define VERTICES 0
#define INDICES 1// 开始全局变量。
// 顶点坐标向量。
static float vertices[] =
{30.0, 30.0, 0.0,10.0, 10.0, 0.0,70.0, 30.0, 0.0,90.0, 10.0, 0.0,70.0, 70.0, 0.0,90.0, 90.0, 0.0,30.0, 70.0, 0.0,10.0, 90.0, 0.0
};// 顶点颜色向量。
static float colors[] =
{0.0, 0.0, 0.0,1.0, 0.0, 0.0,0.0, 1.0, 0.0,0.0, 0.0, 1.0,1.0, 1.0, 0.0,1.0, 0.0, 1.0,0.0, 1.0, 1.0,1.0, 0.0, 0.0
};// 三角形按顺序剥离顶点索引。
static unsigned int stripIndices[] = { 0, 1, 2, 3, 4, 5, 6, 7, 0, 1 };
static unsigned int buffer[2]; // 缓冲区 ID 数组。
// 全局结束// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);// 获取指向顶点缓冲区的指针。float* bufferData = (float*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);// 随机改变颜色值。for (int i = 0; i < sizeof(colors) / sizeof(float); i++)bufferData[sizeof(vertices) / sizeof(float) + i] = (float)rand() / (float)RAND_MAX;// 释放顶点缓冲区。glUnmapBuffer(GL_ARRAY_BUFFER);// 绘制方形环。glDrawElements(GL_TRIANGLE_STRIP, 10, GL_UNSIGNED_INT, 0);glutSwapBuffers();
}// 定时器函数。
void animate(int someValue)
{glutPostRedisplay();glutTimerFunc(500, animate, 1);
}// 初始化例程。
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);glGenBuffers(2, buffer); // 生成缓冲区 ID。// 绑定顶点缓冲区并预留空间。glBindBuffer(GL_ARRAY_BUFFER, buffer[VERTICES]);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices) + sizeof(colors), NULL, GL_STATIC_DRAW);// 将顶点坐标数据复制到顶点缓冲区的前半部分。glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);// 将顶点颜色数据复制到顶点缓冲区的后半部分。glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertices), sizeof(colors), colors);// 绑定并填充索引缓冲区。glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer[INDICES]);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(stripIndices), stripIndices, GL_STATIC_DRAW);// 启用两个顶点数组:坐标和颜色。glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_COLOR_ARRAY);// 指定指向相应数据开头的顶点和颜色指针。glVertexPointer(3, GL_FLOAT, 0, 0);glColorPointer(3, GL_FLOAT, 0, (void *)(sizeof(vertices)));glutTimerFunc(5, animate, 1);
}// OpenGL 窗口重塑例程。
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.0);glMatrixMode(GL_MODELVIEW);glLoadIdentity();
}// 键盘输入处理例程。
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;default:break;}
}// 主程序。
int main(int argc, char **argv)
{glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("squareAnnulusVBO.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

让我们了解 squareAnnulusVBO.cpp 是如何工作的。 setup() 例程是首先要查看的例程。

glGenBuffers(2, buffer)

返回两个可用的缓冲区 id,我们将使用它们来标识数组缓冲区中的两个 VBO。 通常,形式为 glGenBuffers(n, buffer) 的调用会返回 n 个这样的 id。 绑定命令

glBindBuffer(GL_ARRAY_BUFFER, buffer[VERTICES])

激活第一个 VBO,buffer[VERTICE],参数 GL ARRAY BUFFER 声明它用于顶点数据。

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices) + sizeof(colors),NULL, GL_STATIC_DRAW)

为当前绑定到 GL ARRAY BUFFER 的 VBO 保留 sizeof(vertices) + sizeof(colors) 字节的空间,当然,也就是 buffer[VERTICE]。 参数NULL表示此时缓冲区未初始化数据。 最后一个参数 GL STATIC DRAW 是对 OpenGL 系统的使用提示,该数据将被指定一次并多次用作绘图命令的来源。 命令 glBufferData(target, size, *data, usage) 的一般形式为当前绑定到目标的缓冲区对象分配 size 字节的存储空间,用 *data 指向的应用程序内存数据填充它,前提是此指针不为 NULL, 还提供使用提示用法。 使用提示允许系统优化数据存储以提高性能。

接下来的两个命令

glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertices), sizeof(colors), colors);

是更新命令。 特别是,我们使用它们用坐标和颜色值更新 VBO buffer[VERTICE]。 命令 glBufferSubData(target, offset, size, *data) 的作用是将 *data 指向的应用程序数据的 size 字节复制到当前绑定到目标的缓冲区对象中,从缓冲区开头的偏移字节偏移量开始。 所以,上面的两个命令显然用顶点坐标值填充buffer[VERTICE]的前半部分,用颜色值填充后半部分。

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer[INDICES]);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(stripIndices),stripIndices, GL_STATIC_DRAW);

激活第二个 VBO buffer[INDICES],参数 GL ELEMENT ARRAY BUFFER 将其声明为索引数据,并使用来自 stripIndices 数组的数据对其进行初始化。

接下来启用顶点数组

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

最后

glVertexPointer(3, GL_FLOAT, 0, 0);
glColorPointer(3, GL_FLOAT, 0, (void *)(sizeof(vertices)));

在实验 3.3 之后的讨论中指定顶点指针; 然而,两者中的最后一个参数,而不是像早期程序中那样指向应用程序内存的指针,例如 squareAnnulus4.cpp,现在是相对于当前绑定的 VBO 开始的偏移量。 接下来是更简单的绘图程序。 这里要注意的有趣的事情是另一种缓冲区更新方法,与 glBufferSubData() 不同。

接下来是更简单的绘图程序。 这里要注意的有趣的事情是另一种缓冲区更新方法,与 glBufferSubData() 不同

float* bufferData = (float*)glMapBuffer(GL_ARRAY_BUFFER,GL_WRITE_ONLY);

将指向当前绑定到 GL ARRAY BUFFER 的 VBO 的数据存储的指针检索到变量 bufferData 中,即 buffer[VERTICE]。 第二个参数 GL WRITE ONLY 表示访问只能写入 VBO。 该命令的一般形式是 glMapBuffer(target, access),其中 target 是目标缓冲区对象,access 是 GL READ ONLY、GL WRITE ONLY 和 GL READ WRITE 之一。

for (int i = 0; i < sizeof(colors)/sizeof(float); i++) bufferData[sizeof(vertices)/sizeof(float) + i] = (float)rand()/(float)RAND_MAX;

随机更新 buffer[VERTICES] 中的颜色值,记住 bufferData 的类型意味着我们将以浮点大小(而不是之前的字节)为单位偏移到缓冲区存储中。 更新完成后,

glUnmapBuffer(GL_ARRAY_BUFFER)

释放 VBO,然后

glDrawElements(GL_TRIANGLE_STRIP, 10, GL_UNSIGNED_INT, 0);

绘制方形环。 再次注意,最后一个参数不是指向应用程序内存中索引数组开头的指针,而是相对于(索引数据)VBO 开头的偏移量。 注释掉上面的 for 循环,让缓冲区只在 setup() 中使用来自顶点和颜色数组的数据更新——当然,你会看到与 squareAnnulus?.cpp 中相同的方形环。我们包含它以定期更新颜色值并实际显示更改的环。 我们的下一个项目是缓存半球的顶点和索引数据——Multidraw.cpp。

Vertex Array Objects

一个包含许多对象的繁忙场景,每个对象都用自己的顶点数组编码,也可能缓冲在 VBO 中,可能需要在这些数组和缓冲区集之间切换多次,导致诸如 glBindBuffer() 和 glVertexPointer 之类的调用激增 。 从 3.0 版本开始,OpenGL 提供了一种巧妙的机制来处理这个问题:顶点数组对象,或 VAO,是一个容器,用于保存指定一个或多个顶点数组的所有调用。 因此,一旦所有这些指定特定对象顶点数组的调用都与 VAO 相关联,则只需在绘制对象之前激活该 VAO; 换句话说,VAO 可以被认为是封装了与对象关联的存储状态。 图 3.10 显示了该方案,尽管并非所有属性都可能存在于每个对象中。 让我们开始编码。

/
// squareAnnulusAndTriangleVAO.cpp
//
// 该程序将squareAnnulusAndTriangle.cpp的三角形添加到squareAnnulusVBO.cpp
// 并存储正方形的顶点数组和缓冲区的定义调用和
// 顶点数组对象(VAO)中的三角形。
//
// Sumanta Guha
/#include <cmath>
#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h> #define VERTICES 0
#define INDICES 1
#define ANNULUS 0
#define TRIANGLE 1// 开始全局变量。
// 环的顶点坐标向量。
static float vertices1[] =
{30.0, 30.0, 0.0,10.0, 10.0, 0.0,70.0, 30.0, 0.0,90.0, 10.0, 0.0,70.0, 70.0, 0.0,90.0, 90.0, 0.0,30.0, 70.0, 0.0,10.0, 90.0, 0.0
};// 环的顶点颜色向量。
static float colors1[] =
{0.0, 0.0, 0.0,1.0, 0.0, 0.0,0.0, 1.0, 0.0,0.0, 0.0, 1.0,1.0, 1.0, 0.0,1.0, 0.0, 1.0,0.0, 1.0, 1.0,1.0, 0.0, 0.0
};// 三角形的顶点坐标向量。
static float vertices2[] =
{40.0, 40.0, 0.0,60.0, 40.0, 0.0,60.0, 60.0, 0.0
};// 三角形的顶点颜色向量。
static float colors2[] =
{0.0, 1.0, 1.0,1.0, 0.0, 0.0,0.0, 1.0, 0.0
};// 三角形按顺序剥离顶点索引。
static unsigned int stripIndices[] = { 0, 1, 2, 3, 4, 5, 6, 7, 0, 1 };static unsigned int buffer[2]; // 缓冲区 ID 数组。static unsigned int vao[2]; // VAO id 数组。
// End globals.// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);// 绘制圆环。glBindVertexArray(vao[ANNULUS]);glDrawElements(GL_TRIANGLE_STRIP, 10, GL_UNSIGNED_INT, 0);// 绘制三角形。glBindVertexArray(vao[TRIANGLE]);glDrawArrays(GL_TRIANGLES, 0, 3);glFlush();
}// 初始化例程。
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);glGenVertexArrays(2, vao); // 生成 VAO id。// BEGIN 将 VAO id vao[ANNULUS] 绑定到随后的顶点数组调用集合。glBindVertexArray(vao[ANNULUS]);glGenBuffers(2, buffer);// 绑定顶点缓冲区并预留空间。glBindBuffer(GL_ARRAY_BUFFER, buffer[VERTICES]);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices1) + sizeof(colors1), NULL, GL_STATIC_DRAW);// 将顶点坐标数据复制到顶点缓冲区的前半部分。glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices1), vertices1);// 将顶点颜色数据复制到顶点缓冲区的后半部分。glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertices1), sizeof(colors1), colors1);// 绑定并填充索引缓冲区。glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer[INDICES]);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(stripIndices), stripIndices, GL_STATIC_DRAW);// 启用两个顶点数组:坐标和颜色。glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_COLOR_ARRAY);// 指定指向相应数据开头的顶点和颜色指针。glVertexPointer(3, GL_FLOAT, 0, 0);glColorPointer(3, GL_FLOAT, 0, (void *)(sizeof(vertices1)));// END bind VAO id vao[ANNULUS].// BEGIN 将 VAO id vao[TRIANGLE] 绑定到随后的顶点数组调用集。glBindVertexArray(vao[TRIANGLE]);glGenBuffers(1, buffer);// 绑定顶点缓冲区并预留空间。glBindBuffer(GL_ARRAY_BUFFER, buffer[VERTICES]);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices2) + sizeof(colors2), NULL, GL_STATIC_DRAW);// 将顶点坐标数据复制到顶点缓冲区的前半部分。glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices2), vertices2);// 将顶点颜色数据复制到顶点缓冲区的后半部分。glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertices2), sizeof(colors2), colors2);// 启用两个顶点数组:坐标和颜色。glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_COLOR_ARRAY);// 指定指向相应数据开头的顶点和颜色指针。glVertexPointer(3, GL_FLOAT, 0, 0);glColorPointer(3, GL_FLOAT, 0, (void *)(sizeof(vertices2)));// END bind VAO id vao[TRIANGLE].
}// OpenGL 窗口重塑例程。
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.0);glMatrixMode(GL_MODELVIEW);glLoadIdentity();
}// 键盘输入处理例程。
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;default:break;}
}// Main routine.
int main(int argc, char **argv)
{glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("squareAnnulusAndTriangleVAO.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}
glGenVertexArrays(2, vao)

在squareAnnulusAndTriangleVAO.cpp 的初始化例程中,返回数组vao 中VAO 的两个可用ID。 通常,形式为 glGenVertexArrays(n, vao) 的调用会返回 n 个这样的 id。

接下来,由 // BEGIN…-// END… 注释对括起来的第一个语句块,即,

// BEGIN bind VAO id vao[ANNULUS] ...glBindVertexArray(vao[ANNULUS]);glGenBuffers(2, buffer);...glVertexPointer(3, GL_FLOAT, 0, 0);glColorPointer(3, GL_FLOAT, 0, (void *)(sizeof(vertices1)));// END bind VAO id vao[ANNULUS].

以绑定命令 glBindVertexArray(vao[ANNULUS]) 开始,它激活第一个 VAO vao[ANNULUS]。 然后,与此 VAO 相关联的是上述块中的其余调用,它们是从 squareAnnulusVBO.cpp 逐行复制的,特别是从专用于为后者中的方形环设置 VBO 和顶点数组的块 程序。

由注释对括起来的下一个语句块

// BEGIN bind VAO id vao[TRIANGLE] ...glBindVertexArray(vao[TRIANGLE]);glGenBuffers(1, buffer);...glVertexPointer(3, GL_FLOAT, 0, 0);glColorPointer(3, GL_FLOAT, 0, (void *)(sizeof(vertices2)));// END bind VAO id vao[TRIANGLE].

同样将初始绑定后的所有调用与 VAO vao[TRIANGLE] 相关联,为三角形定义顶点数组。 现在让我们看看绘图程序,VAO 提供了很多简化。

依次激活环和三角形对应的VAO,并绘制它们。 显然,在 setup() 中将它们各自的存储状态的定义预先打包到 VAO 中,为我们节省了在 drawScene() 中绘制环形和三角形的大量调用(如果多次出现任一对象,节省会更大 )。 请注意,如果 vaoID 是由 glGenVertexArrays() 调用(如在我们的 setup() 中)新鲜返回的,则 glBindVertexArray(vaoID) 会创建一个新的 VAO,并将其与后续的顶点数组规范相关联。 另一方面,如果 vaoID 是已经创建的 VAO 的 id(如在 drawScene() 中),则 glBindVertexArray(vaoID) 激活该 VAO。

显示列表

一组命令,例如,定义一个对象,如轮子或机器人手臂,重复调用,可以缓存在所谓的显示列表中。 显示列表存储在运行显示单元的机器上,并且通常经过预编译和优化。 当需要调用特定的命令集时,程序只需调用显示列表而不是重新发出它们。 显示列表在客户端-服务器环境中特别有效,其中两者通过总线或网络进行通信并且目标是最小化流量。 一旦服务器(运行显示单元的机器)保存了显示列表,就可以通过来自客户端(运行程序的机器)的单个命令调用它。 显示列表的另一个优点是它们提供了一种封装对象的逻辑方式(想想 C++ 中的对象类)。

///
// helixList.cpp
//
// 此程序使用显示列表绘制多个螺旋线。
//
// Sumanta Guha.
///#include <cstdlib>
#include <cmath>
#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h> #define PI 3.14159265// Globals.
static unsigned int aHelix; // List index.// Initialization routine.
void setup(void)
{float t; // Angle parameter.aHelix = glGenLists(1); // Return a list index.// 开始创建显示列表。glNewList(aHelix, GL_COMPILE);// Draw a helix.glBegin(GL_LINE_STRIP);for (t = -10 * PI; t <= 10 * PI; t += PI / 20.0)glVertex3f(20 * cos(t), 20 * sin(t), t);glEnd();glEndList();// 结束创建显示列表。glClearColor(1.0, 1.0, 1.0, 0.0);
}// Drawing routine.
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);glColor3f(1.0, 1.0, 1.0);glColor3f(1.0, 0.0, 0.0);glPushMatrix();glTranslatef(0.0, 0.0, -70.0);glCallList(aHelix); // Execute display list.glPopMatrix();glColor3f(0.0, 1.0, 0.0);glPushMatrix();glTranslatef(30.0, 0.0, -70.0);glScalef(0.5, 0.5, 0.5);glCallList(aHelix); // Execute display list.glPopMatrix();glColor3f(0.0, 0.0, 1.0);glPushMatrix();glTranslatef(-25.0, 0.0, -70.0);glRotatef(90.0, 0.0, 1.0, 0.0);glCallList(aHelix); // Execute display list.glPopMatrix();glColor3f(1.0, 1.0, 0.0);glPushMatrix();glTranslatef(0.0, -20.0, -70.0);glRotatef(90.0, 0.0, 0.0, 1.0);glCallList(aHelix); // Execute display list.glPopMatrix();glColor3f(1.0, 0.0, 1.0);glPushMatrix();glTranslatef(-40.0, 40.0, -70.0);glScalef(0.5, 0.5, 0.5);glCallList(aHelix); // Execute display list.glPopMatrix();glColor3f(0.0, 1.0, 1.0);glPushMatrix();glTranslatef(30.0, 30.0, -70.0);glRotatef(90.0, 1.0, 0.0, 0.0);glCallList(aHelix); // Execute display list.glPopMatrix();glFlush();
}// OpenGL window reshape routine.
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);glMatrixMode(GL_MODELVIEW);glLoadIdentity();
}// Keyboard input processing routine.
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;default:break;}
}// Main routine.
int main(int argc, char **argv)
{glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("helixList.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

运行 helixList.cpp,它显示了相同螺旋的六个副本,经过不同的变换和着色。 图 3.12 是屏幕截图。 下面是 helixList.cpp 初始化例程的片段,它创建显示列表以绘制螺旋:

aHelix = glGenLists(1);glNewList(aHelix, GL COMPILE);glBegin(GL LINE STRIP);for(t = -10 * PI; t <= 10 * PI; t += PI/20.0)    glVertex3f(20 * cos(t), 20 * sin(t), t);glEnd();glEndList();

调用 glGenLists(range) 返回可用显示列表索引大小范围块的基(起始)值。 如果大小范围的块不可用,则返回 0。 要缓存在显示列表中的命令集——在 helixList.cpp 的情况下是螺旋绘制例程——被分组在 glNewList(listName, mode) 和 glEndList() 语句之间。 参数 listName – helixList.cpp 中的 aHelix – 是标识列表的索引。 参数模式可以是 GL COMPILE(仅存储,如在程序中)或 GL COMPILE AND EXECUTE(存储并立即执行)。 最后,helixList.cpp 的绘制例程调用 glCallList(aHelix) 六次来执行显示列表。 glPushMatrix()-glPopMatrix() 语句对,以及这些对中的建模转换(即,glTranslatef()、glRotatef()、glScalef())用于定位和缩放螺旋的副本——您可以忽略 它们暂时与显示列表无关。

剪裁平面

glClipPlane(GL CLIP PLANEi, *equation);

指定第 i 个附加剪切平面,其中方程指向指定方程系数的数组 {A, B, C, D}
A x + B y + C z + D = 0 Ax + By + Cz + D = 0 Ax+By+Cz+D=0
的新剪裁平面。 如果通过调用 glEnable(GL CLIP PLANEi) 启用该平面,则位于开放半空间中的对象的点 (x, y, z)
A x + B y + C z + D < 0 Ax + By + Cz + D < 0 Ax+By+Cz+D<0
被剪掉; 等价地,只有那些位于封闭物体上的点 (x, y, z)一半的空间
A x + B y + C z + D ≥ 0 Ax + By + Cz + D ≥ 0 Ax+By+Cz+D≥0
被渲染。 通过调用 glDisable(GL CLIP PLANEi) 禁用第 i 个附加剪切平面。 当然,剪切平面本身永远不会被绘制。 例如,在图 3.25 中,只有飞机的前部可见。

Viewports

场景的视口是绘制场景的 OpenGL 窗口区域。 默认情况下,它是整个窗口。 但是,可以使用 glViewPort() 调用来绘制较小的矩形子区域。

调用 glViewport(x, y, w, h) 将视口指定为 OpenGL 窗口的矩形子区域,其左下角位于点 (x, y),宽度为 w,高度为 h。 单位为像素,OpenGL窗口中的坐标是原点位于左下角,x轴的增加方向向右,y轴的增加方向向上。 参见图 3.33。 通过在绘图例程中调用多个 glViewport() 可以在单个 OpenGL 窗口中创建多个视口。 特定视口的内容由其定义的 glViewport() 调用之后和下一个(如果有)之前的语句定义。

转换、动画和视图

运行 box.cpp,它显示了一个轴对齐——即边平行于坐标轴——尺寸为 5 × 5 × 5 的 FreeGLUT 线框框。图 4.1 是一个屏幕截图。 请注意透视缩短——盒子的背面看起来比正面小——因为 glFrustum() 语句指定的视锥体中的透视投影。

#include <iostream>#include <GL/glew.h>#include <GL/freeglut.h> // 绘图程序。void drawScene(void){  glClear(GL_COLOR_BUFFER_BIT);   glColor3f(0.0, 0.0, 0.0);   glLoadIdentity();   // 建模转换。    glTranslatef(0.0, 0.0, -15.0);  glutWireCube(5.0); // Box.  glFlush();}// 初始化例程。void setup(void){   glClearColor(1.0, 1.0, 1.0, 0.0);}// OpenGL 窗口重塑例程。void resize(int w, int h){   glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION);    glLoadIdentity();   glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);    glMatrixMode(GL_MODELVIEW);}void keyInput(unsigned char key, int x, int y){ switch (key)    {   case 27:        exit(0);        break;  default:        break;  }}// Main routine.int main(int argc, char **argv){  glutInit(&argc, argv);  glutInitContextVersion(4, 3);   glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);   glutInitWindowSize(500, 500);   glutInitWindowPosition(100, 100);   glutCreateWindow("box.cpp");  glutDisplayFunc(drawScene); glutReshapeFunc(resize);    glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE;    glewInit(); setup();    glutMainLoop();}

注释掉

glTranslatef(0.0, 0.0, -15.0);

你现在看到了什么? 没有!

平移命令 glTranslatef(p, q, r) 在 x 方向平移 p 个单位,在 y 方向平移 q 个单位,在 z 方向平移 r 个单位。 准确地说,对象的每个点 (x, y, z) 都映射到点 (x + p, y + q, z + r),这当然是向量相加 (x, y, z) + (p, q, r)。 这一切都发生在世界空间中,因此坐标在世界系统中(如果您需要对世界空间进行记忆,请返回第 2.2 节)。 请参见图 4.2,其中还显示了由 glTranslatef(p, q, r) Translate的整个框。

命令 glutWireCube(5.0) 本身创建了一个以原点为中心的边长为 5 的盒子,其顶点位于 (±2.5, ±2.5, ±2.5),每个顶点对应于八个顶点之一 符号的可能组合。 该框显然完全位于由 glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0) 指定的视锥体之外——实际上,完全位于视平面 z = -5 的裁剪侧。 但是,glTranslatef(0.0, 0.0, -15.0) 在 -z 方向上将盒子推了 15 个单位,将其放置在视锥体内并使其可见(见图 4.3)。 这就是为什么注释掉这个语句会导致一个空白窗口的原因。

平移到视锥体中。

缩放

添加缩放命令,特别是将box.cpp的建模转换块替换为:

// Modeling transformations.glTranslatef(0.0, 0.0, -15.0);glScalef(2.0, 3.0, 1.0);

准确地说,缩放命令 glScalef(u, v, w) 将对象的每个点 (x, y, z) 映射到点 (ux, vy, wz)。 这具有在 x 方向上以因子 u、y 方向上 v 和 z 方向上 w 的因子拉伸对象的效果。

让我们看看在前面的实验中盒子是如何通过缩放来转换的。 缩放框的顶点通过变换 (x, y, z) 7→ (2x, 3y, 1z) 从原始顶点获得。 例如,(2.5, 2.5, 2.5) 7→ (5.0, 7.5, 2.5), (-2.5, 2.5, 2.5) 7→ (-5.0, 7.5, 2.5) 等等。 因此,新的顶点是 (±5.0, ±7.5, ±2.5),这给出了一个 10 × 15 × 5 的框,正如将 glScalef(2.0, 3.0, 1.0) 应用于 5 × 5 × 5 框所期望的那样。

Rotation

通过将 box.cpp 的建模转换和对象定义部分(我们更喜欢茶壶)替换为以下内容来添加旋转命令:

// Modeling transformations.glTranslatef(0.0, 0.0, -15.0);glRotatef(60.0, 0.0, 0.0, 1.0);glutWireTeapot(5.0);

旋转命令 glRotatef(A, p, q, r) 围绕一个轴从原点 O = (0, 0, 0) 到点 (p, q, r) 旋转对象。旋转量是 A◦,从 (p, q, r) 到原点测量逆时针(逆时针)。在这个实验中,然后,从 z 轴向下看,旋转是逆时针 60°。

如果您对绕轴旋转点 P 的直观想法是沿着该轴上的假想圆柱体转动的点,如图 4.12 所示,那么,您是完全正确的。不过,我们接下来要做的是将 P 的旋转 glRotatef(A, p, q, r) 描述为一个物理过程,我们希望能够找到一个公式。继续阅读时,请参阅图 4.13。假设(p, q, r) |= O,这样就确实可以画出通过(p, q, r)的轴l和原点O;事实上,如果 (p, q, r) = O 那么 glRotatef(A, p, q, r) 不是一个有效的操作。现在,首先,如果给定的点 P 位于 l 本身,那么情况很简单——旋转不会移动它。那么,假设 P 不在 l 上。这是旋转映射的方式:

  1. 垂线从 P 到 l 上的点 Q。 将线段 P Q 表示为 L。L 位于垂直于 l 到 Q 的平面 h 上。

  2. 在 (p, q, r) 的一侧沿 l 定位一个观察者在 V 处足够远的位置,以便在看向原点时能够看到 h。

  1. 将线段 L 在平面 h 上绕 Q 旋转角度 A°,由观察者测量。
  2. 如果L0是L旋转后的新位置,则P映射到L0对应的端点P 0 。

组合建模转换

通过将 box.cpp 的建模转换块替换为以下内容来应用三个建模转换:

// Modeling transformations.glTranslatef(0.0, 0.0, -15.0);glTranslatef(10.0, 0.0, 0.0);glRotatef(45.0, 0.0, 0.0, 1.0);

看起来盒子首先围绕 z 轴旋转了 45°,然后向右平移了 10 个单位。 见图 4.17(a)。 当然,第一个转换 glTranslatef(0.0, 0.0, -15.0) 用于将盒子沿 z 轴“踢”到视锥体中。

接下来,交换最后两个转换,即向右平移和旋转,将建模转换块替换为:

// Modeling transformations.glTranslatef(0.0, 0.0, -15.0);glRotatef(45.0, 0.0, 0.0, 1.0);glTranslatef(10.0, 0.0, 0.0);

看起来盒子现在首先向右平移,然后绕 z 轴旋转,使其上升。

平移命令glTranslatef(5.0, 0.0, 0.0)给出的变换t1对应矩阵
M 1 = [ 1.0 0.0 0.0 5.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] M1 = \begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 5.0 \\ \ 0.0 & 1.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} M1=⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​5.00.00.01.0​⎦⎥⎥⎤​
这是通过乘法验证的

[ 1.0 0.0 0.0 5.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] [ x y z 1 ] = [ x + 0.5 y z 1 ] \begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 5.0 \\ \ 0.0 & 1.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} \begin {bmatrix} \ x \\ \ y \\ \ z \\ \ 1 \\ \end {bmatrix} =\begin {bmatrix} \ x + 0.5 \\ \ y \\ \ z \\ \ 1 \\ \end {bmatrix} ⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​5.00.00.01.0​⎦⎥⎥⎤​⎣⎢⎢⎡​ x y z 1​⎦⎥⎥⎤​=⎣⎢⎢⎡​ x+0.5 y z 1​⎦⎥⎥⎤​
同理,验证平移命令glTranslatef(0.0, 10.0, 0.0)给出的变换t2对应矩阵
M 2 = [ 1.0 0.0 0.0 0.0 0.0 1.0 0.0 10.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] M2 = \begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 1.0 & 0.0 & 10.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} M2=⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​0.010.00.01.0​⎦⎥⎥⎤​
现在,如果将 t2 后跟 t1 应用于顶点 V ,则 V 映射如下:
V 7 → t 1 ( t 2 ( V ) ) = M 1 ( M 2 V ) = ( M 1 M 2 ) V V 7→ t1 (t2 (V )) = M1 (M2V ) = (M1 M2 )V V7→t1(t2(V))=M1(M2V)=(M1M2)V
(矩阵乘法的结合性应用于第二个等式)。 可以乘以如下矩阵来验证
( M 1 M 2 ) V = ( [ 1.0 0.0 0.0 5.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] [ 1.0 0.0 0.0 0.0 0.0 1.0 0.0 10.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] ) [ x y z 1 ] (M1M2)V = \left( \begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 5.0 \\ \ 0.0 & 1.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} \begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 1.0 & 0.0 & 10.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} \right) \begin {bmatrix} \ x \\ \ y \\ \ z \\ \ 1 \\ \end {bmatrix} (M1M2)V=⎝⎜⎜⎛​⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​5.00.00.01.0​⎦⎥⎥⎤​⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​0.010.00.01.0​⎦⎥⎥⎤​⎠⎟⎟⎞​⎣⎢⎢⎡​ x y z 1​⎦⎥⎥⎤​

= [ 1.0 0.0 0.0 5.0 0.0 1.0 0.0 10.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] [ x y z 1 ] = [ x + 5.0 y + 10.0 z 1 ] =\begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 5.0 \\ \ 0.0 & 1.0 & 0.0 & 10.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} \begin {bmatrix} \ x \\ \ y \\ \ z \\ \ 1 \\ \end {bmatrix} =\begin {bmatrix} \ x + 5.0 \\ \ y + 10.0\\ \ z \\ \ 1 \\ \end {bmatrix} =⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​5.010.00.01.0​⎦⎥⎥⎤​⎣⎢⎢⎡​ x y z 1​⎦⎥⎥⎤​=⎣⎢⎢⎡​ x+5.0 y+10.0 z 1​⎦⎥⎥⎤​
这确实对应于代码序列如何转换 (x, y, z)

glTranslatef(5.0, 0.0, 0.0);glTranslatef(0.0, 10.0, 0.0);

简单地说,两个变换的组合矩阵是它们矩阵的乘积。 这概括了。 如果连续应用变换 tn, tn−1, . . . , t 1 (按这个顺序,tn 是第一个)到顶点 V ,然后它被映射到
t 1 ( t 2 ( . . . t n ( V ) . . . ) ) = M 1 ( M 2 ( . . . ( M n V ) . . . ) ) = ( M 1 M 2... M n ) V t1 (t2 (. . . tn (V ) . . .)) = M1 (M2 (. . . (Mn V ) . . .)) = (M1 M2 . . . Mn )V t1(t2(...tn(V)...))=M1(M2(...(MnV)...))=(M1M2...Mn)V
其中矩阵 Mi 对应于变换 ti,1 ≤ i ≤ n,而且,第二个等式再次使用矩阵乘法的结合性。 人们看到变换组合的矩阵正是与各个变换对应的矩阵的乘积。

我们现在已经足以解释 OpenGL 本身是如何进行转换的。 考虑代码序列:

modelingTransformation 1; // t1modelingTransformation 2; // t2modelingTransformation 3; // t3...modelingTransformation n-1; // tn−1modelingTransformation n; // tnobject;

其中转换 ti 对应于命令modelingTransformationi

现在,OpenGL 维护了一个 4 × 4 的模型视图矩阵,称之为 M ,它最初是identity

I = [ 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] I =\begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 1.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} I=⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​0.00.00.01.0​⎦⎥⎥⎤​
由于绘图程序是在运行时处理的,遇到的每个连续建模变换的矩阵从左边乘以当前模型视图矩阵,乘积成为新的模型视图矩阵。 例如,假设 ti 的矩阵是 Mi 并且没有更早的转换,上面代码序列的模型视图矩阵 M 的连续值在下面的注释中指示:

//  M  =  I, initially modelingTransformation  1;  //  M  =  I M1 = M1 modelingTransformation  2;  //  M  =  M1 M2 modelingTransformation  3;  //  M  =  M1 M2 M3 ... modelingTransformation  n-1; // M = M1 M2 . . . Mn−1 modelingTransformation  n;  // M = M1 M2 . . . Mn−1 Mn object;

请记住,它只是上面的一个模型视图矩阵 M 其值正在变化。 此外,对象绘制语句是通过将对象的左侧顶点乘以当前模型视图矩阵来处理的,例如,对于上面的代码序列,对象的每个顶点 V 变换如下:
V → M V = ( M 1 M 2... M n − 1 M n ) V V → M V = (M1 M2 . . . Mn−1 Mn )V V→MV=(M1M2...Mn−1Mn)V
然而,由结合性

( M 1 M 2... M n − 1 M n ) V = M 1 ( M 2 ( . . . M n − 1 ( M n V ) . . . ) ) = t 1 ( t 2 ( . . . t n − 1 ( t n ( V ) . . . ) ) (M1 M2 . . . Mn−1 Mn)V =M1 (M2 (. . . Mn−1(MnV ) . . .)) =t1 (t2 (. . . tn−1 (tn (V ) . . .)) (M1M2...Mn−1Mn)V=M1(M2(...Mn−1(MnV)...))=t1(t2(...tn−1(tn(V)...))
我们看到,从前面等式的最后一行,变换 tn 应用于 V ,然后是 tn−1 等等,直到最后是 t1,确实按代码顺序向后! 因此,结论是 OpenGL 应用转换的后向顺序只是它处理矩阵的特定且完全合乎逻辑的方式的结果。

让我们以前面的示例 4.1 为基础。 我们在那里看到对应于 glTranslatef(5.0, 0.0, 0.0) 的矩阵是
M 1 = [ 1.0 0.0 0.0 5.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] M1 = \begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 5.0 \\ \ 0.0 & 1.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end{bmatrix} M1=⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​5.00.00.01.0​⎦⎥⎥⎤​
对应于 glTranslatef(0.0, 10.0, 0.0) 的是
M 2 = [ 1.0 0.0 0.0 0.0 0.0 1.0 0.0 10.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 ] M2 = \begin {bmatrix} \ 1.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 1.0 & 0.0 & 10.0 \\ \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} M2=⎣⎢⎢⎡​ 1.0 0.0 0.0 0.0​0.01.00.00.0​0.00.01.00.0​0.010.00.01.0​⎦⎥⎥⎤​
此外,对应于 glScalef(2.0, 1.0, 3.0) 的矩阵很容易验证为
M 3 = [ 2.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 0.0 0.0 0.0 1.0 ] M3 = \begin {bmatrix} \ 2.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 1.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 3.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} M3=⎣⎢⎢⎡​ 2.0 0.0 0.0 0.0​0.01.00.00.0​0.00.03.00.0​0.00.00.01.0​⎦⎥⎥⎤​
并且,对应于 glRotatef(90.0, 0.0, 1.0, 0.0) 的矩阵
M 4 = [ 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 ] M4 = \begin {bmatrix} \ 0.0 & 0.0 & 1.0 & 0.0 \\ \ 0.0 & 1.0 & 0.0 & 0.0 \\ \ 1.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} M4=⎣⎢⎢⎡​ 0.0 0.0 1.0 0.0​0.01.00.00.0​1.00.00.00.0​0.00.00.01.0​⎦⎥⎥⎤​
因此,假设没有更早的转换,在代码序列中的前四个语句之后

glTranslatef(5.0, 0.0, 0.0);glTranslatef(0.0, 10.0, 0.0);glScalef(2.0, 1.0, 3.0);glRotatef(90.0, 0.0, 1.0, 0.0)glBegin(GL POINTS);glVertex3f(4.0, 0.0, 0.0);glEnd();

当前的模型视图矩阵是
M 1 M 2 M 3 M 4 = [ 0.0 0.0 2.0 5.0 0.0 1.0 0.0 10.0 − 3.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 ] M1M2M3M4= \begin {bmatrix} \ 0.0 & 0.0 & 2.0 & 5.0 \\ \ 0.0 & 1.0 & 0.0 & 10.0 \\ \ -3.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} M1M2M3M4=⎣⎢⎢⎡​ 0.0 0.0 −3.0 0.0​0.01.00.00.0​2.00.00.00.0​5.010.00.01.0​⎦⎥⎥⎤​
读者可以通过将四个矩阵相乘来验证。 相应地,前面代码序列中的四个变换将顶点 (4.0, 0.0, 0.0) 映射到由下式给出的点
[ 0.0 0.0 2.0 5.0 0.0 1.0 0.0 10.0 − 3.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 ] [ 4.0 0.0 0.0 1 ] [ 5.0 10.0 − 12.0 1 ] \begin {bmatrix} \ 0.0 & 0.0 & 2.0 & 5.0 \\ \ 0.0 & 1.0 & 0.0 & 10.0 \\ \ -3.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} \begin {bmatrix} \ 4.0 \\ \ 0.0\\ \ 0.0 \\ \ 1 \\ \end {bmatrix} \begin {bmatrix} \ 5.0 \\ \ 10.0\\ \ -12.0 \\ \ 1 \\ \end {bmatrix} ⎣⎢⎢⎡​ 0.0 0.0 −3.0 0.0​0.01.00.00.0​2.00.00.00.0​5.010.00.01.0​⎦⎥⎥⎤​⎣⎢⎢⎡​ 4.0 0.0 0.0 1​⎦⎥⎥⎤​⎣⎢⎢⎡​ 5.0 10.0 −12.0 1​⎦⎥⎥⎤​
事实上,它是 (5.0, 10.0, -12.0)。 现在让我们“解包”左上方的 4 × 4 矩阵并写入
[ 5.0 10.0 − 12.0 1 ] = [ 0.0 0.0 2.0 5.0 0.0 1.0 0.0 10.0 − 3.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 ] [ 4.0 0.0 0.0 1 ] \begin {bmatrix} \ 5.0 \\ \ 10.0\\ \ -12.0 \\ \ 1 \\ \end {bmatrix} =\begin {bmatrix} \ 0.0 & 0.0 & 2.0 & 5.0 \\ \ 0.0 & 1.0 & 0.0 & 10.0 \\ \ -3.0 & 0.0 & 0.0 & 0.0 \\ \ 0.0 & 0.0 & 0.0 & 1.0 \\ \end {bmatrix} \begin {bmatrix} \ 4.0 \\ \ 0.0\\ \ 0.0 \\ \ 1 \\ \end {bmatrix} ⎣⎢⎢⎡​ 5.0 10.0 −12.0 1​⎦⎥⎥⎤​=⎣⎢⎢⎡​ 0.0 0.0 −3.0 0.0​0.01.00.00.0​2.00.00.00.0​5.010.00.01.0​⎦⎥⎥⎤​⎣⎢⎢⎡​ 4.0 0.0 0.0 1​⎦⎥⎥⎤​

KaTeX parse error: Undefined control sequence: \matrix at position 20: …1M2M3M4 \left[ \̲m̲a̲t̲r̲i̲x̲{ \ 4.0 \\ …

因此,通过首先应用 glRotatef(90.0, 0.0, 1.0, 0.0),然后应用 glScalef(2.0, 1.0, 3.0),然后 . . … 其实,我们接下来请读者用几何来验证上面的代数推导。

放置多个对象

接下来我们考虑应用建模转换以期望的方式相对于彼此放置多个对象的重要问题。

将原始 box.cpp 的整个显示例程替换为

// 绘图程序。void drawScene(void){    glClear(GL_COLOR_BUFFER_BIT);   glColor3f(0.0, 0.0, 0.0);   glLoadIdentity();   // 建模转换。    glTranslatef(0.0, 0.0, -15.0);  // glRotatef(45.0, 0.0, 0.0, 1.0);  glTranslatef(5.0, 0.0, 0.0);    glutWireCube(5.0); // Box.  //更多的建模转换。  glTranslatef(0.0, 10.0, 0.0);   glutWireSphere(2.0, 10, 8); // Sphere.  glFlush();}

让我们先分别了解前面实验中长方体和球体的位置,然后再相对于彼此。 单独理解展示位置实际上相当简单。 例如,要放置球体,请从它在代码中创建的位置向后,将遇到的连续建模转换(在本例中都是转换)应用于它,并在途中忽略一个非转换语句 glutWireCube()。 结果是球体以 (5.0, 10.0, -15.0) 为中心。 同样,可以看到该框以 (5.0, 0.0, -15.0) 为中心。 这种情况下的相对放置也不难。 显然,球体由 glTranslatef(0.0, 10.0, 0.0) 变换。 结果是球体的中心在盒子的垂直上方 10 个单位。

继续之前的实验,取消对 glRotatef() 语句的注释。

// 绘图程序。void drawScene(void){    glClear(GL_COLOR_BUFFER_BIT);   glColor3f(0.0, 0.0, 0.0);   glLoadIdentity();   // 建模转换。    glTranslatef(0.0, 0.0, -15.0);  glRotatef(45.0, 0.0, 0.0, 1.0); glTranslatef(5.0, 0.0, 0.0);    glutWireCube(5.0); // Box.  //更多的建模转换。  glTranslatef(0.0, 10.0, 0.0);   glutWireSphere(2.0, 10, 8); // Sphere.  glFlush();}

图 是屏幕截图。 再次,各个展示位置相当简单。 从创建位置向后工作,我们看到,在转换为 (5.0, 10.0, 0.0) 后,球体围绕 z 轴逆时针旋转 45°,当然,最终在 -z 方向上推动了 15 个单位 方向。 我们不会计算其中心的确切最终坐标。 在这种情况下,这是特别有趣的相对位置。 球体不再垂直于盒子上方,尽管它们之间的转换是仍然是 glTranslatef(0.0, 10.0, 0.0)。

在尝试解释发生了什么之前,让我们先回到基础知识。 考虑下面绘制两个对象的代码序列:

modelingTransformation 1; // t1modelingTransformation 2; // t2...modelingTransformation n-1; // tn−1modelingTransformation n; // tnobject1;modelingTransformation n+1; // tn+1...modelingTransformation m; // tmobject2;

假设modelingTransformation i指定的变换ti对应于矩阵Mi,对于1≤i≤m,modelview矩阵M的连续取值如下所示:

// M = I, initiallymodelingTransformation 1; // M = IM1 = M1modelingTransformation 2; // M = M1 M2...modelingTransformation n-1; // M = M1 M2 . . . Mn−1modelingTransformation n; // M = M1 M2 . . . Mn−1 Mnobject1; // M does not changemodelingTransformation n+1; // M = M1 M2 . . . Mn−1 Mn Mn+1...modelingTransformation m; // M = M1 M2 . . . Mn−1 Mn Mn+1 . . . Mmobject2;

因此,最终 object2 调用的每个顶点 V 都根据以下条件进行变换:
V → ( M 1... M m − 1 M m ) V = t 1 ( . . . t m − 1 ( t m ( V ) ) . . . ) V → (M1 . . . Mm−1 Mm )V = t1(. . . tm−1 (tm(V )) . . .) V→(M1...Mm−1Mm)V=t1(...tm−1(tm(V))...)

如果在代码中 object1 在 object2 之前,则 object2 在 object1 的局部坐标系中的位置由两者之间的转换语句决定,仅此而已。

这个命题说的是,如果在代码中 object1 在 object2 之前,那么后者在前者的坐标系中被冻结在一个位置,这个位置完全由两者之间的转换语句决定。 因此,相对于对象 1 移动对象 2 需要改变它们之间的变换。 正如我们将看到的,这一点的实际重要性怎么强调都不为过。 现在让我们根据前面的命题来尝试理解实验 4.12 中球体相对于盒子的相对位置。 我们将通过一种常用的解构代码技术来做到这一点,即在首先将它们全部剥离后逐步添加回转换。

建模转换和对象定义部分如下:

// Modeling transformations.glTranslatef(0.0, 0.0, -15.0);glRotatef(45.0, 0.0, 0.0, 1.0);glTranslatef(5.0, 0.0, 0.0);glutWireCube(5.0); // Box.//More modeling transformations.glTranslatef (0.0, 10.0, 0.0);glutWireSphere (2.0, 10, 8); // Sphere.

Modelview Matrix Stack 和隔离变换

模型视图矩阵,我们已经描述为通过右侧乘法对转换进行建模来修改,实际上是模型视图矩阵堆栈的最顶层之一。 这个特定的矩阵称为当前模型视图矩阵。 事实上,OpenGL 维护着三种不同的矩阵栈:模型视图、投影和纹理。 glMatrixMode(mode) 命令,其中 mode 是 GL MODELVIEW、GL PROJECTION 或 GL TEXTURE,确定当前处于活动状态的堆栈。 这是一个激发使用模型视图矩阵堆栈的实验:

// 绘图程序。void drawScene(void){    glClear(GL_COLOR_BUFFER_BIT);   glColor3f(0.0, 0.0, 0.0);   glLoadIdentity();   glTranslatef(0.0, 0.0, -15.0);  glPushMatrix(); glScalef(1.0, 2.0, 1.0);    glutWireCube(5.0); // Box torso.    glPopMatrix();  glTranslatef(0.0, 7.0, 0.0);    glutWireSphere(2.0, 10, 8); // Spherical head.      glPopMatrix();  glFlush();}

Controlling Animation

在分析动画程序之前,我们首先需要解释几个动画——相关技术。

/        // rotatingHelix1.cpp 这个程序,基于 helix.cpp,通过旋转动画一个螺旋// 它通过键盘按压围绕其轴。 相互作用:// 按空格键转动螺旋线。  Sumanta Guha./#include <cstdlib>#include <cmath>#include <iostream>#include <GL/glew.h>#include <GL/freeglut.h> #define PI 3.14159265// Globals.static float angle = 0.0; // Angle of rotation.// Drawing routine.void drawScene(void){   float R = 20.0; // Radius of helix.    float t; // Angle parameter along helix.    glClear(GL_COLOR_BUFFER_BIT);   glColor3f(0.0, 0.0, 0.0);   glPushMatrix(); // 技巧:在旋转之前将螺旋线的轴沿 y 轴对齐 // 然后将其返回到其原始位置。    glTranslatef(0.0, 0.0, -60.0);  glRotatef(angle, 0.0, 1.0, 0.0);    glTranslatef(0.0, 0.0, 60.0);   glBegin(GL_LINE_STRIP); for (t = -10 * PI; t <= 10 * PI; t += PI / 20.0)     glVertex3f(R * cos(t), t, R * sin(t) - 60.0);   glEnd();    glPopMatrix();  glutSwapBuffers();}// Initialization routine.void setup(void){  glClearColor(1.0, 1.0, 1.0, 0.0);}// OpenGL window reshape routine.void resize(int w, int h){   glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION);    glLoadIdentity();   glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);    glMatrixMode(GL_MODELVIEW); glLoadIdentity();}// Routine to increase the rotation angle.void increaseAngle(void){   angle += 5.0; if (angle > 360.0) angle -= 360.0;  glutPostRedisplay();}// Keyboard input processing routine.void keyInput(unsigned char key, int x, int y){   switch (key)    {   case 27:        exit(0);        break;  case ' ':     increaseAngle();        break;  default:        break;  }}// Routine to output interaction instructions to the C++ window.void printInteraction(void){    std::cout << "Interaction:" << std::endl; std::cout << "Press space to turn the helix." << std::endl;}// Main routine.int main(int argc, char **argv){  printInteraction(); glutInit(&argc, argv);  glutInitContextVersion(4, 3);   glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);   glutInitWindowSize(500, 500);   glutInitWindowPosition(100, 100);   glutCreateWindow("rotatingHelix1.cpp");   glutDisplayFunc(drawScene); glutReshapeFunc(resize);    glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE;    glewInit(); setup();    glutMainLoop();}
  1. 交互式地,通过键盘或鼠标输入,在回调例程的帮助下调用转换。 实验 4.16。 运行rotatingHelix1.cpp,每次按下空格都会调用increaseAngle() 例程来旋转螺旋。 请注意 increaseAngle() 中的 glutPostRedisplay() 命令,它要求重绘屏幕。 按住空格键不断转动螺旋线。 图 4.27 是屏幕截图。 结尾

  1. 自动地,通过使用语句 glutIdleFunc(idle function) 指定一个称为空闲函数的函数空闲函数。 只要没有 OpenGL 事件以其他方式挂起,就会调用 idle 函数。 实验 4.17。 运行rotatingHelix2.cpp,对rotationHelix1.cpp 稍作修改,其中按下空格会导致例程increaseAngle() 和NULL(什么都不做)交替指定为空闲函数。 动画的速度取决于处理器的速度和执行空闲功能的可用性——用户无法影响它。
#include <cstdlib>#include <cmath>#include <iostream>#include <GL/glew.h>#include <GL/freeglut.h> #define PI 3.14159265// Globals.static int isAnimate = 0; // Animated?static float angle = 0.0; // Angle of rotation.// Drawing routine.void drawScene(void){  float R = 20.0; // Radius of helix.    float t; // Angle parameter along helix.    glClear(GL_COLOR_BUFFER_BIT);   glColor3f(0.0, 0.0, 0.0);   glPushMatrix(); // The Trick: to align the axis of the helix along the y-axis prior to rotation // and then return it to its original location. glTranslatef(0.0, 0.0, -60.0);  glRotatef(angle, 0.0, 1.0, 0.0);    glTranslatef(0.0, 0.0, 60.0);   glBegin(GL_LINE_STRIP); for (t = -10 * PI; t <= 10 * PI; t += PI / 20.0)     glVertex3f(R * cos(t), t, R * sin(t) - 60.0);   glEnd();    glPopMatrix();  glutSwapBuffers();}// Initialization routine.void setup(void){  glClearColor(1.0, 1.0, 1.0, 0.0);}// OpenGL window reshape routine.void resize(int w, int h){   glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION);    glLoadIdentity();   glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);    glMatrixMode(GL_MODELVIEW); glLoadIdentity();}// Routine to increase the rotation angle.void increaseAngle(void){   angle += 5.0; if (angle > 360.0) angle -= 360.0;  glutPostRedisplay();}// Keyboard input processing routine.void keyInput(unsigned char key, int x, int y){   switch (key)    {   case 27:        exit(0);        break;  case ' ':     if (isAnimate)      {           isAnimate = 0;         glutIdleFunc(NULL);     }       else        {           isAnimate = 1;         glutIdleFunc(increaseAngle);        }       break;  default:        break;  }}// Routine to output interaction instructions to the C++ window.void printInteraction(void){    std::cout << "Interaction:" << std::endl; std::cout << "Press space to toggle between animation on and off." << std::endl;}// Main routine.int main(int argc, char **argv){ printInteraction(); glutInit(&argc, argv);  glutInitContextVersion(4, 3);   glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);   glutInitWindowSize(500, 500);   glutInitWindowPosition(100, 100);   glutCreateWindow("rotatingHelix2.cpp");   glutDisplayFunc(drawScene); glutReshapeFunc(resize);    glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE;    glewInit(); setup();    glutMainLoop();}

  1. 半自动地,通过指定一个称为计时器函数的例行计时器函数,并调用 glutTimerFunc(period, timer function, value)。 在执行 glutTimerFunc() 语句并传递给它的参数值后,定时器函数在周期毫秒内被调用。
#include <cstdlib>
#include <cmath>
#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h> #define PI 3.14159265// Globals.
static int isAnimate = 0; // Animated?
static int animationPeriod = 50; // Time interval between frames.
static float angle = 0.0; // Angle of rotation.// Drawing routine.
void drawScene(void)
{float R = 20.0; // Radius of helix.float t; // Angle parameter along helix.glClear(GL_COLOR_BUFFER_BIT);glColor3f(0.0, 0.0, 0.0);glPushMatrix();// The Trick: to align the axis of the helix along the y-axis prior to rotation// and then return it to its original location.glTranslatef(0.0, 0.0, -60.0);glRotatef(angle, 0.0, 1.0, 0.0);glTranslatef(0.0, 0.0, 60.0);glBegin(GL_LINE_STRIP);for (t = -10 * PI; t <= 10 * PI; t += PI / 20.0)glVertex3f(R * cos(t), t, R * sin(t) - 60.0);glEnd();glPopMatrix();glutSwapBuffers();
}// Initialization routine.
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);
}// OpenGL window reshape routine.
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);glMatrixMode(GL_MODELVIEW);glLoadIdentity();
}// Routine to increase the rotation angle.
void increaseAngle(void)
{angle += 5.0;if (angle > 360.0) angle -= 360.0;
}// Routine to animate with a recursive call made after animationPeriod msecs.
void animate(int value)
{if (isAnimate){increaseAngle();glutPostRedisplay();glutTimerFunc(animationPeriod, animate, 1);}
}// Keyboard input processing routine.
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;case ' ':if (isAnimate) isAnimate = 0;else{isAnimate = 1;animate(1);}break;default:break;}
}// Callback routine for non-ASCII key entry.
void specialKeyInput(int key, int x, int y)
{if (key == GLUT_KEY_DOWN) animationPeriod += 5;if (key == GLUT_KEY_UP)if (animationPeriod > 5) animationPeriod -= 5;glutPostRedisplay();
}// Routine to output interaction instructions to the C++ window.
void printInteraction(void)
{std::cout << "Interaction:" << std::endl;std::cout << "Press space to toggle between animation on and off." << std::endl<< "Press the up/down arrow keys to speed up/slow down animation." << std::endl;
}// Main routine.
int main(int argc, char **argv)
{printInteraction();glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("rotatingHelix3.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glutSpecialFunc(specialKeyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

双缓冲

平滑动画的第二个关键技术是双缓冲。

颜色缓冲区是一个空间,几乎总是在 GPU 内存中,它存储光栅像素的 RGBA 值,通常为 R、G、B 和 A 中的每个 8 位,每个像素总共 32 个动画位。因此,它是一个颜色缓冲区,用于保存单个帧的数据,并在该帧被绘制到监视器时被读取。在双缓冲系统中为两个颜色缓冲区提供了空间,这样一个缓冲区(可视缓冲区)保存当前显示在监视器上的帧,而下一帧正在第二个缓冲区(可绘制缓冲区)中绘制。当可绘制缓冲区中的帧绘制完成时,缓冲区被交换,以便下一帧现在变得可见,同时开始绘制紧随其后的帧。这个绘制和交换循环在动画中重复。图 4.29 说明了该过程。术语:可视缓冲区通常称为前端缓冲区或主缓冲区,而可绘制缓冲区称为后台缓冲区或交换缓冲区。任一缓冲区也称为刷新缓冲区。双缓冲通过对观众隐藏连续帧之间的过渡,极大地提高了动画质量。另一方面,使用单缓冲,观看者“看到”在包含当前帧的同一缓冲区中绘制的下一帧。结果可能是令人不快的重影,之所以这样称呼是因为在创建下一个图像时前一个图像仍然存在。双缓冲显示模式是通过在 main 中调用 glutInitDisplayMode() 并使用 GLUT DOUBLE 作为参数之一来启用的(而不是 GLUT SINGLE 并在绘图例程的末尾插入对 glutSwapBuffers() 的调用(而不是 glFlush())。 rotationHelix*.cpp 程序都是双缓冲的。

通过在 main 中的 glutInitDisplayMode() 调用中将 GLUT DOUBLE 替换为 GLUT SINGLE,并在绘图例程中将 glutSwapBuffers() 替换为 glFlush(),从而在rotationHelix2.cpp 中禁用双缓冲。

备注 4.6。双缓冲通常在 GPU 中实现,两个缓冲区都位于 VRAM 中,通过简单地将指针的值更改为 VRAM 中可显示数据的开始来实现交换。这种在两个缓冲区的位置之间来回移动的方法称为乒乓缓冲。

备注 4.7。本节开头描述的转换和绘制动画循环中的“绘制”作为动画的实现方式,与上面描述的绘制和交换循环中的“绘制”作为双缓冲的方式之间存在差异运作。第一个是程序员发起的操作——通常使用 glutPostRedisplay() 调用——其中世界空间被投影和缩放(回忆第 2 章的拍摄和打印)并光栅化到颜色缓冲区中。另一方面,draw-and-swap 中的“draw”实际上是用颜色缓冲区的内容绘制屏幕,特别是 OpenGL 窗口——一个更合适的术语可能是“渲染”。事实上,我们在实验 4.19 中提到了这种差异。

备注 4.8。如果循环的速率与显示器安装的刷新率不同步,则渲染帧的绘制和交换循环可能会导致视觉伪影。例如,如果 GPU 绘制帧的速率为每秒 80 帧,而显示器的刷新率为 60Hz,即每秒 60 帧,那么 GPU 只需 0.0125 秒。创建一个帧,而较慢的监视器需要 0.0167 秒。显示一个。因此,如果在前缓冲区绘制完成后立即将后台缓冲区与前台缓冲区交换,那么当仅渲染前一帧的 75% (= 0.0125/0.0167) 时,监视器开始显示新帧,可能会导致分散注意力的屏幕撕裂。可以通过施加所谓的垂直同步或 v-sync 来避免屏幕撕裂,它允许后台缓冲区仅在前台缓冲区完全渲染后,即在显示器刷新后才与前台缓冲区交换。然而,V-sync 有其自身的问题,双缓冲系统中最明显的问题是 GPU 必须被锁定,无法写入后台缓冲区

正在等待垂直空白中断或 VBI ,这是监视器刷新已完成的信号。不仅 GPU 被强制使用不足,帧的绘制时间和显示时间之间也存在滞后。这个问题可以通过实现三重缓冲来稍微缓解,其中有两个后台缓冲区而不是一个,并且 GPU 在完成当前帧后立即继续在另一个后台缓冲区中绘制下一帧。 GPU 继续在两个后台缓冲区之间来回移动,直到发生 VBI,此时最近完成的后台缓冲区被交换到前面。请记住,无法在 OpenGL 中启用垂直同步以及三重缓冲。为此,必须访问窗口管理系统。在继续编写代码之前,CG 动画与现实世界中的动画之间的差异需要仔细研究,因为它乍一看可能不直观。 CG 中的动画不包括发出命令,例如以 100 像素/秒的速度向上移动航天器图像;相反,它包括发出一个命令来创建和渲染一个(静态)框架,航天器在其当前位置,然后创建一个新的(静态)框架,航天器绘制在一个位置为 X(由程序员)在它的旧框架之前渲染这个帧,然后创建另一个框架,工艺向前移动另一个 X 并渲染,依此类推(见图 4.30)。换句话说,动画是逐帧的。取决于 X 和帧速率是工艺似乎在监视器上前进的速度。此外,每一帧都是从头开始,逐像素绘制,没有从前一帧中提取任何内容。因此,例如,我们不能要求与图 4.30 中静止小行星对应的像素从第 0 帧开始“保持原样”,而我们只重绘航天器;相反,每个连续的帧都是全新的。一个有趣的结果是,我们可以使用与(更真实的)帧 0、帧 1、帧 2 完全相同的计算工作来渲染图 4.30 中的(相当奇怪的)序列帧 0、帧 2、帧 1。当然,在现实世界中,航天器从第 0 帧移动到第 1 帧,然后再移动到第 2 帧,每一帧都是下一帧的实际时间前兆;在 CG 中并非如此,其中每一帧都是独立绘制的,渲染顺序由程序员决定。

动画代码

球围绕圆环飞行

运行 ballAndTorus.cpp。 按空间开始球在环面周围飞行(纵向旋转)和进出(横向旋转)。 按向上和向下箭头键更改动画的速度。 按“x/X”、“y/Y”和“z/Z”改变视点。 图 4.31 是屏幕截图。 球的动画很有趣,我们将对其进行解构。 注释掉球块中的所有建模变换,如下:

#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h> // Globals.
static float latAngle = 0.0; // Latitudinal angle.
static float longAngle = 0.0; // Longitudinal angle.
static float Xangle = 0.0, Yangle = 0.0, Zangle = 0.0; // Angles to rotate scene.
static int isAnimate = 0; // Animated?
static int animationPeriod = 100; // Time interval between frames.// Drawing routine.
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glLoadIdentity();glTranslatef(0.0, 0.0, -25.0);// Rotate scene.glRotatef(Zangle, 0.0, 0.0, 1.0);glRotatef(Yangle, 0.0, 1.0, 0.0);glRotatef(Xangle, 1.0, 0.0, 0.0);// Fixed torus.glColor3f(0.0, 1.0, 0.0);glutWireTorus(2.0, 12.0, 20, 20);// Begin revolving ball.glRotatef(longAngle, 0.0, 0.0, 1.0);glTranslatef(12.0, 0.0, 0.0);glRotatef(latAngle, 0.0, 1.0, 0.0);glTranslatef(-12.0, 0.0, 0.0);glTranslatef(20.0, 0.0, 0.0);glColor3f(0.0, 0.0, 1.0);glutWireSphere(2.0, 10, 10);// End revolving ball.glutSwapBuffers();
}// Timer function.
void animate(int value)
{if (isAnimate){latAngle += 5.0;if (latAngle > 360.0) latAngle -= 360.0;longAngle += 1.0;if (longAngle > 360.0) longAngle -= 360.0;glutPostRedisplay();glutTimerFunc(animationPeriod, animate, 1);}
}// Initialization routine.
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);glEnable(GL_DEPTH_TEST); // Enable depth testing.
}// OpenGL window reshape routine.
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);glMatrixMode(GL_MODELVIEW);
}// Keyboard input processing routine.
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;case ' ':if (isAnimate) isAnimate = 0;else{isAnimate = 1;animate(1);}break;case 'x':Xangle += 5.0;if (Xangle > 360.0) Xangle -= 360.0;glutPostRedisplay();break;case 'X':Xangle -= 5.0;if (Xangle < 0.0) Xangle += 360.0;glutPostRedisplay();break;case 'y':Yangle += 5.0;if (Yangle > 360.0) Yangle -= 360.0;glutPostRedisplay();break;case 'Y':Yangle -= 5.0;if (Yangle < 0.0) Yangle += 360.0;glutPostRedisplay();break;case 'z':Zangle += 5.0;if (Zangle > 360.0) Zangle -= 360.0;glutPostRedisplay();break;case 'Z':Zangle -= 5.0;if (Zangle < 0.0) Zangle += 360.0;glutPostRedisplay();break;default:break;}
}// Callback routine for non-ASCII key entry.
void specialKeyInput(int key, int x, int y)
{if (key == GLUT_KEY_DOWN) animationPeriod += 5;if (key == GLUT_KEY_UP) if (animationPeriod > 5) animationPeriod -= 5;glutPostRedisplay();
}// Routine to output interaction instructions to the C++ window.
void printInteraction(void)
{std::cout << "Interaction:" << std::endl;std::cout << "Press space to toggle between animation on and off." << std::endl<< "Press the up/down arrow keys to speed up/slow down animation." << std::endl<< "Press the x, X, y, Y, z, Z keys to rotate the scene." << std::endl;
}// Main routine.
int main(int argc, char **argv)
{printInteraction();glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("ballAndTorus.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glutSpecialFunc(specialKeyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

球的预期纬度旋转是通过环面的中间进出圆 C1。 C1 的半径,称为环面的外半径,为 12.0,由 glutWireTorus(2.0, 12.0, 20, 20) 的第二个参数指定。此外,C1 以原点为中心并位于 xy 平面上。因此,暂时忽略纵向运动,球从其起始位置的横向旋转是围绕平行于 y 轴(L 与 C1 相切)通过 (12, 0, 0) 的线 L。这种旋转将导致球的中心沿以 (12, 0, 0) 为中心的圆 C2 移动,位于 xz 平面上,半径为 8。 由于 glRotatef() 始终围绕径向轴旋转,因此如何获得想要绕 L 旋转,一条非径向线?使用技巧(如果你不记得,请参阅示例 4.3)。首先,向左平移,使 L 沿 y 轴对齐,然后绕 y 轴旋转,最后反转第一个平移,使 L 回到原来的位置。这意味着取消注释相应的三个建模转换,如下所示:

// Drawing routine.void drawScene(void){ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity();   glTranslatef(0.0, 0.0, -25.0);  // Rotate scene.    glRotatef(Zangle, 0.0, 0.0, 1.0);   glRotatef(Yangle, 0.0, 1.0, 0.0);   glRotatef(Xangle, 1.0, 0.0, 0.0);   // Fixed torus. glColor3f(0.0, 1.0, 0.0);   glutWireTorus(2.0, 12.0, 20, 20);   // Begin revolving ball.    glRotatef(longAngle, 0.0, 0.0, 1.0);    glTranslatef(12.0, 0.0, 0.0);   glRotatef(latAngle, 0.0, 1.0, 0.0); glTranslatef(-12.0, 0.0, 0.0);  glTranslatef(20.0, 0.0, 0.0);   glColor3f(0.0, 0.0, 1.0);   glutWireSphere(2.0, 10, 10);    // End revolving ball.  glutSwapBuffers();}

按空格键仅查看纬度旋转。 注意:两个连续的Translate语句可以合二为一,但这样代码就不太容易解析了。 最后,取消注释 glRotatef(longAngle, 0.0, 0.0, 1.0) 以实现绕 z 轴的纵向旋转。 纵向旋转的角速度设置为比横向旋转慢 5 倍——animate() 例程中 longAngle 和 latAngle 的增量分别为 1° 和 5°。 这意味着球在完成一圈之前会进出环面五次。

我们想添加一个与ballAndTorus.cpp 的球一起标记的卫星。 以下代码添加到绘图例程的末尾 - 就在 // 结束旋转球之后。

void drawScene(void){    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity();   glTranslatef(0.0, 0.0, -25.0);  // Rotate scene.    glRotatef(Zangle, 0.0, 0.0, 1.0);   glRotatef(Yangle, 0.0, 1.0, 0.0);   glRotatef(Xangle, 1.0, 0.0, 0.0);   // Fixed torus. glColor3f(0.0, 1.0, 0.0);   glutWireTorus(2.0, 12.0, 20, 20);   // Begin revolving ball.    glRotatef(longAngle, 0.0, 0.0, 1.0);    glTranslatef(12.0, 0.0, 0.0);   glRotatef(latAngle, 0.0, 1.0, 0.0); glTranslatef(-12.0, 0.0, 0.0);  glTranslatef(20.0, 0.0, 0.0);       glColor3f(0.0, 0.0, 1.0);   glutWireSphere(2.0, 10, 10);    // End revolving ball.  glTranslatef(4.0, 0.0, 0.0);    // Satellite    glColor3f(1.0, 0.0, 0.0);   glutWireSphere(0.5, 5, 5);  glutSwapBuffers();}

对卫星进行旋转

飘扬的旗帜

#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h>
#include <cmath>#define PI 3.14159265// Globals.
static float s = 0.0; // 正弦曲线的偏移量。
static int p = 20; // 沿标志长度(即正弦曲线部分)的段数。
static int q = 4; // 沿标志宽度的段数。
static float *vertices = NULL; // 包含标志上顶点的顶点数组。
static float Xangle = 60.0, Yangle = 0.0, Zangle = 0.0; // 旋转场景的角度。
static int isAnimate = 0; // 动画?
static int animationPeriod = 100; // 帧之间的时间间隔。// 使用标志顶点坐标填充顶点数组的例程。
void fillVertexArray(void)
{int k = 0;for (int j = 0; j <= q; j++)for (int i = 0; i <= p; i++){vertices[k++] = -20.0 + 40.0 * (float)i / p;vertices[k++] = 5.0 * sin(s + (float)i / p * 1.8 * PI) - 5.0 * sin(s);vertices[k++] = -10.0 + 20.0 * (float)j / q;}
}// 绘图程序。
void drawScene(void)
{int i, j;glVertexPointer(3, GL_FLOAT, 0, vertices);glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glLoadIdentity();glTranslatef(0.0, 0.0, -35.0);// 旋转场景。glRotatef(Zangle, 0.0, 0.0, 1.0);glRotatef(Yangle, 0.0, 1.0, 0.0);glRotatef(Xangle, 1.0, 0.0, 0.0);// 填充顶点数组。fillVertexArray();// Flag.glColor3f(0.0, 0.0, 0.0);for (j = 0; j < q; j++){glBegin(GL_TRIANGLE_STRIP);for (i = 0; i <= p; i++){glArrayElement((j + 1) * (p + 1) + i);glArrayElement(j * (p + 1) + i);}glEnd();}// 旗杆。glLineWidth(5.0);glBegin(GL_LINES);glVertex3f(-20.0, 0.0, -10.0);glVertex3f(-20.0, 0.0, 20.0);glEnd();glLineWidth(1.0);glutSwapBuffers();
}// 定时器函数。
void animate(int value)
{if (isAnimate){s += 1.8 * PI / p;if (s > 2.0 * PI) s -= 2.0 * PI;glutPostRedisplay();glutTimerFunc(animationPeriod, animate, 1);}
}// 初始化例程。
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);glEnable(GL_DEPTH_TEST); // 启用深度测试。glEnableClientState(GL_VERTEX_ARRAY);vertices = new float[3 * (p + 1) * (q + 1)]; // 具有 p 和 q 值的数组分配。
}// OpenGL 窗口重绘例程。
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);glMatrixMode(GL_MODELVIEW);
}// 键盘输入处理例程。
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;case ' ':if (isAnimate) isAnimate = 0;else{isAnimate = 1;animate(1);}break;case 'x':Xangle += 5.0;if (Xangle > 360.0) Xangle -= 360.0;glutPostRedisplay();break;case 'X':Xangle -= 5.0;if (Xangle < 0.0) Xangle += 360.0;glutPostRedisplay();break;case 'y':Yangle += 5.0;if (Yangle > 360.0) Yangle -= 360.0;glutPostRedisplay();break;case 'Y':Yangle -= 5.0;if (Yangle < 0.0) Yangle += 360.0;glutPostRedisplay();break;case 'z':Zangle += 5.0;if (Zangle > 360.0) Zangle -= 360.0;glutPostRedisplay();break;case 'Z':Zangle -= 5.0;if (Zangle < 0.0) Zangle += 360.0;glutPostRedisplay();break;default:break;}
}// 非 ASCII 键输入的回调例程。
void specialKeyInput(int key, int x, int y)
{if (key == GLUT_KEY_DOWN) animationPeriod += 5;if (key == GLUT_KEY_UP) if (animationPeriod > 5) animationPeriod -= 5;glutPostRedisplay();
}// 将交互指令输出到 C++ 窗口的例程。
void printInteraction(void)
{std::cout << "Interaction:" << std::endl;std::cout << "Press space to toggle between animation on and off." << std::endl<< "Press the up/down arrow keys to speed up/slow down animation." << std::endl<< "Press the x, X, y, Y, z, Z keys to rotate the scene." << std::endl;
}// Main routine.
int main(int argc, char **argv)
{printInteraction();glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("flag.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glutSpecialFunc(specialKeyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

旗帜通过动画改变形状,而在早期每个实验中的动画对象,一个球,从未改变形状,但始终作为刚性对象移动。 那么,旗帜是如何创建和改变形状的呢? 从几何上可以看出,从glBegin(GL TRIANGLE STRIP)。 . . glEnd() 块在绘图例程中,标志是一个矩形表,包括四个三角形条带,每个三角形条带 40 个三角形。 标志的顶点取自顶点数组,该数组由 fillVertexArray() 例程填充,其中包含以下语句:

// 使用标志顶点坐标填充顶点数组的例程。void fillVertexArray(void){    int k = 0;    for (int j = 0; j <= q; j++)        for (int i = 0; i <= p; i++)        {            vertices[k++] = -20.0 + 40.0 * (float)i / p;            vertices[k++] = 5.0 * sin(s + (float)i / p * 1.8 * PI) - 5.0 * sin(s);            vertices[k++] = -10.0 + 20.0 * (float)j / q;        }}

内部 for 循环中的第一条语句缩放标志以占据沿 x 轴从 -20 到 20 的范围——实际上,用 t 表示 (float)i/p,我们看到 t 从 0 到 1 变化为 i 从 0 到 p 变化,因此 x 从 -20 到 20 变化。第三条语句同样将标志缩放为在 z 方向上占据 -10 到 10。 第二个语句通过将其顶点的 y 值写入为 t 的正弦函数来生成标志的波纹度,特别是,
y = 5 s i n ( s + 1.8 π t ) − 5 s i n s y = 5 sin(s + 1.8πt) − 5 sin s y=5sin(s+1.8πt)−5sins
其中 s 是移位变量。 让我们暂时忽略最后的“− 5 sin s”,这样方程就变得简单了
y = 5 s i n ( s + 1.8 π t ) y = 5 sin(s + 1.8πt) y=5sin(s+1.8πt)
因为 t 从 0 变化到 1,因为 i 从 0 变化到 p,所以上面的等式绘制了一条从 x = s 到 x = s + 1.8π 的正弦曲线(前面的比例因子 5 只是为了调整标志的大小) 适当)。 随着 animate() 例程增加 s,这个绘制的弧,总是沿 x 方向 1.8π,沿正弦曲线向右移动。 参见图 4.38。 或者,等效地,人们可以将向左移动的正弦曲线本身视为从固定窗口绘制的一条弧线。 这种运动正是在旗帜的轮廓中产生波浪的原因。 现在,让我们回到原来的等式(4.3):加上“- 5 sin s”,使得标志的 t = 0 端的值是固定的,特别是,该值始终为 0,因为我们想要 附着在杆子上的边缘静止不动。 为什么我们选择正弦曲线绘制弧的 x 范围大小为 1.8π,而不是完整的 2π 周期? 我们将让读者在下面的练习中研究这一点。

小丑头

#include <cstdlib>#include <cmath>#include <iostream>#include <GL/glew.h>#include <GL/freeglut.h>#define PI 3.14159265static float angle = 0.0; // 帽子的旋转角度。static int isAnimate = 0; // 动画?static int animationPeriod = 100; // 帧之间的时间间隔。void drawScene(void){    float t; // 沿螺旋线的参数。    glClear(GL_COLOR_BUFFER_BIT);    glLoadIdentity();    // 将场景置于视锥体中。    glTranslatef(0.0, 0.0, -9.0);    // 头。    glColor3f(0.0, 0.0, 1.0);    glutWireSphere(2.0, 20, 20);    glutSwapBuffers();}// 常规增加旋转角度。void increaseAngle(void){    angle += 5.0;    if (angle > 360.0) angle -= 360.0;}//定时器功能。void animate(int value){    if (isAnimate)    {        increaseAngle();        glutPostRedisplay();        glutTimerFunc(animationPeriod, animate, 1);    }}// 初始化例程。void setup(void){    glClearColor(1.0, 1.0, 1.0, 0.0);}// OpenGL 窗口重绘例程。void resize(int w, int h){    glViewport(0, 0, w, h);    glMatrixMode(GL_PROJECTION);    glLoadIdentity();    glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);    glMatrixMode(GL_MODELVIEW);}// 键盘输入处理例程。void keyInput(unsigned char key, int x, int y){    switch (key)    {    case 27:        exit(0);        break;    case ' ':        if (isAnimate) isAnimate = 0;        else        {            isAnimate = 1;            animate(1);        }        glutPostRedisplay();        break;    default:        break;    }}// 非 ASCII 键输入的回调例程。void specialKeyInput(int key, int x, int y){    if (key == GLUT_KEY_DOWN) animationPeriod += 5;    if (key == GLUT_KEY_UP) if (animationPeriod > 5) animationPeriod -= 5;    glutPostRedisplay();}// 将交互指令输出到 C++ 窗口的例程。void printInteraction(void){    std::cout << "Interaction:" << std::endl;    std::cout << "Press space to toggle between animation on and off." << std::endl              << "Press the up/down arrow keys to speed up/slow down animation." << std::endl;}// Main routine.int main(int argc, char **argv){    printInteraction();    glutInit(&argc, argv);    glutInitContextVersion(4, 3);    glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);    glutInitWindowSize(500, 500);    glutInitWindowPosition(100, 100);    glutCreateWindow("clown3.cpp");    glutDisplayFunc(drawScene);    glutReshapeFunc(resize);    glutKeyboardFunc(keyInput);    glutSpecialFunc(specialKeyInput);    glewExperimental = GL_TRUE;    glewInit();    setup();    glutMainLoop();}

void drawScene(void){    float t; // 沿螺旋线的参数。    glClear(GL_COLOR_BUFFER_BIT);   glLoadIdentity();       // 将场景置于视锥体中。   glTranslatef(0.0, 0.0, -9.0);   // 头。   glColor3f(0.0, 0.0, 1.0);   glutWireSphere(2.0, 20, 20);    glutSwapBuffers();}

添加帽子

接下来,我们想要一顶绿色的圆锥形帽子。 命令 glutWireCone(base, height, slices, stacks) 绘制一个线框圆锥体,底面半径为 base 和高度 height。 圆锥的底面位于 xy 平面上,其轴沿 z 轴,顶点指向 z 轴的正方向。 见图 4.40(a)。 参数 slices 和 stacks 决定了网格的细度(图中未显示)。

void drawScene(void){    float t; // 沿螺旋线的参数。    glClear(GL_COLOR_BUFFER_BIT);    glLoadIdentity();    // 将场景置于视锥体中。    glTranslatef(0.0, 0.0, -9.0);    // 头。    glColor3f(0.0, 0.0, 1.0);    glutWireSphere(2.0, 20, 20);  // 帽子.  glColor3f(0.0, 1.0, 0.0);   glutWireCone(2.0, 5.0, 20, 20);    glutSwapBuffers();}

不好! 由于 glutWireCone() 对齐的方式,帽子遮住了小丑的脸。 这很容易解决。 将帽子沿 z 轴向上平移 2 个单位,并将其绕 x 轴旋转 -90° 以将其排列在头顶上。 最后,将它绕 z 轴旋转 30°! 此时修改clown1.cpp的绘制例程如下:

void drawScene(void){    float t; // 沿螺旋线的参数。    glClear(GL_COLOR_BUFFER_BIT);    glLoadIdentity();    // 将场景置于视锥体中。    glTranslatef(0.0, 0.0, -9.0);    // 头。    glColor3f(0.0, 0.0, 1.0);    glutWireSphere(2.0, 20, 20);  // 帽子Transformations    glRotatef(30.0, 0.0, 0.0, 1.0); glRotatef(-90.0, 1.0, 0.0, 0.0);    glTranslatef(0.0, 0.0, 2.0);    // 帽子.  glColor3f(0.0, 1.0, 0.0);   glutWireCone(2.0, 5.0, 20, 20);    glutSwapBuffers();}

让我们通过将圆环连接到它的底部来为帽子添加一个帽檐。 命令 glutWireTorus(inRadius, outRadius, side, ring) 绘制内半径为 inRadius(圆环的圆形截面的半径)和外半径为 outRadius(通过圆环中间的圆的半径)的线框圆环。 环面的轴沿 z 轴并以原点为中心。 见图 4.40(b)。 在绘制圆锥体的调用之后插入调用 glutWireTorus(0.2, 2.2, 10, 25) ,因此绘制例程变为:

void drawScene(void){    float t; // 沿螺旋线的参数。    glClear(GL_COLOR_BUFFER_BIT);    glLoadIdentity();    // 将场景置于视锥体中。    glTranslatef(0.0, 0.0, -9.0);    // 头。    glColor3f(0.0, 0.0, 1.0);    glutWireSphere(2.0, 20, 20);  // 帽子Transformations    glRotatef(30.0, 0.0, 0.0, 1.0); glRotatef(-90.0, 1.0, 0.0, 0.0);    glTranslatef(0.0, 0.0, 2.0);    // 帽子.  glColor3f(0.0, 1.0, 0.0);   glutWireCone(2.0, 5.0, 20, 20);     glutWireTorus(0.2, 2.2, 10, 25);    glutSwapBuffers();}

观察到帽檐被适当地绘制在帽子的底部,尽管头部和帽子之间的建模转换仍然保持在那里。要制作动画,让我们通过围绕 y 轴旋转帽子来围绕小丑的头部旋转。我们操纵空格键在动画打开和关闭之间切换,使用向上/向下箭头键来改变速度。迄今为止的所有更新都包含在 clown2.cpp 中。图 4.39(b) 是屏幕截图。一个没有小红耳朵的小丑算什么?!球体对耳朵有用。产生振荡运动的一种简单方法是利用在 -1 和 1 之间变化的函数 sin(angle)。首先将任一耳朵平移到距头部单位距离的位置,然后将每个耳朵重复平移一段距离 sin(angle) ),每次增加角度。注意:在此类应用程序中需要注意的一个技术问题是角度在 OpenGL 语法中以度为单位测量,例如,在 glRotatef(angle, p, q, r) 中,而 C++ 数学库假设角度以弧度给出.乘以 π/180 将度数转换为弧度。

产生旋转效果

void drawScene(void){    float t; // 沿螺旋线的参数。    glClear(GL_COLOR_BUFFER_BIT);    glLoadIdentity();    // 将场景置于视锥体中。    glTranslatef(0.0, 0.0, -9.0);    // 头。    glColor3f(0.0, 0.0, 1.0);    glutWireSphere(2.0, 20, 20);  // 帽子Transformations     //添加旋转效果    glRotatef(angle, 0.0, 1.0, 0.0);   glRotatef(30.0, 0.0, 0.0, 1.0); glRotatef(-90.0, 1.0, 0.0, 0.0);    glTranslatef(0.0, 0.0, 2.0);    // 帽子.  glColor3f(0.0, 1.0, 0.0);   glutWireCone(2.0, 5.0, 20, 20); glutWireTorus(0.2, 2.2, 10, 25);    glutSwapBuffers();}

添加耳朵

void drawScene(void){    float t; // 沿螺旋线的参数。    glClear(GL_COLOR_BUFFER_BIT);    glLoadIdentity();    // 将场景置于视锥体中。    glTranslatef(0.0, 0.0, -9.0);    // 头。    glColor3f(0.0, 0.0, 1.0);    glutWireSphere(2.0, 20, 20);  // 帽子Transformations    glRotatef(angle, 0.0, 1.0, 0.0);    glRotatef(30.0, 0.0, 0.0, 1.0); glRotatef(-90.0, 1.0, 0.0, 0.0);    glTranslatef(0.0, 0.0, 2.0);    // 帽子.  glColor3f(0.0, 1.0, 0.0);   glutWireCone(2.0, 5.0, 20, 20); glutWireTorus(0.2, 2.2, 10, 25);    glPushMatrix();    // 左耳的glTranslatef。    glTranslatef(sin((PI/180.0)*angle), 0.0, 0.0);    glTranslatef(3.5, 0.0, 0.0);    // 左耳。    glColor3f(1.0, 0.0, 0.0);    glutWireSphere(0.5, 10, 10);    glPopMatrix();    glPushMatrix();    //  右耳Transformations     glTranslatef(-sin((PI/180.0)*angle), 0.0, 0.0);    glTranslatef(-3.5, 0.0, 0.0);    // 右耳    glColor3f(1.0, 0.0, 0.0);    glutWireSphere(0.5, 10, 10);    glPopMatrix();    glPushMatrix();    // 弹簧到左耳的变形    glTranslatef(-2.0, 0.0, 0.0);    glScalef(-1 - sin( (PI/180.0) * angle ), 1.0, 1.0);    // 弹到左耳    glColor3f(0.0, 1.0, 0.0);    glBegin(GL_LINE_STRIP);    for(t = 0.0; t <= 1.0; t += 0.05)    glVertex3f(t, 0.25 * cos(10.0 * PI * t), 0.25 * sin(10.0 * PI * t));    glEnd();    glPopMatrix();    glPushMatrix();    // 弹簧到右耳Transformations    glTranslatef(2.0, 0.0, 0.0);    glScalef(1 + sin( (PI/180.0) * angle ), 1.0, 1.0);    // 弹到右耳    glColor3f(0.0, 1.0, 0.0);    glBegin(GL_LINE_STRIP);    for(t = 0.0; t <= 1.0; t += 0.05)    glVertex3f(t, 0.25 * cos(10.0 * PI * t), 0.25 * sin(10.0 * PI * t));    glEnd();    glPopMatrix();    glutSwapBuffers();}

盛开的花

植物的茎由四个直段组成,萼片(花的基部)被建模为一个半球,而六个花瓣是同一个圆圈的副本。 半球和圆都通过动画过程中的缩放来重塑。 绘制两者的程序 drawHemisphere() 和 drawCircle 分别改编自 hemisphere.cpp 和 circle.cpp。 由于无法在运行时对显示列表的调用进行参数化,不幸的是,必须将两个定义花萼和花瓣的函数放置在绘图例程中,以允许它们访问不断变化的全局动画参数 t——事实上,间接通过 drawScene() 顶部的变量 hemisphereScaleFactor、petalAspectRatio 和 petalOpenAngle,随 t 变化。 配置茎、萼片和花瓣所涉及的参数都通过使用动画参数 t 的线性插值从开始值更改为结束值。 例如,

hemisphereScaleFactor = (1 - t) * 0.1 + t * 0.75

当 t 从 0 到 1 时,hemisphereScaleFactor 从 0.1 线性变化到 0.75。

// Drawing routine.void drawScene(void){ // 控制植物和花卉配置的参数作为动画参数 t 的函数 // t 用于在每个配置参数的开始和结束值之间进行线性插值。  float angleFirstSegment = (1 - t)*60.0 + t*80.0; // 在 60 和 90 之间插值的第一个植物段的角度。 float angleSecondSegment = (1 - t)*-30.0 + t*-20.0; // 第二个植物段的角度。 float angleThirdSegment = (1 - t)*-30.0 + t*-20.0; // 第三个植物段的角度。  float angleFourthSegment = (1 - t)*-30.0 + t*-20.0; // 第四植物段的角度。  float hemisphereScaleFactor = (1 - t)*0.1 + t*0.75; // 对半球的圆形底面进行因子缩放。    float petalAspectRatio = (1 - t)*0.1 + t*1.0; // 缩放花瓣圆以使其成为椭圆。    float petalOpenAngle = (1 - t)*-10.0 + t*-60.0; // 花瓣张开的角度。   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity();   glTranslatef(0.0, -10.0, -30.0);    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);  // 萼片显示列表。  glNewList(base + 1, GL_COMPILE);   glColor3f(1.0, 0.0, 0.0);   glPushMatrix(); glRotatef(90.0, 0.0, 0.0, 1.0); // 半球被缩放为椭球体。   glScalef(hemisphereScaleFactor, 1.0, hemisphereScaleFactor);        drawHemisphere(2.0, 6, 6);  glPopMatrix();  glEndList();    // 开始花瓣展示列表。    glNewList(base + 2, GL_COMPILE);   glColor3f(1.0, 0.0, 1.0);   glPushMatrix(); // 花瓣被翻译为接触萼片。  glTranslatef(2.0, 0.0, 2.0*hemisphereScaleFactor);  // 花瓣打开到给定的角度。  glTranslatef(-2.0, 0.0, 0.0);   glRotatef(petalOpenAngle, 0.0, 1.0, 0.0);   glTranslatef(2.0, 0.0, 0.0);    // 圆被缩放为椭圆。 glScalef(1.0, petalAspectRatio, 1.0);   drawCircle(2.0, 10);    glPopMatrix();  glEndList();    // 结束花瓣展示列表。    // 旋转场景。    glRotatef(Zangle, 0.0, 0.0, 1.0);   glRotatef(Yangle, 0.0, 1.0, 0.0);   glRotatef(Xangle, 1.0, 0.0, 0.0);   // 第一个茎段。   glRotatef(angleFirstSegment, 0.0, 0.0, 1.0);    glCallList(base);   // 第二个茎段。   glTranslatef(5.0, 0.0, 0.0);    glRotatef(angleSecondSegment, 0.0, 0.0, 1.0);   glCallList(base);   //第三茎段。 glTranslatef(5.0, 0.0, 0.0);    glRotatef(angleThirdSegment, 0.0, 0.0, 1.0);    glCallList(base);   // 第四茎段。    glTranslatef(5.0, 0.0, 0.0);    glRotatef(angleFourthSegment, 0.0, 0.0, 1.0);   glCallList(base);   // 萼片。  glTranslatef(7.0, 0.0, 0.0);    glCallList(base + 1);  // 第一瓣。 glPushMatrix(); glRotatef(30.0, 1.0, 0.0, 0.0); glCallList(base + 2);  glPopMatrix();  // 第二瓣。 glPushMatrix(); glRotatef(90.0, 1.0, 0.0, 0.0); glCallList(base + 2);  glPopMatrix();  // 第三瓣。 glPushMatrix(); glRotatef(150.0, 1.0, 0.0, 0.0);    glCallList(base + 2);  glPopMatrix();  //第四瓣。  glPushMatrix(); glRotatef(210.0, 1.0, 0.0, 0.0);    glCallList(base + 2);  glPopMatrix();  // 第五瓣。 glPushMatrix(); glRotatef(270.0, 1.0, 0.0, 0.0);    glCallList(base + 2);  glPopMatrix();  // 第六瓣  glPushMatrix(); glRotatef(330.0, 1.0, 0.0, 0.0);    glCallList(base + 2);  glPopMatrix();  glutSwapBuffers();}

Viewing Transformation

了解视图转换

将 OpenGL 相机视为位于原点,其镜头向下指向 -z 方向(视线)并且其顶部沿 +y 方向(向上方向)对齐。这实际上是 OpenGL 相机的默认姿势。见图 4.44(a)。

但是请记住,OpenGL 相机只是一个概念设备!正如第 2 章所述,我们看到的绘制对象的渲染完全由查看框或视锥体的形状决定,而后者又由程序员指定的投影语句决定(例如,glOrtho() 和 glFrustum()) .图 4.44(b) 让我们想起了这个过程。没有这样的相机!尽管如此,想象我们正在观看的是通过相机的直觉是很有吸引力的。特别是在视锥体的情况下,人们可以想象一个点相机在原点,胶片在它前面的观察面上,如图 4.44(b) 所示。

考虑通过移动和转动相机来改变视图也很直观。这正是视图转换 gluLookAt() 出现的地方。 现在,命令 gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz) 模拟 - 标记单词模拟 - OpenGL 的相机首先被移动到位置 eye = (eyex , eyey, eyez ),然后指向 center = (centerx , centery, centerz ),最后,绕其视线 (los) 旋转 - 将 eyex 连接到中心的线 - 使其向上方向是由 up = (upx , upy, upz ) 确定的。参见图 4.43。我们很快就会看到向上的方向实际上是如何由向上决定的。备注 4.12。在逻辑上,视图转换 gluLookAt() 是三个参数的函数,每个参数都是一个 3D 点或向量。备注 4.13。现在,我们要求读者假设我们有一个由 glFrustum() 语句定义的视锥体,而不是由 glOrtho() 定义的视锥体,因为在前者的情况下,点相机在逻辑上被放置在原点,但对于后者,将它放在哪里并不明显。然而,一旦 gluLookAt() 的工作变得清晰,这个明显的问题就会得到解决。

// 绘图程序。void drawScene(void){    glClear(GL_COLOR_BUFFER_BIT);   glColor3f(0.0, 0.0, 0.0);   glLoadIdentity();   gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);    glTranslatef(0.0, 0.0, -15.0);  glutWireCube(5.0); // Box torso.    glPopMatrix();  glFlush();}

所查看的内容没有变化(图 4.45)。实际上,命令 glTranslatef(0.0, 0.0, -15.0) 和 gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) 是完全等效的。要理解为什么这两个语句是等价的,请注意 gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) 将眼睛带到 (0, 0, 15),向下看 z 轴朝向 (0, 0, 0) 处的中心,视锥体(其顶点是眼睛)与其一起移动。假设相机的向上方向保持不变。现在,比较图 4.46(a) 和 (b):盒子在第一个(平截头体平移回来)和第二个(盒子向前平移)中的外观是否应该不同?不,因为它相对于截锥体的位置在两者中是相同的。在这个程序中,命令 gluLookAt() 优于 glTranslatef() 的便利在于我们已经能够根据我们想要如何拍摄盒子来安排相机,而不是移动盒子本身。由于 box.cpp 与 gluLookAt() 而不是 glTranslatef(),如前面的实验,经常使用,修改后的程序存储为 boxWithLookAt.cpp。

连续将 gluLookAt() 调用的参数 centerx , centery , centerz (中间三个)更改为以下参数:

  1. 0.0, 0.0, 10.0
  2. 0.0, 0.0, −10.0
  3. 0.0, 0.0, 20.0
  4. 0.0, 0.0, 15.0

视图不会随着实验的两个参数集 1 和 2 发生变化,因为观察者从眼睛到中心的视线没有变化。 图 4.47(a) 和 (b) 显示了各自的配置。

// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);glColor3f(0.0, 0.0, 0.0);glLoadIdentity();gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 10.0, 0.0, 1.0, 0.0);   glutWireCube(5.0); // Box torso.glPopMatrix();glFlush();
}

gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, -10.0, 0.0, 1.0, 0.0);

第 3 组(图 4.47©)产生一个空白屏幕,因为眼睛看向“错误的方向”。

gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 20.0, 0.0, 1.0, 0.0);

最后一组(图 4.47(d))混淆了 OpenGL,因为眼睛和中心重合,使得无法确定视线。 再次出现一个空白屏幕。 请注意,在所有情况下,gluLookAt() 都不会改变截锥体的形状,只会改变它的位置和对齐方式。 这里还有一些中心组供您尝试

gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 15.0, 0.0, 1.0, 0.0);

使用 gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) 调用恢复原始 boxWithLookAt.cpp 程序,并再次首先用 glutWireTeapot(5.0) 替换该框。 运行:截图如图4.48(a)所示。 接下来,依次将参数 upx、upy、upz——gluLookAt() 的最后三个参数——改成如下:

  1. 1.0, 0.0, 0.0 (Figure 4.48(b))
  2. 0.0, −1.0, 0.0 (Figure 4.48©)
  3. 1.0, 1.0, 0.0 (Figure 4.48(d))

连续案例的屏幕截图如图 4.48(b)-(d) 所示。 相机似乎围绕它的视线(z 轴)旋转,因此它的向上方向指向

// 绘图程序。void drawScene(void){    glClear(GL_COLOR_BUFFER_BIT);   glColor3f(0.0, 0.0, 0.0);   glLoadIdentity();   gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);        glutWireTeapot(5.0); // Teapot.     glPopMatrix();  glFlush();}

gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0);

gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0);

gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0);

相机似乎围绕它的视线(z 轴)旋转,因此它的向上方向指向

在我们说明向上向量如何确定相机向上方向的规则之前,这里有一些关于我们需要的向量点积的事实。 如果您已经有点积基础知识,请跳过此部分。

R3 中两个向量 u 和 v 的点积(也称为标量积)是一个标量,记为 u·v,定义如下:

(a) 如果 u 或 v 中的任何一个为零,则 u · v 为零;

(b) 如果不是,则 u · v 的值为 |u||v| cos θ,其中 θ 是 u 和 v 之间的角度。

事实证明,u·v 由以下简单公式给出,其中 u = (ux , uy , uz ) 和 v = (vx, vy , vz ):
u ⋅ v = u x v x + u y v y + u z v z u · v = uxvx + uy vy + uz vz u⋅v=uxvx+uyvy+uzvz
例 确定两个向量 u = (1, 0, 2) 和 v = (−2, 3, 4) 之间的角度 θ。
∣ u ∣ ∣ v ∣ c o s θ = u ⋅ v = u x v x + u y v y + u z v z = 1 ∗ − 2 + 0 ∗ 3 + 2 ∗ 4 = 6 |u||v| cos θ = u · v = ux vx + uy vy + uz vz = 1 ∗ −2 + 0 ∗ 3 + 2 ∗ 4 = 6 ∣u∣∣v∣cosθ=u⋅v=uxvx+uyvy+uzvz=1∗−2+0∗3+2∗4=6
所以
c o s θ = 6 ∣ u ∣ ∣ v ∣ = 6 1 2 + 0 2 + 2 2 ( − 2 ) 2 + 3 2 + 4 2 = 6 5 29 ≃ 0.49827 cos \theta=\frac{6}{|u||v|}=\frac{6}{\sqrt{1^{2}+0^{2}+2^{2}} \sqrt{(-2)^{2}+3^{2}+4^{2}}}=\frac{6}{\sqrt{5} \sqrt{29}} \simeq 0.49827 cosθ=∣u∣∣v∣6​=12+02+22 ​(−2)2+32+42 ​6​=5 ​29 ​6​≃0.49827

当我们介绍 gluLookAt() 时,我们说它模拟 OpenGL 相机运动。这是完全正确的。 OpenGL 相机永远不会将其默认姿势留在原点,其镜头指向 -z 方向,顶部沿 +y 方向对齐。换句话说,视锥体(或盒子)停留在它最初用 glFrustum()(或 glOrtho())创建的地方。事实上,观察变换是通过用等效的建模变换序列替换它来模拟的。我们实际上在 4.6.1 节之前看到了一个简单的例子,其中发现命令 gluLookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) 和 glTranslatef(0.0, 0.0, -15.0)是等价的。这是一个针对一般情况的激励思想实验:你和一个朋友和一个相机在一个空旷的地方。她站在你面前 10 米处,但通过取景器看,你认为她应该离你更近 3 米。有两种选择:(1)你,即相机,向她平移(走)3米,或者(2)她,即场景,向你平移3米。请参见图 4.56 的左上角。

两种情况下的图片都是一样的。忽略背景,因为它是一个同质的开放领域!这是获得两个选项等价的另一种方法。假设你已经申请了 (1),当你从那里借来的那个人开始大喊它真的很贵,你会介意不要移动它,而是把它放在第一次设置的地方。换句话说,您必须通过重新安排场景来进行管理。因此,要取消 (1) 的效果并将相机带回其原始位置,请将 (1) 的相反操作应用于相机和场景(以免改变图片)。当然,结果与首先仅应用 (2) 相同。请参见图 4.56 左下方大框中的两个图表。再次通过取景器看,你会觉得如果你的朋友不是站在画面中央而是站在一边,构图会更好。同样,(1)您可以旋转相机,例如顺时针 45°,或(2)您的朋友可以沿着以您所在位置为中心的圆圈逆时针旋转 45°,如图 4.56 的右上角。

在这两种情况下,图片完全相同。再一次,我们可以想象通过首先应用 (1) 到达 (2),然后通过将 (1) 的反向应用到相机和场景来撤消它,如右下角大框中的两个图如图 4.56 所示。现在应该相当简单地理解视图转换与建模转换序列的等效性。

变换 gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz) 要求相机 (i) 首先平移到位置 (eyex, eyey, eyez),然后 (ii) 旋转那个位置,直到它指向 (centerx , centery, centerz ),

最后, (iii) 围绕它的视线旋转,直到它的向上方向平行于向量 up2,(upx , upy, upz ) 的分量垂直于视线。让我们按要求移动相机。图 4.57(a) 显示了生成的配置。

逐渐取消它的运动,而不是像前面的思想实验一样移动场景。那么,使相机恢复默认状态的这些反向运动的总和将等同于观看变换。通过应用 glTranslatef(-eyex, -eyey, -eyez) 取消第一个Translate。然后相机位于原点,但仍指向平行于视线向量 los = (centerx, centery, centerz) - (eyex, eyey, eyez) 并且其顶部仍然平行于 up2 。见图 4.57(b)。假设 p 是一个包含 los 和 z 轴的平面——图中阴影部分。如果 los 不沿 z 轴分布,则 p 是唯一的;如果是,则 p 可以是包含公共线的任何平面。选择一个与 p 垂直的非零向量 w = (wx, wy, wz),即 w 与 los 和 z 轴都垂直。设 A 为从 w 向下看时逆时针测量的平面 p 上从 los 到 -z 的角度。注意:当然,如果 los 位于 z 轴,那么只有指向 +z 方向时,我们才需要进行 180° 旋转;否则,如果它已经指向 -z 方向,则无事可做。 126 应用 glRotatef(A, wx, wy, wz) 然后旋转相机直到它的视线与 -z 方向匹配。此外,它的顶部与向量平行,称为 up0 2,这是将 glRotatef(A, wx, wy, wz) 应用于 up2 的结果。现在,新的顶部方向 up0 2 垂直于 z 轴,因为在上一步中对 los 和 up2 应用了相同的旋转 glRotatef(A, wx, wy, wz),它们是垂直的,以获得向量分别朝向 -z 和 up0 。 2因此,up0 位于 xy 平面上。请参见图 4.57©,其中从 z 轴的负侧 2 观察相机。最后,将相机恢复到其默认位置所剩下的就是旋转

Orientation and Euler Angles

gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz)

相当于平移后跟两个旋转:

glRotatef(B, 0.0, 0.0, 1.0);
glRotatef(A, wx, wy, wz);
glTranslatef(−eyex, −eyey, −eyez);

特定旋转的轴 glRotatef(A, wx, wy, wz) 是可变的,取决于视线。 事实上,它被选择为垂直于视线和 z 轴。 然而,有可能找到一个平移,然后是一系列旋转,每个旋转都围绕一个固定轴,相当于给定的视图变换。 特别地,人们可以证明

gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz)

相当于:

glRotatef(−γ, 0.0, 0.0, 1.0);
glRotatef(−β, 0.0, 1.0, 0.0);
glRotatef(−α, 1.0, 0.0, 0.0);
glTranslatef(−eyex, −eyey, −eyez);

其中每个旋转都围绕坐标轴旋转,对于合适的角度 α、β 和 γ(减号用于稍后更简单的符号)。 就是这样。

图 4.60——图 4.57 的多合一版本——显示,正如我们在下面解释的,下面的四个变换序列将相机从 gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz) 事实上,它们是等价的。 最好的方法是从表示相机初始配置的粗体向量开始,并按照 (1)-(4) 的变换序列一一进行: (1) glTranslatef(-eyex, -eyey, -eyez) 使眼睛 到原点。 (2) glRotatef(−α, 1.0, 0.0, 0.0) 使 los 绕 x 轴旋转,直到它位于 xz 平面上。 (3) glRotatef(-β, 0.0, 1.0, 0.0) 绕 y 轴旋转 los 直到它指向 -z 方向。 (4) glRotatef(−γ, 0.0, 0.0, 1.0) 围绕它的 los(现在指向 z 轴)旋转相机,直到它的顶部与 +y 方向对齐。

gluLookAt(0.0, 0.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0);

作为围绕坐标轴的一系列旋转(不需要平移,因为眼睛已经在原点)

glRotatef(90.0, 0.0, 0.0, 1.0);glRotatef(135.0, 0.0, 1.0, 0.0);glRotatef(90.0, 1.0, 0.0, 0.0);

动画中的变换和碰撞检测

#include <cstdlib>
#include <cmath>
#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h>#define PI 3.14159265#define ROWS 8  // 小行星的行数。
#define COLUMNS 6 // 小行星的列数。
#define FILL_PROBABILITY 100 // 特定行列槽位的百分比概率
// 填充小行星。 它应该是 0 到 100 之间的整数。
// Globals.
static long font = (long)GLUT_BITMAP_8_BY_13; // 字体选择。
static int width, height; // OpenGL 窗口的大小。
static float angle = 0.0; // 飞船的角度。
static float xVal = 0, zVal = 0; // 飞船的坐标。
static int isCollision = 0; // 航天器和小行星之间有碰撞吗?
static unsigned int spacecraft; // 显示列表基本索引。
static int frameCount = 0; // 帧数// 绘制位图字符串的例程。
void writeBitmapString(void *font, char *string)
{char *c;for (c = string; *c != '\0'; c++) glutBitmapCharacter(font, *c);
}// 小行星类。
class Asteroid
{public:Asteroid();Asteroid(float x, float y, float z, float r, unsigned char colorR,unsigned char colorG, unsigned char colorB);float getCenterX() { return centerX; }float getCenterY() { return centerY; }float getCenterZ() { return centerZ; }float getRadius() { return radius; }void draw();private:float centerX, centerY, centerZ, radius;unsigned char color[3];
};
// 小行星默认构造函数。
Asteroid::Asteroid()
{centerX = 0.0;centerY = 0.0;centerZ = 0.0;radius = 0.0; // 表示该位置不存在小行星。color[0] = 0;color[1] = 0;color[2] = 0;
}// 小行星建造器。
Asteroid::Asteroid(float x, float y, float z, float r, unsigned char colorR,unsigned char colorG, unsigned char colorB)
{centerX = x;centerY = y;centerZ = z;radius = r;color[0] = colorR;color[1] = colorG;color[2] = colorB;
}// 绘制小行星的功能。
void Asteroid::draw()
{if (radius > 0.0) // 如果小行星存在。{glPushMatrix();glTranslatef(centerX, centerY, centerZ);glColor3ubv(color);glutWireSphere(radius, (int)radius * 6, (int)radius * 6);glPopMatrix();}
}Asteroid arrayAsteroids[ROWS][COLUMNS]; // Global array of asteroids.// 例行计算每秒绘制的帧数。
void frameCounter(int value)
{if (value != 0) // 第一次调用 frameCounter() 时没有输出(来自 main())。std::cout << "FPS = " << frameCount << std::endl;frameCount = 0;glutTimerFunc(1000, frameCounter, 1);
}// 初始化例程。
void setup(void)
{int i, j;spacecraft = glGenLists(1);glNewList(spacecraft, GL_COMPILE);glPushMatrix();glRotatef(180.0, 0.0, 1.0, 0.0); // 使航天器最初指向 $z$ 轴。glColor3f(1.0, 1.0, 1.0);glutWireCone(5.0, 10.0, 10, 10);glPopMatrix();glEndList();// 初始化全局数组Asteroids。for (j = 0; j < COLUMNS; j++)for (i = 0; i < ROWS; i++)if (rand() % 100 < FILL_PROBABILITY)// 如果 rand()%100 >= FILL_PROBABILITY 默认构造函数小行星保留在插槽中// 这表明那里没有小行星,因为默认的半径是 0。{// 根据列数是偶数还是奇数来定位小行星// 使航天器面向小行星场的中间。if (COLUMNS % 2) // Odd number of columns.arrayAsteroids[i][j] = Asteroid(30.0 * (-COLUMNS / 2 + j), 0.0, -40.0 - 30.0 * i, 3.0,rand() % 256, rand() % 256, rand() % 256);else // Even number of columns.arrayAsteroids[i][j] = Asteroid(15 + 30.0 * (-COLUMNS / 2 + j), 0.0, -40.0 - 30.0 * i, 3.0,rand() % 256, rand() % 256, rand() % 256);}glEnable(GL_DEPTH_TEST);glClearColor(0.0, 0.0, 0.0, 0.0);glutTimerFunc(0, frameCounter, 0); // frameCounter() 的初始调用。
}// 检查两个球体是否以 (x1,y1,z1) 和 (x2,y2,z2) 为中心的函数
// 半径 r1 和 r2 相交。
int checkSpheresIntersection(float x1, float y1, float z1, float r1,float x2, float y2, float z2, float r2)
{return ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2) <= (r1 + r2) * (r1 + r2));
}// 检查航天器在基地中心时是否与小行星相撞的函数
// 飞船在 (x, 0, z) 处,它与 -z 方向成 a 角对齐。
// 碰撞检测是近似的,因为我们使用边界球代替航天器。
int asteroidCraftCollision(float x, float z, float a)
{int i, j;// 检查与每个小行星的碰撞。for (j = 0; j < COLUMNS; j++)for (i = 0; i < ROWS; i++)if (arrayAsteroids[i][j].getRadius() > 0) // If asteroid exists.if (checkSpheresIntersection(x - 5 * sin((PI / 180.0) * a), 0.0,z - 5 * cos((PI / 180.0) * a), 7.072,arrayAsteroids[i][j].getCenterX(), arrayAsteroids[i][j].getCenterY(),arrayAsteroids[i][j].getCenterZ(), arrayAsteroids[i][j].getRadius()))return 1;return 0;
}// 绘图程序。
void drawScene(void)
{frameCount++; // 每次重绘增加帧数。int i, j;glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// 开始左视口。glViewport(0, 0, width / 2.0, height);glLoadIdentity();glPushMatrix();glColor3f(1.0, 0.0, 0.0);glRasterPos3f(-28.0, 25.0, -30.0);if (isCollision) writeBitmapString((void *)font, "Cannot - will crash!");glPopMatrix();// 固定相机。gluLookAt(0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);// 绘制 arrayAsteroids 中的所有小行星。for (j = 0; j < COLUMNS; j++)for (i = 0; i < ROWS; i++)arrayAsteroids[i][j].draw();// 画宇宙飞船。glPushMatrix();glTranslatef(xVal, 0.0, zVal);glRotatef(angle, 0.0, 1.0, 0.0);glCallList(spacecraft);glPopMatrix();// 结束左视口。// 开始右视口。glViewport(width / 2.0, 0, width / 2.0, height);glLoadIdentity();// 在隔离的(即,在 gluLookAt 之前)块中写入文本。glPushMatrix();glColor3f(1.0, 0.0, 0.0);glRasterPos3f(-28.0, 25.0, -30.0);if (isCollision) writeBitmapString((void *)font, "Cannot - will crash!");glPopMatrix();// 在视口左侧画一条垂直线将两个视口分开glColor3f(1.0, 1.0, 1.0);glLineWidth(2.0);glBegin(GL_LINES);glVertex3f(-5.0, -5.0, -5.0);glVertex3f(-5.0, 5.0, -5.0);glEnd();glLineWidth(1.0);// 将相机定位在锥体的尖端并指向锥体的方向。gluLookAt(xVal - 10 * sin((PI / 180.0) * angle),0.0,zVal - 10 * cos((PI / 180.0) * angle),xVal - 11 * sin((PI / 180.0) * angle),0.0,zVal - 11 * cos((PI / 180.0) * angle),0.0,1.0,0.0);// 绘制 arrayAsteroids 中的所有小行星。for (j = 0; j < COLUMNS; j++)for (i = 0; i < ROWS; i++)arrayAsteroids[i][j].draw();// 结束右视口。glutSwapBuffers();
}// OpenGL 窗口重塑例程。
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 250.0);glMatrixMode(GL_MODELVIEW);// Pass the size of the OpenGL window.width = w;height = h;
}// Keyboard input processing routine.
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;default:break;}
}// 非 ASCII 键输入的回调例程。
void specialKeyInput(int key, int x, int y)
{float tempxVal = xVal, tempzVal = zVal, tempAngle = angle;// 计算下一个位置。if (key == GLUT_KEY_LEFT) tempAngle = angle + 5.0;if (key == GLUT_KEY_RIGHT) tempAngle = angle - 5.0;if (key == GLUT_KEY_UP){tempxVal = xVal - sin(angle * PI / 180.0);tempzVal = zVal - cos(angle * PI / 180.0);}if (key == GLUT_KEY_DOWN){tempxVal = xVal + sin(angle * PI / 180.0);tempzVal = zVal + cos(angle * PI / 180.0);}// 角度校正。if (tempAngle > 360.0) tempAngle -= 360.0;if (tempAngle < 0.0) tempAngle += 360.0;// 只有在不会与小行星发生碰撞的情况下才将航天器移动到下一个位置。if (!asteroidCraftCollision(tempxVal, tempzVal, tempAngle)){isCollision = 0;xVal = tempxVal;zVal = tempzVal;angle = tempAngle;}else isCollision = 1;glutPostRedisplay();
}// 将交互指令输出到 C++ 窗口的例程。
void printInteraction(void)
{std::cout << "Interaction:" << std::endl;std::cout << "Press the left/right arrow keys to turn the craft." << std::endl<< "Press the up/down arrow keys to move the craft." << std::endl;
}// Main routine.
int main(int argc, char **argv)
{printInteraction();glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);glutInitWindowSize(800, 400);glutInitWindowPosition(100, 100);glutCreateWindow("spaceTravel.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glutSpecialFunc(specialKeyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

左视口显示了一个圆锥形航天器的固定相机和 48 个固定的球形小行星的全局视图,这些小行星排列在 6 × 8 网格中。 右侧视口显示了来自连接到飞行器尖端的前置摄像头的视图。

按上下箭头键向前和向后移动飞船,按左右箭头键转动它。 实施近似碰撞检测以防止飞行器撞上小行星。 图 4.62 是航天器行进并稍微转动后的屏幕截图。 程序的帧率显示在调试窗口中

可以通过重新定义 ROWS 和 COLUMNS 来更改小行星网格的大小。 特定行列槽被填充的概率由填充概率指定为百分比 - 小于 100 的值会导致小行星的非均匀分布。

Viewing Transformation

工艺的形状由 glutWireCone(5.0, 10.0, 10, 10) 语句定义; 准确地说,它是一个底半径为 5 和高度为 10 的圆锥体。航天器的配置由 xV al、zV al 和角度的值指定,这三个全局变量都是 spaceTravel.cpp 的全局变量。 图 4.63(a) 是沿 xz 平面截面的通用配置。 飞行器底座中心坐标为 (xV al, 0, zV al),而其轴与负 z 方向所成的角度是角度。 飞行器轴的中间 A 将用于碰撞检测。 右视口的相机位于飞行器的尖端,指向正前方。 现在,计算眼睛的坐标是简单的三角学,即飞行器的尖端,以及它指向的假想点中心的坐标,该中心位于飞行器轴前方 1 个单位处:
e y e = ( x V a l − 10 s i n ( a n g l e ) , 0 , z V a l − 10 c o s ( a n g l e ) ) c e n t e r = ( x V a l − 11 s i n ( a n g l e ) , 0 , z V a l − 11 c o s ( a n g l e ) ) eye = ( xV al − 10 sin( angle), 0, zV al − 10 cos(angle) ) \\center = ( xV al − 11 sin(angle), 0, zV al − 11 cos(angle) ) eye=(xVal−10sin(angle),0,zVal−10cos(angle))center=(xVal−11sin(angle),0,zVal−11cos(angle))
眼睛和中心的这些方程解释了 gluLookAt( ) 命令用于 drawScene() 例程中的右视口。

碰撞检测在 spaceTravel.cpp 中实现的碰撞检测虽然近似但很简单。航天器被包围在一个假想的边界球体 S 中,球体 S 以圆锥轴的中心 A 为中心,半径等于距离 |AC|从 A 到其底边边界上的点 C。

见图 4.63(b)。如果 B 是底的中心,那么根据圆锥的尺寸可以得出 |AB| = |BC| = 5;因此,
∣ A C ∣ = ∣ A B ∣ 2 + ∣ B C ∣ 2 = 50 = 7.071 |AC| = \sqrt{|AB|^2 + |BC|^2} = 50 = 7.071 ∣AC∣=∣AB∣2+∣BC∣2 ​=50=7.071

因此,我们指定 S 的半径为 7.072(实际上比 |AC| 稍大)。 S 的中心 A 的坐标由图 4.63(a) 中的三角函数获得:
A = ( x V a l − 5 sin ⁡ ( angle  ) , 0 , z Val  − 5 cos  ( angle  ) ) A=(x V a l-5 \sin (\text { angle }), 0, z \text { Val }-5 \text { cos }(\text { angle })) A=(xVal−5sin( angle ),0,z Val −5 cos ( angle ))
小行星 T ,我们检测到飞行器的边界球体 S 和 T 之间的碰撞。很容易确定两个球体 S 和 T 之间是否存在碰撞:将它们中心之间的距离 d 与其半径之和 r1 + r2 进行比较;如果 d ≤ r1 + r2(例如,如图 4.63© 中所示),则存在冲突,否则不存在。相应的检查在例程 checkSpheresIntersection() 中实现。这种碰撞检测测试是近似的,实际上是保守的,因为即使飞行器本身不与小行星相交,飞行器的边界球也可能与小行星相交(实际上,如图 4.63© 所示)。当然,实现这种近似碰撞检测的原因是,准确确定飞行器是否与小行星碰撞需要更多的计算,这会在运行时减慢速度(倾向于数学的读者可能想考虑精确计算)。向上和向下箭头键被编程为沿其轴向任一方向移动飞行器 1 的距离,向左和向右箭头将飞行器旋转 5° 的 134 角,前提是不会与飞行器发生碰撞在新位置的小行星。

动画关节图

//
// animateMan1.cpp
// 相互作用:
// 按 a 在开发和动画模式之间切换。
//
// 在开发模式下:
// 按空格键选择一个部分。
// 按向上翻页/向下翻页键旋转所选部分。
// 按左/右/上/下箭头键移动整个配置。
// 按 r/R 旋转视点。
// 按 z/Z 放大/缩小。
// 按 n 创建一个新配置 - 其他配置是重影
//(新配置是当前配置的副本,因此必须将其移动以使其可见)。
// 按 Tab 键选择一个配置 - 它被突出显示,其他人为鬼影。
// 按退格键重置当前配置。
// 按delete键删除当前配置。
//
// 在动画模式下:
// 按向上/向下箭头键加快/减慢动画速度。
//
// Sumanta Guha.
//#include <cstdlib>
#include <iostream>
#include <cmath>
#include <vector>
#include <fstream>#include <GL/glew.h>
#include <GL/freeglut.h>#define PI 3.14159265// Globals.
static float highlightColor[3] = { 0.0, 0.0, 0.0 }; // 强调颜色。
static float lowlightColor[3] = { 0.7, 0.7, 0.7 }; // 淡化颜色
static float partSelectColor[3] = { 1.0, 0.0, 0.0 }; // 选择表示颜色。
static long font = (long)GLUT_BITMAP_8_BY_13; // 字体选择。
static int animateMode = 0; // 在动画模式?
static int animationPeriod = 1000; // 帧之间的时间间隔。
static std::ofstream outFile; // 用于写入配置数据的文件。// Camera class.
class Camera
{public:Camera();void incrementViewDirection();void decrementViewDirection();void incrementZoomDistance() { zoomDistance += 1.0; }void decrementZoomDistance() { zoomDistance -= 1.0; }float getViewDirection() const { return viewDirection; }float getZoomDistance() const { return zoomDistance; }private:float viewDirection;float zoomDistance;
};Camera camera;// Camera constructor.
Camera::Camera()
{viewDirection = 0.0;zoomDistance = 30.0;
}// 增加相机视角的函数。
void Camera::incrementViewDirection()
{viewDirection += 5.0;if (viewDirection > 360.0) viewDirection -= 360.0;
}// 减少相机视角的函数。
void Camera::decrementViewDirection()
{viewDirection -= 5.0;if (viewDirection < 0.0) viewDirection += 360.0;
}// 人类。
class Man
{public:Man();void incrementSelectedPart();void incrementPartAngle();void decrementPartAngle();void setPartAngle(float angle) { partAngles[selectedPart] = angle; }void incrementUpMove() { upMove += 0.1; }void decrementUpMove() { upMove -= 0.1; }void setUpMove(float move) { upMove = move; }void incrementForwardMove() { forwardMove += 0.1; }void decrementForwardMove() { forwardMove -= 0.1; }void setForwardMove(float move) { forwardMove = move; }void setHighlight(int inputHighlight) { highlight = inputHighlight; }void draw();void outputData();void writeData();private:// 人配置值。float partAngles[9]; // 9 个身体部位的 0 到 360 度角 - 躯干、左右// 上臂,左右下臂,左右上臂// 腿,左右小腿。// 所有零件平行于同一平面移动。float upMove, forwardMove; // 向上和向前平移组件// 在平行于零件旋转的平面上// 因此所有平移和部分旋转// 沿着一个固定平面。int selectedPart; // 选定的零件号 - 该零件可以交互旋转// 在开发模式下。int highlight; // 如果当前选择了 man。
};// 人配置的全局向量。
std::vector<Man> manVector;// 遍历 manVector 的全局迭代器。
std::vector<Man>::iterator manVectorIterator;
std::vector<Man>::iterator manVectorAnimationIterator;// Man constructor.
Man::Man()
{for (int i = 0; i < 9; i++)partAngles[i] = 0.0;upMove = 0.0;forwardMove = 0.0;selectedPart = 0;highlight = 1;
}// 增加选定部分的功能..
void Man::incrementSelectedPart()
{if (selectedPart < 8) selectedPart++;else selectedPart = 0;
}// 增加所选零件角度的功能。
void Man::incrementPartAngle()
{partAngles[selectedPart] += 5.0;if (partAngles[selectedPart] > 360.0) partAngles[selectedPart] -= 360.0;
}// 减少所选零件角度的功能。
void Man::decrementPartAngle()
{partAngles[selectedPart] -= 5.0;if (partAngles[selectedPart] < 0.0) partAngles[selectedPart] += 360.0;
}// 绘制人的功能。
void Man::draw()
{if (highlight || animateMode) glColor3fv(highlightColor);else glColor3fv(lowlightColor);glPushMatrix();//向上和向前translations.glTranslatef(0.0, upMove, forwardMove);// Torso begin.if (highlight && !animateMode) if (selectedPart == 0)glColor3fv(partSelectColor);glRotatef(partAngles[0], 1.0, 0.0, 0.0);glPushMatrix();glScalef(4.0, 16.0, 4.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Torso end.// Head begin.glPushMatrix();glTranslatef(0.0, 11.5, 0.0);glPushMatrix();glScalef(2.0, 3.0, 2.0);glutWireSphere(1.0, 10, 8);glPopMatrix();glPopMatrix();// Head end.// Left upper and lower arm begin.glPushMatrix();// Left upper arm begin.if (highlight && !animateMode) if (selectedPart == 1) glColor3fv(partSelectColor);glTranslatef(3.0, 8.0, 0.0);glRotatef(180.0 + partAngles[1], 1.0, 0.0, 0.0);glTranslatef(0.0, 4.0, 0.0);glPushMatrix();glScalef(2.0, 8.0, 2.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Left upper arm end.// Left lower arm begin.if (highlight && !animateMode) if (selectedPart == 2) glColor3fv(partSelectColor);glTranslatef(0.0, 4.0, 0.0);glRotatef(partAngles[2], 1.0, 0.0, 0.0);glTranslatef(0.0, 4.0, 0.0);glPushMatrix();glScalef(2.0, 8.0, 2.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Left lower arm end.glPopMatrix();// Left upper and lower arm end.// Right upper and lower arm begin.glPushMatrix();// Right upper arm begin.if (highlight && !animateMode) if (selectedPart == 3) glColor3fv(partSelectColor);glTranslatef(-3.0, 8.0, 0.0);glRotatef(180.0 + partAngles[3], 1.0, 0.0, 0.0);glTranslatef(0.0, 4.0, 0.0);glPushMatrix();glScalef(2.0, 8.0, 2.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Right upper arm end.// Right lower arm begin.if (highlight && !animateMode) if (selectedPart == 4) glColor3fv(partSelectColor);glTranslatef(0.0, 4.0, 0.0);glRotatef(partAngles[4], 1.0, 0.0, 0.0);glTranslatef(0.0, 4.0, 0.0);glPushMatrix();glScalef(2.0, 8.0, 2.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Right lower arm end.glPopMatrix();// Right upper and lower arm end.// Left upper and lower leg with foot begin.glPushMatrix();// Left upper leg begin.if (highlight && !animateMode) if (selectedPart == 5) glColor3fv(partSelectColor);glTranslatef(1.5, -8.0, 0.0);glRotatef(partAngles[5], 1.0, 0.0, 0.0);glTranslatef(0.0, -4.0, 0.0);glPushMatrix();glScalef(2.0, 8.0, 2.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Left upper leg end.// Left lower leg with foot begin.if (highlight && !animateMode) if (selectedPart == 6) glColor3fv(partSelectColor);glTranslatef(0.0, -4.0, 0.0);glRotatef(partAngles[6], 1.0, 0.0, 0.0);glTranslatef(0.0, -4.0, 0.0);// Lower leg.glPushMatrix();glScalef(2.0, 8.0, 2.0);glutWireCube(1.0);glPopMatrix();// Foot.glTranslatef(0.0, -5.0, 0.5);glPushMatrix();glScalef(2.0, 1.0, 3.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Left lower leg with foot end.glPopMatrix();// Left upper and lower leg with foot end.// Right upper and lower leg with foot begin.glPushMatrix();// Right upper leg begin.if (highlight && !animateMode) if (selectedPart == 7) glColor3fv(partSelectColor);glTranslatef(-1.5, -8.0, 0.0);glRotatef(partAngles[7], 1.0, 0.0, 0.0);glTranslatef(0.0, -4.0, 0.0);glPushMatrix();glScalef(2.0, 8.0, 2.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Right upper leg end.// Right lower leg with foot begin.if (highlight && !animateMode) if (selectedPart == 8) glColor3fv(partSelectColor);glTranslatef(0.0, -4.0, 0.0);glRotatef(partAngles[8], 1.0, 0.0, 0.0);glTranslatef(0.0, -4.0, 0.0);// Lower leg.glPushMatrix();glScalef(2.0, 8.0, 2.0);glutWireCube(1.0);glPopMatrix();// Foot.glTranslatef(0.0, -5.0, 0.5);glPushMatrix();glScalef(2.0, 1.0, 3.0);glutWireCube(1.0);glPopMatrix();if (highlight && !animateMode) glColor3fv(highlightColor);// Right lower leg with foot end.glPopMatrix();// Right upper and lower leg with foot end.glPopMatrix();
}// 将配置数据输出到文件的函数。
void Man::outputData()
{int i;for (i = 0; i < 9; i++) outFile << partAngles[i] << " ";outFile << upMove << " " << forwardMove << std::endl;
}// 绘制位图字符串的例程。
void writeBitmapString(void *font, char *string)
{char *c;for (c = string; *c != '\0'; c++) glutBitmapCharacter(font, *c);
}// 将浮点数转换为字符字符串的例程.
void floatToString(char *destStr, int precision, float val)
{sprintf(destStr, "%f", val);destStr[precision] = '\0';
}// 写入配置数据的例行程序。
void Man::writeData()
{char buffer[33];floatToString(buffer, 4, partAngles[0]);glRasterPos3f(-28.0, 10.0, 0.0);writeBitmapString((void *)font, "torso = ");glRasterPos3f(-11.0, 10.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, partAngles[1]);glRasterPos3f(-28.0, 8.0, 0.0);writeBitmapString((void *)font, "left upper arm = ");glRasterPos3f(-11.0, 8.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, partAngles[2]);glRasterPos3f(-28.0, 6.0, 0.0);writeBitmapString((void *)font, "left lower arm = ");glRasterPos3f(-11.0, 6.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, partAngles[3]);glRasterPos3f(-28.0, 4.0, 0.0);writeBitmapString((void *)font, "right upper arm = ");glRasterPos3f(-11.0, 4.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, partAngles[4]);glRasterPos3f(-28.0, 2.0, 0.0);writeBitmapString((void *)font, "right lower arm = ");glRasterPos3f(-11.0, 2.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, partAngles[5]);glRasterPos3f(-28.0, 0.0, 0.0);writeBitmapString((void *)font, "left uppper leg = ");glRasterPos3f(-11.0, 0.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, partAngles[6]);glRasterPos3f(-28.0, -2.0, 0.0);writeBitmapString((void *)font, "left lower leg = ");glRasterPos3f(-11.0, -2.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, partAngles[7]);glRasterPos3f(-28.0, -4.0, 0.0);writeBitmapString((void *)font, "right upper leg = ");glRasterPos3f(-11.0, -4.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, partAngles[8]);glRasterPos3f(-28.0, -6.0, 0.0);writeBitmapString((void *)font, "right lower leg = ");glRasterPos3f(-11.0, -6.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, upMove);glRasterPos3f(-28.0, -8.0, 0.0);writeBitmapString((void *)font, "upMove = ");glRasterPos3f(-11.0, -8.0, 0.0);writeBitmapString((void *)font, buffer);floatToString(buffer, 4, forwardMove);glRasterPos3f(-28.0, -10.0, 0.0);writeBitmapString((void *)font, "forwardMove = ");glRasterPos3f(-11.0, -10.0, 0.0);writeBitmapString((void *)font, buffer);
}// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT);glLoadIdentity();// 在隔离(即,在 gluLookAt 之前)翻译块中写入文本.glPushMatrix();glTranslatef(0.0, 0.0, -30.0);glColor3fv(highlightColor);glRasterPos3f(-28.0, 25.0, 0.0);if (!animateMode){writeBitmapString((void *)font, "DEVELOP MODE");manVectorIterator->writeData();}else writeBitmapString((void *)font, "ANIMATE MODE");glPopMatrix();// 放置相机.gluLookAt(camera.getZoomDistance() * sin(camera.getViewDirection()*PI / 180.0), 0.0,camera.getZoomDistance() * cos(camera.getViewDirection()*PI / 180.0), 0.0,0.0, 0.0, 0.0, 1.0, 0.0);// 由于屏幕左侧的数据文本,将 man 向右移动 10 个单位.glTranslatef(10.0, 0.0, 0.0);if (!animateMode) // Develop mode.{// 在 man Vector 中绘制所有配置。for (auto man : manVector) { man.draw(); }}else // Animated mode -// 使用单独的迭代器以保持开发模式迭代器不变。{manVectorAnimationIterator->draw();}// 下面从这里开始绘制场景中的其他(固定)对象。// Black floor.glColor3f(0.0, 0.0, 0.0);glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);for (float z = -25.0; z < 100.0; z += 5.0){glBegin(GL_TRIANGLE_STRIP);for (float x = -50.0; x < 50.0; x += 5.0){glVertex3f(x, -25.0, z);glVertex3f(x, -25.0, z + 5.0);}glEnd();}// 绿色球体。glColor3f(0.0, 1.0, 0.0);glTranslatef(0.0, -20.0, 10.0);glPushMatrix();glScalef(5.0, 5.0, 5.0);glutWireSphere(1.0, 10, 8);glPopMatrix();glutSwapBuffers();
}// Timer function.
void animate(int value)
{if (animateMode){manVectorAnimationIterator++;if (manVectorAnimationIterator == manVector.end())manVectorAnimationIterator = manVector.begin();glutPostRedisplay();glutTimerFunc(animationPeriod, animate, 1);}
}// 将配置写入文件的函数。
void outputConfigurations(void)
{outFile.open("animateManDataOut.txt");for (auto man : manVector) { man.outputData(); }outFile.close();
}// 初始化例程。
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);// 使用单一配置初始化全局 manVector。manVector.push_back(Man());// 为 manVector 初始化全局迭代器。manVectorIterator = manVector.begin();manVectorAnimationIterator = manVector.begin();// 初始化相机。camera = Camera();
}// OpenGL 窗口重塑例程。
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);glMatrixMode(GL_MODELVIEW);
}// 键盘输入处理例程.
void keyInput(unsigned char key, int x, int y)
{std::vector<Man>::iterator localManVectorIterator;switch (key){case 27:exit(0);break;case 'a': // 在开发和动画模式之间切换。if (animateMode == 0){manVectorAnimationIterator = manVector.begin();outputConfigurations(); //在开发模式结束时将配置数据写入文件。animateMode = 1;animate(1);}else animateMode = 0;glutPostRedisplay();break;case 'r': // 旋转相机。camera.incrementViewDirection();glutPostRedisplay();break;case 'R': // 旋转相机。camera.decrementViewDirection();glutPostRedisplay();break;case 'z': // 放大.camera.decrementZoomDistance();glutPostRedisplay();break;case 'Z': // 缩小.camera.incrementZoomDistance();glutPostRedisplay();break;case 'n': // 创建新的人配置。// 关闭高亮显示当前配置。manVectorIterator->setHighlight(0);localManVectorIterator = manVectorIterator;manVectorIterator++;// 插入当前配置的副本并突出显示。manVectorIterator =manVector.insert(manVectorIterator, Man(*localManVectorIterator));manVectorIterator->setHighlight(1);glutPostRedisplay();break;case ' ': // 选择下一个身体部位。manVectorIterator->incrementSelectedPart();glutPostRedisplay();break;// Tab - 选择下一个人的配置。case 9:// 关闭突出显示当前配置。manVectorIterator->setHighlight(0);// 增量迭代器 - 如果已经结束就开始。manVectorIterator++;if (manVectorIterator == manVector.end())manVectorIterator = manVector.begin();// 突出显示当前配置。manVectorIterator->setHighlight(1);glutPostRedisplay();break;// Backspace - 重置当前的 man 配置,case 8:if (manVectorIterator != manVector.begin()) // 不是第一次配置。{// 复制以前的配置并突出显示。localManVectorIterator = manVectorIterator;localManVectorIterator--;manVectorIterator =manVector.insert(manVectorIterator, Man(*localManVectorIterator));manVectorIterator->setHighlight(1);// 删除当前配置。manVectorIterator++;manVectorIterator = manVector.erase(manVectorIterator);// 返回迭代器。manVectorIterator--;}else // 第一次配置{// 删除当前配置。manVectorIterator = manVector.erase(manVectorIterator);// 创建新配置.manVector.insert(manVectorIterator, Man());}glutPostRedisplay();break;// 删除 - 删除当前的 man 配置。case 127:if (manVector.size() > 1){manVectorIterator = manVector.erase(manVectorIterator);if (manVectorIterator != manVector.begin())manVectorIterator--;// Highlight current configuration.manVectorIterator->setHighlight(1);}glutPostRedisplay();break;default:break;}
}// 非 ASCII 键输入的回调例程。
void specialKeyInput(int key, int x, int y)
{if (key == GLUT_KEY_PAGE_DOWN) manVectorIterator->decrementPartAngle();if (key == GLUT_KEY_PAGE_UP) manVectorIterator->incrementPartAngle();if (key == GLUT_KEY_LEFT) manVectorIterator->decrementForwardMove();if (key == GLUT_KEY_RIGHT) manVectorIterator->incrementForwardMove();if (key == GLUT_KEY_DOWN){if (!animateMode) manVectorIterator->decrementUpMove();else animationPeriod += 10;}if (key == GLUT_KEY_UP){if (!animateMode) manVectorIterator->incrementUpMove();else if (animationPeriod > 10) animationPeriod -= 10;}glutPostRedisplay();
}// Routine to output interaction instructions to the C++ window.
void printInteraction(void)
{std::cout << "Interaction:" << std::endl;std::cout << "Press a to toggle between develop and animate modes." << std::endl<< std::endl<< "In develop mode:" << std::endl<< "Press the space bar to select a part." << std::endl<< "Press the page up/page down keys to rotate the selected part." << std::endl<< "Press the left/right/up/down arrow keys to move the whole configuration." << std::endl<< "Press r/R to rotate the viewpoint." << std::endl<< "Press z/Z to zoom in/out." << std::endl<< "Press n to create a new configuration - other configurations are ghosted" << std::endl<< "(the new configuration is a copy of the current one so it must be moved to be visible)." << std::endl<< "Press tab to choose a configuration - it is highlighted, others ghosted." << std::endl<< "Press backspace to reset current configuration." << std::endl<< "Press delete to delete current configuration." << std::endl<< std::endl<< "In animate mode:" << std::endl<< "Press the up/down arrow keys to speed up/slow down animation." << std::endl;
}// Main routine.
int main(int argc, char **argv)
{printInteraction();glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("animateMan1.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glutSpecialFunc(specialKeyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

运行 animateMan2.cpp。 这只是 animateMan1.cpp 的简化版本,其目的是为文件 animateManDataIn.txt 中列出的配置序列制作动画,很可能是从 animateMan1.cpp 的开发模式中生成的。 按“a”在动画开/关之间切换。 与在 animateMan1.cpp 中一样,按向上或向下箭头键可加快或减慢动画速度。 通过“r/R”和“z/Z”键的相机功能也保持不变。 将 animateMan1.cpp 视为工作室,将 animateMan2.cpp 视为电影院。 animateManDataIn.txt 的当前内容使人在球上做手弹簧。 图 4.67 是屏幕截图。

简单的正交阴影

在本章开头介绍标度变换时,我们说过退化标度偶尔会有应用。 这是一个创建和动画简单阴影的方法。

运行第 11 章的 ballAndTorusLitOrthoShadowed.cpp。

// 画球绕圆环飞行,如果阴影为真,则两者均为黑色,否则为彩色。
void drawFlyingBallAndTorus(int shadow)
{glShadeModel(GL_SMOOTH);glPushMatrix();glTranslatef(0.0, 10.0, 0.0);glRotatef(90.0, 1.0, 0.0, 0.0);// Fixed torus.if (shadow) glColor3f(0.0, 0.0, 0.0);else glColor3f(0.0, 1.0, 0.0);glutSolidTorus(2.0, 12.0, 80, 80);// Begin revolving ball.glRotatef(longAngle, 0.0, 0.0, 1.0);glTranslatef(12.0, 0.0, 0.0);glRotatef(latAngle, 0.0, 1.0, 0.0);glTranslatef(-12.0, 0.0, 0.0);glTranslatef(20.0, 0.0, 0.0);if (shadow) glColor3f(0.0, 0.0, 0.0);else glColor3f(0.0, 0.0, 1.0);glutSolidSphere(2.0, 20, 20);// End revolving ball.glPopMatrix();
}

这个程序显然基于 ballAndTorus.cpp,有光照,以及在方格地板上绘制的阴影。按空格键开始球绕圆环行进,按上下箭头键改变其速度。图 4.68 是屏幕截图。结束 忽略此时可能没有意义的与照明有关的调用。它们(奇怪的是)与阴影的绘制方式完全无关,这就是我们接下来要理解的。请注意,首先,例程 drawFlyingBallAndTorus() 将球和环面从 ballAndTorus.cpp 水平重新定位,以便它们的阴影(推测是由远处的头顶光源投射)落在位于 xz 平面上的地板上。假设光源在垂直上方很重要,因为它证明了绘制阴影就像由平行于 y 轴的光线投射一样,换句话说,作为平行或正交投影到地板上 - 称为正交阴影。实际的阴影绘制本身非常简单——绘制例程中的以下几行代码可以解决问题:

glPushMatrix();
glScalef(1.0, 0.0, 1.0);
drawFlyingBallAndTorus(1);
glPopMatrix();

drawFlyingBallAndTorus() 的参数值 1 导致球和环面都被绘制为黑色,而退化缩放命令 glScalef(1.0, 0.0, 1.0) 将它们所有顶点的 y 值折叠为 0,创建一个平坦的黑色对象 正是它们从平行于 y 轴的光线在 xz 平面(地板平面)上的投影。

Selection and Picking

Selection

然而,无数的动画应用程序要求用户使用鼠标或类似鼠标的设备在屏幕上选择和移动对象(想到射击游戏)。因此,我们认为解释如何实现这种交互性很重要。不幸的是,在屏幕上选择一个对象——这实际上意味着决定所选择的像素属于哪个对象——考虑到合成相机管道的运作方式,这并不是一个简单的操作。特别是,对象进入管道,被处理并作为一组片段(片段 = 像素 + 颜色值)出现,然后渲染到屏幕上。图 4.69 是一个概念图。管道并非设计为可逆的,因此没有简单的方法可以从屏幕空间“爬回”到世界空间并确定给定像素属于哪个对象。那么如何进行采摘呢?幸运的是,OpenGL 提供了对拣选以及它称为选择的过程的支持,事实上,这使得拣选成为可能。让我们从选择开始。

选择的基本思想很简单。简而言之,就是允许用户指定一个查看体积,然后选择与该体积相交或撞击的对象。为此,用户必须首先通过调用 glRenderMode(GL SELECT) 进入称为选择模式的渲染模式。在选择模式下,帧缓冲区没有绘制任何内容;相反,处理基元只是为了确定它们与指定观看量的交叉点并生成所谓的命中记录。为了帮助从命中记录中确定产生它的图元或图元,OpenGL 提供了一个用户操作的所谓的名称栈。用户可以以在原语和名称之间建立对应关系的方式将名称加载到名称堆栈中。命中记录在其创建时包含名称堆栈的内容,因此,基于原语和名称之间的对应关系,可以确定命中涉及的内容。让我们借助实时代码来了解具体细节。

实验 4.38。运行 selection.cpp,它的灵感来自于红皮书中的一个类似程序。它使用选择模式来确定矩形的身份,通过调用 drawRectangle() 绘制,矩形与投影语句 glOrtho (-5.0, 5.0, -5.0, 5.0, -5.0, 5.0) 创建的视域相交,这是一个以原点为中心的 10 × 10 × 10 轴对齐框。图 4.70 是屏幕截图。命中记录输出到命令窗口。在下面的讨论中,我们仔细解析了程序。 我们将视体积称为 glOrtho(-5.0, 5.0, -5.0, 5.0, -5.0, 5.0),用于“选择”与其相交的矩形,即选择体积。请注意,它与 resize() 例程中的 glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0) 调用定义的程序自身的查看量不同。 drawConfiguration 例程显示的是选择体积的轮廓和两个矩形,一个红色一个绿色,都在它里面。如果您不相信图 4.70 中场景的透视图,因为您可能不相信,请通过 drawRectangle() 调用的参数验证两个矩形确实位于选择体积内。 selectHits() 例程是代码中的下一个,是所有操作的所在。让我们仔细看看它。第一条语句 glSelectBuffer(1024, buffer);指定称为命中缓冲区的数组,用于存储命中记录及其大小(以字节为单位)。下一条语句 glRenderMode(GL SELECT);使 OpenGL 进入选择模式。下一个语句块 glMatrixMode(GL PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(-5.0, 5.0, -5.0, 5.0, -5.0, 5.0); glMatrixMode(GL 模型视图); glLoadIdentity();使矩阵模式更改为投影,当前投影矩阵(即在 resize() 例程中定义的)被保存,对应于命中测试的选择体积被放置在投影矩阵堆栈的顶部和,最后,重新进入模型视图矩阵模式,并将当前模型视图矩阵设置为标识。接下来的语句对,即 glInitNames(); glPushName(0);初始化一个空的名字栈,并将名字 0 压入栈中(名字总是非负整数)。我们不会使用 0 来命名任何原语,而是将它推下去,以便我们在使用 glLoadName() 时可以用“真实”名称替换一些东西。初始配置如图 4.71(a) 所示。以下命令集操作名称堆栈并相应地“绘制”图元。请记住,在选择模式下,实际上没有任何内容被绘制到帧缓冲区中,换句话说,没有看到任何事情发生。

#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h>// 全局变量。
static int hits; // 命中缓冲区中的条目数。
static unsigned int buffer[1024]; // 命中缓冲区。
static int isHitBufferProcessed = 0; // 是否处理了命中缓冲区?// 绘制边长为 2 的矩形的例程,平行于以 (x, y, z) 为中心的 xy 平面,颜色为 (R, G, B)。
void drawRectangle(float x, float y, float z, float R, float G, float B)
{glColor3f(R, G, B);glPushMatrix();glTranslatef(0.0, 0.0, z);glRectf(x - 1.0, y - 1.0, x + 1.0, y + 1.0);glPopMatrix();
}// 绘制用于选择的配置的例程。
void drawConfiguration(void)
{glMatrixMode(GL_MODELVIEW);glLoadIdentity();gluLookAt(7.5, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);glutWireCube(10.0); // 选择体积的轮廓。// 确保在 selectHits() 程序中也绘制了下面的矩形!drawRectangle(0.0, 0.0, 3.0, 1.0, 0.0, 0.0); // 矩形 1.drawRectangle(0.0, 0.0, -3.0, 0.0, 1.0, 0.0); // 矩形 2.glFlush();
}// 测试基元是否与选择体积相交的程序。
void selectHits(void)
{glSelectBuffer(1024, buffer);// 指定数组以选择模式写入命中记录。glRenderMode(GL_SELECT); // 进入选择模式。// 保存在调整大小例程中定义的查看体积后设置选择体积。glMatrixMode(GL_PROJECTION);glPushMatrix(); // Copy viewing volume.glLoadIdentity(); // Load identity.glOrtho(-5.0, 5.0, -5.0, 5.0, -5.0, 5.0); // Load selection volume.glMatrixMode(GL_MODELVIEW); // 绘制前返回模型视图模式。glLoadIdentity();glInitNames(); // 将名称堆栈初始化为空。glPushName(0); // 将名称 0 放在堆栈顶部。// 确保在 drawConfiguration() 程序中也绘制了下面的矩形!glLoadName(1); // // 在栈顶用名称 1 替换名称 0。drawRectangle(0.0, 0.0, 3.0, 1.0, 0.0, 0.0); // 矩形 1(红色)。glLoadName(2); // 在栈顶用名称 2 替换名称 1。drawRectangle(0.0, 0.0, -3.0, 0.0, 1.0, 0.0); // 矩形 2(绿色)。hits = glRenderMode(GL_RENDER); // 返回渲染模式,返回hit次数。// 恢复resize例程的查看量,返回modelview模式。glMatrixMode(GL_PROJECTION);glPopMatrix();glMatrixMode(GL_MODELVIEW);
}// 在命中缓冲区中输出命中记录。
void processHitBuffer(int hits, unsigned int buffer[])
{unsigned int *ptr, numberNames;std::cout << "Number of hits = " << hits << std::endl << std::endl;ptr = buffer;for (int i = 0; i < hits; i++){std::cout << "Hit record " << i << ":" << std::endl;numberNames = *ptr;std::cout << "Number of names in hit record = " << numberNames << std::endl;ptr++;std::cout << "Min z-value of hit primitives = " << (float)*ptr / 0xffffffff << std::endl; // Normalize to between 0 and 1.ptr++;std::cout << "Max z-value of hit primitives = " << (float)*ptr / 0xffffffff << std::endl; // Normalize to between 0 and 1.ptr++;std::cout << "List of names in hit record (copied from name stack): ";for (int j = 0; j < (int)numberNames; j++){std::cout << *ptr << "  ";ptr++;}std::cout << std::endl << std::endl;}
}// 绘图程序。
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glColor3f(0.0, 0.0, 0.0);drawConfiguration(); // 配置被绘制到帧缓冲区。selectHits(); // // 命中记录在命中缓冲区中(不绘制任何内容)。if (isHitBufferProcessed == 0) // 确保命中缓冲区只输出一次,即使// the screen is redrawn.{processHitBuffer(hits, buffer); // Hit buffer contents are output.isHitBufferProcessed = 1;}
}// 初始化例程。
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);glEnable(GL_DEPTH_TEST); // 启用深度测试。
}// OpenGL 窗口重塑例程。
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);glMatrixMode(GL_MODELVIEW);
}// Keyboard input processing routine.
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;default:break;}
}// Main routine.
int main(int argc, char **argv)
{glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA | GLUT_DEPTH);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("selection.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

glSelectBuffer(1024, buffer);

指定称为命中缓冲区的数组,用于存储命中记录及其大小(以字节为单位)。 下一个声明

glRenderMode(GL_SELECT);

使 OpenGL 进入选择模式。

glMatrixMode(GL PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrtho(-5.0, 5.0, -5.0, 5.0, -5.0, 5.0);
glMatrixMode(GL MODELVIEW);
glLoadIdentity();

使矩阵模式更改为投影,保存当前投影矩阵(即在 resize() 例程中定义的那个),对应于命中测试的选择体积将被放置在投影矩阵堆栈的顶部和 ,最后,重新进入模型视图矩阵模式,并将当前模型视图矩阵设置为标识。

接下来的语句

glInitNames();
glPushName(0);

初始化一个空的名字栈,并将名字 0 压入栈中(名字总是非负整数)。 我们不会使用 0 来命名任何原语,而是将它推下去,以便我们在使用 glLoadName() 时可以用“真实”名称替换一些东西。 初始配置如图 4.71(a) 所示。 以下命令集操作名称堆栈并相应地“绘制”图元。 请记住,在选择模式下,实际上没有任何内容被绘制到帧缓冲区中,换句话说,没有看到任何事情发生。

glLoadName(1);
drawRectangle(0.0, 0.0, 3.0, 1.0, 0.0, 0.0); // Rectangle 1 (red).
glLoadName(2);
drawRectangle(0.0, 0.0, -3.0, 0.0, 1.0, 0.0); // Rectangle 2 (green).

图 4.71(b) 和 © 分别描绘了绘制红色和绿色矩形时的名称堆栈。

hits = glRenderMode(GL RENDER);

将 OpenGL 返回到默认渲染模式,其中对象确实被绘制到帧缓冲区,同时返回当前在命中缓冲区中的命中记录数。 请注意, glRenderMode() 的返回值仅在退出选择模式或另一种称为反馈的模式时才有意义,我们不会使用这种模式,并且在离开渲染模式时没有意义。

glMatrixMode(GL_PROJECTION)
glPopMatrix();
glMatrixMode(GL_MODELVIEW);

从 resize() 例程中恢复投影矩阵并将 OpenGL 返回到模型视图矩阵模式。

当 selectHits() 被执行时,命中记录会按照我们接下来描述的规则写入命中缓冲区。当以下两个条件都成立时,命中记录将写入命中缓冲区:

(a) 遇到名称堆栈操作或 glRenderMode() 命令

(b) 发生命中(即,绘制的图元与选择相交卷)自此类命令的前一个实例。注意:命中记录是在执行(a)项的命令之前写入的。每个命中记录包含四个字段,顺序如下:

  1. 写入记录时名称堆栈中的名称数量。

  2. 属于自上次命中记录写入以来命中选择体积的图元的顶点的最小 z 值。该值通过除以选择体积的深度为 [0, 1] 范围内的数字进行标准化,然后乘以 232 - 1,四舍五入,并作为 32 位无符号整数存储在命中记录中。

  3. 属于自上次命中记录写入以来命中选择体的图元的最大 z 值,同样存储。

  4. 写入记录时名称堆栈中名称的顺序,从底部开始。

(这个序列可能是空的。)它是 processHitBuffer() 例程,由 drawScene() 调用,它逐步遍历命中缓冲区,将其内容输出到命令窗口。上面的第 2 项和第 3 项,顶点的最小和最大 z 值,通过除以 232 - 1 归一化回 0 和 1 之间。有两个命中记录,如您在命令窗口中看到的那样。第一个 (1, 0.2, 0.2, {1}) 是在执行 glLoadName(2) 调用之前生成的,因为后者是名称堆栈操作命令本身,并且因为命中(红色矩形)发生在前一个名称堆栈之后操作命令 (glLoadName(1))。如果首先观察记录创建时名称堆栈的配置,则该记录的内容很容易理解。

图 4.71(b)。 因此,该堆栈上的一个名称“1”解释了记录的第一个和最后一个条目(1、0.2、0.2、{1})。 此外,红色矩形的所有顶点距离观察框正面的深度为 2,当除以框的深度进行归一化时,它变为 2/10 = 0.2,解释了第二个和第三个条目。 第二个命中记录 (1, 0.8, 0.8, {2}) 是在处理 hits = glRenderMode(GL RENDER); 之前生成的。 语句,我们让读者去解析它的内容。 请记住, selection.cpp 的矩形实际上是在屏幕上绘制的,因为 drawScene() 调用的 drawConfiguration() 例程具有它们的副本。 希望以下练习能够充分阐明命中记录是如何生成的。

Picking

图 4.72 说明了这个想法。 V 是由程序的投影语句定义的视锥体。因此,对象在透视投影到 V 的观察面之后被绘制到 OpenGL 窗口(我们将使用 OpenGL 窗口识别 V 的观察面而不会造成伤害,因为从一个到另一个是一个简单的缩放)。因此,我们可以通过确定那些与像 V 这样的基部以 P 为中心的细长平截头体相交的对象,来找到从 OpenGL 0 窗口中选择点 P 中选取的对象,因为正是这些对象的投影与 P 相交。当然,根据V 0 的大小存在一些误差,V 0 越薄,拾取越准确。而且,很明显,它是在检测与 V 0 的交集时选择的。幸运的是,除了上一节讨论的选择机制之外,OpenGL 在创建用于拾取的合适选择体积方面还有更多帮助:GLU例程 gluPickMatrix() 定义了一个选择体积,它是一个用户指定大小的截头体,以用户指定的点为中心。这是它的工作原理。命令的顺序

glLoadIdentity();
gluPickMatrix(pickX, pickY, width, height, viewport);
glFrustum(); or gluPerspective(); or glOrtho();

其中最后一条语句是从程序的调整大小例程中复制的,导致投影矩阵堆栈的顶部矩阵被替换为对应于一个选择体积的矩阵,该选择体积的正面是一个宽度 × 高度的矩形,以 OpenGL 窗口的点为中心,( window) 坐标分别等于 pickX 和 pickY 。 此外,最后一个参数指向一个整数数组 viewport[4],其中包含视口的 x 和 y 坐标及其宽度和高度,按顺序排列; 它是通过调用 glGetIntegerv(GL VIEWPORT, viewport) 设置的。 从功能上讲, gluPickMatrix() 命令实际上生成一个矩阵,称为选择矩阵。 让我们开始在一个简单的类似游戏的应用程序中使用选择机制。

ballAndTorusPicking.cpp

#include <iostream>#include <GL/glew.h>
#include <GL/freeglut.h>// Globals.
static float latAngle = 0.0; // Latitudinal angle.
static float longAngle = 0.0; // Longitudinal angle.
static float Xangle = 0.0, Yangle = 0.0, Zangle = 0.0; // Angles to rotate scene.
static int isAnimate = 0; // Animated?
static int animationPeriod = 100; // Time interval between frames.
static int highlightFrames = 10; // Number of frames to keep highlight. 10
static int isSelecting = 0; // In selection mode?
static int hits; // Number of entries in hit buffer.
static unsigned int buffer[1024]; // Hit buffer.
static unsigned int closestName = 0; // Name of closest hit.// Draw ball and torus.
void drawBallAndTorus(void)
{glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glLoadIdentity();glTranslatef(0.0, 0.0, -25.0);// Rotate scene.glRotatef(Zangle, 0.0, 0.0, 1.0);glRotatef(Yangle, 0.0, 1.0, 0.0);glRotatef(Xangle, 1.0, 0.0, 0.0);// Fixed torus.if (isSelecting) glLoadName(1); // Name of torus is 1.if ((highlightFrames > 0) && (closestName == 1)) glColor3f(1.0, 0.0, 0.0); // Highlight if selected.else glColor3f(0.0, 1.0, 0.0);glutWireTorus(2.0, 12.0, 20, 20);// Begin revolving ball.glRotatef(longAngle, 0.0, 0.0, 1.0);glTranslatef(12.0, 0.0, 0.0);glRotatef(latAngle, 0.0, 1.0, 0.0);glTranslatef(-12.0, 0.0, 0.0);glTranslatef(20.0, 0.0, 0.0);if (isSelecting) glLoadName(2); // Name of ball is 2.if ((highlightFrames > 0) && (closestName == 2)) glColor3f(1.0, 0.0, 0.0); // Highlight if selected.else glColor3f(0.0, 0.0, 1.0);glutWireSphere(2.0, 10, 10);// End revolving ball.if (isSelecting) glPopName(); // Clear name stack.glutSwapBuffers();
}// Drawing routine.
void drawScene(void)
{glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glColor3f(1.0, 1.0, 1.0);// Draw ball and torus in rendering mode.isSelecting = 0;drawBallAndTorus();
}// Process hit buffer to find record with smallest min-z value.
void findClosestHit(int hits, unsigned int buffer[])
{unsigned int *ptr, minZ;minZ = 0xffffffff; // 2^32 - 1ptr = buffer;closestName = 0;for (int i = 0; i < hits; i++){ptr++;if (*ptr < minZ){minZ = *ptr;ptr += 2;closestName = *ptr;ptr++;}else ptr += 3;}if (closestName != 0) highlightFrames = 10;
}// The mouse callback routine.
void pickFunction(int button, int state, int x, int y)
{int viewport[4]; // Viewport data.if (button != GLUT_LEFT_BUTTON || state != GLUT_DOWN) return; // Don't react unless left button is pressed.glGetIntegerv(GL_VIEWPORT, viewport); // Get viewport data.glSelectBuffer(1024, buffer); // Specify buffer to write hit records in selection mode(void)glRenderMode(GL_SELECT); // Enter selection mode.// Save the viewing volume defined in the resize routine.glMatrixMode(GL_PROJECTION);glPushMatrix();// Define a selection volume corresponding to selecting in 3 x 3 region around the cursor.glLoadIdentity();gluPickMatrix((float)x, (float)(viewport[3] - y), 3.0, 3.0, viewport);glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0); // Copied from the reshape routine.glMatrixMode(GL_MODELVIEW); // Return to modelview mode before drawing.glLoadIdentity();glInitNames(); // Initializes the name stack to empty.glPushName(0); // Puts name 0 on top of stack.// Determine hits by calling drawBallAndTorus() so that names are assigned.isSelecting = 1;drawBallAndTorus();hits = glRenderMode(GL_RENDER); // Return to rendering mode, returning number of hits.// Restore viewing volume of the resize routine and return to modelview mode.glMatrixMode(GL_PROJECTION);glPopMatrix();glMatrixMode(GL_MODELVIEW);// Determine closest of the hit objects (if any).findClosestHit(hits, buffer);glutPostRedisplay();
}// Timer function.
void animate(int value)
{if (isAnimate){latAngle += 5.0;if (latAngle > 360.0) latAngle -= 360.0;longAngle += 1.0;if (longAngle > 360.0) longAngle -= 360.0;}if (highlightFrames > 0) highlightFrames--;glutPostRedisplay();glutTimerFunc(animationPeriod, animate, 1);
}// Initialization routine.
void setup(void)
{glClearColor(1.0, 1.0, 1.0, 0.0);glEnable(GL_DEPTH_TEST); // Enable depth testing.glutTimerFunc(5, animate, 1);
}// OpenGL window reshape routine.
void resize(int w, int h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);glMatrixMode(GL_MODELVIEW);
}// Keyboard input processing routine.
void keyInput(unsigned char key, int x, int y)
{switch (key){case 27:exit(0);break;case ' ':if (isAnimate) isAnimate = 0;else isAnimate = 1;glutPostRedisplay();break;case 'x':Xangle += 5.0;if (Xangle > 360.0) Xangle -= 360.0;glutPostRedisplay();break;case 'X':Xangle -= 5.0;if (Xangle < 0.0) Xangle += 360.0;glutPostRedisplay();break;case 'y':Yangle += 5.0;if (Yangle > 360.0) Yangle -= 360.0;glutPostRedisplay();break;case 'Y':Yangle -= 5.0;if (Yangle < 0.0) Yangle += 360.0;glutPostRedisplay();break;case 'z':Zangle += 5.0;if (Zangle > 360.0) Zangle -= 360.0;glutPostRedisplay();break;case 'Z':Zangle -= 5.0;if (Zangle < 0.0) Zangle += 360.0;glutPostRedisplay();break;default:break;}
}// Callback routine for non-ASCII key entry.
void specialKeyInput(int key, int x, int y)
{if (key == GLUT_KEY_DOWN) animationPeriod += 5;if (key == GLUT_KEY_UP) if (animationPeriod > 5) animationPeriod -= 5;glutPostRedisplay();
}// Routine to output interaction instructions to the C++ window.
void printInteraction(void)
{std::cout << "Interaction:" << std::endl;std::cout << "Press space to toggle between animation on and off." << std::endl<< "Press the up/down arrow keys to speed up/slow down animation." << std::endl<< "Press the x, X, y, Y, z, Z keys to rotate the scene." << std::endl << std::endl<< "Click left mouse button in OpenGL window to select either ball or torus." << std::endl;
}// Main routine.
int main(int argc, char **argv)
{printInteraction();glutInit(&argc, argv);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE);glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);glutInitWindowSize(500, 500);glutInitWindowPosition(100, 100);glutCreateWindow("ballAndTorusPicking.cpp");glutDisplayFunc(drawScene);glutReshapeFunc(resize);glutKeyboardFunc(keyInput);glutSpecialFunc(specialKeyInput);glutMouseFunc(pickFunction);glewExperimental = GL_TRUE;glewInit();setup();glutMainLoop();
}

运行 ballAndTorusPicking.cpp,它保留了 ballAndTorus.cpp 所基于的所有功能,并添加了通过单击鼠标左键选择球或环面的功能。 选中的物体会变红。 有关屏幕截图,请参见图 4.73。

ballAndTorusPicking.cpp 的 drawBallAndTorus() 例程几乎是 ballAndTorus.cpp 的整个 drawScene() 例程,除了有两个主要区别: (a) 在选择模式下,调用 glLoadName() 来标记名称为 1 的环面,名称为 2 的球。 (b) 如果选择了圆环或球之一——名称包含在全局最接近名称中——只要全局 highlightFrames 大于 0,它就会被涂成红色。鼠标回调 pickFunction()是沿着 selection.cpp 的 selectHits() 的行写的。重要的区别在于命中的选择量是在 gluPickMatrix() 调用的帮助下指定的。而且,当然,不是在 selection.cpp 中绘制矩形,而是在选择模式下执行的 drawBallAndTorus()。 pickFunction() 调用的例程 findClosestHit() 是对 selection.cpp 的 processHitBuffer() 例程的有趣修改。如果有多个击中记录,这意味着球和圆环都在鼠标点击下落下, findClosestHit() 比较它们的 min-z 字段以确定更接近观察者的那个。注意:有时球或圆环在看起来像是明确的点击时不会亮起,或者当两者都落在同一个鼠标点击下时,更远的一个会亮起。那是因为点击,或者更确切地说,它的选择体积,在网格线之间滑动!可能的解决方案包括通过将 gluPickMatrix() 的宽度和高度参数从两者的当前值 3 增加来使网格更精细或拾取不那么敏感。

Computer Graphics Through OpenGL From Theory to Experiments - 学习笔记2 Tricks of the Trade opengl基础相关推荐

  1. OpenGL蓝宝书第九章学习笔记:片段着色器和帧缓存

    前言 本篇在讲什么 OpenGL蓝宝书第九章学习笔记之片段着色器和帧缓存 本篇适合什么 适合初学OpenGL的小白 本篇需要什么 对C++语法有简单认知 对OpenGL有简单认知 最好是有OpenGL ...

  2. 《OpenGL超级宝典第5版》学习笔记(一)—— 第一个OpenGL程序

    // GLTools库包含了一个用于操作矩阵和向量的3D数学库,并依靠GLEW获得OpenGL3.3中用来产生和渲染一些简单3D对象的函数, // 以及对视觉平截头体.相机类和变换矩阵进行管理的函数的 ...

  3. 【OpenGL学习笔记】地月系

    OpenGL学习笔记2-地月系 文章目录 OpenGL学习笔记2-地月系 前言 运行结果 纹理图片 一.TexturePool 1.**TexturePool.h** 2.**TexturePool. ...

  4. 《Computer Graphics with OpenGL》计算机图形学读书笔记 02——计算机图形学软件

    这里是<Computer Graphics with OpenGL>英文原版第四版的读书笔记,预计每一章写一篇读书笔记.本篇为第二章,简要介绍计算机图形学的相关软件.图形学相关软件可分为两 ...

  5. Interactive Computer Graphics - A Top-Down Approach with Shader-Based OpenGL

    2015-09-30 我很早之前就关注这本书了.在刚开始学习计算机图形学的时候,我首先购买的是另外一本,名叫 Computer Graphics with OpenGL,说到这本书,到手的时候我就用胶 ...

  6. python开发cs程序_CSE209代做、代写Computer Graphics、代做CS/python编程设计代写Python程序|代做Processing...

    CSE209代做.代写Computer Graphics.代做CS/python编程设计代写Python程序|代做ProcessingCSE209 Computer Graphics~1~CSE209 ...

  7. Mathematics for Computer Graphics

    Mathematics for Computer Graphics 最近严重感觉到数学知识的不足! http://bbs.gameres.com/showthread.asp?threadid=105 ...

  8. 计算机图形(Computer Graphics)经典书籍推荐(1)

    这些书都是非常非常经典!!!!! 1- An Introduction to Ray Tracing. 1989 2- Physically Based Rendering_From Theory T ...

  9. GAMES101笔记_Lec01_计算机图形学概述 Overview of Computer Graphics

    作为一名想要了解图形学的学生,已经在无数地方看到有人推荐闫令琪老师的GAMES101课程,但由于自己是美术专业,在笼统看过这门课程之后认为这门课有一定学习难度,所以为了打下比较扎实的基础和方便自己日后 ...

最新文章

  1. Java面向对象知识概括归纳与总结
  2. [翻译] Shimmer
  3. eclipse中java获取js的值_javascript – 如何在Eclipse中使用Selenium将外部.js导入我的Java测试?...
  4. 阿里集团搜索和推荐关于效率稳定性的思考和实践
  5. BZOJ 3836 Codeforces 280D k-Maximum Subsequence Sum (模拟费用流、线段树)
  6. php 打包下载网络图片,PHP实现图片批量打包下载功能
  7. COLLEGE.sql(复制的时候注意路径!!!)
  8. 网站域名检测是否被QQ/微信拦截工具
  9. 本周四直播丨Oracle中为什么没有double write?那支持原子写吗?
  10. 利用file权限读取/etc/passwd
  11. 基于强化学习和析取图模型的统一调度框架
  12. 算法: 239. 滑动窗口的最大值
  13. 硬件工程师 VS 软件工程师
  14. 计算机一寸照编辑教程,超简单的一寸照制作及排版教程,再也不花冤枉钱!
  15. 2021.09青少年软件编程(Python)等级考试试卷(五级)
  16. Postman下载到使用【待更新】
  17. 宋登高 php,HDwiki百科建站第一期
  18. 机器学习之感知机python实现
  19. 电脑休眠唤醒后无法显示WIFI列表
  20. 苹果电脑新手指南:什么是Dock程序坞

热门文章

  1. HackTheBox-Jeeves
  2. 算法 - n个数字形成的圆圈中循环删除第m个数字(C++)
  3. Python学习——Python海龟制图中的文字
  4. node.js -v15.0.0下载安装配置教程笔记
  5. 软件工程项目——大学生综测评分计算管理系统
  6. 高新技术企业定义和好处
  7. 有利可图网_有利可图的项目手册-现在可用
  8. 如何鉴别电脑电源是否虚标(实际输出瓦数)
  9. 计算机上如何保存ico格式,PS怎么保存ico格式
  10. 【Smarty】Smarty的下载、配置与Helloworld