赞
踩
#include <GL/glew.h> #include <GL/freeglut.h> // Drawing routine. void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0, 0.0, 0.0); // Draw a polygon with specified vertices. glBegin(GL_POLYGON); glVertex3f(20.0, 20.0, 0.0); // glVertex3f(30.0, 20.0, 0.0) glVertex3f(80.0, 20.0, 0.0); glVertex3f(80.0, 80.0, 0.0); glVertex3f(20.0, 80.0, 0.0); glEnd(); glFlush(); } // 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(); glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.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("square.cpp"); glutDisplayFunc(drawScene); glutReshapeFunc(resize); glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE; glewInit(); setup(); glutMainLoop(); }
square.cpp 中的以下六个语句创建了正方形:
// Draw a polygon with specified vertices.
glBegin(GL_POLYGON);
glVertex3f(20.0, 20.0, 0.0);
glVertex3f(80.0, 20.0, 0.0);
glVertex3f(80.0, 80.0, 0.0);
glVertex3f(20.0, 80.0, 0.0);
glEnd();
正方形的角显然是由 glBegin(GL POLYGON) 和 glEnd() 之间的四个顶点声明语句指定的。让我们确定这些 glVertex3f() 语句与角点的对应关系。如果,假设顶点是在嵌入 OpenGL 窗口的某个坐标系中指定的 例如,如果 x 轴水平向右增加,y 轴垂直向下增加,如图
则 glVertex3f(20.0, 20.0, 0.0) 将对应于正方形的左上角 glVertex3f(80.0, 20.0 , 0.0) 到右上角,依此类推。然而,即使假设确实存在这些附加到 OpenGL 窗口的不可见轴,我们如何知道它们在哪里或它们是如何定向的?一种方法是“摆动”正方形的角!例如,将第一个顶点声明从 glVertex3f(20.0, 20.0, 0.0) 更改为 glVertex3f(30.0, 20.0, 0.0) 并观察哪个角移动。还要确定原点所在的位置。
// Draw a polygon with specified vertices.
glBegin(GL_POLYGON);
// glVertex3f(20.0, 20.0, 0.0);
glVertex3f(30.0, 20.0, 0.0);
glVertex3f(80.0, 20.0, 0.0);
glVertex3f(80.0, 80.0, 0.0);
glVertex3f(20.0, 80.0, 0.0);
glEnd();
那么,似乎square.cpp在OpenGL窗口中设置坐标,使得x轴的增加方向水平向右,y轴的增加方向垂直向上,而且原点似乎对应于较低的窗口的一角
glVertex3f(*, *, *) 声明的三个参数中的最后一个显然是 z 坐标。 顶点在 3 维空间中指定(简称为 3-space 3or,数学上为 R )。 事实上,OpenGL 允许我们在 3 空间中绘制并创建真正的 3D 场景,这是它的主要名声。 但是,我们将 3 维场景视为渲染到计算机屏幕的 2 维部分,特别是矩形 OpenGL 窗口的图片。 很快我们将看到 OpenGL 如何将 3D 场景转换为 2D 渲染。
顶点坐标值究竟是什么意思? 例如,square.cpp 的 (20.0, 20.0, 0.0) 处的顶点为 20 mm., 20 cm。 或者沿 x 轴和 y 轴距原点 20 个像素,或者是否存在其他一些 OpenGL 固有的绝对距离单位? 让我们做下面的实验。 *
实验 2.2。 主程序的 glutInitWindowSize() 参数值决定了 OpenGL 窗口的形状; 事实上,一般来说,glutInitWindowSize(w, h) 创建一个宽 w 像素、高 h 像素的窗口。 将 square.cpp 的初始 glutInitWindowSize(500, 500) 更改为 glutInitWindowSize(300, 300),然后更改 glutInitWindowSize(500, 250)(图 2.5)。 绘制的正方形的大小甚至形状都会随着 OpenGL 窗口而变化。 因此,正方形的坐标值在屏幕上似乎没有任何绝对单位。
glutInitWindowSize(300, 300)
glutInitWindowSize(500, 250)
当然,您可以通过用鼠标拖动它的一个角来直接重塑 OpenGL 窗口的形状,而不是在程序中重置 glutInitWindowSize()。
理解坐标实际代表什么首先需要理解OpenGL的渲染机制,它本身从程序的投影语句开始。 在 square.cpp 的情况下,这个语句是
glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.0)
在 resize() 例程中,它确定了程序员在其中绘制场景的虚构查看框。 一般
glOrtho(left, right, bottom, top, near, far)
设置一个观察框,如图 ,在 8 个点处有角:
(left, bottom, −near), (right, bottom, −near), (left, top, −near),
(right, top, −near), (left, bottom, −far), (right, bottom, −far),
(left, top, −far), (right, top, −far)
它是一个边沿轴对齐的盒子,其跨度沿 x 轴从左到右,沿 y 轴从下到上,沿 z 轴从 -far 到 -near。 square.cpp的投影语句glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.0)对应的查看框如图2.7(a)所示。
观察盒位于 3D 空间中,也称为世界空间,因为它包含我们创建的每个对象。 读者此时肯定会想知道世界空间本身的坐标轴是如何校准的——例如,是沿轴一英寸、一厘米或其他东西的单位——因为观察框的大小取决于此。 一旦解释了渲染过程,答案就会很明显。 至于现在的绘图,顶点声明 glVertex3f(x, y, z) 对应于世界空间中的点 (x, y, z)。 例如,由 glVertex3f(20.0, 20.0, 0.0) 声明的正方形的角位于 (20.0, 20.0, 0.0)。 square.cpp 的正方形,如图 2.7(b) 所示,完全位于查看框内。 一旦程序员绘制了整个场景,如果投影语句glOrtho()如square.cpp中那样,那么渲染过程分为两步:
拍摄:首先,物体垂直投影到观察框的正面,即z = -near 平面上的面。例如,图 2.8(a)(与图 2.7(b) 相同)中的正方形的投影如图 2.8(b) 所示。观察框的正面称为观察面,它所在的平面称为观察平面。这一步就像在胶片上拍摄场景一样。事实上,人们可以将观察盒想象成那些古老的盒式相机的一个巨大版本,摄影师躲在胶片后面——观察脸——并用黑布盖住她的头;事实上,如此之大,以至于整个场景实际上都在盒子里。此外,请注意,在这个类比中,没有镜头,只有电影。
打印:接下来,按比例缩放查看面以适合矩形 OpenGL 窗口。这一步就像在纸上打印胶片一样。在 square.cpp 的情况下,打印将我们从图 2.8(b) 带到 ©。例如,如果 square.cpp 的窗口大小更改为纵横比(= 宽度/高度)为 2 之一,通过将 glutInitWindowSize(500, 500) 替换为 glutInitWindowSize(500, 250),打印将带我们从图 2.8(b) 到 (d)(实际上将正方形扭曲成矩形)。
固定世界系统
通过将 glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.0) 替换为 glOrtho(-100, 100.0, -100.0, 100.0, -1.0, 1.0) 来仅更改 square.cpp 的查看框。 新观察盒中正方形的位置不同,拍摄和打印的结果也不同。
我们已经有机会使用 glutInitWindowSize(w, h) 命令,它将 OpenGL 窗口的大小设置为以像素为单位的宽度 w 和高度 h。一个伴随命令是 glutInitWindowPosition(x, y) 来指定计算机屏幕上 OpenGL 窗口左上角的位置 (x, y)。
通过在 square.cpp 中原始方块的代码之后插入以下内容来添加另一个方块:
glBegin(GL POLYGON);
glVertex3f(120.0,120.0,0.0);
glVertex3f(180.0,120.0,0.0);
glVertex3f(180.0,180.0,0.0);
glVertex3f(120.0,180.0,0.0);
glEnd();
从其顶点坐标的值来看,第二个正方形显然完全位于观察框之外。
如果你现在运行,在 OpenGL 窗口中没有第二个方块的迹象。 这是因为 OpenGL 在渲染之前将场景剪辑到查看框内,因此在外面绘制的对象或对象的一部分是不可见的。 裁剪是图形管道中的一个阶段。 这个时候我们不关心它的实现,只关心效果。
为了更生动地说明剪裁,首先通过删除最后一个顶点将原始 square.cpp 的正方形替换为三角形; 特别是,将多边形代码替换为以下内容:
glBegin(GL POLYGON);
glVertex3f(20.0, 20.0, 0.0);
glVertex3f(80.0, 20.0, 0.0);
glVertex3f(80.0, 80.0, 0.0);
glEnd();
接下来,通过将第一个顶点更改为 glVertex3f(20.0, 20.0, 0.5) 来提升 z 轴上的第一个顶点; 通过将其 z 值更改为 1.5,然后是 2.5,最后是 10.0,进一步提升它。 确保你相信你在最后三种情况下看到的确实是一个夹在查看框内的三角形.
glVertex3f(20.0, 20.0, 1.5);
glVertex3f(20.0, 20.0, 2.5);
观察框有六个面,每个面都位于不同的平面上,OpenGL 在这六个平面中的每一个平面的一侧裁剪场景,因此称为裁剪平面。 人们可能会想象一把刀切下每个平面,如图 2.17 所示。 具体来说,在glOrtho(left, right, bottom, top, near, far)设置的view box的情况下,裁剪掉的是平面左边x=left,平面右边x=right, 等等。
除了限制查看框的六个平面之外,程序员还可以定义裁剪平面。
如果第二个顶点从 glVertex3f(80.0, 20.0, 0.0) 上升到 glVertex3f(80.0, 20.0, 1.5),那么图形也会被剪裁.
// Drawing routine.
void drawScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(0.0, 0.0, 0.0);
glPointSize(15.0); // Set point size.
glBegin(GL_POLYGON);
glVertex3f(20.0, 20.0, 1.5);
glVertex3f(80.0, 20.0, 0.0);
glVertex3f(80.0, 80.0, 0.0);
glVertex3f(20.0, 80.0, 0.0);
glEnd();
glFlush();
}
OpenGL 在将多边形三角 polygon 分为所谓的三角形扇形后绘制多边形,多边形的第一个顶点(按代码顺序)为扇形的中心
square.cpp 中方块的颜色由三个指定drawScene() 例程中 glColor3f(0.0, 0.0, 0.0) 语句的参数 每个都给出三个主要成分之一的值, blue, green 和 red 。
确定 glColor3f() 的三个参数中的哪一个指定蓝色、绿色和红色分量,方法是依次将每个参数设置为 1.0,将其他参数设置为 0.0
通常, glColor3f(red, green, blue) 调用指定前景色或绘图颜色,即应用于正在绘制的对象的颜色。 每个颜色分量的值(应该是 0.0 到 1.0 之间的数字)决定了它的强度。 例如,glColor3f(1.0, 1.0, 0.0) 是最亮的黄色,而 glColor3f(0.5, 0.5, 0.0) 是较弱的黄色
// Drawing routine.
void drawScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 0.0, 0.0);
glPointSize(15.0); // Set point size.
glBegin(GL_POLYGON);
glVertex3f(20.0, 20.0, 0.0);
glVertex3f(80.0, 20.0, 0.0);
glVertex3f(80.0, 80.0, 0.0);
glVertex3f(20.0, 80.0, 0.0);
glEnd();
glFlush();
}
前景色是一组变量之一,称为状态变量,它决定了 OpenGL 的状态。 其他状态变量包括点大小、线宽、线条点画、材料属性等。 随着我们的深入,我们会遇到几个,或者您可以参考红书*获取完整列表。 OpenGL 将保持其当前状态并发挥作用,直到做出更改状态变量的声明。 出于这个原因,OpenGL 通常被称为状态机。 接下来的几个实验说明了关于状态变量如何控制渲染的要点。。
将 square.cpp 的多边形声明部分替换为以下绘制两个正方形:
// Drawing routine. void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0, 0.0, 0.0); glPointSize(15.0); // Set point size. glBegin(GL_POLYGON); glVertex3f(20.0, 20.0, 0.0); glVertex3f(80.0, 20.0, 0.0); glVertex3f(80.0, 80.0, 0.0); glVertex3f(20.0, 80.0, 0.0); glEnd(); glColor3f(0.0, 1.0, 0.0); glBegin(GL_POLYGON); glVertex3f(40.0, 40.0, 0.0); glVertex3f(60.0, 40.0, 0.0); glVertex3f(60.0, 60.0, 0.0); glVertex3f(40.0, 60.0, 0.0); glEnd(); glFlush(); }
一个小的绿色方块出现在一个较大的红色方块内。 显然,这是因为第一个方块的前景色是红色的,而第二个方块的前景色是绿色的。 有人说红色绑定到第一个正方形——或者更准确地说,绑定到它的四个顶点中的每一个——而绿色绑定到第二个正方形。 这些绑定值指定任一正方形的颜色属性。 通常,决定如何渲染的那些状态变量的值共同形成一个基元的属性集。 通过剪切指定红色方块的七个语句并将它们粘贴在与绿色方块相关的语句之后,翻转两个方块在代码中出现的顺序。 绿色方块被红色方块覆盖,不再可见。 OpenGL 程序仍然是一个 C++ 程序,它逐行处理代码,因此对象是按代码顺序绘制的。
将 square.cpp 的多边形声明部分替换为:
glBegin(GL POLYGON);
glColor3f(1.0, 0.0, 0.0);
glVertex3f(20.0, 20.0, 0.0);
glColor3f(0.0, 1.0, 0.0);
glVertex3f(80.0, 20.0, 0.0);
glColor3f(0.0, 0.0, 1.0);
glVertex3f(80.0, 80.0, 0.0);
glColor3f(1.0, 1.0, 0.0);
glVertex3f(20.0, 80.0, 0.0);
glEnd();
如图,绑定到正方形四个顶点的不同颜色值显然是在正方形的其余部分内插的。 事实上,这在 OpenGL 中最常见:在图元顶点指定的数值属性值在其整个内部进行插值。 结束 现在我们有一个正方形,它不是左右对称的,也不是从下到上对称的
OpenGL 的几何图元(也称为绘图图元或简称为图元)是程序员以类似乐高积木的方式使用的部件,用于创建从最简陋到极其复杂的对象。 事实上,图钉和航天器都由同一个小系列的 OpenGL 基元组装而成。 到目前为止,我们看到的唯一基元是多边形。
在接下来 OpenGL 如何绘制上述每个图元的解释中,假设在 glBegin(primitive) 和 glEnd() 之间的代码中声明的 n 个顶点是 v0 , v1, 。 . . , vn−1 ,即原语的声明形式为:
glBegin(primitive);
glVertex3f(*, *, *); // v0
glVertex3f(*, *, *); // v1
...
glVertex3f(*, *, *); // vn−1
glEnd();
在每个顶点绘制一个点
v
0
,
v
1
,
.
.
.
,
v
n
−
1
v0 , v1, . . . , vn−1
v0,v1,...,vn−1
void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glPointSize(15.0); // Set point size. glBegin(GL_POINTS); glColor3f(1.0, 0.0, 0.0); glVertex3f(20.0, 20.0, 0.0); glColor3f(0.0, 1.0, 0.0); glVertex3f(80.0, 20.0, 0.0); glColor3f(0.0, 0.0, 1.0); glVertex3f(80.0, 80.0, 0.0); glColor3f(1.0, 1.0, 0.0); glVertex3f(20.0, 80.0, 0.0); glEnd(); glFlush(); }
在顶点之间绘制一个不连续的直线段序列(此后,我们将简单地使用术语“线段”),一次取两条。 特别是,它绘制了线段
v
0
v
1
,
v
2
v
3
,
.
.
.
,
v
n
−
2
v
n
−
1
v0 v1, v2 v3 , . . . , vn−2 vn−1
v0v1,v2v3,...,vn−2vn−1
如果 n 不是偶数,则简单地忽略最后一个顶点 vn-1。
void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glLineWidth(15.0); // Set point size. glBegin(GL_LINES); glColor3f(1.0, 0.0, 0.0); glVertex3f(20.0, 20.0, 0.0); glColor3f(0.0, 1.0, 0.0); glVertex3f(80.0, 20.0, 0.0); glColor3f(0.0, 0.0, 1.0); glVertex3f(80.0, 80.0, 0.0); glColor3f(1.0, 1.0, 0.0); glVertex3f(20.0, 80.0, 0.0); glEnd(); glFlush(); }
绘制段的连接序列
v
0
v
1
,
v
1
v
2
,
.
.
.
,
v
n
−
2
v
n
−
1
v0 v1 , v1 v2, . . . , vn−2 vn−1
v0v1,v1v2,...,vn−2vn−1
void drawScene(void){ glClear(GL_COLOR_BUFFER_BIT); glLineWidth(15.0); // Set point size. glBegin(GL_LINE_STRIP); glColor3f(1.0, 0.0, 0.0); glVertex3f(20.0, 20.0, 0.0); glColor3f(0.0, 1.0, 0.0); glVertex3f(80.0, 20.0, 0.0); glColor3f(0.0, 0.0, 1.0); glVertex3f(80.0, 80.0, 0.0); glColor3f(1.0, 1.0, 0.0); glVertex3f(20.0, 80.0, 0.0); glEnd(); glFlush();}
这样的序列称为polygonal line 或 polyline.
与 GL_LINE_STRIP 相同,只是绘制了一个额外的线段 vn−1 v0 来完成一个loop:
v
0
v
1
,
v
1
v
2
,
.
.
.
,
v
n
−
2
v
n
−
1
,
v
n
−
1
v
0
v0v1, v1 v2 , . . . , vn−2vn−1, vn−1 v0
v0v1,v1v2,...,vn−2vn−1,vn−1v0
void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glLineWidth(15.0); // Set point size. glBegin(GL_LINE_LOOP); glColor3f(1.0, 0.0, 0.0); glVertex3f(20.0, 20.0, 0.0); glColor3f(0.0, 1.0, 0.0); glVertex3f(80.0, 20.0, 0.0); glColor3f(0.0, 0.0, 1.0); glVertex3f(80.0, 80.0, 0.0); glColor3f(1.0, 1.0, 0.0); glVertex3f(20.0, 80.0, 0.0); glEnd(); glFlush(); }
这样的段序列称为折线环。
一次使用三个顶点绘制一系列三角形。 特别地,三角形是
v
0
v
1
v
2
,
v
3
v
4
v
5
,
.
.
.
,
v
n
−
3
v
n
−
2
v
n
−
1
v0 v1 v2 , v3 v4 v5 , . . . , vn−3vn−2vn−1
v0v1v2,v3v4v5,...,vn−3vn−2vn−1
如果 n 是 3 的倍数;
如果不是,最后一个或两个顶点将被忽略。每个三角形的顶点的给定顺序,特别是第一个的 v0、v1、v2,第二个的 v3、v4、v5 等等,决定了它的方向——是顺时针 (CW) 还是逆时针 (CCW)
指定 2D 图元的方向,因此其顶点顺序,很重要,因为这使 OpenGL 能够决定观看者看到的正面或背面。GL TRIANGLES 是一个二维图元,默认情况下,三角形被绘制为填充。但是,可以通过应用 glPolygonMode(face, mode) 命令来选择不同的绘图模式,其中面可能是 GL_FRONT、GL_BACK 或 GL_FRONT AND BACK 之一,模式之一是 GL FILL、GL LINE 或 GL POINT。
如上所述,primitive 是面向前还是面向后取决于它的方向。不过,现在为了简单起见,我们将仅使用 GL FRONT AND BACK inglPolygonMode() 调用,它将给定的绘图模式应用于图元,而不管哪个面是可见的。 GL FILL 选项当然是 2D 图元的默认选项,而 GL LINE 在轮廓(或也称为线框)中绘制图元,而 GL POINT 仅绘制顶点。事实上,通过查看轮廓来解读 2D 基元通常更容易,正如我们将在下面介绍三角形条带的实验中看到的那样。
通过在绘图例程中插入调用 glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 并进一步将 GL_TRIANGLES 替换为 GL_TRIANGLE_STRIP 来继续前面的实验。 显示程序的相关部分如下:
// Drawing routine. void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0, 0.0, 0.0); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glBegin(GL_TRIANGLES); glVertex3f(10.0, 90.0, 0.0); glVertex3f(10.0, 10.0, 0.0); glVertex3f(35.0, 75.0, 0.0); glVertex3f(30.0, 20.0, 0.0); glVertex3f(90.0, 90.0, 0.0); glVertex3f(80.0, 40.0, 0.0); glEnd(); glFlush(); }
绘制一系列三角形 - 称为三角形条 - 如下:第一个三角形是 v0 v1 v2,第二个 v1v3 v2(v0 被丢弃,v3 被引入),第三个 v2v3 v4(v1 被丢弃,v4 被引入) ), 等等。 形式上,条带中的三角形是
v
0
v
1
v
2
,
v
1
v
3
v
2
,
v
2
v
3
v
4
,
.
.
.
,
v
n
−
3
v
n
−
2
v
n
−
1
(
i
f
n
i
s
o
d
d
)
v0v1 v2 , v1v3v2 , v2 v3v4, . . . , vn−3 vn−2 vn−1 (if n is odd)
v0v1v2,v1v3v2,v2v3v4,...,vn−3vn−2vn−1(ifnisodd)
or
v
0
v
1
v
2
,
v
1
v
3
v
2
,
v
2
v
3
v
4
,
.
.
.
,
v
n
−
3
v
n
−
1
v
n
−
2
(
i
f
n
i
s
e
v
e
n
)
v0v1 v2 , v1 v3v2 , v2 v3 v4, . . . , vn−3 vn−1 vn−2 (if n is even)
v0v1v2,v1v3v2,v2v3v4,...,vn−3vn−1vn−2(ifniseven)
顶点的顺序对于确定三角形的方向很重要,
滑动窗口选择带中三角形的顶点。
// Drawing routine. void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0, 0.0, 0.0); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glBegin(GL_TRIANGLE_STRIP); glVertex3f(10.0, 90.0, 0.0); glVertex3f(10.0, 10.0, 0.0); glVertex3f(35.0, 75.0, 0.0); glVertex3f(30.0, 20.0, 0.0); glVertex3f(90.0, 90.0, 0.0); glVertex3f(80.0, 40.0, 0.0); glEnd(); glFlush(); }
围绕第一个顶点绘制一系列三角形(称为三角形扇形),如下所示:第一个三角形是 v0 v1 v2 ,第二个是 v0v2 v3 ,依此类推。 完整的序列是
v
0
v
1
v
2
,
v
0
v
2
v
3
,
.
.
.
,
v
0
v
n
−
2
v
n
−
1
v0v1 v2 , v0 v2v3 , . . . , v0 vn−2 vn−1
v0v1v2,v0v2v3,...,v0vn−2vn−1
// Drawing routine. void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0, 0.0, 0.0); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glBegin(GL_TRIANGLE_FAN); glVertex3f(10.0, 90.0, 0.0); glVertex3f(10.0, 10.0, 0.0); glVertex3f(35.0, 75.0, 0.0); glVertex3f(30.0, 20.0, 0.0); glVertex3f(90.0, 90.0, 0.0); glVertex3f(80.0, 40.0, 0.0); glEnd(); glFlush(); }
用顶点序列绘制多边形
v
0
v
1...
v
n
−
1
v0 v1 . . . vn−1
v0v1...vn−1
对于要绘制的任何内容,n 必须至少为 3
在 z = 0 平面上绘制一个矩形,其边平行于 x 轴和 y 轴。 特别地,矩形在 (x1 , y1 , 0) 和 (x2 , y2 , 0) 处具有对角对角。 四个顶点的完整列表是 (x1 , y1 , 0), (x2, y1, 0), (x2, y2, 0) 和 (x1, y2, 0)。 创建的矩形是二维的,其顶点顺序取决于两个顶点 (x1 , y1 , 0) 和 (x2 , y2 , 0) 相对于彼此的情况,如右下方的两张图所示 图。 注意 glRectf() 是一个独立的调用; 它不像其他原语那样是 glBegin() 的参数。
// Drawing routine.
void drawScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 1.0, 0.0);
glRectf(20.0, 20.0, 80.0, 80.0);
glEnd();
glFlush();
}
重要提示:前面的两个基元 GL POLYGON 和 glRectf() 都已从 OpenGL 的后续版本(例如 4.x)的核心配置文件中删除 但是,它们可以通过兼容性配置文件访问。 为什么多边形和矩形被丢弃不难理解:两者都可以由三角形组成,所以真的是多余的。 我们在本书的前半部分仍然使用它们的原因是因为它们提供了一种易于理解的方法来制作对象——例如,我们第一个程序 square.cpp 的正方形多边形对于初学者来说肯定比三角形条带更直观 .
我们看到 OpenGL 几何图元是组成的点、直线段和平面片,后者是三角形、矩形和多边形。 那么,如何绘制圆盘、椭圆、螺旋、啤酒罐和飞碟等弯曲物体呢? 答案是用直线和平面 OpenGL 基元足够好地近似它们,以至于观看者无法区分它们。 正如摇摆人曾经说过的那样,“真诚是一种非常重要的人类品质。 如果你没有它,你就得伪造它!” 在下一个实验中,我们伪造了一个圆圈。
#include <cstdlib> #include <cmath> #include <iostream> #include <GL/glew.h> #include <GL/freeglut.h> #define PI 3.14159265358979324 // Globals. static float R = 40.0; // Radius of circle. static float X = 50.0; // X-coordinate of center of circle. static float Y = 50.0; // Y-coordinate of center of circle. static int numVertices = 5; // Number of vertices on circle. // Drawing routine. void drawScene(void) { float t = 0; // Angle parameter. int i; glClear(GL_COLOR_BUFFER_BIT); glLineWidth(15.0); // Set point size. // Draw a line loop with vertices at equal angles apart on a circle // with center at (X, Y) and radius R, The vertices are colored randomly. glBegin(GL_LINE_LOOP); for (i = 0; i < numVertices; ++i) { glColor3f((float)rand() / (float)RAND_MAX, (float)rand() / (float)RAND_MAX, (float)rand() / (float)RAND_MAX); glVertex3f(X + R * cos(t), Y + R * sin(t), 0.0); t += 2 * PI / numVertices; } glEnd(); glFlush(); } // 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(); glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.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; case '+': numVertices++; glutPostRedisplay(); break; case '-': if (numVertices > 3) numVertices--; glutPostRedisplay(); break; default: break; } } // Routine to output interaction instructions to the C++ window. void printInteraction(void) { std::cout << "Interaction:" << std::endl; std::cout << "Press +/- to increase/decrease the number of vertices on the circle." << 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("circle.cpp"); glutDisplayFunc(drawScene); glutReshapeFunc(resize); glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE; glewInit(); setup(); glutMainLoop(); }
通过按“+”直到它“变成”一个圆圈, 按“-”减少顶点数。 随机的颜色有点吸引眼球。 结束 circle.cpp 循环的顶点,它们均匀分布在圆上,统称为点的样本,或者简单地称为圆的样本。 循环本身显然是一个正多边形的边界。 显然,样本越密集,循环就越接近圆。
#include <cstdlib> #include <cmath> #include <iostream> #include <GL/glew.h> #include <GL/freeglut.h> #define PI 3.14159265 #define N 40.0 // 圆盘边界上的顶点数。 // Globals. static int isWire = 0; // 是线框? static long font = (long)GLUT_BITMAP_8_BY_13; // 字体选择。 // 绘制位图字符串的例程。 void writeBitmapString(void *font, char *string) { char *c; for (c = string; *c != '\0'; c++) glutBitmapCharacter(font, *c); } // 绘制圆盘的函数,圆盘的中心为 (X, Y, Z),半径为 R,平行于 xy 平面。 void drawDisc(float R, float X, float Y, float Z) { float t; int i; glBegin(GL_TRIANGLE_FAN); glVertex3f(X, Y, Z); for (i = 0; i <= N; ++i) { t = 2 * PI * i / N; glVertex3f(X + cos(t) * R, Y + sin(t) * R, Z); } glEnd(); } // 绘图程序。 void drawScene(void) { float angle; int i; glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除缓冲区,包括深度缓冲区。 glPolygonMode(GL_FRONT, GL_FILL); // 左上角圆环:白盘覆盖红盘。 glColor3f(1.0, 0.0, 0.0); drawDisc(20.0, 25.0, 75.0, 0.0); glColor3f(1.0, 1.0, 1.0); drawDisc(10.0, 25.0, 75.0, 0.0); // 右上圆环:白色圆盘在阻挡它的红色圆盘前面。 glEnable(GL_DEPTH_TEST); // 启用深度测试。 glColor3f(1.0, 0.0, 0.0); drawDisc(20.0, 75.0, 75.0, 0.0); glColor3f(1.0, 1.0, 1.0); drawDisc(10.0, 75.0, 75.0, 0.5); // 将此 z 值与红色圆盘的 z 值进行比较。 glDisable(GL_DEPTH_TEST); // 禁用深度测试。 // 下圆环:有一个真正的洞。 if (isWire) glPolygonMode(GL_FRONT, GL_LINE); else glPolygonMode(GL_FRONT, GL_FILL); glColor3f(1.0, 0.0, 0.0); glBegin(GL_TRIANGLE_STRIP); for (i = 0; i <= N; ++i) { angle = 2 * PI * i / N; glVertex3f(50 + cos(angle) * 10.0, 30 + sin(angle) * 10.0, 0.0); glVertex3f(50 + cos(angle) * 20.0, 30 + sin(angle) * 20.0, 0.0); } glEnd(); // Write labels. glColor3f(0.0, 0.0, 0.0); glRasterPos3f(15.0, 51.0, 0.0); writeBitmapString((void *)font, "Overwritten"); glRasterPos3f(69.0, 51.0, 0.0); writeBitmapString((void *)font, "Floating"); glRasterPos3f(38.0, 6.0, 0.0); writeBitmapString((void *)font, "The real deal!"); 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 wirefrime and filled for the lower annulus." << std::endl; } // 主程序。 int main(int argc, char **argv) { printInteraction(); glutInit(&argc, argv); glutInitContextVersion(4, 3); glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA | GLUT_DEPTH); glutInitWindowSize(500, 500); glutInitWindowPosition(100, 100); glutCreateWindow("circularAnnuluses.cpp"); glutDisplayFunc(drawScene); glutReshapeFunc(resize); glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE; glewInit(); setup(); glutMainLoop(); }
三个外观相同的红色圆形环以三种不同的方式绘制:
i) 左上角:没有真正的洞。 白色圆盘会覆盖代码后面出现的红色圆盘:
// 左上角圆环:白盘覆盖红盘。
glColor3f(1.0, 0.0, 0.0);
drawDisc(20.0, 25.0, 75.0, 0.0);
glColor3f(1.0, 1.0, 1.0);
drawDisc(10.0, 25.0, 75.0, 0.0);
注意:子程序 drawDisc() 的第一个参数是半径,其余三个参数是中心坐标。
ii) 右上:也没有真正的洞。 白色圆盘比红色圆盘更靠近观察者,从而将其挡住:
// 右上圆环:白色圆盘在阻挡它的红色圆盘前面。 glEnable(GL_DEPTH_TEST); // 启用深度测试。 glColor3f(1.0, 0.0, 0.0); drawDisc(20.0, 75.0, 75.0, 0.0); glColor3f(1.0, 1.0, 1.0); drawDisc(10.0, 75.0, 75.0, 0.5); // 将此 z 值与红色圆盘的 z 值进行比较。 glDisable(GL_DEPTH_TEST); // 禁用深度测试。
观察到白色圆盘中心的 z 值大于红色圆盘的 z 值,使其更靠近观察面。 我们将暂时讨论一个原语阻止另一个原语的机制。
iii) 下:一个真正的圆形环带一个真正的洞:
// 下圆环:有一个真正的洞。
if (isWire) glPolygonMode(GL_FRONT, GL_LINE); else glPolygonMode(GL_FRONT, GL_FILL);
glColor3f(1.0, 0.0, 0.0);
glBegin(GL_TRIANGLE_STRIP);
for (i = 0; i <= N; ++i)
{
angle = 2 * PI * i / N;
glVertex3f(50 + cos(angle) * 10.0, 30 + sin(angle) * 10.0, 0.0);
glVertex3f(50 + cos(angle) * 20.0, 30 + sin(angle) * 20.0, 0.0);
}
glEnd();
按空格键查看三角形条的线框。
启用深度缓冲区(也称为 z-buffer )会导致 OpenGL 在渲染之前消除被其他对象遮挡(或遮挡)的对象部分。准确地说,如果对象的投影(想想从该点发出的光线)朝向观察面被另一个对象阻挡,则不会绘制对象的某个点。 circularAnnuluses.cpp 的右上环的制作见图 2.45(a):白色圆盘遮挡了它后面的红色圆盘部分(因为投影是正交的,遮挡部分的形状和大小完全相同)白色圆盘)。此过程称为隐藏表面去除或深度测试或可见性确定。从数学上讲,在正交投影的情况下隐藏表面去除的结果如下。在查看框的跨度内固定 x 值 X 和 y 值 Y 值。考虑属于查看框中的对象的点集 S,它们的 x 值等于 X 且 y 值等于 Y 。准确地说,S 是通过 (X, Y, 0) 平行于 z 轴的直线 L 与查看框中的对象相交的点集。显然,S 的形式为 S = {(X, Y, z)},其中 z 根据相交对象而变化(如果 L 在查看框中不与任何内容相交,则为空)。假设 S 不为空,让 Z 成为这些 z 值中最大的一个。换句话说,在属于观察盒中 x 值等于 X 和 y 值等于 Y 的对象的点中,(X, Y, Z) 具有最大的 z 值并且离观察面最近。接下来,观察 S 中的所有点投影到观察面上 P 用 (X, Y, Z) 的颜色属性进行渲染。这意味着只有 (X, Y, Z),
最靠近观察面的,是由 S 中的点绘制的,其余的,在它后面,被遮住了。例如,在图 2.45(b) 中,分别为红色、绿色和蓝色的三个点 A、B 和 C 共享相同的前两个坐标值,即 X = 30 和 Y = 20。因此,所有三个沿线 L 投影到观察面上的同一点 P。由于 A 具有三个中最大的 z 坐标,因此它遮挡了其他两个,因此 P 被绘制为红色。 z 缓冲区本身是 GPU 中包含 z 值的一块内存,每个像素一个。如果启用了深度测试,那么在处理图元以进行渲染时,会将其每个点(或更准确地说,其每个像素)的 z 值与具有相同 (x, y ) - 当前驻留在 z 缓冲区中的值。如果传入像素的 z 值较大,则其 RGB 属性和 z 值将替换当前像素的 RGB 属性和 z 值;如果不是,则丢弃传入像素的数据。例如,如果图 2.45(b) 中的点在代码中出现的顺序是 C、A 和 B,那么 P 对应的像素处的颜色和 z 缓冲区值如何变化:最接近观察face, 是由 S 中的点绘制的,其余的,在它后面,被遮住了。例如,在图 2.45(b) 中,分别为红色、绿色和蓝色的三个点 A、B 和 C 共享相同的前两个坐标值,即 X = 30 和 Y = 20。因此,所有三个沿线 L 投影到观察面上的同一点 P。由于 A 具有三个中最大的 z 坐标,因此它遮挡了其他两个,因此 P 被绘制为红色。 z 缓冲区本身是 GPU 中包含 z 值的一块内存,每个像素一个。如果启用了深度测试,那么在处理图元以进行渲染时,会将其每个点(或更准确地说,其每个像素)的 z 值与具有相同 (x, y ) - 当前驻留在 z 缓冲区中的值。如果传入像素的 z 值较大,则其 RGB 属性和 z 值将替换当前像素的 RGB 属性和 z 值;如果不是,则丢弃传入像素的数据。例如,如果图 2.45(b) 中的点在代码中出现的顺序是 C、A 和 B,那么 P 对应的像素处的颜色和 z 缓冲区值是如何变化的:
画 C; // P对应的像素取蓝色
// 和 z 值 -0.5。
画 A; // P对应的像素变为红色
// 和 z 值 0.3:A 的值覆盖 C 的值。
画 B; // P对应的像素保留红色
// 和 z 值 0.3:B 被丢弃。
在 GPU 中的实际实现中,z-buffer 中每个像素的值在 0 到 1 之间,其中 0 对应于观察框的近面,1 对应于远面。 发生的情况是,在将它们记录到 z 缓冲区之前,系统将(通过缩放,但不一定是线性的)世界空间 z 值每个变换到 [0, 1] 范围内,并使用符号翻转,以便像素更远 从观察面有更高的 z 值。 因此,在此转换之后,较低的值实际上赢得了在 z 缓冲区中可见的竞争。 但是,在编写 OpenGL 代码时,我们是在世界空间中操作的,其中查看框中较高的 z 值更靠近查看面,因此我们无需关心这种实现的特殊性。
接下来我们通过绘制螺旋线或更科学地绘制螺旋线来获得更严肃的 3D。 一个螺旋线,虽然它本身是一个一维的物体——实际上画成一条线——只能在 3 空间中真实地制作。
#include <cstdlib> #include <cmath> #include <iostream> #include <GL/glew.h> #include <GL/freeglut.h> #define PI 3.14159265 // 绘图程序。 void drawScene(void) { float R = 20.0; // Radius of helix. float t; // Angle parameter. glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0, 0.0, 0.0); glBegin(GL_LINE_STRIP); for (t = -10 * PI; t <= 10 * PI; t += PI / 20.0) glVertex3f(R * cos(t), R * sin(t), t - 60.0); // glVertex3f(R * cos(t), t, R * sin(t) - 60.0); glEnd(); glFlush(); } // 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(); //glOrtho(-50.0, 50.0, -50.0, 50.0, 0.0, 100.0); 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("helix.cpp"); glutDisplayFunc(drawScene); glutReshapeFunc(resize); glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE; glewInit(); setup(); glutMainLoop(); }
$$
x = R cos t, y = R sin t, z = t − 60.0, −10π ≤ t ≤ 10π
$$
对于以 (0, 0, 0) 为中心的圆,将这些与方程 (2.1) 进行比较,将 X = 0 和 Y = 0 放在之前的方程中。 不同之处在于,随着 t 的增加,螺旋在圆周旋转时同时沿 z 轴爬升(因此,它实际上绕 z 轴盘绕)。 通常,最后一个坐标只写 z = t; 然而,我们使用“-60.0”将螺旋线沿 z 轴向下推得足够远,使其完全包含在观察盒中。
好的,现在运行 helix.cpp。 我们看到的只是一个圆圈,如图 。 没有任何向上或向下盘绕的迹象。 原因当然是在观察面上的正投影使螺旋变平。 让我们看看将螺旋线直立是否会有所不同,特别是,使其绕 y 轴盘旋。 因此,替换声明
glVertex3f(R * cos(t), R * sin(t), t - 60.0)
在绘图程序中
glVertex3f(R * cos(t), t, R * sin(t) - 60.0);
因为它压缩了一个维度,所以正射投影通常不适合 3D 场景。 实际上,OpenGL 提供了另一种投影,称为透视投影,更适合大多数 3D 应用程序。 透视投影是通过 glFrustum() 调用实现的。 一个 glFrustum(left, right, bottom, top, near, far) 调用建立了一个视锥体,而不是一个观察框——一个截锥体是一个截棱锥,它的顶部被一个平行于它的底部的平面切掉了——在 以下方式:
金字塔的顶点在原点。 截锥体的正面或观察面是位于 z = -near 平面上的矩形,其角为 (left, bottom, -near ), (right, bottom, -near ), (left, top, -near ), 和 (right, top, -near )。 平面 z = -near 是观察平面 - 事实上,它是截断
金字塔。
从顶点发出的金字塔的四个边缘穿过观察面的四个角。 平截头体的底面或背面是矩形,其顶点正好位于金字塔的四个边与 z = -far 平面相交的位置。 通过与前顶点的比例,基顶点的坐标
是:
((far/near) left,(far/near) bottom, −far),
((far/near) right,(far/near) bottom, −far),
((far/near) left, (far/near) top, −far),
((far/near) right, (far/near) top, −far)
glFrustum() 参数的值通常设置为使视锥体关于 z 轴对称; 特别是,right 和 top 被选为正数,left 和 bottom 分别为负数。 参数 Near 和 far 应该,两者都为正且接near < far,这意味着截锥体完全位于 z 轴的负侧,其底部位于观察面后面。
选择
//glOrtho(-50.0, 50.0, -50.0, 50.0, 0.0, 100.0);
glFrustum(-5.0, 5.0, -5.0, 5.0, 5.0, 100.0);
glVertex3f(R * cos(t), R * sin(t),t - 60.0);
glVertex3f(R * cos(t), t, R * sin(t) - 60.0);
1.在 x = −π 和 x = π 之间画一条正弦曲线
#include <cstdlib> #include <cmath> #include <iostream> #include <GL/glew.h> #include <GL/freeglut.h> #define PI 3.14159265 // 绘图程序。 void drawScene(void) { float R = 20.0; // Radius of helix. float t; // Angle parameter. glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0, 0.0, 0.0); glLineWidth(5); glBegin(GL_LINE_STRIP); for (t = -PI; t <= PI; t += PI / 20.0) glVertex3f(10 * t, R * sin(t), 0); glEnd(); glFlush(); } // 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(); glOrtho(-50.0, 50.0, -50.0, 50.0, 0.0, 100.0); //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("helix.cpp"); glutDisplayFunc(drawScene); glutReshapeFunc(resize); glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE; glewInit(); setup(); glutMainLoop(); }
画一个椭圆。 回想一下 xy 平面上椭圆的参数方程,以 (X, Y ) 为中心,长半轴长度为 A,半短轴长度为 B(图 2.55(b)):
x
=
X
+
A
c
o
s
t
,
y
=
Y
+
B
s
i
n
t
,
z
=
0
,
0
≤
t
≤
2
π
x = X + A cos t, y = Y + B sin t, z = 0, 0 ≤ t ≤ 2π
x=X+Acost,y=Y+Bsint,z=0,0≤t≤2π
// 绘图程序。 void drawScene(void) { float R = 20.0; // Radius of helix. float X = 0; float Y = 0; float A = 23; float B = 15; float t; // Angle parameter. glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0, 0.0, 0.0); glLineWidth(5); glBegin(GL_LINE_STRIP); for (t = -10 * PI; t <= 10 * PI; t += PI / 20.0) glVertex3f(X + A * cos(t), Y + B * sin(t), 0); glEnd(); glFlush(); }
将字母“A”绘制为二维图形,如图 2.55© 中的阴影区域。 首先在方格纸上对其进行三角测量可能会有所帮助。 尝试将尽可能多的三角形打包成尽可能少的三角形条带(因为每次绘图调用都会消耗 GPU,但请参阅下一个实验)。 允许用户在circularAnnuluses.cpp 的底部环带填充和线框之间切换。 注意:这个练习的变化可以通过询问不同的字母来进行。 请记住,像“S”这样的弯曲字母比直边字母更难。
// Drawing routine. void drawScene(void) { int i; glClear(GL_COLOR_BUFFER_BIT); if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glColor3f(0.0, 0.0, 0.0); glBegin(GL_TRIANGLE_STRIP); glVertex3f(10.0, 10.0, 0.0); glVertex3f(20.0, 10.0, 0.0); glVertex3f(40.0, 90.0, 0.0); glVertex3f(50.0, 90.0, 0.0); glVertex3f(70.0, 10.0, 0.0); glVertex3f(80.0, 10.0, 0.0); glVertex3f(55.0, 50.0, 0.0); glVertex3f(60.0, 40.0, 0.0); glVertex3f(25.0, 50.0, 0.0); glVertex3f(30.0, 40.0, 0.0); glEnd(); glFlush(); }
在图 2.55(d) 中绘制数字“8”作为二维对象。 以两种不同的方式执行此操作:(i) 绘制 4 个光盘并使用 z 缓冲区,以及 (ii) 作为真正的三角测量,允许用户在填充和线框之间切换。 对于 (ii),图 2.55(d) 建议了一种将“8”分成两个三角形条带的方法。
// 绘制圆盘的函数,圆盘的中心为 (X, Y, Z),半径为 R,平行于 xy 平面。 void drawDisc(float R, float X, float Y, float Z) { float t; int i; glBegin(GL_TRIANGLE_FAN); if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glVertex3f(X, Y, Z); for (i = 0; i <= N; ++i) { t = 2 * PI * i / N; glVertex3f(X + cos(t) * R, Y + sin(t) * R, Z); } glEnd(); } // Drawing routine. void drawScene(void) { int i; glClear(GL_COLOR_BUFFER_BIT); if (isWire) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glColor3f(0.0, 0.0, 0.0); glColor3f(0.0, 1.0, 0.0); drawDisc(15.0, 0.0, 10.0, 0.0); glColor3f(1.0, 1.0, 1.0); drawDisc(5.0, 0.0, 10.0, 0.0); glColor3f(0.0, 1.0, 0.0); drawDisc(20.0, 0.0, -18.0, 0.0); glColor3f(1.0, 1.0, 1.0); drawDisc(10.0, 0.0, -18.0, 0.0); glFlush(); }
我们的下一个 3 空间绘图项目更具挑战性:一个半球,它是一个二维对象。我们还将制定某些设计原则,这些原则将在专门用于绘图的第 10 章中进行扩展(尽早开始无害)。备注 2.23。半球是二维物体,因为它是一个表面。回想一下,螺旋线是一维的,因为它是线状的。现在,半球和螺旋都需要 3 个空间才能“坐下”;他们不能少做。例如,您可以在一张纸(2 格)上草绘,但它不会是真实的。另一方面,一个圆——另一个 1D 对象——确实在 2 空间中愉快地坐着。考虑一个半径为 R 的半球,以原点 O 为中心,其圆底位于 xz 平面上。假设这个半球上的点 P 的球坐标是 θ 的经度(从 y 轴的正侧看时从 x 轴逆时针测量)和 φ 纬度(从 xz 平面测量) y 轴的正侧)。见图 2.57(a)。 P 的笛卡尔坐标是由基本三角函数
(
R
c
o
s
φ
c
o
s
θ
,
R
s
i
n
φ
,
−
R
c
o
s
φ
s
i
n
θ
)
(R cos φ cos θ, R sin φ, −R cos φ sin θ)
(Rcosφcosθ,Rsinφ,−Rcosφsinθ)
θ 的范围是 0 ≤ θ ≤ 2π,φ 的范围是 0 ≤ φ ≤ π / 2。
在 (p + 1)(q + 1) 个点 Pij , 0 ≤ i ≤ p, 0 ≤ j ≤ q 的网格处对半球进行采样,其中 Pij 的经度为 (i/p) ∗ 2π 及其纬度 (j /q) * π/2。 换句话说,沿着 q + 1 个等距纬度中的每一个选择 p + 1 个纵向等距点。 参见图 2.57(b),其中 p = 10 和 q = 4。样本点 Pij 并非都是不同的。 事实上,P0j = Ppj ,对于所有 j,因为同一点的经度为 0 和 2π; 并且,对于所有 i,点 Piq 与具有纬度 π/2 和任意经度的北极相同。 现在的计划是用三角形带来近似每对相邻纬度之间的圆形带——这样的带将交替地从任一纬度取顶点。 准确地说,我们将绘制一个三角形带,其顶点位于
P
0
,
j
+
1
,
P
0
j
,
P
1
,
j
+
1
,
P
1
j
,
.
.
.
,
P
p
,
j
+
1
,
P
p
j
P0,j+1 , P0j , P1,j+1, P1j , . . . , Pp,j+1 , Ppj
P0,j+1,P0j,P1,j+1,P1j,...,Pp,j+1,Ppj
对于每个 j,0 ≤ j ≤ q − 1,总共 q 个三角形条带。 这些 q 三角形条带在一起将近似于半球本身。
/// // hemisphere.cpp // 这个程序用一组纬度三角形条带近似一个半球。 // 相互作用: // 按 P/p 增加/减少纵向切片的数量。 // 按 Q/q 增加/减少纬度切片的数量。 // 按 x, X, y, Y, z, Z 转动半球。 /// #include <cmath> #include <iostream> #include <GL/glew.h> #include <GL/freeglut.h> #define PI 3.14159265 // Globals. static float R = 5.0; // Radius of hemisphere. static int p = 6; // Number of longitudinal slices. static int q = 4; // Number of latitudinal slices. static float Xangle = 0.0, Yangle = 0.0, Zangle = 0.0; // Angles to rotate hemisphere. // Initialization routine. void setup(void) { glClearColor(1.0, 1.0, 1.0, 0.0); } // Drawing routine. void drawScene(void) { int i, j; glClear(GL_COLOR_BUFFER_BIT); glLoadIdentity(); // Command to push the hemisphere, which is drawn centered at the origin, // into the viewing frustum. glTranslatef(0.0, 0.0, -10.0); // Commands to turn the hemisphere. glRotatef(Zangle, 0.0, 0.0, 1.0); glRotatef(Yangle, 0.0, 1.0, 0.0); glRotatef(Xangle, 1.0, 0.0, 0.0); // Hemisphere properties. glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glColor3f(0.0, 0.0, 0.0); // 纬向三角形条带阵列,每条平行于赤道,堆叠一个 // 从赤道到北极的另一个上方。 for (j = 0; j < q; j++) { // One latitudinal triangle strip. glBegin(GL_TRIANGLE_STRIP); for (i = 0; i <= p; i++) { glVertex3f(R * cos((float)(j + 1) / q * PI / 2.0) * cos(2.0 * (float)i / p * PI), R * sin((float)(j + 1) / q * PI / 2.0), -R * cos((float)(j + 1) / q * PI / 2.0) * sin(2.0 * (float)i / p * PI)); glVertex3f(R * cos((float)j / q * PI / 2.0) * cos(2.0 * (float)i / p * PI), R * sin((float)j / q * PI / 2.0), -R * cos((float)j / q * PI / 2.0) * sin(2.0 * (float)i / p * PI)); } glEnd(); } 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); } // Keyboard input processing routine. 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; } } // Routine to output interaction instructions to the C++ window. 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("hemisphere.cpp"); glutDisplayFunc(drawScene); glutReshapeFunc(resize); glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE; glewInit(); setup(); glutMainLoop(); }
#include <GL/glew.h> #include <GL/freeglut.h> // Drawing routine. void drawScene(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0, 1.0, 0.0); glRectf(20.0, 20.0, 80.0, 80.0); glEnd(); glFlush(); } // 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(); glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.0); // glOrtho(-100, 100.0, -100.0, 100.0, -1.0, 1.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("square.cpp"); glutDisplayFunc(drawScene); glutReshapeFunc(resize); glutKeyboardFunc(keyInput); glewExperimental = GL_TRUE; glewInit(); setup(); glutMainLoop(); }
我们从 main() 开始:
glutInit(&argc, argv) 初始化 FreeGLUT 库。 FreeGLUT [49],GLUT(OpenGL Utility Toolkit)的继承者,是一个管理窗口和监控鼠标和键盘输入的调用库(需要这样一个单独的库的原因是OpenGL本身只是一个图形调用库) .
glutInitContextVersion(4, 3); glutInitContextProfile(GLUT_COMPATIBILITY_PROFILE); 告诉 FreeGLUT 程序需要一个 OpenGL 4.3 上下文——这个上下文是 OpenGL 实例和系统其余部分之间的接口——在实现遗留命令时向后兼容。 例如,这允许我们使用 OpenGL 2.1 中的 glBegin()-glEnd() 操作进行绘制,这些操作不属于 OpenGL 4.3 的核心配置文件。 备注 2.25。 如果您的显卡不支持 OpenGL 4.3,那么程序可能会编译但无法运行,因为系统无法提供所要求的上下文。 在这种情况下,您可能会通过用 glutInitContextVersion(3, 3) 甚至 glutInitContextVersion(2, 1) 替换上面的第一行来精简上下文。 当然,使用后代调用的程序将不会运行,但您在本书的早期应该没问题。
glutInitDisplayMode(GLUT SINGLE | GLUT RGBA) 表示我们需要一个 OpenGL 上下文来支持单缓冲帧,每个像素都有红色、绿色、蓝色和 alpha 值。
glutInitWindowSize(500, 500); glutInitWindowPosition(100, 100); 正如我们已经看到的,设置 OpenGL 窗口的大小及其在计算机屏幕上左上角的位置。
glutCreateWindow(“square.cpp”) 实际上用指定的字符串参数作为标题创建了 OpenGL 上下文及其关联的窗口。
glutDisplayFunc(drawScene);
glutReshapeFunc(resize);
glutKeyboardFunc(keyInput);
分别在绘制 OpenGL 窗口、调整大小和接收键盘输入时注册要调用的例程——所谓的回调例程。
glewExperimental = GL_TRUE;
glewInit();
初始化处理 OpenGL 扩展加载的 GLEW,设置开关以便公开即使在预发布驱动程序中实现的扩展。
setup() 调用初始化例程。
glutMainLoop 开始事件处理循环,根据需要调用注册的回调例程。
我们已经看到初始化例程 setup() 中唯一的命令,即 glClearColor(1.0, 1.0, 1.0, 0.0) 指定了 OpenGL 窗口的清除颜色。 绘制 OpenGL 窗口的回调例程是:
void drawScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(0.0, 0.0, 0.0);
// Draw a polygon with specified vertices.
glBegin(GL_POLYGON);
...
glEnd();
glFlush();
}
第一个命令将 OpenGL 窗口清除为指定的清除颜色,换句话说,以背景颜色绘制。 下一个命令 glColor3f() 设置前景或绘图颜色,用于绘制在 glBegin()-glEnd() 对中指定的多边形(我们已经仔细检查了这个多边形)。 最后, glFlush() 强制队列中的所有命令实际执行——清空或刷新命令缓冲区——在这种情况下,这意味着多边形被绘制。 调整 OpenGL 窗口大小并首次创建时的回调例程是 void resize(int w, int h)。 窗口管理器将调整大小的 OpenGL 窗口(或初始窗口,当它第一次创建时)的宽度 w 和高度 h 作为参数提供给调整大小例程。 第一条命令
glViewport(0, 0, w, h);
square.cpp 的调整大小例程指定了 OpenGL 窗口的矩形部分,在其中进行实际绘图; 对于给定的参数,它是整个窗口。 我们将在下一章更仔细地研究 glViewPort() 及其应用。 接下来的三个命令
glMatrixMode(GL PROJECTION);
glLoadIdentity();
glOrtho(0.0, 100.0, 0.0, 100.0, -1.0, 1.0);
激活投影矩阵堆栈,将单位矩阵放在此堆栈的顶部,然后将单位矩阵乘以与最终 glOrtho() 命令对应的矩阵。 如果关于矩阵的所有这些现在没有多大意义,请不要担心 - 上面的第三条语句设置 square.cpp 的查看框的方式此时就足够了。
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
调整大小例程的激活模型视图矩阵堆栈并将单位矩阵放在顶部以准备绘图例程中的模型视图转换命令 - 移动对象的命令,而 square.cpp 中恰好没有
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。