opengl学习笔记
摘自www.opengl.org楼主会不定时更新
|
|
OpenGL编程指南 |
学习OpenGL的官方指南1.1版
- 第一章OpenGL简介
- 第二章国家管理和绘制几何对象
- 第3章查看
- 第4章颜色
- 第5章照明
- 第7章显示列表
- 第九章纹理映射
- 第10章Framebuffer
- 第十一章细分和四项
- 第12章评价者和NURBS
- 第13章选择与反馈
- 第14章现在你知道了
- 附录A.作业顺序
- 附录B. 国家变量
- 附录E. 计算正常向量
- 附录F. 齐次坐标和变换矩阵
- 附录G. 编程提示
- 附录H. OpenGL不变性
- 附录I. 色板
|
|
编程指南 >第1章 |
第1章
|
后缀 |
数据类型 |
典型相应的C语言类型 |
OpenGL类型定义 |
b |
8位整数 |
签字字 |
GLbyte |
小号 |
16位整数 |
短 |
表示GLshort |
一世 |
32位整数 |
int或long |
GLint,GLsizei |
F |
32位浮点数 |
浮动 |
GLfloat,GLclampf |
ð |
64位浮点数 |
双 |
GLdouble,GLclampd |
UB |
8位无符号整数 |
无符号字符 |
GLubyte,GLboolean |
我们 |
16位无符号整数 |
无符号短 |
GLushort |
UI |
32位无符号整数 |
unsigned int或unsigned long |
GLuint,GLenum,GLbitfield |
因此,这两个命令
glVertex2i(1,3); glVertex2f(1.0,3.0);
是等效的,除了第一个将顶点的坐标指定为32位整数,第二个将它们指定为单精度浮点数。
注意: OpenGL的实现在选择用于表示OpenGL数据类型的C数据类型方面有一定的余地。如果您在整个应用程序中坚决使用OpenGL定义的数据类型,则在不同实现之间移植代码时,将避免不匹配的类型。
一些OpenGL命令可以取最后一个字母v,这表示该命令使用指向一个值(或数组)的指针而不是一系列单个参数。许多命令都具有向量和非向量版本,但是一些命令仅接受各自的参数,而其他命令则要求将至少一些参数指定为向量。以下几行显示如何使用设置当前颜色的命令的向量和非向量版本:
glColor3f(1.0,0.0,0.0); GLfloat color_array [] = {1.0,0.0,0.0}; glColor3fv(color_array);
最后,OpenGL定义了typedef GLvoid。这最常用于接受指向数组数组的指针的OpenGL命令。
在本指南的其余部分(实际代码示例除外)中,OpenGL命令仅由其基本名称引用,并包含星号以指示命令名称可能更多。例如,glColor *()代表用于设置当前颜色的命令的所有变体。如果我们想要指出特定命令的一个版本的具体问题,我们将包含定义该版本所需的后缀。例如,glVertex * v()是指用于指定顶点的所有命令的矢量版本。
OpenGL作为状态机
OpenGL是一个状态机。你把它放入各种状态(或模式),然后才能保持有效,直到你改变它们。正如你已经看到的,当前的颜色是一个状态变量。您可以将当前颜色设置为白色,红色或任何其他颜色,然后每个对象都用该颜色绘制,直到将当前颜色设置为其他颜色为止。当前的颜色只是OpenGL维护的许多状态变量之一。其他控制诸如当前的观看和投影变换,线和多边形点状图案,多边形绘图模式,像素包装约定,灯的位置和特征以及正在绘制的对象的材料属性。许多状态变量指使用glEnable()或glDisable()命令启用或禁用的模式,。
每个状态变量或模式都有一个默认值,并且您可以随时查询系统中每个变量的当前值。通常,您使用以下六个命令之一执行此操作:glGetBooleanv(),glGetDoublev(),glGetFloatv(),glGetIntegerv(),glGetPointerv()或glIsEnabled()。您选择哪些命令取决于要在其中给出答案的数据类型。某些状态变量具有更具体的查询命令(如glGetLight *(),glGetError()或glGetPolygonStipple())。另外,您可以在属性堆栈上保存状态变量的集合glPushAttrib()或glPushClientAttrib(),临时修改它们,然后使用glPopAttrib()或glPopClientAttrib()还原值。对于临时状态更改,您应该使用这些命令而不是任何查询命令,因为它们可能会更有效率。
有关可以查询的状态变量的完整列表,请参阅附录B. 对于每个变量,附录还列出了一个建议的glGet *()命令,它返回变量的值,它所属的属性类和变量的默认值。
OpenGL渲染管道
OpenGL的大多数实现具有类似的操作顺序,称为OpenGL渲染管道的一系列处理阶段。如图1-2所示,这种排序不是OpenGL如何实现的严格规则,而是为预测OpenGL将要做的事情提供可靠的指导。
如果您是三维图形的新手,即将到来的描述可能看起来像消防水带上的饮用水。你现在可以看看,但是在阅读本书的每一章时,请回到图1-2。
下图显示了OpenGL处理数据所需的Henry Ford装配线方法。几何数据(顶点,线和多边形)沿着包括评估者和每顶点操作在内的一行框的路径,而像素数据(像素,图像和位图)在一部分过程中被不同地对待。在将最终像素数据写入帧缓冲区之前,两种类型的数据都经历相同的最终步骤(光栅化和每个片段操作)。
图1-2:操作顺序
现在,您将看到有关OpenGL渲染管道关键阶段的更多详细信息。
显示列表
所有数据,无论是描述几何或像素,都可以保存在显示列表中,以供当前或以后使用。(在显示列表中保留数据的替代方法是立即处理数据 - 也称为即时模式)。当执行显示列表时,保留的数据将从显示列表发送,就像应用程序立即发送的一样模式。(有关显示列表的更多信息,请参阅第7章。)
评价者
所有几何图元最终由顶点描述。参数曲线和曲面可以最初由控制点和称为基函数的多项式函数描述。评估者提供了一种从控制点导出用于表示曲面的顶点的方法。该方法是多项式映射,可以从控制点产生表面法线,纹理坐标,颜色和空间坐标值。(有关评估者的更多信息,请参阅第12章。)
每顶点操作
对于顶点数据,接下来是“每顶点操作”阶段,它将顶点转换为基元。一些顶点数据(例如,空间坐标)被4×4个浮点矩阵变换。空间坐标从3D世界中的位置投影到屏幕上的位置。(有关转换矩阵的详细信息,请参见第3章。)
如果启用高级功能,这个阶段甚至更繁忙。如果使用纹理,则可以在此处生成和转换纹理坐标。如果启用了照明,则使用变换的顶点,表面法线,光源位置,材料属性和其他照明信息执行照明计算,以产生颜色值。
原始大会
剪切是原始装配的主要部分,是消除由平面限定的半空间外的几何形状的部分。点裁剪简单地传递或拒绝顶点; 线或多边形裁剪可以根据线或多边形的剪裁方式添加额外的顶点。
在某些情况下,这之后是透视分割,这使得遥远的几何对象看起来比较近的物体小。然后应用视口和深度(z坐标)操作。如果启用了剔除,并且原语是多边形,则可能会被剔除测试所拒绝。根据多边形模式,可以将多边形绘制为点或线。(请参阅第2章“多边形细节”。)
该阶段的结果是完整的几何图元,它们是具有相关颜色,深度和有时纹理坐标值的转换和剪切顶点以及光栅化步骤的准则。
像素操作
虽然几何数据通过OpenGL渲染管道有一条路径,但像素数据采用不同的路线。系统内存中的阵列中的像素首先从多种格式之一解压缩到适当数量的组件中。接下来,数据被缩放,偏置,并由像素图处理。结果被钳位,然后被写入纹理存储器或发送到光栅化步骤。(见第8章“成像管道”)。
如果从帧缓冲器读取像素数据,则执行像素传送操作(比例,偏移,映射和钳位)。然后将这些结果打包成适当的格式并返回到系统内存中的数组。
有特殊的像素复制操作将帧缓冲区中的数据复制到帧缓冲区的其他部分或纹理内存。在将数据写入纹理存储器或返回到帧缓冲器之前,通过像素传输操作进行单次通过。
纹理装配
OpenGL应用程序可能希望将纹理图像应用到几何对象上,使其看起来更逼真。如果使用几个纹理图像,将它们放入纹理对象中是明智的,以便您可以轻松地在其中切换。
一些OpenGL实现可能有特殊的资源来加速纹理性能。可能会有专门的高性能纹理记忆。如果该内存可用,则可以优先考虑纹理对象来控制这种有限和有价值的资源的使用。(见第9章)
光栅化
栅格化是将几何和像素数据转换成片段。每个片段正方形对应于帧缓冲区中的像素。在顶点连接成线条时,考虑到线和多边形点,线宽,点大小,阴影模型和支持抗锯齿的覆盖计算,或者为填充的多边形计算内部像素。为每个片段平方分配颜色和深度值。
片段操作
在值实际存储到帧缓冲区之前,执行一系列可能会改变甚至丢弃片段的操作。所有这些操作都可以启用或禁用。
可能遇到的第一个操作是纹理化,其中从纹理存储器为每个片段生成纹理(纹理元素)并应用于片段。然后可以应用雾计算,然后进行剪刀测试,alpha测试,模板测试和深度缓冲测试(深度缓冲区用于隐藏表面去除)。失败的测试可能会导致片段平方的继续处理。然后,可以执行混合,抖动,逻辑操作和掩码的掩码。(见第6章以及第10章)最后,将彻底processedfragment被吸入到相应的缓冲器,在那里它终于前进到是像素,取得其最终的休息处。
OpenGL相关库
OpenGL提供了强大但原始的渲染命令集,所有更高级别的绘图都必须按照这些命令进行。此外,OpenGL程序必须使用窗口系统的底层机制。有许多库可以让您简化编程任务,其中包括:
- OpenGL实用程序库(GLU)包含几个使用较低级别的OpenGL命令执行诸如为特定的视图方向和投影设置矩阵,执行多边形细分和渲染曲面等任务的例程。该库是每个OpenGL实现的一部分。GLU的部分在OpenGL参考手册中有描述。本指南中描述了更有用的GLU例程,它们与正在讨论的主题相关,如第11 章和第12章“GLU NURBS界面”中的所有内容。GLU例程使用前缀glu。
- 对于每个窗口系统,都有一个库来扩展该窗口系统的功能,以支持OpenGL渲染。对于使用X Window系统的机器,OpenGL扩展到X Window系统(GLX)作为OpenGL的附件提供。GLX例程使用前缀glX。对于Microsoft Windows,WGL例程提供Windows到OpenGL界面。所有WGL例程都使用前缀wgl。对于IBM OS / 2,PGL是OpenGL接口的Presentation Manager,其例程使用前缀pgl。
所有这些窗口系统扩展库在附录C中有更详细的描述。此外,GLX程序也在OpenGL参考手册中进行了说明。
- OpenGL实用工具包(GLUT)是由Mark Kilgard编写的一个与窗口系统无关的工具包,用于隐藏不同窗口系统API的复杂性。GLUT是下一节的主题,它在Mark Kilgard的“ Open Window Programming for the X Window System”(ISBN 0-201-48359-9)中有更详细的描述。GLUT例程使用前缀过滤。前言中的“如何获取示例代码”介绍了如何使用ftp获取GLUT的源代码。
- Open Inventor是基于OpenGL的面向对象工具包,它提供了创建交互式三维图形应用程序的对象和方法。Open Inventor是用C ++编写的,它为用户交互提供了预置对象和内置事件模型,用于创建和编辑三维场景的高级应用程序组件,以及以其他图形格式打印对象和交换数据的能力。Open Inventor与OpenGL分开。
包含文件
对于所有OpenGL应用程序,您需要在每个文件中包含gl.h头文件。几乎所有OpenGL应用程序都使用GLU,前面提到的OpenGL实用程序库,它需要包含glu.h头文件。所以几乎每个OpenGL源文件都以
#include <GL / gl.h> #include <GL / glu.h>
如果您直接访问窗口界面库以支持OpenGL,例如GLX,AGL,PGL或WGL,则必须包含其他头文件。例如,如果您正在调用GLX,则可能需要将这些行添加到代码中
#include <X11 / Xlib.h> #include <GL / glx.h>
如果您正在使用GLUT来管理窗口管理器任务,那么您应该包括
#include <GL / glut.h>
请注意,glut.h自动包括gl.h,glu.h和glx.h,因此包括所有三个文件都是多余的。用于Microsoft Windows的GLUT包括访问WGL的相应头文件。
GLUT,OpenGL实用工具包
如你所知,OpenGL包含渲染命令,但是被设计为独立于任何窗口系统或操作系统。因此,它不包含打开窗口或从键盘或鼠标读取事件的命令。不幸的是,编写一个完整的图形程序是不可能的,至少打开一个窗口,最有趣的程序需要一些用户输入或来自操作系统或窗口系统的其他服务。在许多情况下,完整的程序是最有趣的例子,所以本书使用GLUT来简化打开窗口,检测输入等等。如果您的系统上已经实现了OpenGL和GLUT,那么这本书中的示例应该与它们链接时不会改变。
另外,由于OpenGL绘图命令仅限于生成简单几何图元(点,线和多边形)的命令,所以GLUT包含几个创建更复杂的三维对象(例如球体,圆环和茶壶)的例程。这样,程序输出的快照就可以很有趣了。(请注意,OpenGL实用程序库GLU还具有创建与GLUT相同的三维对象(例如球体,圆柱体或锥体)的四维例程。)
GLUT可能对于全功能的OpenGL应用程序可能不满意,但您可能会发现它是学习OpenGL的有用起点。本节的其余部分简要描述了GLUT例程的一小部分,以便您可以按照本书其余部分中的编程示例进行操作。(有关GLUT子集的更多详细信息,请参阅附录D,或者有关GLUT其余部分的信息,请参阅X Window系统的OpenGL编程章节4和5 )。
窗口管理
五个例程执行必要的任务来初始化窗口。
- glutInit(int * argc,char ** argv)初始化GLUT并处理任何命令行参数(对于X,这将是像-display和-geometry这样的选项)。在任何其他GLUT例程之前应该调用glutInit()。
- glutInitDisplayMode(unsigned int mode)指定是使用RGBA还是颜色索引颜色模型。您还可以指定是要使用单缓冲或双缓冲窗口。(如果您正在使用颜色索引模式,则需要将某些颜色加载到颜色映射中;使用glutSetColor()来执行此操作。)最后,您可以使用此例程来指示您希望窗口具有相关联的深度,模板和/或累积缓冲器。例如,如果要使用双缓冲窗口,RGBA颜色模型和深度缓冲区,则可以调用glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)。
- glutInitWindowPosition(int x,int y)指定窗口左上角的屏幕位置。
- glutInitWindowSize(int width,int size)指定窗口的大小(以像素为单位)。
- int glutCreateWindow(char * string)创建一个带有OpenGL上下文的窗口。它返回新窗口的唯一标识符。警告:直到glutMainLoop()被调用(见下一节),窗口尚未显示。
显示回调
glutDisplayFunc(void(* func)(void))是您将看到的第一个也是最重要的事件回调函数。每当GLUT确定窗口的内容需要重新显示时,将执行由glutDisplayFunc()注册的回调函数。因此,您应该放置显示回调函数中需要重新绘制场景的所有例程。
如果您的程序更改了窗口的内容,有时您将不得不调用glutPostRedisplay(void),这会使glutMainLoop()在其下一个机会中调用注册的显示回调。
运行程序
最后一件事就是调用glutMainLoop(void)。现在已经创建了所有已创建的窗口,并且渲染到这些窗口现在已经有效了。事件处理开始,并且注册的显示回调被触发。一旦这个循环进入,它永远不会退出!
示例1-2显示了如何使用GLUT创建示例1-1中所示的简单程序。注意代码重组。为了最大限度地提高效率,仅需调用一次的操作(设置背景颜色和坐标系)现在处于一个称为init()的过程中。渲染(可能重新呈现)场景的操作位于display()过程中,这是注册的GLUT显示回调。
示例1-2:使用GLUT的简单OpenGL程序:hello.c
#include <GL/gl.h> #include <GL/glut.h> void display(void) { /* clear all pixels */ glClear (GL_COLOR_BUFFER_BIT); /* draw white polygon (rectangle) with corners at * (0.25, 0.25, 0.0) and (0.75, 0.75, 0.0) */ glColor3f (1.0, 1.0, 1.0); glBegin(GL_POLYGON); glVertex3f (0.25, 0.25, 0.0); glVertex3f (0.75, 0.25, 0.0); glVertex3f (0.75, 0.75, 0.0); glVertex3f (0.25, 0.75, 0.0); glEnd(); /* don't wait! * start processing buffered OpenGL routines */ glFlush (); } void init (void) { /* select clearing (background) color */ glClearColor (0.0, 0.0, 0.0, 0.0); /* initialize viewing values */ glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0); } /* * Declare initial window size, position, and display mode * (single buffer and RGBA). Open window with "hello" * in its title bar. Call initialization routines. * Register callback function to display graphics. * Enter main loop and process events. */ int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB); glutInitWindowSize (250, 250); glutInitWindowPosition (100, 100); glutCreateWindow ("hello"); init (); glutDisplayFunc(display); glutMainLoop(); return 0; /* ISO C requires main to return int. */ }
处理输入事件
您可以使用这些例程来注册在发生指定事件时调用的回调命令。
- glutReshapeFunc(void(* func)(int w,int h))指示在调整窗口大小时应采取的操作。
- glutKeyboardFunc(void(* func)(unsigned char key,int x,int y))和glutMouseFunc(void(* func)(int button,int state,int x,int y))允许您链接键盘键或按下或释放键或鼠标按钮时调用的例程的鼠标按钮。
- 当鼠标按键被移动时,glutMotionFunc(void(* func)(int x,int y))会注册一个例程来回调。
管理背景过程
如果没有其他事件处于挂起状态,则可以指定要执行的函数 - 例如,当事件循环将以空闲状态时,将使用glutIdleFunc(void(* func)(void))。此例程将指向该函数的指针作为其唯一参数。传递NULL(零)以禁用该函数的执行。
绘制三维对象
GLUT包括用于绘制这些三维对象的几个例程:
锥体 |
二十面体 |
茶壶 |
立方体 |
八面体 |
四面体 |
十二面体 |
领域 |
花托 |
您可以将这些对象绘制为线框或固定阴影对象,并定义表面法线。例如,立方体和球体的例程如下所示:
void glutWireCube(GLdouble size);
void glutSolidCube(GLdouble size);
void glutWireSphere(GLdouble radius,GLint slice, GLint stacks);
void glutSolidSphere(GLdouble radius,GLint 片, GLint 堆栈);
所有这些模型都以世界坐标系的起源为中心。(有关所有这些绘图程序的原型的信息,请参阅。)
动画
在图形计算机上可以做的最令人兴奋的事情之一就是绘制移动图片。无论您是工程师,试图看到您正在设计的机械部件的各个方面,飞行员学习如何使用仿真飞行飞机,或仅仅是电脑游戏爱好者,很明显,动画是计算机图形学的重要组成部分。
在电影院里,通过拍摄一系列照片并在屏幕上以每秒24个的速度投影,实现动作。每个框架移动到透镜后面的位置,快门被打开,并且框架被显示。快门瞬间关闭,同时胶片前进到下一帧,然后显示该帧,依此类推。虽然你每秒钟看24个不同的帧,但你的大脑将它们全部融入到一个平滑的动画中。(老查理·卓别林的电影每秒16帧拍摄,显得很厉害)事实上,大多数现代投影机以每秒48张的速度显示两张照片,以减少闪烁。计算机图形屏幕通常刷新(重画图片)每秒大约60到76次,有些甚至每秒刷新约120次刷新。显然,
电影投影的主要原因是显示时每帧都完成。假设你尝试用这样的程序来做你的百万帧电影的电脑动画:
open_window(); for(i = 0; i <1000000; i ++){ clear_the_window(); draw_frame(ⅰ); wait_until_a_24th_of_a_second_is_over(); }
如果您添加系统清除屏幕并绘制典型框架所需的时间,则根据接近1/24秒清除和绘制的程序,此程序会产生越来越多的令人不安的结果。假设绘图几乎要完整的1/24秒。首先绘制的物品在整个1/24秒内可见,并在屏幕上呈现一个实心图像; 当程序从下一帧开始时,朝向最终绘制的项目立即被清除。他们最多呈现幽灵般的形象,因为在1/24秒钟的大部分时间,你的眼睛正在观看清晰的背景,而不是不幸的物品,最后被画出来。问题是这个程序不显示完全绘制的框架; 相反,您会看到绘图发生。
大多数OpenGL实现提供双缓冲 - 提供两个完整颜色缓冲区的硬件或软件。一个被显示,而另一个被绘制。当框架的绘制完成时,两个缓冲区被交换,所以正在查看的缓冲区现在用于绘制,反之亦然。这就像一个电影放映机,只有两帧在一个循环中; 当一个人被投射在屏幕上时,艺术家正在拼命地擦除并重新绘制不可见的框架。只要艺术家足够快,观众注意到该设置与已经绘制了所有框架的设置之间没有区别,并且投影机只是简单地一个接一个地显示它们。双缓冲,每幅画面仅在绘图完成时显示; 观看者从未看到部分画框。
上述程序的修改版本可以显示平滑的动画图形,如下所示:
open_window_in_double_buffer_mode(); for(i = 0; i <1000000; i ++){ clear_the_window(); draw_frame(ⅰ); swap_the_buffers(); }
刷新暂停
对于某些OpenGL实现,除了简单地交换可见和可绘制的缓冲区之外,swap_the_buffers()例程等待直到当前屏幕刷新周期结束,以便前一个缓冲区被完全显示。该例程也允许从一开始就完全显示新的缓冲区。假设您的系统每秒刷新显示60次,这意味着您可以实现的最快帧速率为每秒60帧(fps),如果所有帧都可以在1/60秒内清除并绘制,您的动画将按照这个速度顺利运行。
在这种系统上经常发生的情况是,框架太复杂,无法画出1/60秒,因此每帧都会显示不止一次。例如,如果绘制帧需要1/45秒,您将获得30 fps,并且图形空闲1 / 30-1 / 45 = 1/90秒/帧或三分之一的时间。
此外,视频刷新率是恒定的,这可能会产生一些意想不到的性能后果。例如,每刷新监视器为1/60秒,帧频恒定,您可以以60 fps,30 fps,20 fps,15 fps,12 fps等运行(60/1,60/2,60 / 3,60 / 4,60 / 5,...)。这意味着,如果您正在编写应用程序并逐渐添加功能(例如,它是一个飞行模拟器,并且您正在添加地面风景),起初您添加的每个功能对整体性能没有影响 - 您仍然可以获得60 fps。那么突然之间,你会添加一个新的功能,并且系统不能在1/60秒内完全画出整个事情,所以动画从60 fps减慢到30 fps,因为它错过了第一个可能的缓冲区 - 交换时间
如果现场的复杂性接近任何魔法时刻(1/60秒,2/60秒,3/60秒等等),那么由于随机变化,一些帧在时间上略微下降,有些稍微下。那么帧速率是不规则的,这可以在视觉上令人不安。在这种情况下,如果您无法简化场景,以便所有的框架都足够快,添加一个有意的,微小的延迟可能会更好,以确保它们都错过,给出一个恒定的,较慢的帧速率。如果您的框架具有非常不同的复杂性,则可能需要更复杂的方法。
Motion = Redraw + Swap
真实动画程序的结构与此描述没有太大差异。通常,对于每个帧,从头开始重绘整个缓冲区比找出哪些部分需要重绘更容易。对于诸如三维飞行模拟器之类的应用来说,这尤其如此,其中飞机方向的微小变化改变了窗外的所有位置。
在大多数动画中,场景中的对象只是用不同的变换重新绘制 - 观察者的观点移动,或者汽车向下移动一点,或者物体稍微旋转。如果非绘图操作需要重新计算,则可达到的帧速率通常会降低。但是请记住,swap_the_buffers()例程中的空闲时间通常可以用于这种计算。
OpenGL没有swap_the_buffers()命令,因为该功能可能在所有硬件上都不可用,在任何情况下,它都非常依赖于窗口系统。例如,如果您使用X Window系统并直接访问,则可以使用以下GLX例程:
void glXSwapBuffers(Display * dpy,Window window);
(有关其他窗口系统的等效例程,请参阅附录C.)
如果您正在使用GLUT库,则需要调用此例程:
void glutSwapBuffers(void);
示例1-3说明了在绘制旋转正方形的示例中使用glutSwapBuffers(),如图1-3所示。以下示例还显示了如何使用GLUT来控制输入设备并打开和关闭空闲功能。在这个例子中,鼠标按钮可以开启和关闭旋转。
图1-3:双缓冲旋转方形
示例1-3:双缓冲程序:double.c
#include <GL/gl.h> #include <GL/glu.h> #include <GL/glut.h> #include <stdlib.h> static GLfloat spin = 0.0; void init(void) { glClearColor (0.0, 0.0, 0.0, 0.0); glShadeModel (GL_FLAT); } void display(void) { glClear(GL_COLOR_BUFFER_BIT); glPushMatrix(); glRotatef(spin, 0.0, 0.0, 1.0); glColor3f(1.0, 1.0, 1.0); glRectf(-25.0, -25.0, 25.0, 25.0); glPopMatrix(); glutSwapBuffers(); } void spinDisplay(void) { spin = spin + 2.0; if (spin > 360.0) spin = spin - 360.0; glutPostRedisplay(); } void reshape(int w, int h) { glViewport (0, 0, (GLsizei) w, (GLsizei) h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-50.0, 50.0, -50.0, 50.0, -1.0, 1.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void mouse(int button, int state, int x, int y) { switch (button) { case GLUT_LEFT_BUTTON: if (state == GLUT_DOWN) glutIdleFunc(spinDisplay); break; case GLUT_MIDDLE_BUTTON: if (state == GLUT_DOWN) glutIdleFunc(NULL); break; default: break; } } /* * Request double buffer display mode. * Register mouse input callback functions */ int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB); glutInitWindowSize (250, 250); glutInitWindowPosition (100, 100); glutCreateWindow (argv[0]); init (); glutDisplayFunc(display); glutReshapeFunc(reshape); glutMouseFunc(mouse); glutMainLoop(); return 0; }
|
|
编程指南 >第2章 |
第二章
|
缓冲 |
名称 |
参考 |
彩色缓冲 |
GL_COLOR_BUFFER_BIT |
|
深度缓冲区 |
GL_DEPTH_BUFFER_BIT |
|
累积缓冲液 |
GL_ACCUM_BUFFER_BIT |
|
模板缓冲区 |
GL_STENCIL_BUFFER_BIT |
在发出清除多个缓冲区的命令之前,如果要使用默认RGBA颜色,深度值,累积颜色和模板索引以外的其他值,则必须设置要清除每个缓冲区的值。除了设置清除颜色和深度缓冲区的当前值的glClearColor()和glClearDepth()命令之外,glClearIndex(),glClearAccum()和glClearStencil()指定用于将颜色索引,累积颜色和模板索引用于清除相应的缓冲区。(参见第4章以及第10章为这些缓冲液和它们的用途的描述。)
OpenGL允许您指定多个缓冲区,因为清除通常是一个缓慢的操作,因为窗口中每个像素(可能数百万)都被触摸,一些图形硬件允许同时清除缓冲区集。不支持同时清除的硬件依次执行。和...之间的不同
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
和
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_DEPTH_BUFFER_BIT);
尽管两者都具有相同的最终效果,但第一个例子可能会在许多机器上运行得更快。肯定不会跑得更慢。
指定颜色
使用OpenGL,被绘制对象的形状描述与其颜色的描述无关。每当绘制特定的几何对象时,使用当前指定的着色方案进行绘制。着色方案可能很简单,“把所有的东西都放在消防车红色”,或者可能像“假设物体是由蓝色塑料制成的那样复杂,有一个黄色的聚光灯指向这样一个方向,在其他地方都有一般的低级红棕色灯。“ 一般来说,OpenGL程序员首先设置颜色或着色方案,然后绘制对象。在颜色或着色方案改变之前,所有对象都以该颜色绘制或使用该着色方案。
例如,伪代码
set_current_color(红色); draw_object(A); draw_object(B); set_current_color(绿色); set_current_color(蓝色); draw_object(C);
绘制对象A和B为红色,对象C为蓝色。第四行将当前颜色设置为绿色的命令将被浪费掉。
着色,照明和阴影都是整个章节或大部分专题的主题。然而,要绘制可以看到的几何图元,您需要一些基本知识来了解如何设置当前颜色; 这些信息在下面的段落中提供。(见第4和第5章有关这些主题的详细信息。)
要设置颜色,请使用命令glColor3f()。它需要三个参数,所有这些参数都是0.0到1.0之间的浮点数。该参数是为了,红色,绿色和蓝色组件的颜色。您可以将这三个值视为指定颜色的“混合”:0.0表示不使用任何组件,1.0表示使用该组件的所有内容。因此,代码
glColor3f(1.0,0.0,0.0);
使系统可以画出最亮的红色,没有绿色或蓝色组件。全零都变黑 相反,所有的都是白色的。将所有三个组件设置为0.5会产生灰色(黑色和白色之间的中间)。这里有八个命令及其设置的颜色。
glColor3f(0.0,0.0,0.0); 黑色 glColor3f(1.0,0.0,0.0); 红 glColor3f(0.0,1.0,0.0); 绿色 glColor3f(1.0,1.0,0.0); 黄色 glColor3f(0.0,0.0,1.0); 蓝色 glColor3f(1.0,0.0,1.0); 品红 glColor3f(0.0,1.0,1.0); 青色 glColor3f(1.0,1.0,1.0); 白色
您可能已经注意到,设置清除颜色的例程glClearColor()需要四个参数,前三个参数与glColor3f()的参数匹配。第四个参数是alpha值; 它在第6章的“混合”中有详细的介绍。现在,将glClearColor()的第四个参数设置为0.0,这是其默认值。
强制完成绘图
如您在第1章中的“OpenGL渲染流水线”中所见大多数现代图形系统可以被认为是装配线。主要中央处理单元(CPU)发出绘图命令。也许其他硬件做几何变换。执行剪切,然后进行阴影和/或纹理化。最后,将这些值写入位平面进行显示。在高端架构中,这些操作中的每一个都由不同的硬件执行,这些硬件被设计为快速执行其特定任务。在这样的架构中,CPU不需要等待每个绘图命令完成,然后再发出下一个绘图命令。当CPU正在向管道发送一个顶点时,转换硬件正在转换发送的最后一个,正在被裁剪之前的一个等等。在这样一个系统中,
此外,应用程序可能在多台机器上运行。例如,假设主程序在其他地方(在称为客户机的机器上)运行,并且正在通过网络连接到客户端的工作站或终端(服务器)上查看绘图结果。在这种情况下,由于相当多的开销通常与每个网络传输相关联,所以每次都通过网络一次发送每个命令可能是非常低效的。通常,在发送之前,客户端将一组命令收集到单个网络数据包中。不幸的是,客户端上的网络代码通常无法知道图形程序完成了绘制帧或场景。在最坏的情况下,它会永远等待足够的附加绘图命令来填充数据包,
因此,OpenGL提供了glFlush()命令,即使它可能不满,也迫使客户端发送网络数据包。哪里没有网络,所有的命令都是在服务器上立即执行,glFlush()可能没有任何效果。但是,如果您正在编写一个要使用和不使用网络正常工作的程序,请在每个框架或场景的末尾包括调用glFlush()。注意,glFlush()不等待绘图完成 - 它只是强制绘图开始执行,从而保证所有以前的命令在有限的时间内执行,即使没有进一步的渲染命令被执行。
还有其他glFlush()有用的情况。
- 在系统内存中构建映像并且不想不断更新屏幕的软件渲染器。
- 收集渲染命令集以摊销启动成本的实施。上述网络传输示例是其中的一个实例。
一些命令 - 例如,以双缓冲模式交换缓冲区的命令 - 在挂起的命令发生之前自动将其挂起。
如果glFlush()不足够,请尝试使用glFinish()。此命令以glFlush()方式刷新网络,然后等待来自图形硬件或网络的通知,指示图形在帧缓冲区中已完成。如果要同步任务,您可能需要使用glFinish(),例如,在使用Display PostScript绘制标签之前,确保您的三维渲染在屏幕上。另一个例子是确保绘图在开始接受用户输入之前已经完成。发布glFinish()后命令,您的图形进程被阻止,直到它从图形硬件接收到绘图完成的通知。请记住,过度使用glFinish()可以降低应用程序的性能,特别是如果您通过网络运行,因为它需要往返通信。如果glFlush()足以满足您的需要,请使用它而不是glFinish()。
- void glFinish(void);
- 强制所有以前发布的OpenGL命令完成。直到前面命令的所有效果都被完全实现为止,此命令才会返回。
坐标系生存套件
每当您最初打开窗口或稍后移动或调整窗口大小时,窗口系统将发送一个事件通知您。如果您使用GLUT,通知将自动化; 任何已经注册到glutReshapeFunc()的例程都将被调用。你必须注册一个回调函数
- 重新建立将成为新的渲染画布的矩形区域
- 定义要绘制对象的坐标系
在第3章中,您将看到如何定义三维坐标系,但是现在,只需创建一个简单的基本二维坐标系即可绘制几个对象。调用glutReshapeFunc(reshape),其中reshape()是示例2-1所示的以下函数。
示例2-1:重新调用回调函数
void reshape(int w,int h) { glViewport(0,0,(GLsizei)w,(GLsizei)h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0,(GLdouble)w,0.0,(GLdouble)h); }
GLUT的内部函数将传递这个函数两个参数:新的,移动的或已调整大小的窗口的宽度和高度(以像素为单位)。glViewport()将绘制的像素矩形调整为整个新窗口。接下来的三个例程调整绘图坐标系,使左下角为(0,0),右上角为(w,h)(见图2-1)。
再说一下,想一下一张图纸。reshape()中的w和h值表示图形纸上有多少列和正方形。那么你必须把轴放在图纸上。所述gluOrtho2D()例行程序把原点,(0,0),在最低,最左边的方一路,并使得每个正方形表示一个单元。现在,当您在本章的其余部分渲染点,线和多边形时,它们将以容易预测的方块出现在本文中。(现在,保持所有的对象二维。)
图2-1:坐标系由w = 50定义,h = 50
描述点,线和多边形
本节介绍如何描述OpenGL几何图元。最终根据它们的顶点描述所有几何图元- 定义点本身的坐标,线段的端点或多边形的角。下一节将讨论这些图元是如何显示的,以及它们在显示器上的控制。
什么是点,线和多边形?
你可能有一个很好的想法,数学家意味着什么是术语点,线和多边形。OpenGL的含义是相似的,但不完全相同。
一个区别来自于基于计算机的计算的局限性。在任何OpenGL实现中,浮点计算的精度都是有限的,它们有四舍五入的错误。因此,OpenGL点,线和多边形的坐标也有相同的问题。
另一个更重要的区别来自于光栅图形显示的局限性。在这样的显示器上,最小的可显示单元是像素,虽然像素可能小于1/100英寸宽,但它们仍然远远大于数学家的无限小(点)或无限薄的概念)。当OpenGL执行计算时,它假定点被表示为浮点数的向量。然而,一个点通常(但不总是)绘制为单个像素,并且具有稍微不同坐标的许多不同点可以由相同像素上的OpenGL绘制。
点
一个点由一组称为顶点的浮点数表示。所有内部计算完成,就好像顶点是三维的。由用户指定为二维(即只有x和y坐标)的顶点由OpenGL 分配给等于零的z坐标。
高级
OpenGL在三维投影几何的均匀坐标中工作,因此对于内部计算,所有顶点都用四个浮点坐标(x,y,z,w)表示。如果w与零不同,这些坐标对应于欧几里德三维点(x / w ,y / w ,z / w)。您可以在OpenGL命令中指定w坐标,但很少这样做。如果没有指定w坐标,那么它被理解为1.0。(有关均匀坐标系的更多信息,请参阅附录F.)
行
在OpenGL中,术语行是指一个线段,而不是数学家的版本在两个方向上延伸到无穷大。有一些简单的方法来指定连接的一系列线段,甚至是一个闭合的连接的一系列段(见图2-2)。然而,在所有情况下,构成连接系列的线根据其端点处的顶点来指定。
图2-2:两个连接的线段系列
多边形
多边形是由线段的单个闭环包围的区域,其中线段由端点处的顶点指定。多边形通常用内部填充的像素绘制,但也可以将其绘制为轮廓或一组点。(参见“多边形细节”。)
一般来说,多边形可能是复杂的,因此OpenGL对构成原始多边形的一些强大的限制。首先,OpenGL多边形的边缘不能相交(数学家会将满足这个条件的多边形称为简单多边形)。第二,OpenGL多边形必须是凸的,意味着他们不能有压痕。正确地说,如果在内部的任何两个点处连接它们的线段也在内部,则区域是凸的。有关无效多边形的一些示例,请参见图2-3。然而,OpenGL并不限制构成凸多边形边界的线段的数量。注意,不能描述具有孔的多边形。它们是非凸的,它们不能用由单个闭环组成的边界绘制。请注意,如果您使用非凸面填充的多边形来呈现OpenGL,则可能会按照您的期望绘制它。例如,在大多数系统上,不会多于多边形的凸包被填充。在某些系统上,小于凸包可能会被填满。
图2-3:有效和无效的多边形
OpenGL对有效多边形类型的限制的原因在于,为限制类多边形提供快速的多边形渲染硬件要简单得多。简单的多边形可以快速渲染。困难的情况很难迅速发现。所以为了获得最大的性能,OpenGL会越过它的手指,并假定多边形是简单的。
许多现实世界的表面由非常多边形,非凸多边形或具有孔的多边形组成。由于所有这样的多边形可以由简单的凸多边形的联合形成,所以在GLU库中提供了用于构建更复杂对象的一些例程。这些例程将复杂的描述和细分,或将它们分解成可以渲染的较简单OpenGL多边形的组。(有关细分例程的更多信息,请参见第11章中的“多边形细分”)。
由于OpenGL顶点总是三维的,所以形成特定多边形边界的点不一定位于空间中的同一平面上。(当然,在许多情况下,如果所有的z坐标都为零,或者如果多边形是三角形)。如果多边形的顶点不在同一平面上,则在空间中进行各种旋转之后,视点的变化和投影到显示屏上,点可能不再形成简单的凸多边形。例如,想象一个四点四边形这些点稍微偏离平面,并且看起来几乎是边缘的。您可以获得一个非常简单的多边形,类似于领结,如图2-4所示,不能保证正确呈现。如果您通过由位于真实表面上的点形成的四边形近似曲面,则这种情况并不常见。您可以随时通过使用三角形来避免问题,因为任何三个点总是位于一个平面上。
图2-4:非平面多边形转换为非常多边形
矩形
由于矩形在图形应用程序中很常见,所以OpenGL提供了一个填充矩形的绘图原语glRect *()。您可以按照“OpenGL几何绘图基元”中所述绘制一个矩形作为多边形,但是您的OpenGL的特定实现可能会为矩形优化了glRect *()。
- void glRect {sifd}(TYPEx1 ,TYPEy1 ,TYPEx2 ,TYPEy2 );
void glRect {sifd} v(TYPE * v1 ,TYPE * v2 ); - Draws the rectangle defined by the corner points (x1, y1) and (x2, y2). The rectangle lies in the plane z=0 and has sides parallel to the x- and y-axes. If the vector form of the function is used, the corners are given by two pointers to arrays, each of which contains an (x, y) pair.
Note that although the rectangle begins with a particular orientation in three-dimensional space (in the x-y plane and parallel to the axes), you can change this by applying rotations or other transformations. (See Chapter 3 for information about how to do this.)
Curves and Curved Surfaces
通过短线段或小多边形区域,任何平滑的曲线或曲面可以近似为任意任意的精度。因此,将曲线和曲面充分地细分,然后用直线段或平面多边形逼近它们,使它们看起来弯曲(见图2-5)。如果你怀疑这真的有效,想象细分,直到每个线段或多边形如此微小,它比屏幕上的像素小。
图2-5:近似曲线
即使曲线不是几何图元,OpenGL确实为细分和绘制提供了一些直接的支持。(有关如何绘制曲线和曲面的信息,请参阅第12章。)
指定顶点
使用OpenGL,所有几何对象最终被描述为一组有序的顶点。您可以使用glVertex *()命令来指定一个顶点。
- void glVertex {234} {sifd} [v](TYPEcoords );
示例2-2提供了使用glVertex *()的一些示例。
示例2-2: glVertex *()的合法用途
glVertex2s(2,3); glVertex3d(0.0,0.0,3.1415926535898); glVertex4f(2.3,1.0,2.2,2.0); GLdouble dvect [3] = {5.0,9.0,1992.0}; glVertex3dv(dvect);
第一个例子表示具有三维坐标(2,3,0)的顶点。(记住,如果没有指定,z坐标被理解为0.)第二个例子中的坐标是(0.0,0.0,3.1415926535898)(双精度浮点数)。第三个例子表示具有三维坐标(1.15,0.5,1.1)的顶点。(请记住x,y和z坐标最终被w坐标除。)在最后一个例子中,dvect是一个指向三个双精度浮点数的数组的指针。
在某些机器上,glVertex *()的矢量格式更有效率,因为只需要将一个参数传递给图形子系统。特殊硬件可能能够在一个批次中发送一整套坐标系。如果您的机器是这样的,那么安排数据是有利的,以便顶点坐标在内存中顺序打包。在这种情况下,通过使用OpenGL的顶点数组操作可能会增加性能。(参见“顶点数组”)
OpenGL几何绘图基元
现在您已经看到如何指定顶点,您仍然需要知道如何告诉OpenGL从这些顶点创建一组点,一条线或多边形。要做到这一点,你括号每组顶点的调用之间在glBegin()和调用glEnd() 。传递给glBegin()的参数决定了哪些几何图元从顶点构造。例如,示例2-3>指定了图2-6所示的多边形顶点。
示例2-3:填充多边形
在glBegin(GL_POLYGON); glVertex2f(0.0,0.0); glVertex2f(0.0,3.0); glVertex2f(4.0,3.0); glVertex2f(6.0,1.5); glVertex2f(4.0,0.0); glEnd();
图2-6:绘制多边形或一组点
如果您使用GL_POINTS而不是GL_POLYGON,则原始图将只是图2-6所示的五个点。glBegin()的以下功能摘要中的表2-2 列出了十个可能的参数和相应的基元类型。
- void glBegin(GLenum mode );
- 标记描述几何图元的顶点数据列表的开头。原型的类型由模式指示,可以是表2-2 所示的任何值。
- 表2-2:几何原始名称和含义
值 |
含义 |
GL_POINTS |
个别点 |
GL_LINES |
顶点对被解释为单独的线段 |
GL_LINE_STRIP |
系列连接线段 |
GL_LINE_LOOP |
same as above, with a segment added between last and first vertices |
GL_TRIANGLES |
triples of vertices interpreted as triangles |
GL_TRIANGLE_STRIP |
linked strip of triangles |
GL_TRIANGLE_FAN |
linked fan of triangles |
GL_QUADS |
quadruples of vertices interpreted as four-sided polygons |
GL_QUAD_STRIP |
linked strip of quadrilaterals |
GL_POLYGON |
boundary of a simple, convex polygon |
void glEnd(void);
- Marks the end of a vertex-data list.
Figure 2-7 shows examples of all the geometric primitives listed in Table 2-2. The paragraphs that follow the figure describe the pixels that are drawn for each of the objects. Note that in addition to points, several types of lines and polygons are defined. Obviously, you can find many ways to draw the same primitive. The method you choose depends on your vertex data.
Figure 2-7 : Geometric Primitive Types
As you read the following descriptions, assume that n vertices (v0, v1, v2, ... , vn-1) are described between a glBegin() and glEnd() pair.
GL_POINTS |
Draws a point at each of the n vertices. |
GL_LINES |
Draws a series of unconnected line segments. Segments are drawn between v0 and v1, between v2 and v3, and so on. If n is odd, the last segment is drawn between vn-3 and vn-2, and vn-1 is ignored. |
GL_LINE_STRIP |
Draws a line segment from v0 to v1, then from v1 to v2, and so on, finally drawing the segment from vn-2 to vn-1. Thus, a total of n-1 line segments are drawn. Nothing is drawn unless n is larger than 1. There are no restrictions on the vertices describing a line strip (or a line loop); the lines can intersect arbitrarily. |
GL_LINE_LOOP |
Same as GL_LINE_STRIP, except that a final line segment is drawn from vn-1 to v0, completing a loop. |
GL_TRIANGLES |
Draws a series of triangles (three-sided polygons) using vertices v0, v1, v2, then v3, v4, v5, and so on. If n isn't an exact multiple of 3, the final one or two vertices are ignored. |
GL_TRIANGLE_STRIP |
使用顶点v0,v1,v2,然后v2,v1,v3(注意顺序),然后是v2,v3,v4等绘制一系列三角形(三面多边形)。排序是为了确保三角形都以相同的方向绘制,以便条带可以正确地形成表面的一部分。保持方向对于某些操作很重要,例如剔除。(参见“反转和剔除多边形面”)对于任何绘制,n必须至少为3。 |
GL_TRIANGLE_FAN |
与GL_TRIANGLE_STRIP相同,除了顶点是v0,v1,v2,然后是v0,v2,v3,然后是v0,v3,v4等等(见图2-7)。 |
GL_QUADS |
使用顶点v0,v1,v2,v3,然后v4,v5,v6,v7等绘制一系列四边形(四边形多边形)。如果n不是4的倍数,则忽略最后的一个,两个或三个顶点。 |
GL_QUAD_STRIP |
绘制一系列从v0,v1,v3,v2,v2,v3,v5,v4,v4,v5,v7,v6等开始的四边形(四边形多边形)(见图2-7)。在绘制任何东西之前,n必须至少为4。如果n为奇数,则忽略最终顶点。 |
GL_POLYGON |
使用点v0,...,vn-1作为顶点绘制多边形。n必须至少为3,否则不绘制。另外,指定的多边形本身不能相交,必须是凸的。如果顶点不满足这些条件,结果是不可预测的。 |
使用glBegin()和glEnd()的限制
关于顶点的最重要的信息是它们的坐标,它们由glVertex *()命令指定。您还可以为每个顶点提供额外的顶点特定数据 - 颜色,法向量,纹理坐标或这些特殊组合 - 使用特殊命令。另外还有一些命令在glBegin()和glEnd()对之间有效。表2-3包含这些有效命令的完整列表。
表2-3: glBegin()和glEnd()之间的有效命令
命令 |
命令目的 |
参考 |
glVertex *() |
设置顶点坐标 |
|
glColor *() |
设置当前颜色 |
|
glIndex *() |
设置当前颜色索引 |
|
glNormal *() |
设定法线矢量坐标 |
|
glTexCoord *() |
设置纹理坐标 |
|
glEdgeFlag *() |
控制边缘的绘制 |
|
glMaterial *() |
设置材料属性 |
|
glArrayElement() |
提取顶点数组数据 |
|
glEvalCoord *(),glEvalPoint *() |
生成坐标 |
|
glCallList(),glCallLists() |
执行显示列表 |
glBegin()和glEnd()对之间没有其他OpenGL命令有效,并使大多数其他OpenGL调用生成错误。当glBegin()和glEnd()之间调用时,一些顶点数组命令,如glEnableClientState()和glVertexPointer()都有未定义的行为,但不一定会产生错误。(此外,与OpenGL相关的例程,如glX *()例程在glBegin()和glEnd()之间有未定义的行为)。这些情况应该避免,调试可能会更加困难。
Note, however, that only OpenGL commands are restricted; you can certainly include other programming-language constructs (except for calls, such as the aforementioned glX*() routines). For example, Example 2-4 draws an outlined circle.
Example 2-4 : Other Constructs between glBegin() and glEnd()
#define PI 3.1415926535898 GLint circle_points = 100; glBegin(GL_LINE_LOOP); for (i = 0; i < circle_points; i++) { angle = 2*PI*i/circle_points; glVertex2f(cos(angle), sin(angle)); } glEnd();
Note: This example isn't the most efficient way to draw a circle, especially if you intend to do it repeatedly. The graphics commands used are typically very fast, but this code calculates an angle and calls the sin() and cos() routines for each vertex; in addition, there's the loop overhead. (Another way to calculate the vertices of a circle is to use a GLU routine; see "Quadrics: Rendering Spheres, Cylinders, and Disks" in Chapter 11.) If you need to draw lots of circles, calculate the coordinates of the vertices once and save them in an array and create a display list (see Chapter 7), or use vertex arrays to render them.
Unless they are being compiled into a display list, all glVertex*() commands should appear between some glBegin() and glEnd() combination. (If they appear elsewhere, they don't accomplish anything.) If they appear in a display list, they are executed only if they appear between a glBegin() and a glEnd(). (See Chapter 7 for more information about display lists.)
Although many commands are allowed between glBegin() and glEnd(), vertices are generated only when a glVertex*() command is issued. At the moment glVertex*() is called, OpenGL assigns the resulting vertex the current color, texture coordinates, normal vector information, and so on. To see this, look at the following code sequence. The first point is drawn in red, and the second and third ones in blue, despite the extra color commands.
glBegin(GL_POINTS); glColor3f(0.0, 1.0, 0.0); /* green */ glColor3f(1.0, 0.0, 0.0); /* red */ glVertex(...); glColor3f(1.0, 1.0, 0.0); /* yellow */ glColor3f(0.0, 0.0, 1.0); /* blue */ glVertex(...); glVertex(...); glEnd();
You can use any combination of the 24 versions of the glVertex*() command between glBegin() and glEnd(), although in real applications all the calls in any particular instance tend to be of the same form. If your vertex-data specification is consistent and repetitive (for example, glColor*, glVertex*, glColor*, glVertex*,...), you may enhance your program's performance by using vertex arrays. (See "Vertex Arrays.")
Basic State Management
在上一节中,您看到了一个状态变量的例子,当前的RGBA颜色以及它如何与一个原语相关联。OpenGL维护许多状态和状态变量。物体可以通过照明,纹理化,隐藏的表面去除,雾化或影响其外观的一些其它状态而被渲染。
默认情况下,大多数这些状态最初是无效的。这些状态可能是昂贵的激活; 例如,打开纹理映射几乎肯定会降低渲染原始图像的速度。然而,由于增强的图形功能,图像的质量将会提高,看起来更加逼真。
要打开和关闭许多这些状态,请使用这两个简单的命令:
- void glEnable(GLenum cap );
void glDisable(GLenum cap );
您还可以检查当前状态是否已启用或禁用。
- GLboolean glIsEnabled(GLenum 功能)
你刚刚看到的状态有两个设置:开和关。然而,大多数OpenGL例程为更复杂的状态变量设置值。例如,例程glColor3f()设置三个值,它们是GL_CURRENT_COLOR状态的一部分。有五个查询例程用于查找为多个状态设置的值:
void glGetBooleanv(GLenum pname ,GLboolean * params );void glGetIntegerv(GLenum pname ,GLint * params );
void glGetFloatv(GLenum pname ,GLfloat * params );
void glGetDoublev(GLenum pname ,GLdouble * params );
void glGetPointerv(GLenum pname ,GLvoid ** params );
- 获取布尔,整数,浮点,双精度或指针状态变量。所述PNAME 参数是指示所述状态变量返回一个符号常数,PARAMS 是一个指针所指示的类型,在其中放置返回的数据的数组。有关 pname 的可能值,请参见附录B中的表。例如,要获取当前的RGBA颜色,附录B中的表格建议您使用glGetIntegerv(GL_CURRENT_COLOR,params )或glGetFloatv(GL_CURRENT_COLOR,params )。如果需要返回所需变量作为请求的数据类型,则执行类型转换。
这些查询例程处理获取状态信息的大多数但不是全部请求。(有关另外16个查询例程,请参见附录B中的“查询命令”。)
显示点,线和多边形
默认情况下,一个点被画成屏幕上的单个像素,一条线被画成固体,一个像素宽,并且多边形被固定地填充。以下段落讨论如何更改这些默认显示模式的细节。
点细节
要控制渲染点的大小,请使用glPointSize()并提供所需的大小(以像素为单位)作为参数。
- void glPointSize(GLfloat size );
- 设置渲染点的宽度(以像素为单位)大小必须大于0.0,默认值为1.0。
屏幕上为各种点宽度绘制的像素的实际收集取决于是否启用抗锯齿。(抗锯齿是渲染点和线的平滑技术;更多详细信息,请参见第6章中的“抗锯齿”)。如果禁用抗锯齿(默认值),则小数宽度将舍入为整数宽度,屏幕对齐绘制像素的正方形区域。因此,如果宽度为1.0,则平方为1像素乘1像素; 如果宽度为2.0,则平方为2像素×2像素,依此类推。
通过启用抗锯齿,绘制圆形像素组,并且边界上的像素通常以小于全强度绘制,以使边缘更平滑。在此模式下,非整数宽度不舍入。
大多数OpenGL实现支持非常大的点大小。抗锯齿点的最大尺寸是可查询的,但是相同的信息不可用于标准的别名点。然而,特定的实现可能将标准的别名点的大小限制为不小于其最大抗锯齿点大小,四舍五入到最接近的整数值。您可以通过glGetFloatv()使用GL_POINT_SIZE_RANGE来获取此浮点值。
行细节
在OpenGL中,你可以指定不同的宽度和线被线带点以各种方式-点线,虚线,交替点划线,等绘制。
宽线
- void glLineWidth(GLfloat width );
- 设置渲染行的宽度(以像素为单位)宽度必须大于0.0,默认值为1.0。
The actual rendering of lines is affected by the antialiasing mode, in the same way as for points. (See "Antialiasing" in Chapter 6.) Without antialiasing, widths of 1, 2, and 3 draw lines 1, 2, and 3 pixels wide. With antialiasing enabled, non-integer line widths are possible, and pixels on the boundaries are typically drawn at less than full intensity. As with point sizes, a particular OpenGL implementation might limit the width of nonantialiased lines to its maximum antialiased line width, rounded to the nearest integer value. You can obtain this floating-point value by using GL_LINE_WIDTH_RANGE with glGetFloatv().
Note: Keep in mind that by default lines are 1 pixel wide, so they appear wider on lower-resolution screens. For computer displays, this isn't typically an issue, but if you're using OpenGL to render to a high-resolution plotter, 1-pixel lines might be nearly invisible. To obtain resolution-independent line widths, you need to take into account the physical dimensions of pixels.
Advanced
With nonantialiased wide lines, the line width isn't measured perpendicular to the line. Instead, it's measured in the y direction if the absolute value of the slope is less than 1.0; otherwise, it's measured in the x direction. The rendering of an antialiased line is exactly equivalent to the rendering of a filled rectangle of the given width, centered on the exact line.
Stippled Lines
To make stippled (dotted or dashed) lines, you use the command glLineStipple() to define the stipple pattern, and then you enable line stippling with glEnable().
glLineStipple(1, 0x3F07); glEnable(GL_LINE_STIPPLE);
- void glLineStipple(GLint factor, GLushort pattern);
- Sets the current stippling pattern for lines. The pattern argument is a 16-bit series of 0s and 1s, and it's repeated as necessary to stipple a given line. A 1 indicates that drawing occurs, and 0 that it does not, on a pixel-by-pixel basis, beginning with the low-order bit of the pattern. The pattern can be stretched out by using factor, which multiplies each subseries of consecutive 1s and 0s. Thus, if three consecutive 1s appear in the pattern, they're stretched to six if factor is 2. factor is clamped to lie between 1 and 255. Line stippling must be enabled by passing GL_LINE_STIPPLE to glEnable(); it's disabled by passing the same argument to glDisable().
With the preceding example and the pattern 0x3F07 (which translates to 0011111100000111 in binary), a line would be drawn with 3 pixels on, then 5 off, 6 on, and 2 off. (If this seems backward, remember that the low-order bit is used first.) If factor had been 2, the pattern would have been elongated: 6 pixels on, 10 off, 12 on, and 4 off. Figure 2-8 shows lines drawn with different patterns and repeat factors. If you don't enable line stippling, drawing proceeds as if pattern were 0xFFFF and factor 1. (Use glDisable() with GL_LINE_STIPPLE to disable stippling.) Note that stippling can be used in combination with wide lines to produce wide stippled lines.
Figure 2-8 : Stippled Lines
One way to think of the stippling is that as the line is being drawn, the pattern is shifted by 1 bit each time a pixel is drawn (or factor pixels are drawn, if factor isn't 1). When a series of connected line segments is drawn between a single glBegin() and glEnd(), the pattern continues to shift as one segment turns into the next. This way, a stippling pattern continues across a series of connected line segments. When glEnd() is executed, the pattern is reset, and - if more lines are drawn before stippling is disabled - the stippling restarts at the beginning of the pattern. If you're drawing lines with GL_LINES, the pattern resets for each independent line.
实施例2-5示出了用几个不同的点状图案和线宽绘制的结果。它还说明如果将线条绘制为一系列单个段而不是单个连接的线条,将会发生什么。运行程序的结果如图2-9所示。
图2-9:宽引线
示例2-5:线条纹图案:lines.c
#include <GL / gl.h> #include <GL / glut.h> #define drawOneLine(x1,y1,x2,y2)glBegin(GL_LINES); \ glVertex2f((x1),(y1)); glVertex2f((x2),(y2)); glEnd(); void init(void) { glClearColor(0.0,0.0,0.0,0.0); glShadeModel(GL_FLAT); } void display(void) { 我的 glClear(GL_COLOR_BUFFER_BIT); / *为所有行选择白色* / glColor3f(1.0,1.0,1.0); / *在第一排,3行,每个都有不同的点数* / glEnable(GL_LINE_STIPPLE); glLineStipple(1,0x0101); / * dotted * / drawOneLine(50.0,125.0,150.0,125.0); glLineStipple(1,0x00FF); / * dotted * / drawOneLine(150.0,125.0,250.0,125.0); glLineStipple(1,0x1C47); / * dash / dot / dash * / drawOneLine(250.0,125.0,350.0,125.0); / *在第二排,3条宽线,每个具有不同的点数* / glLineWidth(5.0); glLineStipple(1,0x0101); / * dotted * / drawOneLine(50.0,100.0,150.0,100.0); glLineStipple(1,0x00FF); / * dotted * / drawOneLine(150.0,100.0,250.0,100.0); glLineStipple(1,0x1C47); / * dash / dot / dash * / drawOneLine(250.0,100.0,350.0,100.0); glLineWidth(1.0); / *在第3排,6行,带短划线/点/短划线* / / *作为单条连接线条的一部分* / glLineStipple(1,0x1C47); / * dash / dot / dash * / glBegin(GL_LINE_STRIP); for(i = 0; i <7; i ++) glVertex2f(50.0 +((GLfloat)i * 50.0),75.0); glEnd(); / *在第4排,6个独立行与同一点* / for(i = 0; i <6; i ++){ drawOneLine(50.0 +((GLfloat)i * 50.0),50.0, 50.0 +((GLfloat)(i + 1)* 50.0),50.0); } / *在第5行,1行,用破折号/点/破折号* / / *和点重复系数为5 * / glLineStipple(5,0x1C47); / * dash / dot / dash * / drawOneLine(50.0,25.0,350.0,25.0); glDisable(GL_LINE_STIPPLE); glFlush(); } void reshape(int w,int h) { glViewport(0,0,(GLsizei)w,(GLsizei)h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0,(GLdouble)w,0.0,(GLdouble)h); } int main(int argc,char ** argv) { glutInit(&argc,argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(400,150); glutInitWindowPosition(100,100); glutCreateWindow(argv [0]); 在里面 (); glutDisplayFunc(显示器); glutReshapeFunc(重塑); glutMainLoop(); 返回0; }
多边形细节
多边形通常是通过填充封闭在边界内的所有像素来绘制的,但您也可以将其作为多边形绘制,或者简单地绘制为顶点上的点。一个填充的多边形可能会被固定地填充或刻成一定的图案。虽然这里省略了精确的细节,但是填充的多边形以如下方式绘制:如果相邻的多边形共享边或顶点,则构成边或顶点的像素被精确地绘制一次 - 它们仅包含在多边形之一中。这样做是为了使部分透明的多边形没有绘制两边,这将使这些边缘看起来更暗(或更亮,取决于您所绘制的颜色)。请注意,它可能会导致一个或多个像素列或列中没有填充像素的窄多边形。抗锯齿多边形比点和线更复杂。(看到详见第六章“抗锯齿”。)
多边形作为点,轮廓或固体
一个多边形有两面 - 前面和后面 - 根据面向观看者的一面可能会有不同的渲染。这允许您对固体物体进行剖面视图,其中内部的部件和外部的部件之间有明显的区别。默认情况下,前面和后面都以相同的方式绘制。要改变这一点,或仅绘制轮廓或顶点,请使用glPolygonMode()。
- void glPolygonMode(GLenum face ,GLenum mode );
- 控制多面体前后面的绘图模式。参数面可以是GL_FRONT_AND_BACK,GL_FRONT或GL_BACK; 模式可以是GL_POINT,GL_LINE或GL_FILL,以指示多边形是否应绘制为点,概述或填充。默认情况下,前面和后面都被绘制。
例如,您可以使用这个例程的两个调用来填充正面和后面的轮廓:
glPolygonMode(GL_FRONT,GL_FILL); glPolygonMode(GL_BACK,GL_LINE);
反转和剔除多边形面
按照惯例,顶点在屏幕上以逆时针顺序显示的多边形称为正面。您可以构建任何“合理”固体的表面 - 数学家将这种表面称为可定向歧管(球体,甜甜圈和茶壶是可取向的;克莱恩瓶和莫比乌斯条不是) - 从一致方向的多边形。换句话说,您可以使用所有顺时针多边形或所有逆时针多边形。(这本质上是可定向的数学定义。)
假设你一直描述一个可定向表面的模型,但是你正好在外面有顺时针方向。您可以通过使用glFrontFace()函数交换OpenGL考虑到背面的内容,为前面的多边形提供所需的方向。
void glFrontFace(GLenum mode);- 控制如何确定面向前的多边形。默认情况下,模式是GL_CCW,它对应于窗口坐标中投影多边形的有序顶点的逆时针方向。如果模式为GL_CW,则顺时针方向的面朝下。
在由具有一致方向的不透明多边形构成的完全封闭的表面中,任何面向后的多边形都不可见 - 它们总是被前面的多边形遮蔽。如果你在这个表面之外,你可以启用剔除OpenGL确定的面向后面的多边形。类似地,如果您在对象内,则只有后向面的多边形是可见的。要指示OpenGL丢弃前面或后面的多边形,请使用glCullFace()命令,并使用glEnable()进行剔除。
- void glCullFace(GLenum mode );
高级
在更技术上,多面体的面是面向前还是背面的决定取决于以窗口坐标计算的多边形面积的符号。计算这个区域的一种方法是
其中x i和y i是n -vertex多边形的第i个顶点的x和y窗口坐标
假设GL_CCW被指定,如果a > 0,那么与该顶点对应的多边形被认为是正面的; 否则,它是面对的。如果GL_CW被指定,并且如果一个 <0,则对应的多边形是面向前方; 否则,它是面对的。
尝试这个
通过添加一些填充的多边形来修改示例2-5。尝试不同的颜色。尝试不同的多边形模式 还可以选择查看其效果。
起刺多边形
默认情况下,填充的多边形以固体图案绘制。它们也可以用32位32位窗口对齐的点模式填充,您可以使用glPolygonStipple()指定。
void glPolygonStipple(const GLubyte * mask );- 定义填充多边形的当前条件模式。的参数掩码是一个指向32 ' 的是真实解释为0和1的一个掩模32位图。在出现1的情况下,绘制多边形中的相应像素,并且在出现0时,不绘制任何内容。图2-10 显示了如何通过掩码中的字符构造点画图案。使用glEnable()和glDisable()以GL_POLYGON_STIPPLE作为参数,启用和禁用多边形点画。掩码数据的解释受glPixelStore *() GL_UNPACK *模式的影响。(请参见第8章中的“控制像素存储模式”。)
除了定义当前的多边形点画图案,您必须启用点画:
glEnable(GL_POLYGON_STIPPLE);
使用具有相同参数的glDisable()来禁用多边形点画。
图2-11显示了不间断绘制的多边形的结果,然后用两种不同的点画图案进行绘制。程序如例2-6所示。由于程序在黑色背景上绘制为白色,因此使用图2-10中的图案作为模板,会发生白色到黑色的反转(从图2-10到图2-11)。
图2-10:构造多边形条纹图案
图2-11:有条纹的多边形
示例2-6:多边形条纹图案:polys.c
#include <GL / gl.h> #include <GL / glut.h> void display(void) { GLubyte fly [] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x03,0x80,0x01,0xC0,0x06,0xC0,0x03,0x60, 0x04,0x60,0x06,0x20,0x04,0x30,0x0C,0x20, 0x04,0x18,0x18,0x20,0x04,0x0C,0x30,0x20, 0x04,0x06,0x60,0x20,0x44,0x03,0xC0,0x22, 0x44,0x01,0x80,0x22,0x44,0x01,0x80,0x22, 0x44,0x01,0x80,0x22,0x44,0x01,0x80,0x22, 0x44,0x01,0x80,0x22,0x44,0x01,0x80,0x22, 0x66,0x01,0x80,0x66,0x33,0x01,0x80,0xCC, 0x19,0x81,0x81,0x98,0x0C,0xC1,0x83,0x30, 0x07,0xe1,0x87,0xe0,0x03,0x3f,0xfc,0xc0, 0x03,0x31,0x8c,0xc0,0x03,0x33,0xcc,0xc0, 0x06,0x64,0x26,0x60,0x0c,0xcc,0x33,0x30, 0x18,0xcc,0x33,0x18,0x10,0xc4,0x23,0x08, 0x10,0x63,0xC6,0x08,0x10,0x30,0x0c,0x08, 0x10,0x18,0x18,0x08,0x10,0x00,0x00,0x08}; GLubyte半色调[] = { 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55, 0xAA,0xAA,0xAA,0xAA,0x55,0x55,0x55,0x55}; glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0,1.0,1.0); / *绘制一个固体,无张开的矩形,* / / *然后两个点画矩形* / glRectf(25.0,25.0,125.0,125.0); glEnable(GL_POLYGON_STIPPLE); glPolygonStipple(fly); glRectf(125.0,25.0,225.0,125.0); glPolygonStipple(半色调); glRectf(225.0,25.0,325.0,125.0); glDisable(GL_POLYGON_STIPPLE); glFlush(); } void init(void) { glClearColor(0.0,0.0,0.0,0.0); glShadeModel(GL_FLAT); } void reshape(int w,int h) { glViewport(0,0,(GLsizei)w,(GLsizei)h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0,(GLdouble)w,0.0,(GLdouble)h); } int main(int argc,char ** argv) { glutInit(&argc,argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(350,150); glutCreateWindow(argv [0]); 在里面 (); glutDisplayFunc(显示器); glutReshapeFunc(重塑); glutMainLoop(); 返回0; }
您可能希望使用显示列表来存储多边形点数模式以最大限度地提高效率。(见第7章“显示清单设计哲学”)。
标记多边形边界
高级
OpenGL只能渲染凸多边形,但实际上会出现许多非凸多边形。要绘制这些非凸多边形,您通常将它们细分为凸多边形 - 通常是三角形,如图2-12所示,然后绘制三角形。不幸的是,如果您将一般多边形分解成三角形并绘制三角形,那么您无法真正使用glPolygonMode()画多边形的轮廓,因为你得到所有的三角形轮廓。要解决这个问题,你可以告诉OpenGL一个特定的顶点是否在边界边缘之前; OpenGL通过与每个顶点一起传递一个位来指示该顶点是否跟随边界边缘来跟踪该信息。然后,当在GL_LINE模式中绘制多边形时,不绘制非边界边。在图2-12中,虚线表示添加的边。
图2-12:细分非凸多边形
默认情况下,所有顶点都标记在边界边界之前,但您可以使用命令glEdgeFlag *()手动控制边缘标志的设置。此命令在glBegin()和glEnd()对之间使用,并且会影响它之后指定的所有顶点,直到下一个glEdgeFlag()调用为止。它仅适用于为多边形,三角形和四边形指定的顶点,而不适用于为三角形或四边形条指定的顶点。
- void glEdgeFlag(GLboolean flag );
void glEdgeFlagv(const GLboolean * flag ); - 指示顶点是否应被视为初始化多边形的边界边缘。如果标志为GL_TRUE,则将边缘标志设置为TRUE(默认值),并将任何创建的顶点视为在边界边缘之前,直到此函数再次被调用,标志为GL_FALSE。
举例2-7绘制了如图2-13所示的轮廓。
图2-13:使用边缘标记绘制的轮廓多边形
示例2-7:标记多边形边界
glPolygonMode(GL_FRONT_AND_BACK,GL_LINE); 在glBegin(GL_POLYGON); glEdgeFlag(GL_TRUE); glVertex3fv(V0); glEdgeFlag(GL_FALSE); glVertex3fv(V1); glEdgeFlag(GL_TRUE); glVertex3fv(V2); glEnd();
正常向量
阿法向量(或正常的简称)是在这是垂直于表面的方向上指向的矢量。对于平坦表面,表面上的每个点的一个垂直方向是相同的,但是对于一般的曲面,法线方向在表面的每个点可能不同。使用OpenGL,您可以为每个多边形或每个顶点指定一个法线。相同多边形的顶点可能共享相同的法线(对于平坦的表面)或具有不同的法线(对于曲面)。但是不能在顶点以外的任何地方分配法线。
物体的法向矢量定义其表面在空间中的取向 - 特别是其相对于光源的取向。OpenGL使用这些向量来确定对象在其顶点接收多少光。照明 - 一个很大的话题本身就是第5章的主题,您可能希望在阅读该章后查看以下信息。在这里简要讨论法向矢量,因为您在定义对象的几何体的同时为对象定义法向量。
您使用glNormal *()将当前法线设置为传入的参数的值。对glVertex *()的后续调用会使指定的顶点分配当前法线。通常,每个顶点具有不同的法线,这需要一系列交替调用,如例2-8所示。
示例2-8:顶点的曲面法线
glBegin(GL_POLYGON); glNormal3fv(N0); glVertex3fv(V0); glNormal3fv(N1); glVertex3fv(V1); glNormal3fv(N2); glVertex3fv(V2); glNormal3fv(N3); glVertex3fv(V3); glEnd();
- void glNormal3 {bsidf}(TYPEnx,TYPEny,TYPEnz);
void glNormal3 {bsidf} v(const TYPE * v); - 设置由参数指定的当前法向量。非向量版本(没有v)需要三个参数,它们指定一个被认为是正常的(nx,ny,nz )向量。或者,您可以使用此函数的向量版本(使用v)并提供三个元素的单个数组来指定所需的法线。的b,s ^,以及我的版本线性扩展他们的参数值在范围[-1.0,1.0]。
找到一个对象的法线没有任何魔法 - 最有可能的是,你必须执行一些可能包括衍生物的计算,但是有几种可以用来实现某些效果的技巧和技巧。附录E解释了如何找到表面的法向量。如果您已经知道如何执行此操作,如果您可以依靠始终提供正常向量,或者您不想使用OpenGL照明设施提供的照明设备,则无需阅读本附录。
注意,在表面的给定点,两个向量垂直于表面,并且它们指向相反的方向。按照惯例,正常是指向正在建模的表面外部的法线。(如果你的模型内部和外部相反,只需将每个法向量从(x,y,z)更改为( - &xgr;, - y, - z))。
另外,请注意,由于正常矢量仅指示方向,它们的长度通常是无关紧要的。您可以指定任何长度的法线,但最终在执行照明计算之前必须将其转换为长度为1。(长度为1的向量称为单位长度,或归一化)。一般来说,应提供标准化的法向量。为了制作单位长度的法向量,将其x,y,z分量的每一个除以法线的长度:
只要您的模型转换仅包括旋转和平移,则正态矢量保持归一化。(有关变换的讨论,请参阅第3章。)如果执行不规则变换(例如缩放或乘以剪切矩阵),或者如果指定了非单位长度法线,则应该在转换后将OpenGL自动归一化法向量。为此,请使用GL_NORMALIZE作为参数调用glEnable()。默认情况下,禁用自动归一化。请注意,自动归一化通常需要额外的计算,这可能会降低应用程序的性能。
顶点数组
您可能已经注意到,OpenGL需要许多函数调用来渲染几何图元。绘制一个20边的多边形需要22个函数调用:一次调用glBegin(),一个调用每个顶点,最后调用glEnd()。在前面的两个代码示例中,附加信息(多边形边界边缘标记或表面法线)为每个顶点添加了函数调用。这可以快速将一个几何对象所需的函数调用次数增加一倍或三倍。对于某些系统,函数调用有很大的开销,可能会阻碍性能。
另外一个问题是在相邻多边形之间共享的顶点的冗余处理。例如,图2-14中的立方体具有六个面和八个共享顶点。不幸的是,使用描述此对象的标准方法,每个顶点必须指定三次:对于使用它的每个面都是一次。因此,24个顶点将被处理,即使八个足够了。
图2-14:六面 八个共享顶点
OpenGL具有顶点数组例程,允许您使用几个数组指定大量与顶点相关的数据,并使用同样少的函数调用访问该数据。使用顶点数组例程,20面多边形中的所有20个顶点可以放在一个数组中,并用一个函数调用。如果每个顶点也有一个表面正常,那么所有20个表面法线都可以放在另一个数组中,也可以用一个函数调用。
在顶点数组中排列数据可能会增加应用程序的性能。使用顶点数组可减少函数调用次数,从而提高性能。此外,使用顶点数组可以允许共享顶点的非冗余处理。(OpenGL的所有实现不支持顶点共享。)
注意:顶点数组是OpenGL 1.1版的标准配置,但不是OpenGL 1.0规范的一部分。使用OpenGL 1.0,一些供应商已经实现了顶点数组作为扩展。
使用顶点数组渲染几何有三个步骤。
激活(启用)多达六个阵列,每个阵列存储不同类型的数据:顶点坐标,RGBA颜色,颜色索引,曲面法线,纹理坐标或多边形边缘标志。
将数据放入数组或数组。数组由它们的内存位置的地址(即指针)访问。在客户端 - 服务器模型中,该数据存储在客户端的地址空间中。
用数据绘制几何。OpenGL通过取消引用指针从所有激活的数组中获取数据。在客户端 - 服务器模型中,数据被传输到服务器的地址空间。有三种方法可以做到这一点:
访问各个数组元素(随机跳跃)
创建单个数组元素的列表(有条不紊地跳跃)
处理顺序数组元素
您选择的取消引用方法可能取决于您遇到的问题类型。
交织的顶点数组数据是另一种常见的组织方法。而不是拥有多达六个不同的数组,每个数组保持不同类型的数据(颜色,表面法线,坐标等),您可能会将不同类型的数据混合到单个数组中。(见“交错数组”两种解决方法)
步骤1:启用数组
第一步是使用枚举参数调用glEnableClientState(),这会激活所选择的数组。理论上,您可能需要调用六次以激活六个可用阵列。实际上,您可能只能在一到四个数组之间激活。例如,您不可能同时激活GL_COLOR_ARRAY和GL_INDEX_ARRAY,因为程序的显示模式支持RGBA模式或颜色索引模式,但可能并不同时支持。
- void glEnableClientState(GLenum array )
指定要启用的数组。符号常量GL_VERTEX_ARRAY,GL_COLOR_ARRAY,GL_INDEX_ARRAY,GL_NORMAL_ARRAY,GL_TEXTURE_COORD_ARRAY和GL_EDGE_FLAG_ARRAY是可接受的参数。
如果您使用照明,则可能需要为每个顶点定义曲面法线。(请参阅“常规向量”。)为了使用顶点数组,您可激活曲面法线和顶点坐标数组:
glEnableClientState(GL_NORMAL_ARRAY); glEnableClientState(GL_VERTEX_ARRAY);
假设您要在某点关闭照明,并使用单一颜色绘制几何。您想要调用glDisable()关闭照明状态(参见第5章)。现在照明已停用,您也想停止更改表面正常状态的值,这是浪费的努力。要这样做,你打电话
glDisableClientState(GL_NORMAL_ARRAY);
- void glDisableClientState(GLenum array );
指定要禁用的数组。接受与glEnableClientState()相同的符号常量。
你可能会问自己为什么OpenGL的架构师创建了这些新的(和long!)命令名称,gl * ClientState()。为什么不能调用glEnable()和glDisable()?一个原因是glEnable()和glDisable()可以存储在显示列表中,但顶点数组的规范不能,因为数据保留在客户端。
步骤2:指定数组的数据
一个简单的方法是单个命令在客户机空间中指定单个数组。有六种不同的例程来指定数组 - 每个数组的一个例程。还有一个命令可以一次指定几个客户机空间数组,全部来自一个交错数组。
- void glVertexPointer(GLint size ,GLenum type ,GLsizei stride ,
const GLvoid * pointer ); - 指定可以访问空间坐标数据的位置。指针是数组中第一个顶点的第一个坐标的内存地址。type 指定数组中每个坐标的数据类型(GL_SHORT,GL_INT,GL_FLOAT或GL_DOUBLE)。size 是每个顶点的坐标数,它必须是2,3或4. stride 是连续顶点之间的字节偏移量。如果步幅为0,则顶点被理解为紧紧包装在阵列中。
要访问其他五个数组,有五个类似的例程:
void glColorPointer(GLint size, GLenum type, GLsizei stride,
const GLvoid *pointer);
void glIndexPointer(GLenum type, GLsizei stride, const GLvoid *pointer);
void glNormalPointer(GLenum type, GLsizei stride,
const GLvoid *pointer);
void glTexCoordPointer(GLint size, GLenum type, GLsizei stride,
const GLvoid *pointer);
void glEdgeFlagPointer(GLsizei stride, const GLvoid *pointer);
例程中的主要区别是大小和类型是唯一的还是必须指定的。例如,表面法线始终具有三个部件,因此指定其大小是多余的。边缘标志始终是单个布尔值,因此不需要提及大小和类型。表2-4显示了大小和数据类型的合法值。
表2-4:顶点数组大小(每顶点数值)和数据类型(续)
命令 |
尺寸 |
类型参数的值 |
glVertexPointer |
2,3,4 |
GL_SHORT,GL_INT,GL_FLOAT,GL_DOUBLE |
glNormalPointer |
3 |
GL_BYTE,GL_SHORT,GL_INT,GL_FLOAT,GL_DOUBLE |
glColorPointer |
3,4 |
GL_BYTE,GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT,GL_INT,GL_UNSIGNED_INT,GL_FLOAT,GL_DOUBLE |
glIndexPointer |
1 |
GL_UNSIGNED_BYTE,GL_SHORT,GL_INT,GL_FLOAT,GL_DOUBLE |
glTexCoordPointer |
1,2,3,4 |
GL_SHORT,GL_INT,GL_FLOAT,GL_DOUBLE |
glEdgeFlagPointer |
1 |
无类型参数(数据类型必须为GLboolean) |
示例2-9为RGBA颜色和顶点坐标使用顶点数组。RGB浮点值及其相应的(x,y)整数坐标将加载到GL_COLOR_ARRAY和GL_VERTEX_ARRAY中。
示例2-9:启用和加载顶点数组:varray.c
static GLint vertices[] = {25, 25, 100, 325, 175, 25, 175, 325, 250, 25, 325, 325}; static GLfloat colors[] = {1.0, 0.2, 0.2, 0.2, 0.2, 1.0, 0.8, 1.0, 0.2, 0.75, 0.75, 0.75, 0.35, 0.35, 0.35, 0.5, 0.5, 0.5};
glEnableClientState(GL_COLOR_ARRAY); glEnableClientState(GL_VERTEX_ARRAY); glColorPointer(3,GL_FLOAT,0,颜色); glVertexPointer(2,GL_INT,0,vertices);
迈
在步长为零的情况下,每种类型的顶点数组(RGB颜色,颜色索引,顶点坐标等)都必须紧密包装。数组中的数据必须是均匀的; 也就是说,数据必须全部为RGB颜色值,所有顶点坐标,或所有其他某些数据类似的数据。
使用除零之外的步幅可能是有用的,特别是在处理交错数组时。在以下数组GLfloats中,有六个顶点。对于每个顶点,有三个RGB颜色值,它们与(x,y,z)顶点坐标交替。
static GLfloat intertwined[] =
{1.0,0.2,1.0,100.0,100.0,0.0, 1.0,0.2,0.2,0.0,200.0,0.0, 1.0,1.0,0.2,100.0,300.0,0.0, 0.2,1.0,0.2,200.0 ,300.0,0.0, 0.2,1.0,1.0,300.0,200.0,0.0, 0.2,0.2,1.0,200.0,100.0,0.0};
Stride允许顶点数组在数组中以规则的间隔访问其所需的数据。例如,为了只引用中的颜色值交织阵列,下面的呼叫从所述数组的开始(其也可以作为传递开始&交织[0] )和向前跳到6 * 的sizeof(GLfloat)个字节,这是颜色和顶点坐标值的大小。该跳转足以达到下一个顶点数据的开头。
glColorPointer(3,GL_FLOAT,6 * sizeof(GLfloat),交织在一起);
对于顶点坐标指针,您需要从数组中进一步开始,在交织的第四个元素(记住C程序员开始计数为零)。
glVertexPointer(3,GL_FLOAT,6 * sizeof(GLfloat),&interwwined [3]);
步骤3:取消引用和渲染
直到顶点数组的内容被解引用,数组保留在客户端,并且它们的内容容易改变。在步骤3中,获取数组的内容,发送到服务器,然后向下发送图形处理流水线进行渲染。
有三种获取数据的方法:从单个数组元素(索引位置),数组元素序列和数组元素的有序列表中获取。
取消单个数组元素
- 无效glArrayElement(闪烁第i个)
- 获取所有当前启用的数组的一(第i )个顶点的数据。对于顶点坐标数组,相应的命令将是glVertex [ size ] [ type ] v(),其中size 是[2,3,4]之一,类型是[s,i,f,d]之一,用于GLshort,GLint,GLfloat和GLdouble。大小和类型都由glVertexPointer()定义。对于其他启用的数组,glArrayElement()调用glEdgeFlagv(),glTexCoord [ size ] [ type ] v(),glColor [ size] [ type ] v(),glIndex [ type ] v()和glNormal [ type ] v()。如果启用顶点坐标数组,则在执行(如果启用)最多五个相应数组值之后,最后执行glVertex * v()例程。
glArrayElement()通常在glBegin()和glEnd()之间调用。(如果调用外部,glArrayElement()设置所有启用的数组的当前状态,除了没有当前状态的顶点)。在示例2-10中,使用从启用顶点的第三个,第四个和第六个顶点绘制三角形数组(再次记住,C程序员开始用零计数数组位置)。
示例2-10:使用glArrayElement()定义颜色和顶点
glEnableClientState (GL_COLOR_ARRAY); glEnableClientState (GL_VERTEX_ARRAY); glColorPointer (3, GL_FLOAT, 0, colors); glVertexPointer (2, GL_INT, 0, vertices); glBegin(GL_TRIANGLES); glArrayElement (2); glArrayElement (3); glArrayElement (5); glEnd();
执行时,后五行代码具有相同的效果
glBegin(GL_TRIANGLES); glColor3fv(colors+(2*3*sizeof(GLfloat)); glVertex3fv(vertices+(2*2*sizeof(GLint)); glColor3fv(colors+(3*3*sizeof(GLfloat)); glVertex3fv(vertices+(3*2*sizeof(GLint)); glColor3fv(colors+(5*3*sizeof(GLfloat)); glVertex3fv(vertices+(5*2*sizeof(GLint)); glEnd();
Since glArrayElement() is only a single function call per vertex, it may reduce the number of function calls, which increases overall performance.
Be warned that if the contents of the array are changed between glBegin() and glEnd(), there is no guarantee that you will receive original data or changed data for your requested element. To be safe, don't change the contents of any array element which might be accessed until the primitive is completed.
Dereference a List of Array Elements
glArrayElement() is good for randomly "hopping around" your data arrays. A similar routine, glDrawElements(), is good for hopping around your data arrays in a more orderly manner.
- void glDrawElements(GLenum mode, GLsizei count, GLenum type,
void *indices); - 使用计数元素数定义几何图元序列,其索引存储在数组索引中。类型必须是GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT或GL_UNSIGNED_INT之一,表示索引数组的数据类型。模式指定什么样的基元被构造,并且是glBegin()接受的相同值之一; 例如,GL_POLYGON,GL_LINE_LOOP,GL_LINES,GL_POINTS等。
glDrawElements()的效果与此命令序列几乎相同:
int i; glBegin (mode); for (i = 0; i < count; i++) glArrayElement(indices[i]); glEnd();
glDrawElements()另外检查以确保模式,计数和类型有效。此外,与上述顺序不同,执行glDrawElements()会导致多个状态不确定。执行glDrawElements()后,如果相应的数组已被使能,当前RGB颜色,颜色索引,正常坐标,纹理坐标和边缘标志是不确定的。
使用glDrawElements(),可以将多维数据集的每个面的顶点放置在索引数组中。示例2-11显示了使用glDrawElements()渲染多维数据集的两种方法。图2-15显示了实例2-11中使用的顶点编号。
图2-15:具有编号顶点的立方体
示例2-11:使用glDrawElements()的两种方法
static GLubyte frontIndices = {4, 5, 6, 7}; static GLubyte rightIndices = {1, 2, 6, 5}; static GLubyte bottomIndices = {0, 1, 5, 4}; static GLubyte backIndices = {0, 3, 2, 1}; static GLubyte leftIndices = {0, 4, 7, 3}; static GLubyte topIndices = {2, 3, 7, 6};
glDrawElements(GL_QUADS,4,GL_UNSIGNED_BYTE,frontIndices); glDrawElements(GL_QUADS,4,GL_UNSIGNED_BYTE,rightIndices); glDrawElements(GL_QUADS,4,GL_UNSIGNED_BYTE,bottomIndices); glDrawElements(GL_QUADS,4,GL_UNSIGNED_BYTE,backIndices); glDrawElements(GL_QUADS,4,GL_UNSIGNED_BYTE,leftIndices); glDrawElements(GL_QUADS,4,GL_UNSIGNED_BYTE,topIndices);
或者还是更好,把所有的指标都压在一起:
static GLubyte allIndices = {4,5,6,7,1,2,6,5, 0,1,5,4,0,3,2,1, 0,4,7,3,2,3,7,6}; glDrawElements(GL_QUADS,24,GL_UNSIGNED_BYTE,allIndices);
注意:在glBegin() / glEnd()对之间封装glDrawElements()是一个错误。
使用glArrayElement()和glDrawElements(),您的OpenGL实现也可能会缓存最近处理的顶点,从而允许您的应用程序“共享”或“重用”顶点。拿起上述的立方体,例如,它有六个面(多边形),但只有八个顶点。每个顶点正好使用三个面。没有glArrayElement()或glDrawElements(),渲染所有六个面都需要处理二十四个顶点,即使十六个顶点将是冗余的。您的OpenGL实现可能能够最大限度地减少冗余,并且可以处理少至八个顶点。(重用顶点可能会限制在单个glDrawElements()调用中的所有顶点一个glBegin() / glEnd()对中的glArrayElement())
取代数组元素的序列
当glArrayElement()和glDrawElements() “绕过”你的数据数组时,glDrawArrays()直接通过它们。
- void glDrawArrays(GLenum mode ,GLint first ,GLsizei count );
- 构造方法使用数组元素开始在几何图元序列第一和在结束第一+ 计数每个已启用的阵列的-1。mode 指定什么类型的基元被构造,并且是glBegin()接受的相同值之一; 例如,GL_POLYGON,GL_LINE_LOOP,GL_LINES,GL_POINTS等。
glDrawArrays()的效果与此命令序列几乎相同:
int i; glBegin (mode); for (i = 0; i < count; i++) glArrayElement(first + i); glEnd();
与glDrawElements()的情况一样,glDrawArrays()也对其参数值执行错误检查,如果相应的数组已启用,则会保留当前RGB颜色,颜色索引,正常坐标,纹理坐标和带有不确定值的边缘标志。
尝试这个
- 更改示例2-13中的二十面体绘图程序以使用顶点数组。
交错数组
高级
这一章(在早期的“跨越”),检查交错阵列的特殊情况。在该部分中,通过调用glColorPointer()和glVertexPointer()来访问交织在RGB颜色和3D顶点坐标上的数组交织在一起。仔细使用步幅有助于正确指定阵列。
static GLfloat intertwined[] =
{1.0,0.2,1.0,100.0,100.0,0.0, 1.0,0.2,0.2,0.0,2.0.0,0.0, 1.0,1.0,0.2,100.0,300.0,0.0, 0.2,1.0,0.2,200.0,300.0,0.0, 0.2,1.0,1.0,300.0,200.0,0.0, 0.2,0.2,1.0,200.0,100.0,0.0};
还有一个巨型例程,glInterleavedArrays(),可以一次指定多个顶点数组。glInterleavedArrays()还启用和禁用相应的数组(因此它结合了步骤1和2)。该阵列交织在一起,完全适合glInterleavedArrays()支持的十四个数据交错配置之一。所以要指定数组的内容与RGB颜色和顶点数组交织在一起,并启用这两个数组,调用
glInterleavedArrays(GL_C3F_V3F,0,intertwined);
对glInterleavedArrays()的调用启用GL_COLOR_ARRAY和GL_VERTEX_ARRAY数组。它会禁用GL_INDEX_ARRAY,GL_TEXTURE_COORD_ARRAY,GL_NORMAL_ARRAY和GL_EDGE_FLAG_ARRAY。
此调用也具有与调用glColorPointer()和glVertexPointer()相同的效果,以将六个顶点的值指定到每个数组中。现在,您已准备好进行步骤3:调用glArrayElement(),glDrawElements()或glDrawArrays()来取消引用数组元素。
- void glInterleavedArrays(GLenum format ,GLsizei stride ,void * pointer )
- 初始化所有六个数组,禁用未以格式指定的数组,并启用指定的数组。格式是14个符号常量之一,代表14个数据配置; 表2-5 显示格式值。stride 指定连续顶点之间的字节偏移量。如果步幅为0,则顶点被理解为紧紧包装在阵列中。指针是数组中第一个顶点的第一个坐标的内存地址。
请注意,glInterleavedArrays()不支持边缘标志。
的力学glInterleavedArrays()是复杂的,并且需要参考实施例2-12和表2-5。在该示例和表中,您将看到et,ec和en,它们是启用或禁用的纹理坐标,颜色和正常数组的布尔值,您将看到st,sc和sv,它们是纹理坐标,颜色和顶点数组的大小(分量数)。tc是RGBA颜色的数据类型,它是唯一可以具有非浮点交错值的数组。pc,pn和pv是用于跳过单个颜色,正常和顶点值的计算步骤,s是步数(如果用户未指定)从一个数组元素跳转到下一个数组元素。
glInterleavedArrays()的效果与在示例2-12中调用命令序列相同,具有表2-5中定义的许多值。所有指针算术以sizeof(GL_UNSIGNED_BYTE)为单位执行。
示例2-12: glInterleavedArrays(format,stride,pointer)的效果
int str; /* set et, ec, en, st, sc, sv, tc, pc, pn, pv, and s * as a function of Table 2-5 and the value of format */ str = stride; if (str == 0) str = s; glDisableClientState(GL_EDGE_FLAG_ARRAY); glDisableClientState(GL_INDEX_ARRAY); if (et) { glEnableClientState(GL_TEXTURE_COORD_ARRAY); glTexCoordPointer(st, GL_FLOAT, str, pointer); } else glDisableClientState(GL_TEXTURE_COORD_ARRAY); if (ec) { glEnableClientState(GL_COLOR_ARRAY); glColorPointer(sc, tc, str, pointer+pc); } else glDisableClientState(GL_COLOR_ARRAY); if (en) { glEnableClientState(GL_NORMAL_ARRAY); glNormalPointer(GL_FLOAT, str, pointer+pn); } else glDisableClientState(GL_NORMAL_ARRAY); glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(sv, GL_FLOAT, str, pointer+pv);
在表2-5中,T和F为True和False。f是sizeof(GL_FLOAT)。c是sizeof(GL_UNSIGNED_BYTE)的4倍,向上舍入为f的最接近的倍数。
表2-5:(续)直接glInterleavedArrays()的变量
格式 |
等 |
EC |
恩 |
ST |
SC |
SV |
TC |
个人计算机 |
PN |
光伏 |
小号 |
GL_V2F |
F |
F |
F |
2 |
0 |
2F |
|||||
GL_V3F |
F |
F |
F |
3 |
0 |
3F |
|||||
GL_C4UB_V2F |
F |
Ť |
F |
4 |
2 |
GL_UNSIGNED_BYTE |
0 |
C |
C + 2F |
||
GL_C4UB_V3F |
F |
Ť |
F |
4 |
3 |
GL_UNSIGNED_BYTE |
0 |
C |
C + 3F |
||
GL_C3F_V3F |
F |
Ť |
F |
3 |
3 |
GL_FLOAT |
0 |
3F |
1207米 |
||
GL_N3F_V3F |
F |
F |
Ť |
3 |
0 |
3F |
1207米 |
||||
GL_C4F_N3F_V3F |
F |
Ť |
Ť |
4 |
3 |
GL_FLOAT |
0 |
4F |
7F |
10F |
|
GL_T2F_V3F |
Ť |
F |
F |
2 |
3 |
2F |
5F |
||||
GL_T4F_V4F |
Ť |
F |
F |
4 |
4 |
4F |
8F |
||||
GL_T2F_C4UB_V3F |
Ť |
Ť |
F |
2 |
4 |
3 |
GL_UNSIGNED_BYTE |
2F |
C + 2F |
C + 5F |
|
GL_T2F_C3F_V3F |
Ť |
Ť |
F |
2 |
3 |
3 |
GL_FLOAT |
2F |
5F |
8F |
|
GL_T2F_N3F_V3F |
Ť |
F |
Ť |
2 |
3 |
2F |
5F |
8F |
|||
GL_T2F_C4F_N3F_V3F |
Ť |
Ť |
Ť |
2 |
4 |
3 |
GL_FLOAT |
2F |
1207米 |
9F |
12F |
GL_T4F_C4F_N3F_V4F |
Ť |
Ť |
Ť |
4 |
4 |
4 |
GL_FLOAT |
4F |
8F |
11F |
15F |
首先学习更简单的格式GL_V2F,GL_V3F和GL_C3F_V3F。如果您使用C4UB中的任何格式,您可能必须使用一个结构数据类型或做一些精细的类型转换和指针数学来将四个无符号字节打包成一个32位字。
对于某些OpenGL实现,使用交错数组可能会增加应用程序性能。使用交错阵列,您的数据的确切布局是已知的。你知道你的数据是紧密包装的,可以在一个块中访问。如果不使用交错阵列,则必须检查步幅和大小信息,以检测数据是否紧密包装。
注意:glInterleavedArrays()仅启用和禁用顶点数组并指定顶点数组数据的值。它不渲染任何东西。您仍然必须完成步骤3,并调用glArrayElement(),glDrawElements()或glDrawArrays()来取消引用指针和渲染图形。
属性组
在“基本状态管理”中,您了解了如何设置或查询单个状态或状态变量。那么你也可以使用一个命令来保存和恢复相关状态变量的集合的值。
OpenGL将相关的状态变量组合成属性组。例如,GL_LINE_BIT属性由五个状态变量组成:行宽度,GL_LINE_STIPPLE使能状态,行规则模式,行规重复计数器和GL_LINE_SMOOTH使能状态。(参见第6章中的“抗锯齿”)。使用命令glPushAttrib()和glPopAttrib(),您可以一次保存并还原所有五个状态变量。
一些状态变量在多个属性组中。例如,状态变量GL_CULL_FACE是多边形和启用属性组的一部分。
在OpenGL 1.1版中,现在有两个不同的属性堆栈。除了原始属性堆栈(保存服务器状态变量的值)之外,还有一个客户端属性堆栈,可以通过命令glPushClientAttrib()和glPopClientAttrib()访问。
一般来说,使用这些命令比获取,保存和恢复值更快。硬件中可能会维护一些值,并且获取它们可能是昂贵的。此外,如果您在远程客户端上运行,则所有属性数据必须通过网络连接进行传输,并在获取,保存和还原时返回。然而,您的OpenGL实现将属性堆栈保留在服务器上,从而避免不必要的网络延迟。
有大约二十个不同的属性组,可以通过glPushAttrib()和glPopAttrib()保存和恢复。有两个客户端属性组,可以通过glPushClientAttrib()和glPopClientAttrib()保存和还原。对于服务器和客户端,属性都存储在堆栈中,该堆栈的深度至少为16个已保存的属性组。(实现的实际堆栈深度可以使用GL_MAX_ATTRIB_STACK_DEPTH和GL_MAX_CLIENT_ATTRIB_STACK_DEPTH与glGetIntegerv()获取。)推送完整的堆栈或弹出一个空的生成错误。
(请参阅附录B中的表格,以确定哪些属性被保存用于特定的掩码值;也就是说,哪些属性在特定的属性组中。)
- void glPushAttrib(GLbitfield mask );
void glPopAttrib(void); - glPushAttrib()通过将它们放在属性堆栈上,将所有由位指示的属性保存在掩码中。glPopAttrib()还原与最后一个glPushAttrib()一起保存的那些状态变量的值。表2-7 列出了可以在逻辑上进行逻辑运算以保存任何属性组合的可能的掩码位。每个位对应于各个状态变量的集合。例如,GL_LIGHTING_BIT是指与照明相关的所有状态变量,包括当前材料颜色,环境,漫反射,镜面反射和发射光,启用的光的列表以及聚光灯的方向。当glPopAttrib() 被调用,所有这些变量都被恢复。
特殊掩码GL_ALL_ATTRIB_BITS用于保存并还原所有属性组中的所有状态变量。
表2-6:(续)属性组
掩码位 |
属性组 |
GL_ACCUM_BUFFER_BIT |
ACCUM缓冲 |
GL_ALL_ATTRIB_BITS |
- |
GL_COLOR_BUFFER_BIT |
颜色缓冲区 |
GL_CURRENT_BIT |
当前 |
GL_DEPTH_BUFFER_BIT |
深度缓冲 |
GL_ENABLE_BIT |
启用 |
GL_EVAL_BIT |
EVAL |
GL_FOG_BIT |
多雾路段 |
GL_HINT_BIT |
暗示 |
GL_LIGHTING_BIT |
灯光 |
GL_LINE_BIT |
线 |
GL_LIST_BIT |
名单 |
GL_PIXEL_MODE_BIT |
像素 |
GL_POINT_BIT |
点 |
GL_POLYGON_BIT |
多边形 |
GL_POLYGON_STIPPLE_BIT |
多边形点画 |
GL_SCISSOR_BIT |
剪刀 |
GL_STENCIL_BUFFER_BIT |
模板缓冲区 |
GL_TEXTURE_BIT |
质地 |
GL_TRANSFORM_BIT |
转变 |
GL_VIEWPORT_BIT |
视 |
- void glPushClientAttrib(GLbitfield mask );
void glPopClientAttrib(void); - glPushClientAttrib()通过将它们推送到客户端属性堆栈来保存由掩码中的位指示的所有属性。glPopClientAttrib()还原与最后一个glPushClientAttrib()一起保存的那些状态变量的值。表2-7 列出了可以在逻辑上进行逻辑或运算以保存客户端属性的任何组合的可能的掩码位。
- 表2-7:客户端属性组
掩码位 |
属性组 |
GL_CLIENT_PIXEL_STORE_BIT |
像素店 |
GL_CLIENT_VERTEX_ARRAY_BIT |
顶点数组 |
GL_ALL_CLIENT_ATTRIB_BITS |
- |
不能被推或弹出 |
反馈 |
不能被推或弹出 |
选择 |
一些提示表面多边形模型的提示
以下是您在构建曲面的多边形近似时可能需要使用的一些技术。在阅读第5 章照明和第7章显示列表之后,您可能需要查看本节。照明条件影响模型在绘制后的外观,并且与显示列表结合使用时,以下某些技术效率更高。当您阅读这些技术时,请记住,启用照明计算时,必须指定法向量以获得正确的结果。
构建表面的多边形近似是一种艺术,并没有经验的替代。但是,这一节列出了一些可能会更容易入门的指针。
- 保持多边形方向一致。确保从外部观察时,表面上的所有多边形都朝向相同的方向(均为顺时针或全部逆时针)。一致的方向对于多边形拣选和双面照明很重要。尝试第一次获得这个权利,因为以后解决问题是非常痛苦的。(如果你使用glScale *() ,以反映周围的对称轴线的一些几何图形,你可能会改变与定向glFrontFace()保持一致的方向。)
- 细分表面时,请注意任何非三角形多边形。三角形的三个顶点保证位于一个平面上; 任何具有四个或更多顶点的多边形可能不是。可以从某些方向观看非平面多边形,使得边缘彼此交叉,并且OpenGL可能不会正确地渲染这样的多边形。
- 显示速度和图像质量之间始终存在折衷。如果将表面细分为少量多边形,则会很快呈现,但可能会出现锯齿状的外观; 如果您将其细分为数百万个小的多边形,则可能看起来不错,但可能需要很长时间才能呈现。理想情况下,您可以为细分例程提供一个参数,指示您想要的细分,如果对象距离眼睛更远,则可以使用较细的细分。此外,当您细分时,使用表面相对平坦的大多边形,以及高曲率区域中的小多边形。
- 对于高质量的图像,最好在轮廓边缘细分多于内部。如果表面要相对于眼睛旋转,这是更坚韧的,因为轮廓边缘保持移动。在法向量垂直于从表面到视点的矢量 - 即当矢量点积为零时,出现轮廓边缘。如果该点积接近零,您的细分算法可能会选择更细分。
- 尽量避免模型中的T形交叉点(见图2-16)。如图所示,不能保证线段AB和BC位于与段AC完全相同的像素上。有时他们会做,有时候他们根本不会改变和取向。这可能导致表面间断地出现裂纹。
图2-16:修改不合需要的T形交叉点
- 如果您正在构造封闭的表面,请确保在闭环开始和结束处使用与坐标完全相同的数字,否则由于数值四舍五入,您可以获得间隙和裂纹。以下是错误代码的二维示例:
/ *不要使用这个代码* / #define PI 3.14159265 #define EDGES 30 / *画一个圆* / glBegin(GL_LINE_STRIP); for(i = 0; i <= EDGES; i ++) glVertex2f(cos((2 * PI * i)/ EDGES),sin((2 * PI * i)/ EDGES)); glEnd();
只有当您的机器设法计算0和(2 * PI * EDGES / EDGES)的正弦和余弦值并且获得完全相同的值时,边缘才能完全相遇。如果你信任你的机器上的浮点单元来做这件事,那么作者有一个桥梁,他们想卖你....要纠正代码,请确保当我 == EDGES,你使用0为正弦和余弦,不是2 * PI * EDGES / EDGES。(或者更简单的是,使用GL_LINE_LOOP而不是GL_LINE_STRIP,并将循环终止条件更改为i <EDGES。)
一个例子:建立二十面体
为了说明近似表面中出现的一些注意事项,我们来看一些示例代码序列。该代码涉及常规二十面体的顶点(这是由十二个面组成的柏拉图式实体,跨越十二个顶点,每个面都是等边三角形)。二十面体可以被认为是球体的粗略近似。示例2-13定义构成二十面体的顶点和三角形,然后绘制二十面体。
示例2-13:绘制二十面体
#define X .525731112119133606 #define Z .850650808352039932 static GLfloat vdata [12] [3] = { {-X,0.0,Z},{X,0.0,Z},{-X,0.0,-Z},{X,0.0,-Z} {0.0,Z,X},{0.0,Z,-X},{0.0,-Z,X},{0.0,-Z,-X} {Z,X,0.0},{-Z,X,0.0},{Z,-X,0.0},{-Z,-X,0.0} }; static GLuint tindices [20] [3] = { {0,4,1},{0,9,4},{9,5,4},{4,5,8},{4,8,1} {8,10,1},{8,3,10},{5,3,8},{5,2,3},{2,7,3} {7,10,3},{7,6,10},{7,11,6},{11,0,6},{0,1,6}, {6,1,10},{9,0,11},{9,11,2},{9,2,5},{7,2,11}}; int i; 在glBegin(GL_TRIANGLES); for(i = 0; i <20; i ++){ / *此处的颜色信息* / glVertex3fv(VDATA [tindices [I] [0] [0]); glVertex3fv(VDATA [tindices [I] [1] [0]); glVertex3fv(VDATA [tindices [I] [2] [0]); } glEnd();
选择奇数X和Z,使得从二值体的原点到任何顶点的距离为1.0。在数组vdata [] []中给出了十二个顶点的坐标,其中第零个顶点是{ - &Xgr; ,0.0,&Zgr; },第一个是{ X,0.0,Z }等等。数组tindices [] []告诉如何将顶点链接成三角形。例如,第一个三角形由第零个,第四个和第一个顶点构成。如果以给定的顺序取三角形的顶点,则所有三角形都具有相同的方向。
提及颜色信息的行应由用于设置第i个面的颜色的命令替代。如果这里没有代码出现,所有的面都以相同的颜色绘制,并且不可能辨别出物体的三维质量。明确指定颜色的另一种方法是定义表面法线并使用照明,如下一节所述。
注意:在本节中描述的所有示例中,除非表面仅绘制一次,否则应该保存计算的顶点和正常坐标,以便每次绘制表面时不需要重复计算。这可以使用您自己的数据结构或通过构建显示列表来完成。(见第七章)
计算表面的正态向量
如果表面要点亮,则需要向表面提供正常的向量。计算该表面上两个向量的归一化交叉乘积提供法向量。对于二十面体的平坦表面,限定表面的所有三个顶点具有相同的法向量。在这种情况下,需要为每组三个顶点指定一次正常。示例2-14中的代码可以替换实施例2-13中的“颜色信息”行来绘制二十面体。
示例2-14:为表面生成正常向量
GLfloat d1 [3],d2 [3],norm [3]; for(j = 0; j <3; j ++){ d1 [j] = vdata [tindices [i] [0]] [j] - vdata [tindices [i] [1]] [j] d2 [j] = vdata [tindices [i] [1]] [j] - vdata [tindices [i] [2]] [j] } normcrossprod(d1,d2,norm); glNormal3fv(norm);
函数normcrossprod()产生两个向量的归一化交叉乘积,如示例2-15所示。
示例2-15:计算两个向量的规范化交叉积
void normalize(float v[3]) { GLfloat d = sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]); if (d == 0.0) { error("zero length vector"); return; } v[0] /= d; v[1] /= d; v[2] /= d; } void normcrossprod(float v1[3], float v2[3], float out[3]) { GLint i, j; GLfloat length; out[0] = v1[1]*v2[2] - v1[2]*v2[1]; out[1] = v1[2]*v2[0] - v1[0]*v2[2]; out[2] = v1[0]*v2[1] - v1[1]*v2[0]; normalize(out); }
如果您使用二十面体作为阴影球体的近似值,则需要使用垂直于球体真实表面的法向矢量,而不是垂直于面部。对于球体,法向量是简单的; 每个点与从原点到相应顶点的向量的方向相同。由于二十面体顶点数据是半径为1的二十面体,所以正常和顶点数据是相同的。下面是代码绘制平滑阴影球体的二十面体逼近(假设照明被启用,如第5章所述):
glBegin(GL_TRIANGLES); for(i = 0; i <20; i ++){ glNormal3fv(VDATA [tindices [I] [0] [0]); glVertex3fv(VDATA [tindices [I] [0] [0]); glNormal3fv(VDATA [tindices [I] [1] [0]); glVertex3fv(VDATA [tindices [I] [1] [0]); glNormal3fv(VDATA [tindices [I] [2] [0]); glVertex3fv(VDATA [tindices [I] [2] [0]); } glEnd();
改进模型
对球体的二十面近似看起来并不好,除非屏幕上的球体图像相当小,但是有一个简单的方法可以提高逼近的准确性。想象一下二十面体刻在一个球体上,并将三角形细分,如图2-17所示。新引入的顶点略微在球体内部,因此通过对它们进行归一化(将它们除以因子使其具有长度1)将其推到表面。该细分过程可以重复任意精度。图2-17所示的三个对象分别使用20,80和320个近似三角形。
图2-17:细分以改善表面的多边形近似
示例2-16执行单个细分,创建80边球面近似。
示例2-16:单细分
void drawtriangle(float *v1, float *v2, float *v3) { glBegin(GL_TRIANGLES); glNormal3fv(v1); vlVertex3fv(v1); glNormal3fv(v2); vlVertex3fv(v2); glNormal3fv(v3); vlVertex3fv(v3); glEnd(); } void subdivide(float *v1, float *v2, float *v3) { GLfloat v12[3], v23[3], v31[3]; GLint i; for (i = 0; i < 3; i++) { v12[i] = v1[i]+v2[i]; v23[i] = v2[i]+v3[i]; v31[i] = v3[i]+v1[i]; } normalize(v12); normalize(v23); normalize(v31); drawtriangle(v1, v12, v31); drawtriangle(v2, v23, v12); drawtriangle(v3, v31, v23); drawtriangle(v12, v23, v31); } for (i = 0; i < 20; i++) { subdivide(&vdata[tindices[i][0]][0], &vdata[tindices[i][1]][0], &vdata[tindices[i][2]][0]); }
实施例2-17是实施例2-16的轻微修改,其将三角形递归地细分到适当的深度。如果深度值为0,则不执行任何细分,并按原样绘制三角形。如果深度为1,则执行单个细分,依此类推。
例2-17:递归细分
void subdivide(float *v1, float *v2, float *v3, long depth) { GLfloat v12[3], v23[3], v31[3]; GLint i; if (depth == 0) { drawtriangle(v1, v2, v3); return; } for (i = 0; i < 3; i++) { v12[i] = v1[i]+v2[i]; v23[i] = v2[i]+v3[i]; v31[i] = v3[i]+v1[i]; } normalize(v12); normalize(v23); normalize(v31); subdivide(v1, v12, v31, depth-1); subdivide(v2, v23, v12, depth-1); subdivide(v3, v31, v23, depth-1); subdivide(v12, v23, v31, depth-1); }
广义细分
可以使用例如实施例2-17中所述的递归细分技术用于其它类型的表面。通常,如果达到一定深度或者曲率上的某些条件得到满足,则递归结束(表面的高度弯曲部分看起来更好,更细分)。
要看一个更通用的解决问题的细分问题,考虑由两个变量u [0]和u [1]参数化的任意表面。假设提供了两个例程:
void surf(GLfloat u [2],GLfloat vertex [3],GLfloat normal [3]); 浮动曲线(GLfloat u [2]);
如果surf()被传递u [],则返回相应的三维顶点和法线向量(长度为1)。如果u []被传递给curv(),则计算并返回该点处曲面的曲率。(有关测量曲面曲率的更多信息,请参阅差分几何的入门教科书。)
示例2-18显示了在达到最大深度或直到三个顶点的最大曲率小于某个截止值之前,将三角形细分的递归例程。
例2-18:广义细分
void subdivide(float u1[2], float u2[2], float u3[2], float cutoff, long depth) { GLfloat v1[3], v2[3], v3[3], n1[3], n2[3], n3[3]; GLfloat u12[2], u23[2], u32[2]; GLint i; if (depth == maxdepth || (curv(u1) < cutoff && curv(u2) < cutoff && curv(u3) < cutoff)) { surf(u1, v1, n1); surf(u2, v2, n2); surf(u3, v3, n3); glBegin(GL_POLYGON); glNormal3fv(n1); glVertex3fv(v1); glNormal3fv(n2); glVertex3fv(v2); glNormal3fv(n3); glVertex3fv(v3); glEnd(); return; } for (i = 0; i < 2; i++) { u12[i] = (u1[i] + u2[i])/2.0; u23[i] = (u2[i] + u3[i])/2.0; u31[i] = (u3[i] + u1[i])/2.0; } subdivide(u1, u12, u31, cutoff, depth+1); subdivide(u2, u23, u12, cutoff, depth+1); subdivide(u3, u31, u23, cutoff, depth+1); subdivide(u12, u23, u31, cutoff, depth+1); }