既然我们已经介绍了3D图形的基本术语以及隐藏在3D图形背后的思路,现在就可以进入正题了。在使用OpenGL之前,我们衡要讨论什么是OpenGL以及什么不是 OpenGL,使读者能够同时了解这种API 的功能和限制。本章概括地描述了OpenGL的操作,并介绍了如何建立3D艺术品的淮染框架。(简单来说本章就是来初步认识OpenGL的)
2.1 什么是OpenGL?
摘要:
严格地来说,OpenGL将定义为 "图形硬件的一种软件接口"。从本质上说,它是一个3D图形和模型库,具有高度的可移植性,并且具有非常快的速度。(一种高效并且可移植性强的图形硬件的API)
使用OpenGL,可以创建优雅而漂亮的3D图像,并且具有非常出色的视觉质量。
使用OpenGL的最大优点是它的速度远远快于光线追踪器或软件渲染引擎。
最初,它使用SGI (Silicon Graphics, Inc.)精心开发和优化的算法。
随着其他厂商不断奉献经验和智慧,发展自己的高性能实现方案,OpenGL也得到了不断的发展。
OpenGL并不像C和C++那样的编程语言,它更像一个C运行时的函数库,提供了一些预先打包的功能。
另一方面,OpenGL规范包含GLSL,即OpenGL着色语言,这实际上是一种非常类似于C语言的程序设计语言。
但是,GLSL并不会对应用程序流程和逻辑进行控制,而是用于渲染操作。
OpenGL意在供那些专门为显示和处理3D图形而进行设计和优化的计算机硬件使用。
(表面我们使用的是OpenGL提供的API,这些实现一般由显示设备厂商提供)
OpenGL具有多种用途,范围涵盖从CAD工程和建筑应用程序到那些恐怖电影中用于实现计算机生成鬼怪的建模程序等各种应用。随着硬件加速和高速PC微处理器越来越普及,3D图形已经成为消费者和商业应用程序的典型组成部分,而不再局限于游戏和科学应用程序
2.1.1 标准的演化
摘要:
OpenGL的前身是SGI公司 IRIS GL。
它最初是个2D图形函数库,后来逐渐演化为由该公司的高端IRIS图形工作站使用的3D编程API。
这种计算机不仅是通用计算机,他们具有专门经过优化的硬件,用于显示复杂的图形。
这种硬件提供了超快的矩阵变换能力(这是3D图形的先决条件)、硬件支持的深度缓冲以及一些其他功能。
但是在某种情况下,出于支持陈旧系统的需要,技术的发展常常受到束缚。
IRIS GL并不是一开始就设计为具有顶点风格的几何图形接口的。后来形势逐渐变得明朗,SGI必须彻底改变才能继续发展。
OpenGL 就是 SGI 对 IRIS GL的移植进行改进和提高的结果。这个新的图形API不仅具有GL的功能,而且是一个"开放"的标准。
它的输入来自其他图形硬件厂商,并且更容易应用到其他硬件平台和操作系统。
OpenGL是为了3D集合图形而完全重新设计的。
OpenGL ARB
如果只有某一家厂商控制标准,那么这个标准就不是真正的开放式标准。SGI当初的业务领域是高端的计算机图形
一旦已经处于某个行业的顶端,你就会发现进一步成长的机会非常有限。
SGI意识到如果它能够做一些事情来推动高端计算机图形硬件时长的成长,那么这对公司也是件非常好的事情。
一个得到众多厂商支持的真正的开放式标准能使程序员(开发者)更容易创建可以适用广泛平台的应用程序和内容。
软件确实能够促进计算机的销售,如果SGI希望卖出更多的计算机,那么就需要在它的计算机上能够运行更多的软件。
其他厂商也意识到了这一点,这样OpenGL体系结构审核委员会(ARB, OpenGL Architecture Reiview Board)就诞生了。
SGI最初控制了OpengGL API的许可, 而 ARB 的创立者包括 SGI、DEC(Digital Equitment Corporation)、IBM、Intel、和 Microsoft。
1992年7月1日,OpenGL 规范 1.0版正式出台。
随着时间的推移,ARB又陷入陆续增加一些新成员,其中有许多来自其他PC硬件社区。
ARB每隔4年召开一次会议,对规范进行维护和改善,并出台计划对OpenGL标准进行升级。
近年来,由于某些原因,SGI的业务不断下滑,具体原因超出本书的讨论范围。
2006年,实际上已经破产的SGI公司把对OpenGL标准的控制权从ARB转交给一个新的工作组: Khronos 小组(www.khronos.org)
Khronos是一个由成员提供资金的行业协会,专注于开放媒体标准的创建和维护。
大多数ARB成员已经成为 Khronos的成员,因此这个变动并没有产生太大的波动。
今天,Khronos小组继续发展和升级OpenGL以及它的姊妹API——OpenGL ES。(将在16章介绍OpenGL ES)
OpenGL以两种形式存在。
第一种形式是 "OpenGL 规范",这个规范定义了行业标准,用非常完整和明确的术语描述了OpenGL。
它完整地定义了OpenGL API、OpenGL的整个状态机,以及各种特性是如何共同工作和运行的。
然后向 ATI、NVIDA、Intel或Apple这样的硬件厂商获取这个规范,并实现它。
第二种形式就是OpenGL的实现,软件开发人员和顾客可以使它生成实时图形。(通过调用OpenGL的API)
OpenGL 扩展机制
读者可能会认为,既然OpenGL是一种 "标准" API,那么硬件提供商在竞争中只要考虑性能(可能还有视觉质量)因素就可以了。
然而,3D图形领域的竞争是非常激烈的,硬件提供商不仅在性能和质量方面拥有持久的创新力,在图形方面和特效方面也是如此。
OpenGL 运行提供商通过它的扩展机制进行创新。
这种机制以两种方式运作。
首先,提供商能够向OpenGL API中增加开发人员使用的新函数。
其次,可以添加能够被已存在的OpenGL函数识别的标记(Token)或枚举(Enumerant)。
利用新的标记或枚举只需在项目中添加一个提供商支持的头文件(Header)。
提供商必须在OpenGL工作组(Khronos小组的一个下属机构)注册它们的扩展,以避免提供商使用其他提供商已经使用的值。
有一个标准的头文件 glext.h 中包含了这些扩展,为我们提供了方便。
游戏需要为特定图形卡而进行重现编译的时代已经一去不复返了。
就像我们已经知道,我们可以查到一个标识提供商和OpenGL驱动版本的字符串。
确定一个扩展是否得到支持需要两个步骤。
首先,我们向OpenGL查询当前实现支持多少扩展。
然后,我们可以通过调用 glGetStringi 函数获取特定扩展的名称。这个函数将返回单个扩展的名称。
(代码: Open GL扩展支持)
GLint nNumExtension;
glGetIntegerv(GL_NUM_EXTENSIONS, &nNumExtension);
例如: 要在Windows中查询交换控制扩展,我们可以依次查阅所有扩展寻找我们需要的。一旦找到,我们要得到这个函数的函数指针并正确地调用它。
(代码: Open GL在Windows中查询交换控制扩展)
GLint nNum;
glGetIntegerv(GL_NUM_EXTENSIONS, &nNum); // 跟上一个示例一样,获取扩展支持数量
// 遍历所有扩展
for (GLint i = 0; i < nNum; ++i) {
// 判断是否支持扩展名为: WGL_EXT_swap_control 的扩展
if (strcmp("WGL_EXT_swap_control", (const char *)glGetStringi(GL_EXTENSIONS, i)) == 0)
{
// 支持则获取这个扩展函数的函数指针
wglSwapIntervalExt = (PFNWGLSWAPINTERVALEXTPROC)wglGetProcAddress("wglSwapIntervalExt");
if (wglSwapIntervalExt != NULL) {
// 调用设置这个扩展
wglSwapIntervalExt(1);
}
}
}
PS: 稍微扩展一下上面的示例(不感兴趣的此处可跳过)
Q: WGL_EXT_swap_control 是什么?(详情OpenGL扩展)
A: 此扩展允许应用程序指定最小周期性 颜色缓冲区交换的数量,以视频帧周期为单位。(摘抄自OpenGL扩展)
在 GLTools 库中有一个快捷工具包,稍后将对其进行讨论。
int gltIsExtSupported(const char *extension);
如果支持指定的扩展,那么这个函数返回1;反之则返回0。
GLTools库包含了一整套OpenGL帮助和使用工具函数,其中有很多在本书中自始至终都在使用。
gltools.h文件包含所有的函数的函数原型。
(代码Open GL在Windows中查询交换控制扩展)这个示例还展示了再Windows下如何获取一个指向新的OpenGL函数的指针。
Windows函数 wglGetProcAddress 返回一个指向OpenGL函数(扩展)名的指针。
获取一个由于操作系统的不同而不同的扩展,这一主题在本书的第3部分将进行更详细的阐述。
幸运的是,在99%的情况下我们使用一个叫做GLEW的快捷方式库就可以了,而我们将自动获得驱动程序支持功能的扩展函数指针。
这是谁的扩展
使用OpenGL扩展,我们可以在代码中规定代码路径来改着渲染表现和视觉质量,或者甚至可以添加只有特殊提供商的硬件才支持的特效。
但是这些扩展是属于谁的?
也就是说,哪家提供商创建一个特定的扩展并为其提供技术支持?
通常情况下我们只通过观察扩展名就能得出结论。
每个扩展都有一个由3个字母组成的前缀,这个前缀标识了这个扩展的来源。
表2.1展示了一些扩展的识别的示例。
前缀 | 提 供 商 |
SGI_ | Silicon Graphics |
ATI_ | ATI Tenchnologies |
AMD_ | Advanced Micro Devices |
NV_ | NVIDIA |
IBM_ | IBM |
WGL_ | Microsoft |
EXT_ | Cross-Vendor |
ARB_ | ARB Approved |
一个提供商为另一个提供商的扩展提供支持的情况并不少见。
例如,一些NVIDIA的扩展就在ATI硬件平台上得到支持并广泛流行。
竞争提供商必须遵循原始提供商的规范(关于扩展如何工作的细节)。
通常人们都认为扩展是好东西,扩展的EXT_前缀表明这个扩展(假定)是与特定供应商无关的,并且得到众多实现的支持。
最后,还有一些ARB承认的扩展。
这些扩展的规范经过了OpenGL ARB的审核以及讨论。
这些扩展常常意味着某些新技术或函数能够加入核心OpenGL规范前的最后步骤。
许可和一致
OpenGL实现可以是软件库,也就是对OpenGL函数调用做出响应、创建三维图像的软件函数库。
OpenGL实现也可以是一个用于完成三维图像渲染任务的硬件设备(通常是显示卡)的驱动程序包。
硬件实现比软件实现要快上许多倍。
而且,现在即使在廉价PC上这类硬件也已经非常普遍。
如果厂商希望创建并销售OpenGL实现,首先必须从Khronos小组获得OpenGL许可。
如果申请者是PC硬件厂商,Khronos小组在批准许可时会顺带提供一个示例实现(纯软件形式)和一个设备驱动程序。
然后,厂商就可以据此创建经过优化的实现,并且可以通过扩展提高产品的价值。
在典型的情况下,厂商之间的竞争就是性能、图像质量和驱动程序稳定性的竞争。
此外,厂商的实现必须通过OpenGL一致性测试。这些测试的设计目标就是保证实现方案是完整的(包含所有必须的函数调用),并且对于一组特定的函数,这种实现所产生的3D渲染输出结果是可以接受的。
软件开发人员在使用OpenGL驱动程序时并不需要获得OpenGL许可或支付任何费用。
OpenGL是得到操作系统原生支持的,并且获得许可的驱动程序是由硬件厂商本身提供的。
2.1.2 OpenGL的未来
绝大多数公司认识到从长远而言,竞争对于每家公司都是件好事,因此它们都认可并支持行业标准,甚至对行业标准作出贡献。
Khronos小组下属的体系结构审核委员会(ARB)如今已经非常壮大,生气勃勃且充满活力。
最近,OpenGL规范的修订工作已经达不到一年就能推进一个版本的速度。
到本书编写时,OpenGL最新的版本已经更新到了3.3和4.0,这两个版本都是在2010年游戏开发者大会上发布的。
15年以来,加起来人们已经在各种类型的OpenGL技术、书籍、教程、示例代码和应用程序上花费了数百万的人工。
这种持续的动力将使OpenGL在可预见的未来保持广大应用程序和硬件平台首选API的地位。
所有这些都使OpenGL获得良好的定位,以充分利用未来3D图形技术创新成果。
OpenGL2.0中加入的OpenGL着色语言(GLSL),使OpenGL显示出了长久的适应性,能够满足不断发展的3D图形程序设计管线所带来的挑战。
最后,OpenGL是一种规范,能够应用于各种各样程序设计规范。从C/C++、Java到Visual Basic,甚至C#这样的新兴程序设计语言,现在都已经用来使用OpenGL创建PC游戏和应用程序。
OpenGL已经被广泛地接受和关注。
OpenGL 和 Direct3D
就像政治上或宗教上的联盟一样,对程序设计语言或API的旋转警察在某种程度上是某些原因和情感考虑。
——"我就是这样成长的" 或者 "这就是最早学习的API,我还是用它最顺手"。
这当然是任何人会选择Direct3D而不选择OpenGL的唯一符合逻辑的原因。
如果读者是3D图形程序设计领域的新手,那么可能还不知道在两种互相竞争的标准OpenGL与Direct3D之间有一场 "战争"。
这是非常遗憾的,因为这两种标准都是可行的选择,而且他们都有各自的优点。
人们经常将OpenGL与DirectX相比,这是不公平的。
DirectX是一个来自微软公司的游戏技术SPI族,其中包括Direct3D这种由微软公司与游戏程序涉及而开发的渲染API。
一个人可能更喜欢自己的汉堡而不是别人的牛扒,但是拿别人的牛排和一家餐馆来比较是不公平的!
实际上,大多数使用OpenGL的Windows游戏同时还使用DirectX的非渲染组件来更方便地进行声音的回放、游戏手柄控制和联网游戏等。
Direct3D是微软公司专有的标准,被广泛地应用于Windows平台的游戏上,并且还有一些Direct3D的变体被用于XBox游戏控制台平台和一些Windows移动设备上。
在Direct3D发展的早期,这种API是非常不好用的,和OpenGL相比缺乏大量特征,并且存在一些固有的软件低效情况。
微软公司采取了一些有争议的策略来帮助Direct3D成为Windows游戏程序设置的 "标准",这种情况持续了几年,被人们称为 "API战争"。
在很多人看来,这场战争还在继续。
公平地说,微软公司到现在为止已经与硬件提供商和软件提供商合作了十多年,现在Direct3D已经成为一种有用且有良好口碑的API,并且在那些只对微软公司平台感兴趣的游戏程序设计人员非常流行。
但是,OpenGL在Windows游戏开发人员中仍然非常流行,并且是那些制作非游戏3D应用程序(例如: 视觉模拟(Vis-sim))行业、内容创建工具、科学可视化和商业图形等的软件开发人员的首选。
在OpenGL和Direct3D之间 "根据情感" 的选择常常可以归结为喜欢或不喜欢Direct3D的面向对象 COM (Component Object Model组件对象模型)方法和OpenGL的状态机抽象化,或者仅仅是出于喜欢或不喜欢微软公司。
在诸如Mac OSX、iPhone、Linux(不仅仅是桌面系统,大多数手持只能电话设备使用的都是UNIX的变体)和Sony或任天堂游戏设备这样的非微软平台上,OpenGL或者类型OpenGL的API则是实际上的标准。
如果我们将整个3D图形产业看作一个整体,OpenGL所占据的份额比Direct3D要大得多。
还有一些原因使我们可能选择OpenGL而不是Direct3D。
第一个原因是,OpenGL的跨平台和可移植的,并且几乎现有的所有3D硬件设备都有对应的OpenGL驱动。
如果读者对游戏感兴趣,那么就可以做一些市场调查。
Windows桌面系统在游戏产业中并没有占据大部分份额。
第二个原因是,OpenGL是一个开放的标准,它能从所有领先的3D硬件提供商的知识和经验中收益。
这些提供商必须合作,使OpenGL对开发人员而言是有吸引力和强大的,毕竟这些开发人员只做了促使人们购买他们硬件的软件。
由于OpenGL是 "到图形硬件的软件接口",让软件提供商参与规范的演变是必要的。
这就是人们可能选择OpenGL而不是Direct3D最后的也是最重要的原因——扩展机制。
扩展机制使硬件供应商不仅能在性能和图像质量方面进行竞争,同时还能在真正的技术革新上一较高下。
硬件提供商可以在硬件上加入新的特征,并且他们愿意的时候通过OpenGL来公开这些新特征。
他们不需要ARB的许可,不需要微软公司的许可,也不需要等待下一班的OpenGL(或Direct3D)发布。
在Direct3D领域没有这些条条框框。
微软公司通过代理来决定将什么加入API中,以及在某种程度上(有些情况应该说是不公平的)影响硬件的体系结构。
最新和最优秀的硬件特性总是能够通过供应商OpenGL驱动程序和相关扩展来轻松获得应用。
例如,当支持DirectX 10的硬件发布时,Windows用户就需要使用Windows Vista才能享受应用了最新DirectX 10特性的游戏。
但是,所有的新功能同时也在OpenGL上通过扩机制发布,并且如果Windows XP用户的游戏使用OpenGL,那么他们马上就鞥呢使用这些新功能,当然同时也能得到最新的硬件和驱动程序。
许多年来,诸如NVIDIA或ATI(现在的AMD)这样的提供商都可以通过OpenGL上编写的演示程序来展示他们最新的硬件革新技术,而他们也确实这样做了。
仅此一点就使OpenGL在最新和最优秀的3D硬件新技术应用方面始终保持稍稍领先Direct3D的优势。
"不鼓励使用" 的功能
十多年来,OpenGL标准通过在每个版本发布时加入新功能来不断演化着。
新功能通常通过扩展过程来进行审核的,在扩展过程中一些特征将作为提供商指定或提供商联合扩展而添加进来,这些特性将被进一步完善并成为ARB扩展,最终将进入核心API规范中。
在这个过程中从来没有哪些功能从OpenGL中被移除。这样就保证了对旧代码百分之百的向下兼容性,而且随着新硬件投入使用,现存应用程序只会运行得更快。
开发人员也能够轻松地逐步升级代码,以在最新的渲染技术或性能强劲的新功能退出时充分利用它们,而不必重写已经完成或正在编写的代码。
然而,实际上这种过程也将到此为止。
随着时间的流逝,GPU和计算机体系结构已经发生了巨大的变革。
15年来一直保持性能上的权衡与工程上的妥协如今已再难适用。
其结果就是,一些OpenGL API变得有些陈旧过时。
很多提供商都首次开始寻求通过移除那些在现代代码中几乎不再适用,或性能远远低下于最新技术的特性和功能来精简OpenGL API的规模。
最终,ARB决定以OpenGL 3.0为突破口,在OpenGL诞生以来首次抛弃一些负担,即过时的OpenGL API。
提供商仍然可以为了一些过时代码而继续支持OpenGL 2.1驱动程序,单定位于OpenGL 3.0或更新版本的最新应用程序应当抛弃旧的API函数和约定。
这在当时看来确实是一个不错的决定。
OpenGL 3.0
ARB 是由图形硬件提供商组成的,而提供商是有客户的,而且必须使这些客户满意。
很多客户(软件开发人员)意识到这种将OpenGL 2.1作为古董束之高阁的模式实际上意味着一件事情,这就是这些驱动程序对于提供商来说将迅速降至低优先级,并且在新的硬件上不会很好地保存或更新,而这些软件开发者将被迫浪费在OpenGL 上价值数百万美元的投资。
最终他们在次序上达成了妥协,即在OpenGL 3.0中,不会真正移除任何功能,而是将这些功能标记为 "不鼓励使用"。
"不鼓励使用"的功能将仍然保留在驱动程序中,但它们是作为一种通告来提供的,通知软件提供商应该停止使用某些OpenGL特性并转向更新的和更现代的工作方式。
据说在OpenGL 3.1中这些特性将被移除,或者说我们认为可能是这样。
OpenGL 3.x
OpenGL 3.1遇到了前所未有的苛刻要求。
这种要求会让任何圆滑的政客都感到确实太过苛刻。
确实,所有"不鼓励使用"功能都从OpenGL核心范围中移除了,但是却引入了一个新的OpenGL扩展GL_ARB_compatibility。
很多正在寻求一个更合理的API的 软件提供商将这个扩展看作仅仅是"加入了所有我们承诺移除但却没有做到的'不鼓励使用'OpenGL特性"。
这意味着一个硬件提供商至少可以选择在OpenGL 3.1驱动程序中不包含任何"不鼓励使用"功能。
但是,这种情况并没有发生。
ARB的成员之一NVIDIA公开声明不会移除任何老旧功能。
在某些种类应用程序(尤其是游戏)的开发人员开始指责这种行为的同时,我们应该客观地说,NVIDIA或者其他硬件提供商还鞥呢怎么做呢?
难道一个硬件提供商应该忽视它的客户而强制推行一种标准,仅仅是因为这家公司认为这样做最符合自己的利益嘛?
我们以前曾经遇到过这种情况。
这样做几乎得不到好的结果,而且没人希望在OpenGL社区中发生这样的丑闻。
OpenGL 3.2在这件事情上做的漂亮了些,它废除了这个扩展,取而代之将OpenGL分成了核心框架和完整框架。
核心框架规范将更加精简,并且不包含任何老旧的"不鼓励使用"功能。
规范的一致性要求具有核心功能,但将兼容框架列为可选项。
事实上,那些"不鼓励使用"OpenGL特性对于如今使用OpenGL的绝大多数开发人员来说要容易理解得多。
这些特性中有很多可能会比最新的方法慢,但是它们更加容易使用,而且非常方便。
工程师们都知道,在简易性、可实现性、可维护性和开发人员熟练程度,当然还有性能之间经常要做出权衡。
性能并不是在所有类型的应用程序开发中都是绝对主要的考虑因素。
兼容框架看起来还要存在相当长的时间。
只有核心
那么,这种情况会给我们带来什么影响呢?
本书的第4版涵盖了OpenGL 2.1的内容,这个版本是允许选择使用着色器的固定管线的经典OpenGL实现。
核心框架则是它的最简形式,"只有着色器"。
这样我们无论做什么,都需要编写一个着色器。
这里没有内建的光照模式,没有方便的矩阵堆栈,没有简单的纹理应用程序,也没有轻松编写代码的立即模式来传输顶点数据。
实际上,一些几何图元也被削减掉了。
难怪众多开发人员并不急于让他们的代码"现代化"了。
让事情变得更糟的是,迄今为止的大多数教程和图书都专注于展示如何从固定管线移植到着色器,似乎这是唯一的方式。
这当然就意味着对于新入行的OpenGL程序员来说掌握OpenGL最简单的方式就是先从固定功能开始,然后再想着色器过渡。
只是这并不是一种促进新的OpenGL核心框架应用的生产方式,也不算本书所采用的方式。
2.2 使用OpenGL
OpenGL是一种过程性而不是描述性的图形API。
事实上,程序员并不需要描述场景和它的外观,而是事先确定一些操作步骤,实现一定的外观或效果。
这些步骤需要调用许多OpenGL命令。
这些命令可以在三维空间中绘制各种图元,例如: 点、直线和三角形等。
另外,OpenGL还支持纹理贴图、混合、透明、动画以及其他许多特殊的效果和功能。
关于这些如何实现的具体内容将在第3章详细介绍。
本章主要关注如何建立并运行OpenGL项目。
OpenGL并不包含任何负责窗口管理、用户交互或文件I/O的函数。
每个宿主环境(例如Mac OSX或Microsoft Windows)都提供了一些函数实现这些功能,并且负责实现一些方法,向OpenGL移交窗口绘制的控制。
我们无法使用类似 "OpenGL文件格式" 这样的东西来表示模型或虚拟环境,因为它们并不存在。
程序员构造这些环境以适合自己的高层需要,然后使用底层的OpenGL命令精心地对它们进行编程。
2.2.1 支持阵容
任何计算机程序都必须包含一些除渲染操作以外的其他东西才能使用。
用户必须通过某种方式来使用键盘、鼠标、游戏手柄或其他一些输入机制来与程序进行互动。
此外,必须打开并保持窗口(大多数但并非全部操作系统中都是如此),找到并载入文件等。
C和C++都是良好的可移植程序设计语言,它们如今在大多数平台上都适用。
但是,在典型的程序中,编程语言要使用API来完成大量工作。
遗憾的是,与操作系统连接意味着与用户或在屏幕中的管理窗口的互动大多数情况下常常是由不可移植的操作系统特定API完成的。
GLUT
首先出现的是AUX,也就是OpenGL辅助函数库。
AUX函数库的目的是帮助人们学习和编写OpenGL程序,而不必为任何平台特定环境的细枝末节而分神,不必顾虑所使用的是UNIX、Windows还是其他平台。
如果使用AUX,我们不是编写"最终"代码,它更像是一个预备阶段,对自己的想法进行测试。
由于缺乏基础的GUI特性,这就限制了这个函数库在创建实用的应用程序方面的应用。
在跨平台的示例程序和演示程序中,AUX渐渐为GLUT函数库所取代。
GLUT代表OpenGL实用工具箱(OpenGL utility toolkit, 不要与标准的GLU——OpenGL utility library, 即OpenGL实用库混淆)。
Mark Kilgard在SGI时编写了GLUT,把它作为AUX函数库的一个功能更强的替代品,并添加了一些GUI功能,至少使示例程序在 X Window下显得更为实用。
它的改进包括实用了弹出式菜单、增加了对其他窗口的管理,甚至提供了对操纵杆的支持。
GLUT并不是一个公众领域的产品,但它是免费的,并且可以自由地进行重新发布。
GLUT在绝大多数UNIX系统(包括Linux)中都得到了支持,并且得到了Mac OSX的本地支持,Apple对这个函数库进行了维护和扩展。
在Windows中,GLUT的开发已经中断。
由于GLUT最初并不是作为一种开源代码的软件,因此一种新的GLUT实现freeglut已经崛起并取代了它的位置。
在本书中,所有基于GLUT的Windows示例程序都利用了freeglut函数库。
读者也可以通过本书的网站下载这个函数库。
在绝大多数情况下,本书使用GLUT作为编程框架。
这出于两个目的。
首先,它可以使本书面向更广的读者。
只要稍下功夫,有经验的Windows、Linux或Mac程序员应该很容易在编程环境中设置GLUT,并且顺序地创建本书的绝大多数示例程序。
第二个目的就是使用GLUT可以使读者不必了解任何特定平台的基本GUI编程。
尽管我们解释了一些基本的GUI概念,但本书并不是一本讲诉GUI编程的书籍,而是专门讲诉OpenGL的。
把OpenGL API的范围限制在GLUT,Windows/Mac/Linux新手也更容易上手。
商业应用程序的所有功能不可能全部包含在3D绘图代码之中。
虽然GLUT确实包含一些有限的GUI功能,但是非常简单和精简,就像GUI的工具包一样,因此我们不能依赖GLUT函数库来完成应用程序的所有任务。
然而,GLUT函数具有非常优秀的学习和演示功能,并且隐藏了像窗口创建和OpenGL环境初始化等平台特定的细节。
即使是经验丰富的程序员,把3D图形集成到完整的应用程序之前,使用GLUT函数来整理3D图形代码也是非常方便的。
GLEW
正如前面所提到的,OpenGL API主要通过扩展机制来发展。
这种扩展机制能够用来获得指向任何加入OpenGL 1.0之后任何版本核心的OpenGL函数的函数指针。
有一个实现OpenGL 3.3 API完全存取的简单方法,就是使用一个自动初始化所有新函数指针并包含所需类型定义、常量和枚举的扩展加载库。
不止一种这样的扩展加载库可供选择,其中一种维护最好的开源库就是GLEW。
通过驱动程序使用这种库依赖初始化全部可用的OpenGL功能并不太容易。
我们需要在项目中添加一个单独的C源文件及头文件,并且在程序启动时调用一个单独的初始化函数。
稍后开始编写我们的第一个OpenGL程序时将讨论相关细节。
为了使事情更简单,GLEW将预先封装在了GLTools库中。
实际上,GLTools库就是基于GLEW库的。
GLTools
每一个工匠都有一个工具箱,里面装满了自己喜爱的工具,程序员也是如此。
有一些有用并且可重用的函数,所有程序员在编写几乎所有OpenGL时都要用到它们。
GLTools是在本书第3版出现的。
随着时间的流逝,这个库已经逐渐发展起来,并提供许多快捷方式和便捷的工具,就像过去的OpenGL应用库(GLU)那样。
GLTools包含一个用于操作矩阵和向量的3D数学库,并依赖GLEW获得OpenGL 3.3中用来产生和渲染一些简单3D对象的函数,以及对视觉平截头体、相机类和变换矩阵进行管理的函数的充分支持。
2.2.2 OpenGL API 特性
OpenGL是由一些充满智慧的人设计的,他们拥有丰富的图形程序设计API设计经验。
他们在函数命名和变量声明方法上面采用了一些标准规则。
API简单清晰,便于提供商进行扩展,并且便于程序员记忆。
OpenGL试图尽可能地避免策略。
这里的策略是指设计者做出的关于程序员如何使用API的假设。
这使OpenGL能够保持灵活、强大和快速。
我们只要灵活地运用API和着色语言,就可以真正地发明一种全新的方法来渲染一个特效或场景。
这种理念为OpenGL的长寿和精华做出了贡献。
即使如此,随着时间的推移,硬件性能出乎意料的发展和开发人员与硬件提供商的创造力在OpenGL经历了这些年的发展后还是给它带来了负面的影响。
尽管如此,OpenGL的基础API仍然显示出对新兴的不可预测特征的适应能力。
只作很少甚至不作改动就能编译十年前源码的能力对于众多应用程序开发人员来说是最重要的优势,而OpenGL多年来一直坚持做到加入新特性的同时尽量少地与旧代码发生冲突。
现在,我们有了更加简洁和现代化的OpenGL,可以重新开始这个过程了。
数据类型
为了使OpenGL代码更易从一个平台移植到另一个平台,OpenGL定义了数据类型。
这些数据类型可以映射到所有平台的特定最小格式。
各种编译器和环境都有自己的规则来定义各种变量类型的大小的内存布局,因此通过使用OpenGL定义的变量类型,可以使代码避免因为类型在变量表示上不一致所带来的的影响。
表2.2列出了OpenGL数据类型和最小位宽。
OpenGL数据类型 | 最小位宽 | 描 述 |
GLboolean | 1 | 布尔值,真或假 |
GLbyte | 8 | 有符号8位整数 |
GLubyte | 8 | 无符号8位整数 |
GLchar | 8 | 字符串 |
GLshort | 16 | 有符号16位整数 |
GLushort | 16 | 无符号16位整数 |
GLhalf | 16 | 半精度浮点数 |
GLint | 32 | 有符号32位整数 |
GLuint | 32 | 无符号32位整数 |
GLsizei | 32 | 无符号32位整数 |
GLenum | 32 | 无符号32位整数 |
GLfloat | 32 | 32位浮点数 |
GLclampf | 32 | [0, 1]范围内的32位浮点数 |
GLbitfield | 32 | 32位 |
GLdouble | 64 | 64位双精度浮点数 |
GLclampd | 64 | [0, 1]范围内的64位双精度浮点数 |
GLint64 | 64 | 有符号64位整数 |
GLuint64 | 64 | 无符号64位整数 |
GLsizeiptr | 本地指针大小 | 无符号整数 |
GLintptr | 本地指针大小 | 有符号整数 |
GLsync | 本地指针大小 | 同步对象句柄 |
所有的数据类型都以GL开头,表示OpenGL。
函数后面是他们的最小位宽和相关描述,请注意它们并没有必要直接与C数据类型直接对应。
OpenGL规范要求这些数据类型所需要的最小存储空间参见表2.2。
但是,虽然某些数值超出表中之处的范围是可能的,但只有大小在指定范围内的数值对OpenGL来说才是有意义的。
请注意,有些类型前面还有个字母u,表示这是一种无符号数据类型。
例如,ubyte表示无符号byte类型。在某些应用中,还有一些更具描述性的名称,就像size表示一个数值的长度或深度那样。
例如,GLsizei是一个OpenGL变量类型,表示整数形式的size参数。
名称Clamp则是一种提示,表示这个值的范围将"截取"在 0.0 ~ 1.0的范围内。
GLboolean变量表示真假条件。
GLenum表示枚举变量。
GLbitfield表示那些包含二进制位段的变量等。
OpenGL并没有对指针和数组作特殊的考虑。
我们可以像下面这样声明一个包含10个GLshort变量的数组。
GLshort shorts[10];
下面这行代码则声明了一个长度为10的指向GLdouble类型变量的指针数组。
GLdouble *doubles[10];
2.2.3 OpenGL 错误
在任何项目中,我们都希望编写出表现良好的程序,能够友好地相应用户,并且有一定程度的灵活性。
使用OpenGL的图形程序也不例外,而且如果我们希望程序能够流畅运行,就需要考虑程序可能出现的错误以及一些出乎意料的情况。
OpenGL提供了一种有用的机制,可以在代码中执行一种偶然健全性检查。
这个功能是非常重要的。
例如,单纯从代码的角度而言,要分辨程序的输出到底是"空间站自由度"还是"空间站融化的蜡笔"几乎是不可能的!
OpenGL在内部保留了一组错误标志(共4个),其中每个标志代表一种不同类型的错误。
当一个错误发生时,与这个错误相对应的标志就会被设置。
为了观察哪些标志被设置,可以调用 glGetError 函数。
GLenum glGetError(void);
glGetError 函数返回表2.3所列出的其中一个值。
如果被设置的标志不止一个,glGetError 仍然只返回一个唯一的值。
当 glGetError 函数被调用时,这个值随后被清除,然后在 glGetError 在此被调用时将返回一个错误标志或GL_NO_ERROR。
通常情况下,我们需要在一个循环中调用 glGetError 函数,持续检查错误标志,直到返回值是GL_NO_ERROR为止。
错误代码 | 描 述 |
GL_INVALID_ENUM | 枚举参数超出范围 |
GL_INVALID_VALUE | 数值参数超出范围 |
GL_INVALID_OPERATION | 在当前的状态中操作非法 |
GL_OUT_OF_MEMORY | 没有足够的内存来执行这条命令 |
GL_NO_ERROR | 没有错误出现 |
如果一个错误是由于对OpenGL的非法调用所致,那么这条命令或函数调用将会被忽略。
对此,我们可能会稍微感到安心,此时,唯一可能造成麻烦的是那些接受指向内存的指针作为参数的函数(如果指针无效,可能导致程序崩溃)。
2.2.4 确认版本
如前面所述,有时候我们希望利用一个特定实现中的一些已知行为。
如果我们确实知道程序将运行于一个特定提供商所生产的图形卡纸上,就想依赖这个生产商特有的一些性能特征来强化程序。
我们可能还希望限制这个特定厂商所提供的驱动程序的最低版本。
为此,需要查询OpenGL的渲染引擎(OpenGL驱动程序)的生产商和版本号。
GL函数库可以通过调用glGetString来返回与它们的版本号和生产商有关的特定信息。
const GLubyte *glGetString(GLenum name);
这个函数返回一个静态的字符串,描述GL函数库中所请求的信息。
附录C中列出了glGetString条目下所有合法的参数值,以及它们所代表的的GL函数库的相关信息。
2.2.5 使用glHint获取线索
俗话说,给猫剥皮的方法不止一种。
在3D图形算法中,情况也是如此。
例如,为了追求高性能,我们常常需要做一些权衡。
或者,如果视觉逼真度是最重要的因素,那么性能就会退居其次。
一种OpenGL实现常常包含两种方法来执行一个特定的任务.
一种是快速的方法,在性能上稍作妥协.
另一种是慢速的方法,着重于改进视觉质量。
glHint函数允许我们指定偏重于视觉质量还是速度,以适应各种不同类型的操作需求。
这个函数定义如下所示:
void glHint(GLenum target, GLenum mode);
我们可以在target参数中指定希望进行修改的行为类型。
附录C中的glHint条目下列出了这些值,其中还包括关于纹理压缩质量和抗锯齿准确性等的提示。
mode参数告诉OpenGL我们最为关心的,例如更快的渲染速度还是最好的输出质量,
或者我们可能并不关心这些(只有在这种情况下我们才会使用默认行为)。
但是,我们还是应该小心在意,因为所有的OpenGL实现都不要求必须在glHint函数的调用上保持一致。
在OpenGL中,这是唯一一个行为完全依赖生产商的函数。
2.2.6 OpenGL状态机
绘制3D图形是一项复杂的任务。
在接下来的章节,我们将讨论许多OpenGL函数。
对于一个特定的几何图形,有许多因素可能会影响它的绘制。
对象是不是与背景混合?
要不要进行正面或背面剔除?
当前限制的是什么纹理?
这样的问题数不胜数。
我们把这类变量的集合称为管线。
状态机是一个抽象的模型,表示一组状态变量的集合。
每个状态变量可以有各种不同的值,或者只能可以打开或关闭等。
当我们在OpenGL中进行绘图时,如果每次都要指定所有这些变量显然有点不切实际。
反之,OpenGL使用了一种状态模型(或称状态机)来追踪所有OpenGL状态变量。
当一个状态值被设置之后,他就一直保持这个状态,知道其他函数对它进行修改为止。
许多状态只能简单地打开或关闭。
例如,深度测试(参见第3章)就是打开和关闭。
打开深度测试的几何图形将会被检查以确保在进行渲染之前总会在任何位于它后面的对象前方。
在深度测试关闭后进行的集合图形绘制(例如2D覆盖)则会不进行深度比较的情况下进行绘制 。
(这部分的内容是关于深度测试的,此处为书本为此做的简单介绍,不懂的详看第3章即可)
为了打开这些类型的状态变量,可以使用下面这个OpenGL函数。
void glEnable(GLenum capability);
我们可以使用下面这个对应的函数,把这些变量的状态设置为关闭。
void glDisable(GLenum capability);
以深度测试为例,可以使用下面这个函数调用打开深度测试。
glEnable(GL_DEPTH_TEST);
也可以使用下面这个函数调用关闭深度测试。
glDisable(GL_DEPTH_TEST);
如果希望对一个状态变量进行查询,以判断它是否已被打开,OpenGL还提供了一种方便的机制。
GLboolean glIsEnabled(GLenum capability);
但是,并不是所有的状态变量都只是简单地打开或关闭。
许多OpenGL函数专门用于设置变量的值,以后这些变量一直保持被设置时的值,知道再次被修改。
我们在任何时候都可以查询这些变量的值。
OpenGL提供了一组查询函数,可以查询布尔值、整数、单精度浮点数和双精度浮点数型变量的值。
这4个函数的原型如下所示:
void glGetBooleanv(GLenum pname, GLboolean *params); // 布尔值
void glGetDoublev(GLenum pname, GLdouble *params); // 双精度浮点型
void glGetFloatv(GLenum pname, GLfloat *params); // 单精度浮点型
void glGetIntegerv(GLenum pname, GLint *params); // 整形
PS: 简单说一下这几个函数的命名吧(懂的此处忽略吧~)
glGet + 类型 + v(variable(变量)) = glGet + Boolean + v = glGetBooleanv
每个函数都会返回单个值,或者返回一个数组,把一些值存储到我们执行的地址中。
附录C的参考部分列出了各种不同的参数(数量非常多)。
现在,读者可能还无法理解其中的大多数参数,但是随着对本书学习的深入,读者将会逐渐开始欣赏OpenGL状态机的简洁和强大。
2.3 建立Windows项目
此处我当大家都会了,就忽略了哈~
(如果不会的话,评论留言吧,因为本篇文章已经很长了,在加上这个建立的介绍可能就更长了,有需要再单独开一篇介绍吧)
2.4 建立 Mac OS X项目
.......
2.5 第一个三角形(终于开始OpenGL的编写了)
现在我们已经打好了基础,终于开始编写代码了!
我们的第一个示例程序仅仅是在蓝色的背景上绘制一个红色的三角形。
这咋看起来似乎没有什么挑战性,但是它实践了所有必要的步骤,并创建了一个完整的颜色框架供本书以后使用。
在创建过程中,我们能够学习GLUT,并使用一个GLTools帮助程序和类。
我们的三角形程序如图2.17所示,而程序清单2.1则完整地列出了我们的第一个程序。
接下来我们将一行一行地讨论它。
(图2.17 [书])
(图2.17 [VS2017])
程序清单2.1 简单绘制一个三角形
(代码略..., 下面会进行拆分讲解,具体代码可以看本章末尾)
2.5.1 要包含什么
在编写任何C++(或者只是C)程序之前,都要先将要用到的函数和类定义的头文件包含起来。
为了达到目的,最低限度也要包含如下头文件。
#include <GL/glew.h> // OpenGL toolkit
#include "GLTools.h"
#include "GLShaderManager.h"
#ifdef __APPLE__
#include "glut/glut.h" // OS X version of GLUT
#else
#define FREEGLUT_STATIC
#include "GL/glut.h" // Windows FreeGlut equivalent
#endif
GLTools.h头文件中包含了大部分GLTools中类似C语言的独立函数,而每个GLTools的C++类则有自己的头文件。
GLShaderManager.h移入了GLTools着色器管理器(Shader Manager)类。
没有着色器,我们就补鞥呢在OpenGL(核心框架)中进行着色。
着色器管理器不仅允许我们创建并管理着色器,还提供了一组"存储着色器"(Stock Shader),它们能够进行一些初步和基础的渲染操作。
在第3章中我们将详细讨论这部分内容。
根据应用程序是否是在Mac上创建的,GLUT将采取不同的处理方式。
在Windows和Linux上,我们使用freeglut的静态库版本,这就需要在它前面添加FREEGLUT_STATIC处理宏。
2.5.2 启动GLUT
下面我们直接跳到程序清单的最后一个函数,即所有C程序的入口点,这里才是程序处理实际开始的地方。
// Main entry point for GLUT based programs
int main(int argc, char* argv[])
控制台模式的C语言和C++程序总是从"main"函数开始处理。
对于有经验的Windows迷来说,在本例中找不到WinMain函数是十分奇怪的。
这是因为哦们是以一个控制台模式的应用程序开始的,所以没必要从创建窗口和消息循环开始。
在Win32中,我们可以从控制台应用程序简历图形窗口,就像我们可以从GUI应用程序简历控制台窗口一样。
GLUT库隐藏了这些细节(请记住,GLUT库就是设计用来隐藏这些平台相关细节的)。
(从而更方便我们学习OpenGL,而不必在去学习诸如Win32等等的知识)
gltSetWorkingDirectory(argv[0]);
GLTools函数glSetWorkingDirectory用来设置当前工作目录。
实际上在Windows中是不必要的,因为工作目录默认就是程序的可执行程序相同的目录。
但是在Mac OS X中,这个程序将当前工作文件夹改为应用捆绑包(Application Bundle)中的/Resource文件夹。
GLUT的优先设定自动进行了这种设置,但是这种方法更加安全,也总是奏效的,即使其他程序改变了这项设置时也是如此。
这在我们以后想要载入纹理文件或模型数据时会派上用场。
接下来,我们将进行一些基于GLUT的标准设置。
glutInit(&argc, argv);
首先要调用glutinit函数,这个函数只是传输命令行参数并初始化GLUT库。
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
然后我们必须告诉GLUT库,在创建窗口时要使用哪种类型的显示模式。
这里的标志告诉它要使用双缓冲窗口(GLUT_DOUBLE)和RGBA颜色模式(GLUT_RGBA)。
双缓冲窗口是指绘制图形命令实际上是在离屏缓冲区执行的,然后迅速切换成窗口视图。
这种方式经常用来生成动画效果,本章稍后将做演示。
GLUT_DEPTH位标志将一个深度缓冲区分配为显示的一部分,因此我们能够执行深度测试。
同样GLUT_STENCIL确保我们也会有一个可用的模板缓冲区。
深度和模板测试后面都会讲解。
glutInitWindowSize(800, 600); // 宽高为 800 x 600 的窗口
接下来,我们要告诉GLUT窗口的大小
glutCreateWindow("Triangle");
并继续创建以 "Triangle" 为标题窗口。
GLUT内部运行一个本地消息循环,拦截适当的消息,然后调用我们为不同时间注册的回调函数。
与使用真正的系统特定框架相比有一定局限性,但是大大简化了组织并运行一个程序的过程,并且支持一个演示框架的最低限度的事件。
在这里,我们必须为窗口改变大小而设置一个回调函数,以便能够设置视点,还要注册一个函数以包含OpenGL渲染代码。
glutReshapeFunc(ChangeSize);
glutDisplayFunc(RenderScene);
ChangeSize和RenderScene函数很快就会讲到,但是在开始运行主消息循环之前,还要解决两件事情。
第一件事情就是初始化GLEW库。
重新调用GLEW库初始化OpenGL驱动程序中所有丢失的入口点,以确保OpenGL API对我们来说完全可用。
调用glewInit一次就能完成这一步,在试图做任何渲染之前,还要检查确定驱动程序的初始化过程中没有出现任何问题。
GLenum err = glewInit();
if (GLEW_OK != err) {
fprintf(stderr, "GLEW ERROR:%s\n", glewGetErrorString(err));
return 1;
}
最后一项准备工作就是调用SetupRC。
SetupRC();
实际上这个函数对GLUT没有什么影响,但是在实际开始渲染之前,我们在这里进行任何OpenGL初始化都非常方便。
这里的RC代表渲染环境(Render Context),这是一个运行中的OpenGL状态的句柄。
在任何OpenGL函数起作用之前必须创建一个渲染环境,而GLUT在我们第一次创建窗口时就完成了这项工作。
关于特定操作的章节(第13章到第16章)中会研究这方面的更多细节。
纵观全书,我们将在这里进行预加载纹理,简历几何图形,渲染器等工作。
最后我们可以开始主消息循环并结束main函数了。
glutMainLoop();
glutMainLoop函数被调用之后,在主窗口被关闭之前都不会返回,并且一个应用程序中只需要调用一次。
这个函数负责处理所有操作系统特定的消息、按键动作等,知道我们关闭程序为止。它还能确保我们注册的这些回调函数被正确地调用。
2.5.3 坐标系基础
在早期的所有窗口化环境中,用户可以在任何时候改变窗口的大小和维度。
甚至在编写一个总是运行在全屏模式下的游戏时,窗口仍然认为至少改变一次窗口大小——就是在窗口创建时。
在进行这些改变时,窗口通常会根据新的维度相应重绘它的内容。
有时候,我们可能想要在一个小的窗口中截取绘图内容,或者在原始的大窗口中显示完整的绘图内容。
为了达到目的,我们常常希望缩放绘图内容来适应窗口,不论绘图内容和窗口的大小如何。
这样一个很小的窗口可以显示一个完整但是很小的绘图内容,而一个很大的窗口也可以显示相似但是更大的绘图内容。
在第一章,我们讨论了视口和视景体是如何影响2D和4D绘图内容在计算机屏幕上2D窗口上的坐标范围和缩放的。
现在,我们来讨论OpenGL中视口和裁剪区域坐标(Clipping Volume Coordinate)。
在某种程度上,设置坐标系是绘制对象将它们显示到我们希望的屏幕位置的先决条件!
尽管我们所绘制的图形是一个2D的平面三角形,但它实际是一个3D坐标空间中绘制的。
在本章中,我们将使用默认的笛卡尔坐标系统,这个坐标系统在x、y和z方向上从 -1 到 +1 延伸。
x是坐标系的横坐标,y是纵坐标,而z轴正方向从屏幕向外指向使用者。
坐标(0, 0, 0)则位于屏幕的正中。
在第4章,我们将更细致地讨论关于建立替代坐标系的问题。
为了达到目的,我们在z=0的xy平面上绘制三角形。
我们视角是从z轴的正半轴看去,所看到的是z=0情况下的三角形(如果读者对这方面的内容感到困惑,可以回顾第1章的相关资料)
图2.18所示是基本笛卡尔坐标系的外观。
许多绘图和图形库都是用窗口坐标(像素)来完成绘制命令。
使用实数浮点(这看上去有点随意)坐标系统进行渲染,这常常令许多新手非常不习惯。
不过,在创建了几个程序之后,读者很快就会对此习以为常。
(图2.18)
定义视口
由于在不同环境下窗口的大小改变的检测和处理方式也不同,GLUT库为此专门提供了glutReshapeFunc函数,这个函数注册了一个回调,供GLUT库在窗口维度改变时调用。
我们传递到glutReshapeFunc的函数原型如下:
void ChangeSize(int w, int h)
我们选择ChangeSize作为这个函数的描述名称,并且在以后的示例中也会使用这个名称。
void ChangeSize(int w, int h)
{
glViewport(0, 0, w, h);
}
ChangeSize函数在窗口大小改变时接受新的宽度和高度。
我们可以使用这个信息,在OpenGL函数glViewort的帮助下修改从目的坐标系到屏幕坐标系上的映射。
要理解视口分辨率,让我们更仔细地观察ChangeSize函数中调用带有窗口的新宽度和高度的glViewport函数的部分。
glViewport函数定义如下:
void glViewport (GLint x, GLint y, GLsizei width, GLsizei height);
其中x参数和y参数代表窗口中视口的左下角坐标,而宽度和高度参数是用像素表示的。
通常x和y都为0,但是我们可以使用视口在窗口中的不同区域渲染多个图形。
视口以实际屏幕坐标定义了窗口中的区域,OpenGL可以在这个区域中进行绘图(如图2.19所示)的裁剪区域被映射到新的窗口。
如果指定了一个比窗口坐标更小的视口,渲染区域就会缩小,如图2.19所示。
(图2.19)
从笛卡尔坐标系到像素
在开始将几何图形光栅化(实际绘制)到屏幕上时,OpenGL负责笛卡尔坐标系和窗口像素间的映射。
我们要牢记一点,就是改变视口并不会改变基础坐标系。
由于我们采用的是默认的从 -1 到 +1 的映射,为三角形改变窗口大小会产生一些有趣的结果,如图2.20所示。
(图2.20)
在图2.20左侧图中,我们可以看到 +1 到 -1 的范围在垂直方向是如何比水平方向延伸得更多的,而在右侧图中,则可以看到相反的效果。
我们要先了解更多内容,然后才能考虑如何改变坐标系以影响窗口大小的改变,就像前面说过的,我们会在第4章完整地做完这项工作。
2.5.4 完成设置
在开始main函数中的GLUT主循环之前,我们先调用SetupRC函数。
这时我们要为程序做一些一次性的设置。
首先要做的就是通过以下调用来设置背景颜色。
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
这个函数设置用来进行窗口清除的颜色,它的函数原型如下所示。
void glClearColor (GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);
在大多数OpenGL实现下,GLclampf都被定义为一个浮点数。
每个参数都包含最终颜色所要求的这种颜色分量的权值。
这个函数不会立即清除背景,而是设置在以后颜色缓冲区被清除(可能是重复的)时使用的颜色
RGB颜色空间
在OpenGL中,某种颜色是由红、绿、蓝和Alpha(用于表示透明度)分量混合而成的。
每种分量值的范围从0.0到1.0。
这类似在Windows中使用RGB宏创建COLORREF值的方式。
不同的是,在Windows中,COLORREF值的每种颜色成分范围在0至255之间,总共可以产生256 x 256 x 256(超过1600万)种颜色。
在OpenGL中,每种成分的值可以是0至1之间任何有效的浮点数,因此理论上可以产生的颜色数量是无限的。
从现实的角度将,在绝大多数设备中,颜色的输出限制在24为(1600万种以上颜色)。
很自然OpenGL接受这个颜色值,并在内部把它转换为能够与可用的视频硬件准确匹配的最接近颜色。
表2.2列出了一些常见的颜色以及它们的分量值。
我们可以在任何与颜色相关的OpenGL函数中使用这些值。
组合颜色 | 红色分量 | 绿色分量 | 蓝色分量 |
Black (黑) | 0.0 | 0.0 | 0.0 |
Red (红) | 1.0 | 0.0 | 0.0 |
Green (绿) | 0.0 | 1.0 | 0.0 |
Yellow (黄) | 1.0 | 1.0 | 0.0 |
Blue (蓝) | 0.0 | 0.0 | 1.0 |
Magenta (洋红) | 1.0 | 0.0 | 1.0 |
Cyan (青) | 0.0 | 1.0 | 1.0 |
Dark gray (深灰) | 0.25 | 0.25 | 0.25 |
Light gray (浅灰) | 0.75 | 0.75 | 0.75 |
Brown (褐) | 0.60 | 0.40 | 0.12 |
Pumpkin orange (南瓜橙) | 0.98 | 0.625 | 0.12 |
Pastel pink (粉红) | 0.98 | 0.04 | 0.70 |
Bamey purple (巴尼紫) | 0.60 | 0.40 | 0.70 |
White (白) | 1.0 | 1.0 | 1.0 |
glClearColor的最后一个参数是alpha分量,它用来进行混合,并且可以产生一些特殊的效果。
例如,透明。
透明是指一个物体运行光线穿过它。
假定我们希望创建一块染成红色的玻璃,并且它的后面正好有一束蓝色的光。
这道蓝光就会影响这块玻璃上的红色(蓝+红=紫)。
我们可以用alpha成分值生成一种半透明的红色,使它看上去像是一块玻璃,它后面的物体也能够显示。
这种类型的效果并不是仅仅靠使用alpha值就行了。
在第3张,我们将详细讨论这个话题。
在这之前,可以一直把alpha值设置为1。
存储着色器
没有着色器,在OpenGL核心框架中就无法进行任何渲染。
在第6章"跳出 '盒子':非存储着色器"中,我们将讨论如何编写着色器,以及如何编译和链接它们从而使它们可用。
在那之前,我们先使用一些简单存储着色器,它们可以用着色器管理器进行管理。
我们要在源文件的开头部分声明一个着色器管理器的实例,如下所示:
GLShaderManager shaderManager;
我们也可以在第3章来熟悉这些着色器,并学习如何使用它们。
但是着色器管理器需要编译和链接它自己的着色器,所以我们必须在OpenGL初始化时调用InitializeStockShaders方法。
shaderManager.InitializeStockShaders();
指定顶点
接下来我们要做的是设置三角形。
在OpenGL中三角形是一种"图元"类型,是一种基本的3D绘图元素。
在第3章,我们会非常详细地讨论在OpenGL将会用到的所有7种图元。
但在这里,我们只要了解一个三角形图元就是空间中的一系列组成一个三角形的顶点或点就可以了。
我们通过将这些顶点放进一个单精度浮点数组来指定它们。
这个数组命名为vVerts,其中包含所有3个顶点的x、y、z笛卡尔坐标系。
请注意我们将所有3个点的z坐标都设为0.
GLfloat vVerts[] = {
-0.5f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f
};
在本书中有两章会讲解关于提交一个批次的顶点用于渲染的内容,即第3章和第12章,其中第12章将涉及更多的底层细节。
一个简单的GLTool封装了(Wrapper Class)会将三角形顶点批次进行封装,而我们则在源文件顶部附近声明一个这个GLBatch类的实例。
GLBatch triangleBatch;
在我们的设置函数中,下列代码建立了一个三角形的批次,仅包含3个顶点。
在第3章我们将对此做进一步的讨论。
triangleBatch.Begin(GL_TRIANGLES, 3);
triangleBatch.CopyVertexData3f(vVerts);
triangleBatch.End();
2.5.5 言归正传
最后,我们终于可以真正开始渲染了!
前面我们将清除颜色设为蓝色,现在我们需要执行一个函数真正进行清除。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glClear函数清除一个或一组特定的缓冲区。
缓冲区是一块存储图像信息的存储空间。
红色、绿色、蓝色和alpha分量通常一起作为颜色缓冲区或像素缓冲区引用。
在OpenGL中有不止一种缓冲区(颜色缓冲区、深度缓冲区和模板缓冲区)供使用。
在本书后面的内容中将详细介绍这些缓冲区。
在前面的示例中,我们使用按位或(Bitwise OR)操作来同时清除所有这3种缓冲区。
在接下来的几章中,我们真正需要理解的是,颜色缓冲区是显示图像在内部存储的地方,以及通过glClear清除缓冲区会将屏幕上最后绘制的内容剔除。
我们还会看到术语"帧缓冲区"(Framebuffer),指的是所有这些缓冲区一起串联工作。
下面的3行代码将真正执行操作,这又是整个第3章的主要课题!
我们设置一组浮点数来表示红色(其alpha值设为1.0),并将它传递到存储着色器,即GLT_SHADER_IDENTITY着色器。
这个着色器只是使用指定颜色以默认笛卡尔坐标系在屏幕渲染几何图形。
//设置一组浮点数来表示红色
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
//传递到存储着色器
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);
//将几何图形提交到着色器
triangleBatch.Draw();
GLBatch的Draw方法指示将集合图形提交到着色器,然后.......啊哈!——红色三角形.........哦,差不多是。
还有最后一个细节。
当设置OpenGL窗口时,我们指定要一个双缓存区的渲染环境。
这就意味着将在后台缓冲区进行渲染,然后在结束时交换到前台。
这种形式能够防止观察者看到可能伴随着动画帧与动画帧之前闪烁的渲染过程。
缓冲区交换将以平台特定的方式进行,但是GLUT有一个单独的函数调用可以完成这项工作。
//指定一个双缓冲区渲染环境,在后台缓冲区进行渲染,然后再结束时交换到前台
glutSwapBuffers();
现在我们可以鞠躬谢幕了。我们已经用OpenGL渲染了第一个三角形。
2.6 加一(亿)点儿活力!
现在我们已经了解如何使GLUT完成一个他U型演示框架所能完成的最重要工作,也就是在屏幕上渲染图形了。
我们可以再加入一点小功能,使它能让用户对渲染进行一些互动,例如,通过按箭头↑←↓→键进行移动图形。
一点动画效果就能使图形演示变得有活力。
示例程序的 "Move" 就能做到这一点。
它在窗口的正中绘制了一个正方形(实际上我们使用了另一个图元,这次是GL_TRIANGLE_FUN)。
在按箭头键时,正方形将上下或左右移动。
分别用向上箭头和向下箭头中的哪一个来实现这两种运动则由读者决定。
2.6.1 特殊按键
GLUT还提供了另一个回调函数,即 glutSpecialFunc。
它注册了一个能够在按一个特殊按键时被调用的函数。
在GLUT的语法中,特殊按键是指功能键或者方向键(↑←↓→,Page up/down等)中的一个。
在主函数中加入下面的代码行,来注册SpecialKeys回调函数。
glutSpecialFunc(SpecialKeys);
它在按键时接受一个相应的按键编码,以及在使用鼠标时光标的x和y坐标位置(像素形式)。
在 "Move" 示例程序中,我们将顶点存储在一个全局(对于这个模型来说)数组中,这样我们就能够在按键时相应修改正方形的位置了。
程序清单2.2展示了SpecialKeys函数的完整代码,这里我们还进行了碰撞检测,这样正方形就不会移出窗口范围了。
请注意,我们可以轻松地更新批次位置,只需复制新的顶点数据即可。
squareBatch.CopyVertexData3f(vVerts);
程序清单2.2 用箭头键操纵正方形在屏幕范围内移动
// Respond to arrow keys by moving the camera frame of reference
void SpecialKeys(int key, int x, int y)
{
GLfloat stepSize = 0.025f;
GLfloat blockX = vVerts[0]; // Upper left X
GLfloat blockY = vVerts[7]; // Upper left Y
if(key == GLUT_KEY_UP)
blockY += stepSize;
if(key == GLUT_KEY_DOWN)
blockY -= stepSize;
if(key == GLUT_KEY_LEFT)
blockX -= stepSize;
if(key == GLUT_KEY_RIGHT)
blockX += stepSize;
// Collision detection
if(blockX < -1.0f) blockX = -1.0f;
if(blockX > (1.0f - blockSize * 2)) blockX = 1.0f - blockSize * 2;
if(blockY < -1.0f + blockSize * 2) blockY = -1.0f + blockSize * 2;
if(blockY > 1.0f) blockY = 1.0f;
// Recalculate vertex positions
vVerts[0] = blockX;
vVerts[1] = blockY - blockSize*2;
vVerts[3] = blockX + blockSize*2;
vVerts[4] = blockY - blockSize*2;
vVerts[6] = blockX + blockSize*2;
vVerts[7] = blockY;
vVerts[9] = blockX;
vVerts[10] = blockY;
squareBatch.CopyVertexData3f(vVerts);
glutPostRedisplay();
}
2.6.2 刷新显示
SpecialKeys函数的最后一行代码用来告诉GLUT需要更新窗口内容。
glutPostRedisplay();
默认情况下,在窗口创建、改变大小或者需要重绘时,GLUT通过调用RenderScene函数来更新窗口。
只要窗口发生最小化、回复、最大化、覆盖或重新显示灯变化,就会发生更新。
我们可以人工调用 glutPostRedisplay 来告诉GLUT发生了某些改变,应该对场景进行渲染了。
不过,用后面将要介绍的方法来完成这项工作尤为方便。
2.6.3 简单的动画片
在 "Move" 示例中,当我们按箭头时,更新了集合图形位置,然后调用 glutPostRedisplay 函数激活屏幕刷新动作。
如果我们将 glutPostRedisplay 函数调用在 RenderScene函数末尾将会发生什么呢?
如果读者想到的是得到一个持续自动刷新的程序,那么恭喜,答对了。
但是不要担心,这并不是一个无线循环。
重绘消息实际是一条传递到一个内部消息循环中的消息,在屏幕刷新的间隔中,也会发生其他窗口事件。
这就是说,我们仍然可以检测按键动作、鼠标移动、改变窗口大小和程序结束等动作。
程序清单2.3显示了我们在 "Move" 示例程序中的 RenderScene 函数经过修改后的代码。
///
// Called to draw scene
void RenderScene(void)
{
// Clear the window with current clearing color
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);
squareBatch.Draw();
// Flush drawing commands
glutSwapBuffers();
BounceFunction();
glutPostRedisplay(); // Redraw
}
2.7 总结
在本章,我们讨论了许多基础知识。我们介绍了OpenGL,并简单地介绍了它的历史。
另外,还介绍了OpenGL工具箱(GLUT),并讨论了使用OpenGL编写程序的基础知识。
利用GLUT,我们还展示了创建窗口并使用OpenGL命令在窗口中进行绘图的最简便方法,并学习了如何使用GLUT函数库创建一个可以改变大小的窗口,并创建了一个简单的动画示例程序。
此外,我们还介绍了使用OpenGL进行绘图的过程——合成和选择颜色、清除屏幕、绘制三角形和矩形并在窗口帧中设置视口。
我们还讨论了各种OpenGL数据类型以及生成OpenGL程序所需要的头文件,并引导读者分别在Visual Studio(Windows系统)和Xcode(Mac OS X)系统中创建项目(详细看书本,本章省略了这部分)。
OpenGL状态机奠定了我们以后用到的几乎所有操作的基础。
扩展机制使我们能够访问自己所使用的硬件驱动程序支持的所有OpenGL特性,而不必考虑自己所使用的是什么开发工具。
我们还学习了如何检查OpenGL错误,确保程序中并未出现任何非法的状态改变或渲染命令。
稍微再熟悉一些代码以后,读者就可以掌握进一步学习所需的一些知识了。
本书的所有示例源码将放到Github中,详细地址:Github-OpenGL-Example
本章的Chapter02的示例源码详细地址:Github-Chapter02