第1章计算机图形学概述 本章学习目标  纵览计算机图形学领域  描述重要的图形输入和输出设备 预览 1.1节介绍计算机图形学的相关领域。1.2节给出了一些当今计算机图形学应用的例子。1.3节介绍用计算机生成图像的基本方法。在1.3.4节中特别介绍了光栅图像的概念,它的应用贯穿本书始终。1.4节描述了一些现在常用的图形显示设备,1.5节简要介绍用于交互式图形应用的各种输入设备。 1.1 什么是计算机图形学 什么是计算机图形学?这是个好问题。“计算机图形学”这个术语,在不同的背景下具有不同的含义。简单来说,计算机图形是计算机产生的图像。如今无论在哪里你都能找到例子,特别是在杂志和电视上。比如本书利用计算机排版: 每个字符都是利用存储在内存中的字符形状库来绘制的。在书和杂志中,计算机制作的图像随处可见。一些图像看上去如此自然,以致于你无法将它们同实景照片区分开来。另外一些图片有人造的或超越现实的外观,这是为了营造某种视觉效果。例如,当今的电影能够展示一些从来不曾存在的景象,这些景象是用计算机精心制作的,融合了真实的和想象的景象。 图1.1 包括反射和阴影的光线跟踪图像计算机图形学可以用来指制作这种图像(如插图1所示的彩色图像)的工具。本书将描述这些工具是什么和怎样使用它们去写程序。图1.1展示了用本书中描述的技术所生成的引人入胜的结果,我们将教会读者怎样制作这样的图像。 这类工具既包含硬件工具,又包含软件工具。硬件工具包括显示器、显卡和显示图形用的打印机等,也包括输入设备,例如鼠标、数据手套和跟踪球等,它们可以让用户指向特定方位和绘制图形。理所当然地,计算机本身就是一个硬件工具,它通过特殊的硬件设备使图形显示和图像捕获更加有效。 至于软件工具,对于一些常见的或许你已经很熟悉了: 操作系统、编辑器(或文本处理器)、编译器和调试器。这些工具几乎在任何编程环境中都能找到。对于图形学,必须有一个图形程序集合,它们本身能够绘制图片。例如,所有的图形库都有画简单的线或圆(或字符,如G)的功能。有些图形库如OpenGL包含的功能远超出这些,既有拖动和管理带下拉菜单的窗口,又有输入框和对话框。一些图形库(也包括OpenGL)还提供了非常复杂的三维功能,它们允许程序员在三维坐标系统中设置一个照相机给物体拍照。 在这本书里我们将展示如何利用图形库写程序,以及如何增强它们的功能。不久以前,程序员还不得不使用高度设备相关的函数库,为特定显示设备类型的特定计算机系统设计程序。当程序需要从一种系统转向另一种系统或从一种设备转向另一种设备时,就非常困难。在大多数情况下,程序员不得不对程序进行实质的改动以便它能被继续使用,这种处理费时且容易出错。幸运的是,现在这种情况好多了。已经存在一些设备无关的图形库,这种图形库允许程序员在程序中使用公共函数集,并使该程序能够在不同系统和显示设备下运行。OpenGL就是这样一种函数库,它也是本书采用的最主要工具。OpenGL创作图形的方法在学术界和工业界得到广泛的应用。我们将在第2章开始对OpenGL进行详细的讨论。 总之,计算机图形学一般是指包括这些工具及其所制作图片的整个研究领域。一般认为这个领域起始于20世纪60年代早期,由Ivan Sutherland在MIT所发表的博士论文“画板: 一种人机交互图形通信系统”开始。随着学术界和工业界对图形学越来越感兴趣,显示技术和管理图像信息的算法得到了快速发展。图形学专业兴趣小组,简称SIGGRAPH,成立于1969年,今天活跃于世界各地(不可错过的SIGGRAPH年会现在每年吸引30000参与者)。更多信息可访问http://www.siggraph.org。今天全球许多公司把计算机图形技术作为其收入的主要来源,大多数高等院校的计算机科学与工程系也都开设了计算机图形学课程。 计算机图形学是一个非常吸引人的研究领域。你将学习编写的程序是创作图片,而不是生成字符串流或数字。相对于抽象数字,人们更愿意接受图形信息,并能够从图片中获取更多的信息。我们的眼-脑系统对于识别可视化的模式是高度协调的。阅读文本,当然也是模式识别的一种方式: 我们快速识别字符形状,把它们转化为文字,然后解释它们的含义。只不过我们浏览图片会更敏锐一些。某些信息作为文本可能是难以理解的废话,转化为图形就变成可被快速识别的形状或模式。在一幅图片中蕴含的信息量可能是巨大的。我们不仅能识别图片中有什么,还能通过它的细节和纹理收集大量的信息。 计算机图形学(OpenGL版)(第3版)第1章 计算机图形学概述人们研究计算机图形学有很多种原因。有的人只是用它作为有效的工具来描绘曲线和表达研究工作中处理的数据,有的人想制作计算机动画游戏,还有的人寻求艺术表达的新方法,多数人希望工作变得更有效率并能更好地交流思想。计算机图形学对这些都能有所帮助。 潜在的用户: 一般来说,用户是对图形学感兴趣的人。计算机程序产生的图形有以下几种方式。  一帧一帧的绘制: 一帧一帧的绘制,期间要用户等待(非常枯燥).  用户控制下的一帧一帧绘制: 一系列的帧可以被连续绘制,就像在PowerPoint中;用户按下一个键移动到下一张幻灯片,但除此外没有其他的交互方式(枯燥程度小很多).  动画: 一系列的帧按一定速率处理,用户很乐意观看(通常是令人兴奋的,例如动画电影“超人特工队”和“怪物史莱克”).  交互式程序: 观看交互式的图形表达时,用户利用输入设备如鼠标或键盘控制帧的流向,且流向是任意的,在写程序时是无法预知的。这种方式将令人赏心悦目。计算机游戏是交互式图形表达的常见例子。 本书对于上述所描述的人群,有时会使用“用户”这个词,有时会使用“观众”这个词。在某些条件下,像玩计算机游戏时,我们可能会使用“玩家”这个词。 图形学也包含输入方面的内容。程序制作、输出图片,或者结合程序中运行的算法与用户输入的数据生成图片。一些程序能够轻松接受通过键盘输入的字符和数字。另一方面,图形程序强调更自然一些的输入方式: 如桌子上鼠标的移动,写字板上输入笔的线划,或者用户头和手的运动。本书将考察交互式计算机图形学的许多技巧;这就是说,我们把自然的用户输入方式与那些图形输出的制作结合起来。 1.2 计算机生成的图片用在哪里 计算机图形学能够绘制高度真实感的物体,还能绘制真实世界里看不到的事物(例如火山内部的景象,想象中的木星表面,或者住院病人的头颅内部)。程序员通过程序中的某些算法描绘感兴趣的物体,然后程序通过该模型生成图片。 本节我们将简单介绍一些计算机图形学的应用领域。这个列表是远远不全的: 它仅仅起到提示作用。我们还将在实例分析中详细介绍其中的某些应用。 1.2.1 艺术、娱乐和出版行业 计算机图形学被广泛应用于电影、电视节目、书刊、游戏和杂志的制作。近几年来图形系统的成本显著下降,人们还开发出强大的软件和硬件工具来充分利用图形系统不断增长的性能。现在天才的设计者们惯于使用计算机来制造特殊的效果、动画和高质量的出版物。图1.2显示了一个计算机动作游戏的例子。 图1.2 一个3D计算机游戏的截屏 计算机游戏的发展 许多读者学习计算机图形学是因为有潜在的兴趣想学习怎样写游戏软件。我们将逐步介绍写游戏的技巧,并概略介绍近几十年来游戏的发展。这是非技术上的总结,它揭示了计算机图形学领域正在(并也会继续)快速发展,并且使计算机游戏更加成功和更具有吸引力。 1952年A. S. Douglas在剑桥大学发表了他的关于人机交互的博士论文。Douglas发展了最早的图形化的计算机游戏之一: 井字旗游戏的一个版本。1958年William Higinbotham创造了另一先驱性的视频游戏,被称为“双人网球”的交互式游戏(很像Pong),制作并展示在Brookhaven国家实验室的示波器上(见图1.3). 图1.3 用于制作“双人网球”游戏的设备 1962年Steve Russell发明了“空间战争”,Nolan Bushnell和Ted Dabney在1971年把它发展成为第一个钱币操作的电玩游戏。1967年Ralph Baer写了第一个使用电视机作为显示器的视频游戏Chase. Nolan Bushnell和Ted Dabney在1972年成立了Atari计算机公司。1980年Atari的Asteroids和Lunar Lander成为了最先注册版权的两款计算机游戏。Atari公司还开发了为大家所熟悉的Nintendo、SEGA和PlayStation等游戏系统。 这只是一个简单的纵览,稍后我们将讨论如何写游戏。特别是我们会着重讲游戏中的场景建模,和程序怎样实现玩家和游戏中角色之间的交互,以及纹理的制作。纹理对于场景的真实感起着非常重要的作用。这些都要求数据更快地从CPU传送到显示器,以及需要显卡功能的不断增强。有一类需要很好显卡的游戏是第一人称射击游戏。1992年推出的德军总部Wolfenstein 3D游戏是第一款这类游戏,它很快引发了其他的热门游戏例如毁灭战士Doom和雷神之锤Quake系列。 这些第一人称射击游戏使游戏者从主要参与者的视角沉浸于场景之中。看上去那么逼真的场景(敌人看上去相当凶恶)、被击中的效果、突然出现的阻止游戏者前进的障碍物,这些都使得游戏非常真实。与德军总部Wolfenstein 3D游戏相关的光线投射算法,将在第十二章探讨。 计算机游戏 图1.2 和图1.4 显示了两种不同类型的计算机游戏。玩家移动操纵杆、按按钮、触动扳机,计算机都必须快速的反应生成图像。一些专用设备常常被用来加速连续图像的生成。街机游戏Arcade games给图形编程人员带来了最艰巨的挑战,因为动作必须高度逼真而且响应速度非常快。 图1.4 一个德军总部Wolfenstein 3D的截屏 电影制作、动画和特技 电视节目经常可以见到计算机动画,一些效果惊人的动画可以被整合成特效电影。通过在胶卷或录像带上写入一系列的图像来制作动画,每个图像每一秒只有轻微的差别,人眼将图像混合就能看到流畅的动作。 通过因特网浏览 我们生活在一个网络世界里,似乎每个人都在网上冲浪。用户移动鼠标到屏幕上一点,点击选择下一个访问的网站,网页的信息就会通过互联网传送过来。浏览器必须快速解释网页上的数据,然后把高质量的文字和图形绘制到屏幕上。图1.5显示了万维网上的一幅截屏。 图1.5 万维网上一个网页的截屏 幻灯片、书籍和杂志设计 计算机图形学通常利用电子排版程序来设计书刊的页面,用户可以通过交互地移动文本和图形来寻求最满意的版面。 另一种发布形式有时被称作陈述图形。高质量的商用或教育用幻灯片被设计用来给各种各样的观众演示,如图1.6的例示。幻灯片常常含有条形统计图表或圆形分格统计图表,以一种便于理解的方式来总结复杂的信息。它们必须是高质量且可视化的,以便于鲜明地表达观点。 图1.6 用于教学演示的幻灯片 绘图系统是制作图像的另一种工具。一个常见的例子就是Adobe Photoshop,既是图像处理软件又是绘图系统。用户利用鼠标或写字板绘画草图,然后选择颜色或图案来创造想要的效果。图1.7显示了这样一个系统的截图。系统提供了各类工具: 以前绘制的图像可以从大容量存储器中取出来和新图像混合;可以通过调色板获取并显示不同颜色;许多不同的纹理可以通过简单的命令创建。这些系统经常用来制作网上常见的那些网页。 图1.7 一个绘图系统的截屏 1.2.2 计算机图形学、感知和图像处理 计算机图形学和图像处理的领域多年来相互融合: 划分它们之间的界限变得越来越难了(也越来越不重要了)。本书主要阐述计算机图形学,但是我们也会讲一些以前属于图像处理领域的技术。 计算机图形学的主要任务就是制作图片和图像,并基于计算机的某种描述或模型合成它们。图像处理的主要任务,从另一方面说,是改善或改变已做好的图像,被修改的图像可能来自数字照片或者从视频捕获。图像处理可以去除图像中的噪声,提高对比度,锐化边缘,修正色彩。软件程序可用来搜寻图像的某些特征,增强它们的高光使图像更加引人注目或更加易于理解。 图1.8(a)显示的是数字扫描一个照片得到的图像。图1.8(b)显示的是提高对比度、去除噪声以及锐化边缘后得到的图像。 图1.8 一个扫描图像的改善 (a) 原始图像; (b) 改善的图像 电影工业最近以戏剧化的方式采用了计算机图形学和图像处理的结合。想象一个经典电影中的场景--比如“海底总动员”或者“谁陷害了兔子罗杰”--就综合了计算机图像生成与数字处理技术。 1.2.3 过程监视 高度复杂的系统如工厂、发电站和空中交通控制系统必须严格监控,必须要有一个人在那里监控随时可能发生的问题并发出警报。监视器必须及时传送实时信息。每秒钟或每当需要的时候,数据就被传送到监测电台,转换成图形信息,呈现给操纵者。 空中交通控制系统由显示不同区域内飞机位置状况的一系列监视器组成。用户看到一个整体的图示。不同的图标通过闪现或改变颜色对用户发出提示或警报。 1.2.4 仿真显示 一些系统,像那些空中交通控制,确实存在并且能够实时监控。另外一些则从来不存在,如计算机中的一个算法和公式,但是它们依然可以被测试和运行,就像它们实际存在一样。通过它们可以获取有价值的模拟数据,这类信息通常使用图形表示来使它们变得易于使用。 多种系统能够被很有效地模拟: 机器人往返于活火山的斜坡;人体对危险的外来物质入侵的反应;或者碳氢化合物的增长对全球变暖的影响。经典的例子就是飞行模拟器。这种系统是一架具有某一型号及飞行特点的模拟飞机,场景包括飞机场、山脉、植被,当然还有天空,所有的模型都能被很好地模拟。 飞机的动态运动通过计算机建模。在飞行模拟期间,当飞行员操作控制系统,程序计算出模拟飞机新的位置和速度,飞行员看到座舱外一个模拟的环境。飞行模拟器是要求最多和最难的图形应用程序,因为必须要快速反应。图1.9展示了一架飞机座舱的真实控制面板。 图1.9 飞行控制面板的模拟 这些例子说明了计算机图形学的重要性: 计算机图形学能够显示物体仿佛它们真的存在一样,而实际上它们只是计算机内的模型而已。 1.2.5 计算机辅助设计 许多学科大量利用交互式计算机图形学来协助系统设计或制造--例如电钻外壳设计。计算机建立探讨中的设备模型,基于模型的图片可以展现给用户检验。设计者可以操作跟踪球或数据手套,对物体进行旋转或放大,从而对其进行查看。为了加速通常采用线框绘制,这就是为什么物体的形状往往是网格结构。 设计者可以仔细观察电钻目前的模型,然后找出需要改进的地方,修改并重新绘制。当形状看上去合适时,用户需要一个更真实的外观,这也许会花费很长时间去绘制。随着今后更深入的讨论,将介绍有提供阴影、高光和复杂细节的全彩色绘制算法。 这里也可能会用到分析和模拟。电钻的外观或许有不错的视觉效果,但外壳可能就太脆弱或太重,或者抓着很不舒服。用于电钻模型的算法通过分析它的重量来测试动配合是否适合它的外壳。算法能更进一步地测试外壳用钢铁或铝是否太昂贵,或者内部的部件在最终的加工工序中是否便于组装。计算机可与设计制造业有力地结盟。本书关注的是根据设计者提供的准确信息制作出物体的图像。 计算机辅助建筑设计 计算机图形学也能够帮助建筑师设计建筑。这个模型可能是一座房子、学校或者医院的建筑平面图,如图1.10所示。建筑师能够调整这个数字平面图,用鼠标把一堵墙移到这里或者把一个窗户调整到那里。CAD软件也可以制作一个完全的绘制版本,结构以三维形式呈现。计算机图形学允许建筑师移动窗户和门的位置,展现不同的纹理如砖或灰泥。通过交互式的控制,建筑师甚至能够做一个预演,给顾客展示房子建好后的样子。 图1.10 一个医院的平面图 1.2.6 科学分析与体可视化 科学数据往往是复杂的,一个实验中变量之间的关系难以可视化。图形学提供了一个极好的工具把科学信息以一种便于理解的方式表达出来。如果能以正确的方式观察数据,往往就会对正在进行的研究有新的发现。通过适当的方式展示数据,还有利于与同行更好的交流。 图1.11举了一个例子: 一张曲面展示了人眼敏感的波动方式。表面高度代表一个数量(例如温度或黏度),三维点的另外两项代表其他的数值。与之相对的是,如果这些数据用一个表格的形式展现,那人们将不得不努力研究这个表格以期获得同样的信息。 体可视化 人体头部扫描是一个复杂的难题,但如果利用图像处理就变得简单了。不同颜色的区域及时提示医生关于大脑每个部位的健康情况。一个像大脑这样的三维物体图像可通过创建一系列切片合成为最终图像如图1.12所示。 图1.11 复杂科学数据的显示 图1.12 利用体可视化的人脑视图 这种方法引发了计算机图形学里的一个专门研究领域称为体可视化: 它用图形来展示海量的数据。例如,多种软件可以辅助识别分析数据中的天气模式来预测恶劣的天气。 另外为了帮助人类理解测量数据,计算机图形学还适合于理解复杂的数学思想。人们已经设计出了一些强大的软件(如Mathematica、 MathLab、MathCad),允许使用者把各种数值输入公式和定理,然后以某一方式显示结果。例如,图1.13(a)显示了由一个数学公式生成的表面。通过键入单独的指令使用Mathematica来显示: ParametricPlot3D[{t,u,Sin[t u]},{t,0,3},{u,0,3}]这个公式的复杂性一眼就可以看得出来,用户能够调整一些参数从不同的视角来观察其表面。图1.13(b)也采用Mathematica来绘制一个三维物体(一个星形的二十面体)来帮助用户研究它的结构。 图1.13 Mathematica的显示 (a) 一个复杂的数学曲面; (b) 一个数学定义的实体 1.3 计算机图形学中制作图像的基本元素 计算机绘制的图像是由什么组成的?组成这些图像的基本元素被称为输出图元。一种有效的分类是:  点  线  折线  文本  填充区域  光栅图像 我们会发现这些类型有些重叠,但是这种分类提供了一个很好的起点。我们依次描述每种类型的图元,以及一些常用程序如OpenGL绘制它们的语句。这些工具更多的细节将在后面章节给出;当然,我们也探讨每个输出图元不同的属性。一个图形图元的属性就是那些影响外观的特点,例如色彩和粗细。 1.3.1 折线 折线是由一系列的直线段相互连接而成。图1.14中每个例子都包括了一些折线: 图1.14(a)所示为一条从恐龙鼻子一直延伸到尾巴的折线;图1.14(b)用一条折线表示的数学方程图;图1.14(c)所示为一个国际象棋棋子的线框图,它包含了许多折线来勾画它的轮廓。 图 1.14 (a) 一幅恐龙的线条画; (b) 一个数学公式的图; (c)一个三维物体的线框图 图1.15 一个由直线段构成的曲线 注意一条折线可以看起来像平滑的曲线。图1.15显示了一个放大的曲线,能够看出它是由许多线段组成的,人眼把它们混合成一条光滑的曲线。 由折线组成的图有时称为线条绘画(素描)。某些设备,像笔式绘图仪,就是专门为制作线条绘画而设计的。 最简单的折线是一条单独的直线段。一条直线段用它的两个端点定义,记为(x1,y1) 和 (x2,y2). 画一条直线的程序类似于drawLine (x1, y1, x2, y2);它画了两个端点间的一条直线。以后我们将详细描述这类的语句,并演示使用它们的大量例子。在某一点,我们可以定义像x1(整数或实数)一样的坐标以及它的颜色。 一个特例是当一条线段收缩为一个点时就被画作一个点。甚至低级的点在计算机图形学中也有重要的用途,正如我们后来将看到的那样。画一个点可以使用如下的程序语句drawDot (x1,y1);但是,正如将要看到的,OpenGL画点会有些不同。 当若干线段组成折线时,每条线段叫做边,两条相邻的边相交于一个顶点。折线的边可以相交,如本节中的那些图所示。折线可由顶点列表定义,每个点由坐标确定: (x0,y0),(x1,y1),(x2,y2),…,(xn,yn) 图1.16 一个折线的例子 例如,图1.16所示的折线由以下坐标序列给定: (2,4), (2,11), (6,14), (12,11), (12,4)… 画折线我们需要一个工具,如下面的一行代码:drawPolyline (poly);变量poly是以某种方式存贮所有顶点的列表。在一个程序中有多种方法来描述一个列表,每种方法都有各自的优缺点。 折线不需要形成一个闭合的图形,但是如果首尾顶点被同一条边相连,它就形成了一个多边形。另外,如果任意两条边都不相交,这种多边形就被称为简单多边形。图1.17展示了一些有趣的多边形;只有图1.17(a)和图1.17(b)是简单多边形。多边形是计算机图形学的基本元素,一部分原因是它们很容易被定义。许多绘制算法可以很好地操作多边形。多边形将在第3章中详细介绍。 图1.17 多边形的例子 线段和折线的属性 折线的重要属性包括边的颜色和粗细,虚线的方式,粗边在端点混合的方式等。一般来说一条折线的所有边都应具有相同的属性。 图1.18中的前两条折线有着粗细的不同。第三条折线是采用虚线方式绘制的。 图1.18 不同属性的折线 当一条线的属性为粗,它的端点是有形状的,用户必须描述两条相邻的边如何结合。图1.19展示了各种可能性。图1.19(a)显示了“平头端”的线会在接头处留下不好看的“裂缝”. 图1.19(b)显示了圆头端的线,连接处平滑。图1.19(c)显示了斜角连接,图1.19(d)显示了平整的斜角连接。一些软件包允许用户选择不同连接类型。 图1.19 在一条折线中两条粗线段的连接方式 线的属性有时可通过下列命令设置: setDash (dash 7) 或 setLineThickness (thickness)1.3.2 文本 一些图形设备有两种不同的显示模式,文本模式和图形模式。文本模式用于字符的简单输入/输出,用来控制操作系统或者编写代码。这种方式显示的文本采用嵌入式的字符发生器。字符发生器能够绘制字母型、数字型和标点型字符,以及一些特殊符号集如、δ和。一般来说这些字符不能在显示器上任意放置,只能放置在规则网格的某一行和列上。 图形模式提供了更丰富的字符形状,字符能够被随意放置。图1.20是以图形方式绘制文本的例子。 图1.20 图形模式绘制的文本 绘制字符串的语句类似于:drawString (x, y, string);字符串的起点在(x, y),绘制的系列字符存储在变量string中。 文本属性 文本有许多属性,其中最重要的是字体、色彩、大小、间距和倾斜度。 字体。字体是一类具有特殊风格和大小的字符形状集。图1.21中显示了不同的字体。 如图1.22(a)所示,每个字符的形状能够用折线表示(或者用更复杂的曲线表示,如第11章中贝塞尔曲线),或通过点阵表示,如图1.22(b)所示。图形包配置有预先定义好的字库,其他类型的字库可从专门的设计公司购买。 图1.21 一些字体的例子 图1.22 字符形状通过(a)折线和通过 (b)点阵模式来表示 字符和字符串的倾斜度。字符可以沿着某一方向倾斜。倾斜的字符串常用来注释图形中的某一部分。 高质量文本的图形表达是一个复杂的研究对象。细微的差别就能将美观的文本变得丑陋。实际上,我们在日常生活中看到了太多的印刷品,以致于我们潜意识里希望字符显示保持特定的形状、间距和细微的平衡。 1.3.3 填充区域 填充图元是指填充时的颜色或图案。填充区域的边界经常是一个多边形(更复杂的区域将在第4章讨论)。图1.23演示了一些填充的多边形。多边形A的边界可见,与之相反B的边界没有绘制。多边形C和D不是简单多边形。多边形D甚至包含了多边形的洞。这样的形状依然能够被填充,但一定要准确地定义哪部分是多边形的内部,因此填充算法的区别依赖于定义。执行填充的算法将在第10章讨论。 图1.23 填充的多边形 填充多边形的语句类似于: fillPolygon (poly, pattern);其中变量poly保存多边形的数据(与表示折线一样的列表),变量pattern是填充图案的描述。我们将在第4章详细讨论。 图1.24展示了如何利用填充来给一个三维物体的每个不同的面着色。这个物体的每个多边形面填充了某一灰度色以对应该面反射的光线数量,这使得物体看上去沉浸于某一方向的光线中。三维物体的着色将在第8章中详细讨论。 图1.24 三维物体的多边形(面)填充以显示正确的着色 填充的属性包括边界的属性,以及填充的图案和颜色。 1.3.4 光栅图像 图1.25(a)展示了一个国际象棋棋子的光栅图像。它由许多非常小的不同灰度的单元组成,其放大的版本如图1.25(b)所示。每个单元被称为像素(“图像元素”的简称)。一般来说人眼不能分辨这些独立的单元,它们混合在一起组成一幅图像。 图 1.25 (a) 一个棋子的光栅图像; (b) 图像的局部放大图 光栅图像以数值数组的形式存储在计算机中。这个数组可以看成是矩形的,由某些行和列组成,每个数值代表存储的像素值。这个数组整体被称为一个像素图,或称为位图(也有人认为位图应该指那些每个像素由单个比特表示的像素图,也就是由0或1组成的像素图). 图1.26举了一个简单例子,该图像用18×19的数组(18行×19列)表示,包括3种灰度。假设这3种灰度级别被编码为数值1、2和7,更高的数值代表更深的灰度级。图1.26(b)表示的是这个图像左上方6×8部分像素图的值。 图1.26 用位图表示的简单图形 光栅图像是怎样制作的?它的三个基本来源如下。 (1) 手工设计图像。 设计者决定每个单元所需的值,把它们记下来。有时一个绘图软件可以帮助如下工作更加自动化: 设计者能够绘制和操作不同的图形,并浏览已完成的结果。当满意时,设计者把结果储存在一个文件里。图1.26就是用这种方式创造的。 (2) 计算机生成图像。 用一个算法来绘制一个在计算机内存中抽象建模的场景。举一个简单的例子,一个由桔黄色灯光照明的场景会包含一个独立的黄色球体。模型描述球体的大小和位置、光源的位置和一个假设的相机。光栅图像相当于相机的胶卷。为了制作这个光栅图像,算法必须计算落在相机胶卷上每个像素的色彩。这就是光线跟踪的图像生成方式,图1.25中的棋子就是用这种方式生成的。 光栅图像也经常会包含含有直线的图像,在这种图像中可以通过设置像素的正确颜色来表示直线。但是它需要大量的计算来确定那些最能拟合给定两个端点的理想直线的像素序列。Bresenham算法提供了一个确定这些像素的有效方法。 图 1.27 (a) 线和文本的集合; (b) 图(a)的放大图,产生了锯齿; (c) 放大到每个像素都清晰可见图1.27(a)展示了一个由若干直线,一段圆弧和一些文本字符组成的光栅图像。图1.27(b)展示的是这个光栅图像的局部放大图,以便看清直线上单独的像素。对于水平或垂直的直线,黑色的方块像素排列得很好,形成了一条整齐的线。但对于其他的直线和圆弧,最佳像素集合表示的只是所需曲线的近似。图1.28 一幅扫描的图像 另外,结果中的锯齿是光栅图像无法避免的。当视图放大到像素可见时,图像就会表现出图1.27(a)的失真状况。 (3) 扫描图片。 照片或视频图像可以被数字化。将网格(虚拟的)放置在原始图像上,在每个网格点上设备读取匹配的最相近的颜色,然后将得到的位图储存在文件里以备后用。图1.28中小猫的图像就是用这种方式生成的。 因为光栅图像只是简单的数值数组,计算机能够很好地处理它们。例如,图1.29中显示了小猫图像的三幅连续放大图。这些是通过像素复制得到的(将在第9章讨论)。在图1.29(a)的每个方向,每个像素被复制3次,图1.29(b)为6次,图1.29(c)为12次。 图1.29 小猫图像的3幅连续放大图 (a) 3倍放大; (b) 6倍放大; (c) 12倍放大 另外一个例子是,人们经常需要整理或者修改扫描图像,例如,去除噪声或突出重要细节。图1.30(a)演示了改变小猫图像的灰度级来提高对比度从而使细节更加清楚的结果;图1.30(b)演示了边界锐化的效果,这是通过一定形式的图像过滤获得的。 图1.30 图像增强的例子 图1.31演示了编辑图像达到某一视觉效果的两个例子。图1.31(a)展示了小猫图像的浮雕效果,图1.31(b)展示该图的几何变形。 图 1.31 (a) 浮雕图像; (b) 几何变形的图像 1.3.5 光栅图像的灰度和色彩表达 光栅图像的一个重要方面就是位图的色彩或灰度的表现方式。这里我们简要概述最常用的几种方式。 灰度光栅图像 如果一个光栅图像只有两种像素值(如0和1),就称为二值图。图1.32(a)是一个简单的二值图,描绘的是计算机屏幕上常见的箭头形状的光标。图 1.32 (a) 一个光标的二值图; (b) 它的位图 它的光栅包括16乘以8个像素。图1.32(b)是这个图像1和0排列的位图。这里1代表黑色,0代表白色,但这种代表值可以被轻易反过来。既然1比特的信息对于区分两个值是足够用的,一个二值图常常被称为每像素1位的图像。 当灰度图的像素具有2个以上的数值时,每个像素就需要大于1位的存储量。灰度图常常按照它们的像素深度来划分,它需要的数目代表它们的灰度级。n位的容量可以表示2n个值,所以像素深度为n的图像可以有2n个灰度级。最常见的n值有:  每像素2位 (2bits/pixel)产生4个灰度级。  每像素4位 (4bits/pixel)产生16个灰度级。  每像素6位 (8bits/pixel)产生256个灰度级。 图1.33显示了从黑到白的16个灰度级。16级中每个像素值与一个二进制的4位数相对应,如0110 或 1110。这里0000代表黑色,1111代表白色,其他14个值代表中间的灰度级。 图1.33 16级灰度 许多灰度图像采用256个灰度级,因为256级已经可以较好表示扫描图像。256级意味着每个像素具有某个8比特的值,如01101110。像素值通常代表亮度,这里00000000代表黑色,11111111代表白色,10000000代表中间的灰色。 像素深度的影响: 灰度量化 有时一个原本使用每像素8比特的图像会改变为每像素使用更少的比特数。如果一种显示设备不能显示一个很高的灰度级别,或者整幅图像占据了太多存储空间,这种情况就可能会发生。图1.34和图1.35显示了小猫图像的像素值用更少位数表示时的影响。图1.34中逼真度的损失几乎看不出来,该图分别采用6或3比特/像素(各自产生64和8个不同的灰度级). 图1.34 图像减为每像素6比特和每像素3比特 图1.35 图像减为(a)每像素2比特和(b)每像素1比特 注意该图的一些区域,原图是灰色渐变的,现在成了一块统一的灰色。这通常被称为条带化,因为原图中本来是渐变的灰度被一系列固定值的灰度块所替代。 图1.35显示了每像素2和1位的例子。在图1.35(a),4级灰度清晰可见,存在大量的条带。图1.35(b)只有黑色和白色,大量的原图信息损失。在第9章将讲述抖动等技术,旨在提高每像素位数太少的图像的质量。 彩色光栅图像 我们喜欢彩色图像是因为它们比灰度图更接近我们的日常经验。近几年来彩色光栅图像变得普及了,因为高质量彩色显示的成本变低了。数字化彩色照片的扫描仪成本现在也非常地适中。 彩色图像的每个像素都有一个彩色值,即一组可以代表颜色的数值。有一些方式可以将数值与彩色联系起来,而其中最常用的一种是将色彩描述为红、绿、蓝三色光的混合。在这种表示方式下,每个像素值是一个3元组,如(23、14、51),依次描述了红、绿、蓝三色光的强度。 用来表示每个像素色彩的比特数常称为它的色彩深度。红、绿、蓝被称为三原色,将在第10章讨论。(红,绿,蓝)三元组的每个值都有一个特定的比特数,色彩深度是这些值的和。3元组的色彩深度允许每个元素1位。例如,像素值(0,1,1)表示红色是关着的,绿色和红色是开着的。大多数情况三原色叠加在一起显示,所以(0,1,1)表示绿光和蓝光叠加在一起,它代表青色。既然每种原色可以有或没有,这里就有8种可能的颜色,如图1.36中表格所示。可以想象,同等数量的红、绿和蓝(1,1,1)产生白色。颜色值显示颜色0, 0, 0黑色0, 0, 1蓝色0, 1, 0绿色0, 1, 1青色1, 0, 0红色1, 0, 1品红1, 1, 0黄色1, 1, 1白色图1.36 颜色值和色彩之间的一般对应 图1.36中三原色的色彩深度是不能满足精度需求的,通常需要更大的色彩深度。因为一个字节(8比特)是计算机操作常用的单位,许多图像都有每元素24或8位的色彩深度。这样一来每个像素有224或超过2亿种色彩,这种图像被认为是真彩色图像,是人眼所能分辨的好的彩色复制品。但是这样的图像会占据大量的内存: 每个像素1字节,一个1280乘以1024像素的高质量图像就需要超过1百万的字节(1MB). 1.4 图形显示设备 我们将简要介绍一些显示计算机图形的硬件设备,这些设备包括显示器和打印机。近30年来已设计出了多种的图形显示设备,并且新设备还在不断涌现,其目的是为了更真实地再现艺术家或工程师所创造的高质量图像。本节我们将介绍当今制作图片的种类,以及如何使用它们和显示它们的各种设备。我们将了解衡量图像质量的方法,以及怎样比较各种不同的显示设备。 1.4.1 线画显示 某些设备只是绘制线条。因为早期时代的技术限制,最早的计算机图形学只能通过画线设备来显示。经典的例子是笔式绘图仪,笔式绘图仪根据计算机指定的位置到纸上移动笔,留下某种颜色的墨迹。某些绘图仪有多种笔,程序可以自动调用以绘制出不同的颜色。通常可选择的色彩非常少: 每种颜色都需要使用一支单独的笔。线画的质量取决于定位笔的精度和线画的清晰度。 有各种不同的笔式绘图仪。平板绘图仪在二维的范围内在一张固定的纸上移动笔。滚筒式绘图仪通过滚筒的移动来提供纸的一个移动方向,通过滚筒上笔的左右移动来提供另一个移动方向。 图1.37 交叉阴影线模拟填充区域 有一类显示器,称为向量、随机扫描或书写显示器,能够产生线条图。它们的内部线路以点对点的方式在显示器表面扫描电子光束,从而留下轨迹。 然而向量显示器不能显示平滑的着色区域或扫描图像。区域填充通常用不同线画图案的交叉阴影线来模拟,如图1.37所示。除了个别的特殊应用,现在光栅显示器已取代了大部分向量显示器。 1.4.2 光栅显示器 现在,显示计算机图像主要是采用光栅显示器。最常见的就是与个人电脑或工作站相连的视频显示器(见图1.38)。其他的常见设备主要用来制作图像的硬拷贝(通常在纸上): 激光打印机、点阵打印机、喷墨打印机和胶片复印机。我们下面介绍其中最重要的几种设备。 图 1.38 (a) PC的显示器; (b) X光的平面显示器 光栅设备有一个显示屏幕,以用于显示图像。该屏幕可以表达为由若干行(如480行)和若干列(如640列),则屏幕能同时显示480×640 ≈ 307200像素。这样的显示器有一个内置的坐标系统将图像的给定像素与屏幕上的物理位置联系起来。图1.39举了一个例子。这里水平坐标x从左到右增长,垂直坐标y从上到下增长。这种颠倒的坐标系统在光栅设备中是最常见的。 光栅显示器总是与帧缓冲区联系在一起。帧缓冲区是存储器中一块足够容纳要显示图片的区域。帧缓冲区可以是显示器自带的物理存储,或者在主机中。个人计算机安装的显卡中有帧缓冲区所需的存储器。 图1.40展示了一个图像是怎样被制作和显示的。图形软件存储在系统内存,通过CPU的指令执行。程序计算出每个像素的数值并把它们装载到帧缓冲区中(这是后面讲述编程时我们要着重讲的部分: 计算正确的像素值并把它们写入帧缓冲区). 图1.39 光栅显示器的内置坐标系 图1.40 使用光栅显示器的计算机流程图 扫描控制器负责真正的显示处理过程。它自动运行(胜于用程序控制),一个像素一个像素地重复同样的工作。它引发帧缓冲区通过转换器将每个像素输送到显示平面合适的物理位置。转换器接收像素值(比方说01001011),然后把它转换为相应的色彩值,并在显示器上生成一个彩色点。 扫描处理 图1.41描述了扫描处理的更多细节。其中的主要问题是帧缓冲区中的每个像素值是怎样输送到显示屏幕上正确的物理位置。帧缓冲区中的每个像素存储在某一存储单元,并对应着显示器上的某一位置。比方说,对应于屏幕上(136, 252)位置的像素值,就存储在帧缓冲区存储单元mem[136][252]中。当扫描控制器传送地址(136, 252)到帧缓冲区,它发送值mem[136][252]。转换器也同时在显示界面上找到位置(136, 252)。值mem[136][252]在转换线路中转换为相应的色彩,并被送到显示界面上的正确位置(136, 252). 图1.41 从帧缓冲区中扫描一个图像到显示器屏幕 在整个帧缓冲区中扫描图像,每个像素值只被扫描一次,它在屏幕上的对应点显示正确的强度或颜色。 在某些设备中,这种扫描必须每秒钟重复许多次,用来保持和刷新显示器上的图像。下面将讲到的视频显示器就要做大量的这种工作。 在介绍完这些一般性质之后,我们将简单介绍一些特殊的、不同类型的光栅设备。 视频显示器 许多视频显示器的功能基于阴极射线管(简称CRT),近似于电视机的显示器。图1.42中对采用视频显示器的系统,在前面所讲一般性描述的基础上又增加了一些细节,特别是从像素值到亮点的转换处理采用图示说明。所示的系统有6比特的色彩深度,所示帧缓冲区有6个位面,每个像素使用每个位面的1比特。 图1.42 彩色视频显示系统的操作 每个像素的红、绿和蓝成分分别使用一对比特值。这些比特值输入到三个数模转换器(DAC)中,将逻辑值(如01)转换成真实的电压值。数字输入值和输出电压之间的对应如图1.43所示,标出的最大值就是DAC能产生的最大电压。 三个电压值分别驱动阴极射线管(CRT)的三个电子枪,轮流发射三条电子光束,其亮度正比于电压值。偏转线圈作用于三个光束使它们能够在阴极射线管的正确位置(x,y)刺 输入电压/明亮度000 Max010.333Max100.666Max111 Max图1.43 一个2比特DAC的输入-输出特性 激三个荧光点。因为采用的是荧光物质,受刺激时一个点变红,一个点变绿,一个点变蓝。这些点紧挨在一起,人眼把它们看成一个合成点,看到的颜色为三原色之和。因此合成点产生4×4×4=64种不同的颜色。 如前面所述,扫描控制器在访问帧缓存区一个像素值mem[x][y]的同时,通过发送正确的信号给偏转线圈,在CRT表面定位于一点(x, y)。因为刺激不在时,荧光点的发光会迅速消失,所以一个CRT图像必须频繁地刷新(一般一秒钟60次)来防止烦人的闪烁。在每个刷新间隔,扫描控制器迅速地扫描整个帧缓冲存储器,把每个像素值传送到显示屏表面的正确位置。 帧缓冲区被一行一行的扫描,每行提供CRT屏幕一根扫描线的像素值。扫描的顺序一般是从左到右,从上到下。历史学家说这种习惯起源于扫描线,就像扫描线编号从上到下从0开始,并由此导致了上下颠倒的坐标系统。 大多数现代系统拥有每像素24或32比特的显示能力。这些设备曾经要比那些每种颜色只有几个字节的设备贵很多,但是现在它们的价格非常适中。24比特的显示器每像素能提供224=16 777 216种不同的颜色,32比特的显示器每像素能提供大约232=2亿种颜色(但是8比特经常用于透明效果的显示,这些我们随后将讲到). 另一方面,单色显示器只有一种颜色但是有很多种亮度。单个的DAC把帧缓冲区中的像素值转换成电压级别,只驱动一个电子光束枪。这种阴极射线管只有一种荧光,所以它只能产生不同亮度的一种颜色。注意帧缓冲区中的6个位平面提供大约26=64级别的灰度。 彩色显示器与显示色彩的联系是固定的(不可编程的)。例如,像素值001101输送00给红色DAC,11给绿色DAC,01给蓝色DAC,制造出一种亮绿和暗蓝的混合色 - 蓝绿色。(参见第11章颜色理论。)近似地,110011显示为一种亮品红色,000010显示为一种中等亮度的蓝色。 第11章讨论了一种帧缓冲区中比特值和色彩的可编程联系--色彩查询表(简称LUT). 1.4.3 视频卡/3D加速器 在我们通览此书时,有许多绘制图形的OpenGL命令,例如画线、圆形和矩形。当执行这样的命令时,OpenGL必须把大量的像素数据从CPU送到显示器。数据的传输,当然需要一些时间,虽然短暂,还是会起到不可忽视的影响,例如需要快速动画的时候,或者正在玩实时人机交互游戏的时候。经过近年来的迅速发展,图形影像已经变得越来越复杂。包括将每幅图像的海量数据迅速传递给显示器,或在足够短的响应时间内运行流畅的动画,都已经变得越来越有挑战性。 图1.44 ATI的视频卡 因为CPU和显示器这两种设备不在同一位置,数据必须通过连线传输。数据的传输路径和控制传输的大规模数字逻辑线路,被统称为总线。一些类型的总线比另外一些有更快的响应,人们一直在努力确保总线是足够快的。(总线越快,价钱越高,性能越好。) 另一个重要的发展是图形卡或3D图形加速器。这种硬件有着特别设计的线路,能够快速响应,从而满足数据从CPU到显示器的实时传输需要。图形卡比过去的‘显示适配器’更昂贵;现在的加速器包含大量的板上集成存储器和特殊工艺的总线逻辑加速数据传输。(至少数据传输不是简单地通过一些逻辑门的开关控制;在数据通过总线传输前必须完成一系列复杂的、基于当前总线活动的时间检查和数据检测。) 无论何时图像成形,定义场景的几何数据必须经过大量的处理步骤。OpenGL简单指定了这些步骤的属性和它们出现的次序,这些步骤通常称为图形管线(graphics pipeline),我们在此书中许多地方都进行了讨论。 图形显示产业的风险是很高的,生产商之间激烈的竞争迫使他们迅速开发更快的图形加速器。不可避免的是,各种类型图形加速器的出现导致了对标准的需求,便于每个潜在的顾客知道他/她要买的加速器真实性能怎么样。接下来我们会介绍其中的一些标准,但是要明确的是这是一个快速变化的领域,所以标准也一直在变化。 表1.1列举了三代视频卡和它们的性能。创始于1989年,VESA(视频电子标准协会)是一个企业范围的显示和视频标准协会。表1.1中提供了每种标准加速器的型号和制造商名称、支持的显示分辨率、每像素能够显示的颜色数和刷新率(整个显示的图像多长时间被重新绘制一次)。通过这些数据,不难估计出从CPU到显示器的数据传输速率。例如,作为VESA会员的VGA图形加速器支持256种颜色(由此看出每像素8比特),每秒钟60次的刷新率意味着每秒钟60×1600×1200×8=921600000比特的数据必须被送到显示器!表1.1 一些主要的图形加速器和特点 年度型 号制造商 最大像素值支持的颜色数刷新率1981MDAMono Display AdapterIBM720×350250Hz1990SVGASuper VGAVESA1600×120025660Hz1997AGPAccelerated Graphics PortInter2048×153616.7百万100Hz 要了解最新的关于视频卡和总线加速的趋势,可在网上使用搜索引擎进行搜索。本书的网址也列举了一些资源。 作为对比,数字化音乐则简单得多。刻录在光盘上的、高质量的音乐只需要每秒钟705 600比特的传播速率!(这个每秒比特值基于每秒44 100个采样的采样率,每个采样代表16比特。) 不可编程的与可编程的硬件 早期的图形芯片和图形卡有固定功能的图形管道。1999年NVIDIA发布了GeForce 256,它是第一个可编程的视频卡。可编程性允许开发者指定他们想要这些强大的GPU做什么--通过编辑管道中加载数据的算法。这些进步使开发者通过精加工自己的顶点和像素绘制程序(或着色器)增强实用等级。如第8章探讨的,他们可执行在每个像素和在每个顶点处的操作。例如,可实现照明、毛发和透明玻璃等效果。 随着图形卡支持更多的指令和开发者要编写更加复杂的着色器,在低级汇编语言里编写着色器已是不可能的了。这导致了一系列高级着色语言的发展,如Cg和OpenGL 2.0的着色语言。Cg是一种类似C/C++的语言,虽然不像汇编语言那么有效,但允许快速开发,可以写出可读性好和可维护的代码,比汇编语言更具平台无关性。 想更全面的了解GPU,可查阅GPU Gems系列书和浏览其出版公司的网址。 1.4.4 其他的光栅显示设备 视频显示器不是唯一的光栅显示设备: 近来还出现了多种其他类型的设备。便携式计算机一般带有平面显示器,如图1.45所示。每个像素被一个水平格线和一个垂直格线定位: 要打开一个像素,则激活对应的水平和垂直格线,在这个像素位置制造一个电场。这可以有效激活该点并改变它的亮度。 图1.45 平板显示器 把电场转换为一个可视点的精确机制依赖于所采用的技术。液晶显示器(LCD)中,电场改变了LCD材料中长水晶分子的极性。这使它允许光穿过平面,或阻止光穿过。有源矩阵板是每个像素位置都对应一个微型晶体管的液晶显示器。晶体管响应电场,根据电场的比例调整液晶,决定显示亮度的不同级别。另外,晶体管能够存储液晶的调整状态,于是显示器就不再需要刷新。这样就产生了一种更明亮的显示器。彩色液晶显示器的分辨率可以达到800×1000像素或更高。 等离子显示器结构上近似于图1.45。不同的是,板内在每个像素位置放置了一个非常小的氖管,这个管子通过电场控制开关。像有源矩阵显示器一样它也不需要刷新。 其他类型的光栅显示器可参阅参考文献,如【Foley90, APeer85】. 1.4.5 硬拷贝光栅设备 常常有人需要图像的永久版本,通常保存在纸或胶卷上。一些光栅设备提供了光栅图像的硬拷贝,通过把帧缓存的信息以像素的形式传输到显示媒质上来保存图像。  激光打印机。激光打印机从其内部的帧缓存中扫描出光栅图案,然后激光迅速地扫过内部绘图表面。在激光扫过的某一点时,表面变得带电,吸引墨粉附着在点上,转到纸上的墨粉绘制成了图片。利用激光定位的高精确度,激光打印机能提供比喷墨打印机和点阵打印机高得多的分辨率。 图1.46(a)展示了点阵打印机(最早大量生产的高速打印机类型)绘制的文本和图形,图1.46(b)展示了激光打印机的输出。点阵打印机的点密度大约只有每英寸70点数(dpi)。现在点阵打印机通常用在收款台打印收据,例如餐馆和商店。另一方面,激光打印机能提供大约每英寸600多点数,所以能打印出高质量的图形。 图1.46 放大的点阵打印机和激光打印机图像 更高密度的图像可通过出版行业的印刷机获得,例如莱诺特朗照相排字机,每英寸2540点数。本书就是在这样的设备上印刷的。 当今,许多印刷机配置有内部的微处理器,通过编程可以解释Postscript. Postscript是一种页面描述语言,它能在打印页上生成高质量的文本和图像。当包含这种描述语言的文件印刷时,Postscript编译器就生成了如图1.47(b)所示的图像。 图1.47 页面描述语言和用它生成的图像  喷墨绘图机。喷墨绘图机用来制作彩色的硬拷贝光栅图像。很小的喷嘴扫过纸张,然后在每个像素的位置喷上正确的墨色。  胶片记录仪。在胶片记录仪中,屏幕是一卷照相胶片,当电子光束以光栅模式扫描时电子束使胶卷曝光。有时,胶片记录仪是独立的帧缓冲设备,或只是直接安装在液晶显示器上的简单相机。胶片记录仪常用来制作高质量的35毫米幻灯片或电影。磁带录像机用来制作存储在帧缓冲中的图像硬拷贝,然后在电视机上播放。 1.5 图形输入的基本单元和设备 许多输入设备可以让用户控制计算机,但是键入命令的方式可能是笨拙的。指向屏幕上显示的特定目标以选择下一步做什么,可能更加轻松自然。 你能以两种方式考虑输入设备: 物理上它是什么,逻辑上它做什么。每种设备是物理上的某件机器像鼠标、键盘或跟踪球,它采用某种方式配合用户手工操作,这些操作被评估并把相应的数字信息送回图形程序。 通过检查送到程序的数据类型,我们首先看看输入设备做些什么,然后我们介绍现在常用的数据类型。 1.5.1 逻辑上的输入图形基元类型 每个设备传送特定类型的数据(比如,数字、字符串或坐标位置)给程序。不同种类的数据被称为输入的基本单元。 重要的逻辑输入基本单元包括:  字符串。字符串产生设备是大家最熟悉的,它生成字符串并为键盘的输入建模。当一个程序需要一个字符串时,程序暂停以等待用户的键入,输入的字符串后跟一终止字符来激活程序继续运行。  赋值. 赋值设备产生一个介于0.0~1.0之间的实数值,可用来确定划线的长度、运动的速度或图像的尺寸。它能够产生0到1之间的任意实数。  定位。交互式图形的基本需求是能够让用户指向显示器的某个位置。定位输入设备实现了这个功能。它可以生成坐标(x, y)。用户操作输入设备(通常是鼠标)是为了把可见的光标指向某点,然后触发选择。它返回给应用程序x、y坐标和触发值。 当程序开始运行时,图形工作站被初始化。每个逻辑输入程序都和一个已安装的物理设备相联接。  选取。选取输入设备是用来识别图像的某一部分以便于进一步处理。一些图形包允许图像定义成段的形式。段是相关图元的集合。例如,在饭店里顾客经常看到服务员按屏幕的不同部分来确认送给厨房的菜单。图1.48(a)展示了饭店销售系统(POS)的例子。销售系统的功能是搜索屏幕来决定与服务员的指压区域相关联的食品项(如主菜、开胃菜、沙拉、餐后甜品、饮料等)。注意,图1.48(a)所示的销售系统除了屏幕以外没有其他的输入设备。销售系统的触摸屏(图1.48(b)所示)定义了段,用来识别用户确认的名字,以加速点菜过程。 图 1.48 (a) 一个POS系统; (b) POS系统触摸屏的放大图 当使用pick()函数,用户通过某一物理输入设备指向图片的某一部分,然后图形包确定指向的是哪一个段。pick()返回段名给应用程序。用户能够擦除、移动或操作这些段。 1.5.2 物理输入设备的类型 我们从另一角度--与个人计算机或工作站相连的机器--来介绍一下输入设备。  键盘。所有的工作站都配备有键盘。它根据要求输入字符串。因此键盘是最常见的生成串的逻辑设备。一些键盘有鼠标键或功能键,常用来产生选取操作。  按钮。有时工作站安装有独立的一排按钮。用户按其中的一个按钮来实现一个选取输入功能。 图1.49 图形写字板 鼠标。鼠标也许是所有输入设备中大家最熟悉的,因为它便于操作。当用户在桌面上滑动鼠标时,鼠标将它的位置变化传送给工作站。工作站内的软件跟踪鼠标的位置,并在屏幕上移动一个图像光标--小箭头或十字。鼠标常用来执行定位功能。鼠标上通常有一些按钮,通过用户点击来触发行动。  写字板。像鼠标一样,写字板用来生成定位。一个写字板,如图1.49所示,提供了一个用户可以用笔在上面滑动的区域。笔尖有一个微型开关。通过按压笔,用户能够触发逻辑功能。 写字板特别适合数字化制图: 用户可以在写字板表面铺一幅图,然后在图上移动笔,按压并输送每个新点给工作站。有时菜单区域也会印在写字板表面,用户可以选择菜单项。每个菜单项对应着一个合适的软件,点击即可运行。一个与写字板相似而又小巧的设备是PDA(个人数字化助手),它也是通过使用笔选择显示屏上的特定位置来完成操作。 操纵杆和跟踪球 图1.50显示了三个相似的输入设备,均是用来控制显示屏上鼠标的位置。图1.50(a)中的拱形操纵杆中有一个控制杆,能够转向任一方向以指示位置。图1.50(b)的跟踪球中有一个大球,能够握在手掌中向任意方向旋转,以改变光标的位置。对于图1.50中的每个设备,内部的线路都把物理运动转变为电子信号,就像鼠标那样。这些设备主要用来作为赋值设备和定位输入设备。 图 1.50 (a) 操纵杆; (b) 跟踪球; (c) 数据手套 数据手套 图1.50(c)中所示的数据手套是一种相对比较新的输入设备。它被设计为通过手和手指的运动,提供给用户清楚的即时控制功能。数据手套内部的传感器获取手的细微运动并把它们转化成数值返回给程序。这种设备特别适合于与程序相关联的运动环境(比如用户控制一个虚拟的机器人手). 三维物体的数字化和运动捕获 图1.51(a)展示了一个能够测量空间中点的位置,并进而能够获取三维形状的设备。当激光束以x,y光栅模式扫描过实体目标表面时,图像捕获设备与物体之间的距离被记录下来。图1.51(b)展示了数字化人形的合成图像。 图1.51 数字化三维模型 图1.52展示了一个相似的设备,它能够实时跟踪一个运动物体上许多点的位置。它能够获取一个舞蹈家的所有运动细节,以用于动画或数据分析。过去也曾使用过一些其他设备,如光笔、拇指轮、桨等(相关的完整描述可在【Foley93】和【Rogers98】等书中找到). 图1.52 获取舞蹈家的动作 本章小结 本章我们介绍了计算机图形学的研究领域,展示了它的各个应用领域。我们还描述了多种绘图设备,其中最广泛使用的是光栅视频显示器、喷墨打印机以及激光打印机。我们也定义了主要的输出图元--折线、文本、填充区域和光栅图像--并描述了各自相关的属性。因为在图形学领域中着色图像在显示设备图片媒介中的重要作用,我们着重强调了光栅图像,对这种图像的探讨将贯穿本书。光栅图像的关键特性是它由数字集合组成,每个数字只能从有限个元素的集合中选取。计算机中二维像素值的存储位置是离散的,这种离散的表示在本书中将多次提到。 我们还描述了各种用于交互式计算机图形学的图形输入设备,并探讨了它们产生的各种输入图元。 本章习题 1.1 说出5种逻辑输入设备的名称(如字符发生器等),并说出能用来提供相应逻辑输入的一种物理输入设备(如键盘). 1.2 说出计算机图形系统的主要部分,简单描述各部分的功能。 1.3 当一种显示器有600×800像素的分辨率,每个像素能显示65 000种颜色,刷新率是每秒60次时,每秒钟有多少比特传送给显示设备? 进一步阅读 一些书提供了很好的关于计算机图形学的介绍。Hearn 和 Baker 【Hearn04】以一种轻松而有趣的方式总结了这个领域,并附带了大量的例子。Foley 和 Van Dam【Foley93】和David Rogers【Rogers98】提供了许多种图形输入和输出设备的附加技术细节。一套被称为“图形学珍宝”的非常好的系列书【Gems】,1990年第一次出版,汇集了全世界图形学研究者和实践者的众多新思想和“珍宝”. 还有一些关于图形学新技术的杂志。其中最有影响的是“IEEE Computer Graphics and Applications" ,它常常介绍一些受图形学影响的新领域进展情况的文章。反映图形学新技术的顶级会议和期刊是计算机图形学专业兴趣小组年会(Proceedings of SIGGRAPH 【SIGGRAPH】)和美国计算机学会图形学会刊【TOGS】. 第2章OpenGL绘图入门 本章学习目标  开始编写图形程序  学习OpenGL程序的基本组成  开发绘制直线、折线和多边形的基本图形工具  学会用鼠标和键盘控制程序 预览 2.1节讨论了生成简单图形程序的基础知识,包括设备无关编程的重要性以及基于窗口和事件驱动编程的一些基本知识。2.2节介绍了OpenGL作为设备无关的应用程序接口(API)的用法(全书都将基于这个用法),说明了如何画不同的基本图形。通过北斗星图、Sierpinski垫片图以及数学函数曲线图等绘图实例说明OpenGL的用途。2.3节讨论了如何基于折线和多边形绘图,并开始建立个人的图形工具库。2.4节讨论了交互式图形编程,通过交互式编程,用户可以使用鼠标和键盘控制程序的运行。本章结尾给出了许多案例,通过这些案例可以加深对本章主要概念的理解。 2.1 生成图像初步 像许多学科一样,通过练习(编写和测试生成各种不同图形的程序)可以很快地掌握计算机图形学。先从简单的任务开始,一旦掌握,就试着变变花样,看看有哪些变化,试着进一步绘制更复杂的场景。 注意: 正如前言里的承诺,对每个书中讨论过的程序,本书提供了一个完整的程序清单。方便之处在于,这些源代码只需复制和粘贴就能是有效的,程序将编译和连接合适的OpenGL库。不便之处在于,读者需要自己一行行去扫描和理解代码(这等同于数学书里的数学公式)。最好的学习办法是认真、仔细地学习每行代码或等式。当你这么做时,就能理解每个新概念。 为了便于开始学习图形学,需要一个可以编写和执行程序的软硬件环境。对于绘图来说,这个环境还必须包括显示图形的硬件(通常是CRT或LCD显示器,我们把它称为“屏幕”)和一个软件工具库。 每个图形程序都以一些初始化工作为开始,由此建立程序所需的显示模型和坐标系。图2.1给出了一些可能遇到的类型。在图2.1(a)中,整个屏幕都用于绘图: 初始化时,将显示器设置为“图形模式”,并且建立了坐标系。坐标x和y以像素为单位,x向右递增,y向下递增。 图2.1 一些常见的显示布局 图2.1(b)显示了现在常用的、基于窗口的系统。此类系统中,显示屏幕可以同时支持多个不同的矩形窗口。对于绘图程序来说,初始化工作包括创建和打开一个新窗口(我们把它称为“屏幕窗口”在图形学中,“窗口”一词用得有点过多,请注意区分。)。根据需要,以后还可以用命令创建多个这样的窗口。绘图命令使用一个附属于窗口的坐标系: 通常,x向右递增,y向下递增。图2.l(c)显示了另一种类型: 初始坐标系是x向右递增,而y向上递增。 计算机图形学 (OpenGL版)(第3版)第2章 OpenGL绘图入门一般来说,每个系统都有一些初级绘图工具,以帮助用户快速入门。最基本的工具有诸如setPixel (x, y, color)之类的名字,它把位于(x, y)上的单个像素设置为用color定义的颜色。这种工具有时有不同的名字,如putPixel()、SetPixel()或drawPoint()。像setPixel()一样,也有一个绘制直线的工具,例如line (x1, y1, x2, y2),它在(x1, y1)和(x2, y2)之间画一条直线。在其他系统中,这个工具可能称为drawLine()或Line()。下面的命令line(100, 50,150,80); line(150,80, 0,290); 可以在图2.1的任一系统中画出图形。实际上,每个系统都有line()或类似的命令。 一些程序员还可以很方便地找到有稍许变化的画线函数,这里介绍两种相关的函数。但是要记住,这些变化并不重要,了解这些变化只是为了加深对基本绘图操作的理解。它们基于当前位置(cp)这个概念,即由当前位置给出画笔的位置,这个概念源自笔式绘图仪。第一个函数moveTo (x,y)移动画笔到位置(x, y)(译者注: 移动,不画线),然后将当前位置设为(x, y);第二个函数lineTo (x, y) 从当前位置到(x, y)之间画出一条直线,并将当前位置(cp)更新为(x, y)。每个命令都将画笔从其当前位置移到一个新位置,一个可见,一个不可见。然后该新位置就变成当前位置(cp)。图2.1所示的图形可以用下面的命令绘制出来。moveTo(100,50); lineTo (150,80); lineTo(0,290);再举一个例子,如图2.2所示,画一个中心在(1, 1),边长为6个单位的正方形。该正方形是对齐的,即它的每条边都平行于某一坐标轴图2.2 一个用moveTo和lineTo命令 画的正方形 (本书经常使用“对齐的”这个术语)。画这个正方形是容易的: 用moveTo操作简单地将画笔移到一个角上,然后用lineTo使画笔画出正方形,最后,让画笔回到起始点。 画这个正方形的伪代码可以是这样的: moveTo(4,4); //画笔移到起始角 lineTo (-2,4); lineTo(-2,-2); lineTo(4,-2); lineTo(4,4); //闭合正方形对于一个具体的系统,精力充沛的程序员就可以用这些初级工具,开发出一整套具有复杂功能的工具包,从而构建一个强大的图形实例代码库。利用这个个人构建的代码库就可以写出任何的图形应用程序。 但是,一个明显的问题是,每种图形显示都使用不同的基本命令来驱动,并且每种环境都有不同的工具集来产生图形的基本单位(点、线等)。这使得把程序从一种环境中移植到另一种环境中变得很困难,并且这是每个程序员迟早都要面临的问题。所以,程序员必须基于新环境库建立必要的工具,这可能要对库或应用程序的整体结构做很大的改动,这对程序员来说工作量是很大的。 2.1.1 设备无关的编程和OpenGL 如果有一个统一的方法可以编写图形应用程序的话,这就太方便了。也就是说,同样的程序可以在不同的图形环境里编译和运行,并且能保证在每台机器上显示相同的图形。这就是众所周知的设备无关图形编程。OpenGL提供了这样的工具: 移植图形程序只需在新机器上安装合适的OpenGL库就可以了,不需要更改应用程序本身,可以用相同的参数在库中调用相同的函数,最终会产生相同的结果。OpenGL绘制图形的方法已经被许多公司所采用, 而且在所有重要的图形环境中OpenGL库已经存在附录1讨论了如何在不同的环境中得到和开始OpenGL. . 你可能已经认识到OpenGL是一个开源图形库,也就是说,组成OpenGL库的所有程序都可以通过因特网(http://www.opengl.org)免费下载。这与那些需付费才能得到或使用起来有某些限制的图形库,形成了鲜明的对比。 OpenGL常常被称为“应用程序接口 (API)" 。这个接口是程序员可以调用的例程,加上如何将这些例程组合在一起工作的模式。程序员仅仅“看见”接口,而不必处理位于常驻图形系统中特定的硬件或者软件。 我们将看到,绘制复杂三维场景时,OpenGL将表现出其最强大的功能,也可以认为,用OpenGL绘制简单二维物体有点大材小用。但是,用OpenGL绘制二维图形也有好处,因为它提供了一个统一的绘图方法。我们利用OpenGL提供的许多默认状态,先从比较简单的构造开始学习,然后再引入OpenGL中更强大的功能去编写程序,产生精心制作的三维图形。 虽然将要用封装好的OpenGL去开发我们的大部分图形工具,我们还是需要考查经典图形算法在OpenGL中是如何工作的。即使你用现成的OpenGL版本编写大多数程序,了解这些工具的实现方法也很重要。在特殊环境中,你可能想换一种算法来完成某项任务,或者可能遇到OpenGL无法解决的新问题(通观全书,应该指出,很少有用OpenGL不能达到的效果或要求). 2.1.2 窗口的编程 如前所述,很多现代图形系统是基于窗口的,并且管理多个重叠窗口的显示。用户可以用鼠标在屏幕上移动窗口,也可以调整窗口的大小。使用OpenGL,我们将在类似于图2.1(c)的窗口上作图。 事件驱动编程 大多数基于窗口的程序,一个显著的特征是事件驱动。这意味着程序要响应不同的事件,如单击鼠标、按下键盘上的按键,或者重新调整窗口的大小。系统自动管理事件队列,该队列接收已经发生的特定事件信息,并按照先来先服务的顺序处理这些信息。程序员将程序组织成回调函数的集合,这些回调函数一有事件发生就执行。当回调函数执行完后,应用程序从队列移走相应的消息,再从调用处恢复成等待状态。在接下来的章节里,我们将看到这方面的例子。程序员必须在每个回调函数内部编写事件产生时将发生什么事情的代码。当然,不需要写什么时候调用回调函数的指令,调用回调函数是系统自动处理的,这一点对程序的编写者和代码的任何读者来说很重要。 因为存在的事件类型不多,所以系统对于每一种可能发生的事件类型都创建了一个回调函数。全书大部分回调函数已经命名和定义好。但是,在你自己的程序中,你可以用你喜欢的方式命名和定义它们。当系统将某个事件从队列中移走时,系统只执行与该事件类型相关的回调函数。对于那些习惯于用“先做这个,再做这个”结构编程的程序员来说,需要改变思路。这种新的结构更像是: “什么也不做,等待事件发生,事件发生后再做指定的事”。这种结构使人想到事件循环。在那种情况下,系统在重复的循环中耐心地等待,直到收到一个事件触发信号。 回调函数和特定事件类型的关联方式常常与系统有很大关系。但是OpenGL提供了下面描述的许多支持库,这些库为事件管理和多种其他功能提供了帮助。其中一个是GLUT库,即“GL实用工具包(the GL Utility Toolkit)" ,它用来打开窗口,管理菜单和处理事件等。 注册回调函数 对程序员来说,一定有一个方法将每种类型的事件与要求的回调函数关联起来,这个方法称之为注册回调函数。程序中用到的每一个事件类型都必须用回调函数注册,该回调函数的名字和定义由程序员选择。下面是使用GLUT库,名为myMouse的回调函数例子,它很方便地注册了与鼠标关联的事件: glutMouseFunc(myMouse); / myMouse是程序员按自己的喜好命名的,glutMouseFunc是GLUT固有的名字/这行代码提示如何处理按下或点击鼠标时产生的事件。glutMouseFunc(注意大小写)是GLUT库的固有函数,但是,回调函数名字myMouse由程序员按自己的兴趣(事件类型)来命名,函数由程序员编写,被系统调用。程序员为myMouse()编写代码,处理每个可能感兴趣的鼠标动作。如果应用程序中没有使用鼠标交互,glutMouseFunc()就不需要写或被注册。 为了帮助程序员简化编程工作,实际上可以利用3个库来开发和运行程序。这里只作简要的介绍,详细的介绍留给后面的章节。下面是OpenGL应用程序的常用库列表。 4种主要的OpenGL库 (1) 基本GL库: OpenGL库的基础。它提供OpenGL的基本函数。我们将看到,每个OpenGL函数都以字符GL开头。 (2) GLUT库: GL实用工具包(the GL Utility Toolkit),上面已经讨论过,它主要用来打开窗口、开发和管理菜单,以及管理事件等。GLUT的菜单功能将在2.5节介绍。 (3) GLU库: GL实用库(the GL Utility Library),它提供高级例程,处理矩阵操作和绘制二次曲面如球和圆柱体(参见第6章). GLU库也提供将非凸和非简单多边形分解成简单形状(如三角形)的实用函数(基本的OpenGL处理不好这些操作)。它还在别的方面为简化程序员的工作提供帮助。所有这些在后面将作详细的介绍,或在因特网和附录的阅读材料【Woo04】中找到。 图2.3 GLUT菜单 (4) GLUI库: 用户接口库(the User Interface Library),只要使用GLUT,GLUI就将适当地运行。GLUI为OpenGL程序提供了良好的控制工具和菜单。2.5节描述了GLUI的菜单和可利用的控制工具。 图2.4展示的伪代码是事件驱动程序中main函数例子的一个基本框架,它为几类基本的事件类型,注册了必要的回调函数。本书中的大多数程序都以此框架(或稍作变化)为基础。主要有5种基本事件类型,每种都可以使用GLUT函数注册。  glutDisplayFunc (myDisplay): 无论系统何时决定重画一个屏幕窗口,它都会发出一个重新绘制事件。出现下列情况之一就会发生该事件: 第一次打开窗口,或覆盖它的窗口被移走而露出该窗口。函数myDisplay()在这里被注册为重新绘制事件的回调函数。  glutReshapeFunc (myReshape): 用户可以对屏幕窗口的形状进行调整,通常是用// 在此包含OpenGL库(像图2.11一样) void main() { glutDisplayFunc(myDisplay); //注册重绘函数 glutReshapeFunc(myReshape); //注册改变窗口形状函数 glutMouseFunc(myMouse); //注册鼠标动作函数 glutMotionFunc(myMotionFunc); //注册鼠标运动函数 glutKeyboardFunc(myKeyboard); //注册键盘动作函数 ...可能初始化其他工作... glutMainLoop(); //进入主循环等待事件发生 } ...所有回调函数功能在此定义 图2.4 使用OpenGL和GLUT的一个事件驱动程序样本 鼠标将窗口的一个角拉伸到一个新位置(只移动窗口并不产生该事件)。函数myReshape()在这里被注册为改变窗口形状的事件。正如我们将要看到的,myReshape ()自动传送参数,报告被调整的窗口的新宽度和高度。  glutMouseFunc (myMouse): 当按下或释放鼠标的某个按钮时,就发生了鼠标事件。函数myMouse()在这里被注册为鼠标事件发生时所调用的函数,它自动传送描述鼠标的位置和按钮状态参数。  glutMotionFunc (myMotionFunc): 当按下一个或几个鼠标按钮并且鼠标移动时,一个鼠标运动事件产生了。注意,正如我们在下面的例子所看到的一样,对于该事件的产生,鼠标按钮无需释放。另一方面,鼠标在没有按钮按下的情况下进入一个窗口,glutPassiveMotionFunc()将产生一个事件。  glutKeyboardFunc(myKeyboard): 这个函数用按下或释放键盘上按键的事件来注册函数myKeyboard (). myKeyboard ()自动传送参数,报告哪个键被按下。为了方便起见,它同时还传送按下键时鼠标的x和y坐标(相对屏幕窗口). 回调函数myMouse、myKeyboard等由程序员命名,只要它们的名字不相同就可以。如果没有定义myMouse,单击鼠标没有任何效果。同样,如果没有定义myKeyboard,程序就没有任何的键盘交互功能。程序员应该注意,使用鼠标或键盘函数时,glutDisplayFunc()不会被自动调用,即重新绘制函数需要在鼠标或键盘回调函数中被显式的调用。相反,函数glutPostRedisplay()将会刷新窗口显示,并且可以在程序中的任何位置被调用。2.4节橡皮矩形例子中能看到函数glutPostRedisplay()的作用。 图2.4中的最后一个函数是glutMainLoop()。当执行这个函数时,程序绘制完初始图形,并进入循环,一直等待事件发生。用鼠标单击关闭窗口按钮(大多数情况下,关闭窗口按钮在每个窗口的右上角),程序正常终止。 2.1.3 如何打开一个窗口画图 画图的第一个任务就是打开一个用于画图的屏幕窗口。这个过程相当复杂,并且与系统相关。因为OpenGL函数是设备无关的,所以它们对于任何指定系统中的窗口控制不提供任何支持。但是前面介绍过的GLUT,却含有可以在所使用系统中打开窗口的函数。 图2.5扩充了上面的框架,显示了将在屏幕窗口中绘制图形的完整main()函数。前5个函数调用工具包打开用于绘图的窗口。在你的第一个图形程序中,可以原样复制这些函数。图2.5中也给出了相应的注释,标出了多种参数的含义,以及怎样替换这些参数实现某种效果的方法。前5个函数初始化并显示屏幕窗口。下面简述每个函数。  glutInit (&argc,argv): 该函数初始化工具包,其参数是传送有关命令行信息的标准参数,这里不会用到它们。  glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB): 该函数指明显示屏幕窗口如何被初始化。内置常数GLUT_SINGLE和GLUT_RGB是“或”的关系,表明应分配单个显示缓存,而且颜色由所需的红、绿、蓝三色的数量来指定(第3章将讨论产生平滑动画的双缓存模式).  glutInitWindowSize(640, 480): 该函数指定屏幕窗口的初始尺寸,宽640像素,高480像素。程序运行时,用户可以根据需要调整窗口的大小。  glutInitWindowPosition(100, 150): 该函数指定窗口在屏幕上相对于左上角的位置,距左边100个像素,距顶端150个像素。程序运行时,用户可以根据需要移动窗口。  glutCreateWindow("my first attempt"): 该函数打开并显示屏幕窗口,并将标题"my first attempt"(意为: 我的第一次尝试)置于标题栏中。//适当的#include语句--参见图2.11 void main(int argc, char argv) { glutInit(&argc, argv); //初始化工具包 glutInitDisplayMode(GLUT_SINGLE|GLUT_RGB); //设置显示模式 glutInitWindowSize(640,480); //设置窗口大小 glutInitWindowPosition(100, 150); //设置窗口在屏幕上的位置 glutCreateWindow("my first attempt"); //打开屏幕窗口 // 注册回调函数 glutDisplayFunc(myDisplay); glutReshapeFunc(myReshape); glutMouseFunc(myMouse); glutKeyboardFunc(myKeyboard); myInit(); //必要的其他初始化工作 glutMainLoop(); //进入循环 } 图2.5 使用OpenGL 实用工具包GLUT打开初始窗口绘图源代码main()中的其他函数注册前面描述过的回调函数,执行其他初始化工作,并且启动事件循环处理。 2.2 OpenGL的基本图形元素 我们要开发的编程技术可以用于绘制许多的几何形状,这些形状将组成有趣的图形。绘制命令将被放在与某个重新绘制事件相关联的回调函数中,例如上文提到的函数myDisplay(). 我们首先必须建立一个描述图形对象的坐标系,并规定它们出现在屏幕窗口上的位置。计算机图形编程工作似乎就是不断地定义并管理各种坐标系。所以,这里以简单坐标系为开端,然后再进入更复杂的工作。 我们从一个直观的坐标系开始。该坐标系直接捆绑在屏幕窗口的坐标系上(如图2.1(c)所示),其测量单位是像素。我们的第一个屏幕窗口例子如图2.6所示,宽为640像素,高为480像素。x坐标从左边界的0增加到右边界的639,y坐标从底边的0增加到上边的479. 在讨论一些基本的元素后,我们将展示如何建立这种和其他种类的坐标系。 图2.6 初始绘图坐标系 OpenGL提供了绘制第一章所述的所有输出元素的工具。其中大多数,如点、线、折线和多边形,都由一个或多个顶点所定义。为了在OpenGL中绘制这些对象,必须传送一系列的顶点。这些顶点应该处于glBegin()和glEnd()这两个OpenGL函数之间。函数glBegin()的参数确定要画哪个对象,指示OpenGL开始收集绘制对象的元素数据,glEnd()命令结束并完成对象的元素列表,并将所有数据送到OpenGL图形绘制管道中进行绘制。 例如,图2.7显示了在宽640像素,高480像素的窗口中画的3个点。这些点是用下面的命令序列绘制的:glBegin (GL_POINTS); //画独立的点 glVertex2i (100, 50); //点 glVertex2i (100, 130); glVertex2i (150, 130); glEnd();常数GL_POINTS内置于OpenGL中。为了绘制其他类型的元素,可以将GL_POINTS置换成GL_ LINES、GL_ POLYGON等。以后会依次介绍它们。 随后将会看到,这些命令将顶点的信息送到图形绘制管道中。在那里,它们要经历几个处理步骤(详见第5章)。虽然实际的OpenGL绘制管道包含许多步骤,对于当前这个目标(画3点),只考虑管道按几个指定顺序操作,有哪些元素被送去显示就行了。 OpenGL中的许多函数,如glVertex2i()或glColor3f(),有几个可变的部分,据此可以区别传送给函数的参数数目及类型。图2.8展示了不同的函数格式。 图2.7 画3个点 图2.8 OpenGL命令格式 前缀gl表明函数来自基本的OpenGL库(相对的,glut来自GL实用工具包GLUT),接着就是基本命令的词根(例如vertex和color),再接下来是发送给函数的参数数目(常常是3或4个),最后是参数的类型(i表示32位整型数,f表示浮点数等,后面有进一步描述)。为描述方便,以后如果仅仅使用基本命令而不去考虑函数的参数特性,我们用一个星号代替,如glVertex (). 例如,为了生成上述同样的3个点,可以使用下面的命令,用浮点值代替整数值。glBegin(GL_ POINTS); glVertex2f(100.0,50.0); //用浮点类型指定点 glVertex2f(100.0,130.0); glVertex2f(150.0,130.0); glEnd();OpenGL数据类型 OpenGL内部使用特定的数据类型。例如,函数glVertex2i()就使用32位的整数值。众所周知,有些系统将C或C++数据类型int理解为16位的整型,而有些系统则把它当作32位的整型。为在不同的系统中得到相同的显示,我们建议在程序中使用数据类型GLint。同样,对于float或double类型的大小,也没有一致的标准。图2.9列出了 OpenGL数据类型,有些类型在本书的后面章节才会碰到。后 缀数 据 类 型典型的C或C++类型 OpenGL类型名 b8位整型 signed char GLbyte s16位整型 short GLshort i32位整型 int或long GLint,GLsizei f32位浮点型 float GLfloat,GLclampf d64位浮点型 double GLdouble,GLclampd ub8位无符号整型 unsigned char GLubyte,GLboolean us16位无符号整型 unsigned short GLushort ui32位无符号整型 unsigned int或unsigned long GLuint,GLenum,GLbitfield图2.9 命令后缀和参数数据类型 OpenGL状态 OpenGL是由许多状态变量组成的状态机,这些状态包括点的大小、绘图的颜色和屏幕窗口大小等。在给定新值之前,状态变量的值一直保持不变。点的大小可以用glPointSize()来设置,它是一个浮点变量。如果该变量是3.0,通常这个点就会绘制成方形,每边都是3个像素。 画图的颜色可以用下面这条语句来设置: glColor3f(red, green, blue); 其中红色、绿色和蓝色的值可以在0.0~1.0之间变化。例如,常见的颜色由下列一定数量的红色、绿色和蓝色构成(灰色由相同数量的红色,绿色和蓝色组成): glColor3f(1.0,0.0,0.0); //设置红色 glColor3f(0.0,1.0,0.0): //设置绿色 glColor3f(0.0,0.0,1.0): //设置蓝色 glColor3f(0.0,0.0,0.0): //设置黑色 glColor3f(0.7,0.7,0.7): //设置浅灰 glColor3f(0.2,0.2,0.2): //设置中灰 glColor3f(0.1,0.1,0.1): //设置深灰 glColor3f(1.0,1.0,1.0): //设置亮白 glColor3f(1.0,1.0,0.0): //设置亮黄 glColor3f(1.0,0.0,1.0): //设置洋红 glColor3f(0.0,1.0,1.0): //设置青色第11章将详细讨论颜色。背景颜色用glClearColor (red, green, blue, alpha)来设定,其中alpha用来设置透明度,后面将会详细讨论它(这里使用0.0)。注意这条指令只是为后面的使用设置状态变量,它不做任何看得见的工作。为了将整个窗口都清除成背景色,需使用 glClear(GL_COLOR_BUFFER_BIT)函数。参数GL_COLOR_BUFFER_BIT是另一个OpenGL内置常数。 坐标系的建立 现在我们建立初始坐标系的方法看起来比较模糊,但是到下一章,讨论窗口、视口和剪裁后就会比较清晰了。在此只考虑几条必需的命令。图2.10中的函数myInit()用来设定坐标系很方便。在后面将会看到,OpenGL会例行公事地执行很多变换,这些变换用矩阵来完成。在myInit()中的命令通过控制管理矩阵来达到此目的。图2.10中的例程gluOrtho2D()用来设定屏幕窗口所需要的变换,图2.10将屏幕窗口设置为640像素宽与480像素高。gluOrtho2D(0, 640.0, 0.0, 480.0)例程来源于GLU库。void myInit(void) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0, 640.0, 0.0, 480.0); }图2.10 建立一个简单的坐标系组合在一起: 一个完整的OpenGL程序 图2.11是一个完整的程序,用来绘制图2.7中简单的3个圆点。随后就会看到,该程序很容易扩展成绘制更有趣物体的程序。初始化函数myInit()设置坐标系、点的大小、背景颜色以及绘图颜色。绘图函数(由程序员提供)封装在回调函数myDisplay()中。当然,回调函数是必须注册的。因为这个程序没有交互功能,所以就没有使用其他的回调函数。绘制完点以后就调用glFlush(),以保证所有的数据被完全处理并显示。这对某些运行在网络上的系统来说是非常重要的: 数据被缓冲到服务器上,只有当缓冲区的数据填充满或执行glFlush()时,数据才会被发送到客户端显示。 2.2.1 几个点丛绘制的例子 点丛是由大量点组成的某种图案。下面介绍几个有趣的点丛例子,用基于图2.11的程序很容易产生并绘制这些点丛: 在为重新绘制事件注册的回调函数glutDisplayFunc()中,做一些适当的改变就可产生每个点丛。你应该去实现并测试每个例子,熟悉它们以积累你的经验。 例2.2.1 北斗星群 图2.12显示了用8个圆点表示的北斗星图,在夜晚的天空中经常会看到它们。 北斗星群中这8颗星星(从夜空中的某一方位看)的命名和位置由下列数据给出: {Dubhe, 289, 190}、{Merak, 320, 128}、{Phecda, 239, 67}、{Megrez, 194, 101}、{Alioth, 129, 83}、{Mizar, 75, 73}、{Alcor, 74, 74}、{Alkaid, 20, 10}(一些人裸眼只能看到北斗星群中的7颗。从前,在美国本土的一些土著部落中,它被用来测试眼睛的敏锐度,赢者将获得一晚的晚餐)。由于所包含的数据点非常少,所以很容易直接将它们列出来,或者将这些数据直接写到代码中。另一方面,绘制许多点时,数据直接列表的方式就不明智#include //根据系统需要使用 #include #include #include //<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>> void myInit(void) { glClearColor(1.0,1.0,1.0,0.0); //设置背景颜色为亮白 glColor3f(0.0f, 0.0f, 0.0f); //设置绘图颜色为黑色 glPointSize(4.0); //设置点的大小为4×4像素 glMatrixMode(GL_PROJECTION); //设置合适的矩阵-后面将解释 glLoadIdentity(); //后面将解释 gluOrtho2D(0.0, 640.0, 0.0, 480.0); //接下来将完整地说明 } //<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>> void myDisplay(void) { glClear(GL_COLOR_BUFFER_BIT); //清屏 glBegin(GL_POINTS); glVertex2i(100, 50); //画一些点(不知道多少) glVertex2i(100, 130); glVertex2i(150, 130); glEnd(); glFlush(); //送所有输出到显示设备 } //<<<<<<<<<<<<<<<<<<<<<<<< main >>>>>>>>>>>>>>>>>>>>>> void main(int argc, char argv) { glutInit(&argc, argv); //初始化工具包 glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); //设置显示模式 glutInitWindowSize(640,480); //设置窗口大小 glutInitWindowPosition(100, 150); //设置屏幕上窗口位置 glutCreateWindow("my first attempt"); //打开带标题的窗口 glutDisplayFunc(myDisplay); //注册重画回调函数 myInit(); glutMainLoop(); //进入循环 } 图2.11 一个完整的画3点的OpenGL程序了。所以程序员就将它们存储在一个文件里或使用数学公式计算来产生这些点,然后程序就会从文件中读出这些点并将它们绘制出来。这些点可以取代图2.7中指定的3个点。用这个星座图做试验是很有趣的,可以尝试不同的点大小,以及不同的背景和绘图颜色。 例2.2.2 Sierpinski垫片 图2.13 显示了迷人的Sierpinski垫片。它的点丛是由程序生成的,这就意味着每个点都由一个过程法则来确定。虽然这个法则非常简单,但是最后的图案显示的却是无穷复杂的分形(参见附录5)。我们首先以直观的方式处理这些生成Sierpinski垫片的规则,并提供实现Sierpinski垫片的一段代码。 图2.12 北斗星群 图2.13 赛平斯基垫片 怎样产生Sierpinski垫片 Sierpinski垫片通过多次调用drawDot()生成,其参数是(x0,y0),(x1,y1),(x2,y2)等一系列点的位置。其中,这些位置参数由一个简单算法确定。drawDot函数为: void drawDot(Glint x,Glint y) { glBegin(GL_POINTS); glVertex2i(x,y); glEnd(); }Sierpinski算法如下: 第k个点表示为Pk=(xk, yk)。每个点都基于前一个点Pk-1。过程如下: (1) 选择3个固定的点T0、T1和T2,构成一个三角形,称为每个Sierpinski垫片的父三角形,如图2.14(a)所示。 (2) 随机挑选父三角形顶点T0、T1和T2中的一点作为要绘制图形的初始点P0. 迭代下面的(3)~(5)步,直至图案填充完毕。 (3) 随机选择T0、T1和T2中的一点,称为T. (4) 构造下一个点Pk,作为T和前一个已建好的点Pk-1之间的中间点为了找到(3, 12)和(5, 37)两点的中间点,分别将它们的x和y值平均后取整:(3, 12)和(5, 37)的中间点为((3+5)/2, (12+37)/2)=(4, 24.5)。注意,坐标无需是整数。。也就是说, Pk= Pk-1和T 之间的中间点。 (5) 用drawDot()绘制Pk. 图2.14(b)展示了该过程的几次迭代。假定初始点P0正好是T0,并让T1是下一个被选中的点,那么所绘制的P1就是T0和T1之间的中点。假定下一个选中T2,那么P2就是P1和T2的中点。接着,假定再次选中T1,那么所形成的P3就是如图所示的样子,依此类推。该过程不断地生成并绘制出新点(概念上来说是永不停止的),Sierpinski垫片图形很快就出现了。 图2.14 构建赛平斯基垫片 //一定要包括math或time头文件 //sierpinski_render 函数 void sierpinski_render(void) { glClear(GL_COLOR_BUFFER_BIT); // 清屏 GLintPoint T[3]={{10,10},{600,10},{300, 600}}; //定义三角形顶点 int index=random()%3; //随机选择初始顶点 GLintPoint point=T[index]; //生成一个含3个顶点的数组 drawDot(point.x, point.y); for(int i=0; i<55000; i++) //画55000个点 { index=random()%3; point.x=(point.x+T[index].x) / 2; point.y=(point.y+T[index].y) / 2; drawDot(point.x,point.y); } glFlush(); } 图2.15 产生赛平斯基垫片函数可以很方便地定义一个简单的结构GLPoint,用于描述坐标为浮点数的点: struct GLPoint { GLfloat x,y; };然后,我们创建并初始化包含3个坐标T[0]、T[1]和T[2]的数组来定义三角形的3个顶点,例如GLPoint T[3] = {{10.0, 10.0}, {300.0, 30.0}, {200.0, 300.0}}。在这个例子中,点产生后不需要将每个点Pk都存储起来,因为我们只是想要绘制它,然后继续下面的步骤。因此,我们设一个变量point来保存这个一直在变化的点,每次迭代中point都会被更新成新值。 用index=rand()%3来随机选择点T[i],rand()以相同的概率返回0、1或2中的任一值。图2.15列出了sierpinski_render函数,该函数运行完将生成55 000个Sierpinski垫片点。在你的程序中,用函数Sierpinski()替换回调函数myDisplay()即可。 rand()是什么 rand()是一个返回0和某个上限值之间的伪随机数(看起来是随机的数)的函数。既然我们想选择父三角形的3个顶点中的一个,我们调用index=rand()%3得到了0~2之间(包含0和2)的一个值。rand()是标准C++库的一部分,它返回0与某个上限值(我们的机器中这个值是32 767,它存储在RAND_MAX变量中)之间的值。对于大多数的随机数产生器,其生成的数似乎是随机的。但实际上,一旦给定第一个数后,其后生成的数也就确定了。因此,我们需给它一个称为“种子”的、合适的初始值。在运行时,这个种子值与系统运行时钟相关联,因为系统时间是一个一直在变化的值。每一个时刻,种子是唯一的。在程序运行的每个时刻,使用系统时间可以真正地返回一个不同的随机序列。 其他有意思的Sierpinski垫片 可以制作多种Sierpinski垫片,例如改变点的颜色,或用鼠标将父三角形的顶点拖到新的位置。后者将在下面的章节中描述,本书的网页上提供了Sierpinski垫片的应用。 例2.2.3 用点集绘制函数 假如想学习某个数学函数f(x)随x变化的情况,例如,下面这个函数:f(x)=e|-x|cos(2πx)  当x在0~4之间变化时,该函数如何变化呢?图2.16中所示的f(x)对x的曲线,就可以很好地将它们之间的变化关系反映出来。 图2.16 e|-x|cos (2πx)对x的函数曲线 为了绘制该函数曲线,可以简单地在x值闭区间集合[0, 4]中进行采样,并在坐标(x,f(x))处画一个点。在连续的x值之间,选择合适的增量,例如0.005,进行采样,该过程可以用以下算法描述: glBegin(GL_ POINTS) ; for(GLdouble x=0; x<4.0;x+=0.005) glVertex2d(x,f(x)); glEnd(); glFlush();但是这里存在一个问题: 它所产生的图形特别小,因为0~4之间的x值位于屏幕窗口左下角的前4个像素上。而且,任何f(.)的负值位于窗口的下面,我们根本看不到它们。所以我们需要进行缩放并重置这些数值,以使它们所产生的曲线正好出现在合适的屏幕窗口区域内。这里采用强制手段来实现该目标,即挑选一些数值使绘制的图形恰当地显示在屏幕上。随后我们将开发一个一般性过程来处理这些调整,即所谓的“将世界坐标系变换为窗口坐标系”的过程。 x方向缩放 假定需要在0~4范围内进行比例变换,使它能覆盖屏幕窗口的整个宽度(屏幕窗口的整个宽度用screenWidth表示,单位为像素)。我们只须按screenWidth/4.0来缩放所有的x值即可,即: sx=xscreenWidth/4.0;当x=0时,sx=0,当x=4.0时,sx=screenWidth,这正是我们所需要的。 y方向缩放与平移 函数f(x)的取值在-1.0~1.0之间,所以必须同时进行缩放并移动它们。假设屏幕窗口的高为screenHeight个像素,为了将曲线置于窗口的中心,就应缩放screenHeight/2,并向上平移screenHeight/2: sy=(y+1.0)screenHeight/2.0; 这里,当y=-1.0时,sy=0;当y=1.0时,sy=screenHeight. 请注意这种将x转换成sx,以及将y转换成sy的转换方式是: sx=Ax+B sy=Cy+D(2.1)  只要适当地选择常数A、B、C和D的值就可以了。A和C执行缩放;B和D执行平移。缩放和平移操作是仿射变换的一种基本形式。第五章将深入探讨仿射变换,它提供了一种简便统一的方法,可以将任意指定范围内的x和y映射到屏幕窗口上。 当A、B、C和D的值被适当地设置后,就可以使用下面的代码来绘制点曲线。GLdouble A, B, C, D, x; A=screenWidth/4.0; B=0.0; C=screenHeight/2.0; D=C; glBegin(GL_POINTS); for(x=0; x<4.0; x+=0.005) glVertex2d(Ax+B, Cf(x)+D); glEnd(); glFlush();图2.17给出了函数绘制的完整程序,阐明了多种成分是怎样相互匹配在一起的。该程序的初始化与图2.7中绘制三个点的程序初始化有些相似。注意屏幕窗口的宽度和高度被定义为常数,并且使用在程序中需要的地方。 图2.18(a)显示了两个重叠的函数,一个是f(x)=sin(x),x从-π到π。第二个函数是f(x)=sin(2x),x的变化范围与前者相同。 图2.18(b)显示了仅用一个参数(参数a)表示的曲线(第3章将详细介绍曲线的参数表示方法),x坐标定义为x(a)=10+sin(15a),变量a在0~2π间变化;y坐标定义为y(a)=sin(a),变量a在0~2π间变化。 图2.18(c)是复变函数sin(z2)的曲线,复变量z在一个正方形网格上变化。每个z的实部和虚部值映射成sin(z2)的实数和虚数,并逐点绘制出图2.18(c). #include //根据系统需要使用合适的include语句 #include #include #include const int screenWidth=640; //屏幕窗口的宽度,以像素为单位 const int screenHeight=480; //屏幕窗口的高度,以像素为单位 GLdouble A, B, C, D; //比例变换和平移值 //<<<<<<<<<<<<<<<<<<<<<<< myInit >>>>>>>>>>>>>>>>>>>> void myInit(void) { glClearColor(1.0,1.0,1.0,0.0); //背景颜色为白 glColor3f(0.0f, 0.0f, 0.0f); //画图颜色为黑 glPointSize(2.0); //点大小为2×2 像素 glMatrixMode(GL_PROJECTION); //设置"相机形状" glLoadIdentity(); gluOrtho2D(0.0, (GLdouble)screenWidth, 0.0, (GLdouble)screenHeight); A=screenWidth/4.0; //设置比例变换和平移值 B=0.0; C=D=screenHeight / 2.0; } //<<<<<<<<<<<<<<<<<<<<<<<< myDisplay >>>>>>>>>>>>>>>>> void myDisplay(void) { glClear(GL_COLOR_BUFFER_BIT); //清屏 glBegin(GL_POINTS); for(GLdouble x=0; x <4.0 ; x+=0.005) { GLdouble func=exp(-x)cos(23.14159265x); glVertex2d(Ax+B, Cfunc+D); } glEnd(); glFlush(); //送所有数据到显示 } //<<<<<<<<<<<<<<<<<<<<<<<< main >>>>>>>>>>>>>>>>>>>>>> void main(int argc, char argv) { glutInit(&argc, argv); //初始工具包 glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); //设置显示模式 glutInitWindowSize(screenWidth, screenHeight); //设置窗口大小 glutInitWindowPosition(100, 150); //设置窗口在屏幕上的位置 glutCreateWindow("Dot Plot of a Function"); //打开屏幕窗口 glutDisplayFunc(myDisplay); //注册重画函数 myInit(); glutMainLoop(); //进入永久循环 图2.17 一个完整的绘制点曲线函数程序图 2.18 三个不同的二维数学函数曲线 练习 练习2.2.1 任意函数f()的函数曲线。 考虑绘制f(x)=e|-x|cos(2πx)的函数曲线,其中x从xlow变到xhigh,f(x)则从ylow到yhigh。求适当的缩放和平移因子,使曲线正好位于宽为W像素、高为H像素的屏幕窗口中。 2.3 OpenGL中的直线绘制 如第1章所述,直线绘制是计算机图形学的基础算法。几乎每个图形系统都配有绘制直线的例程。使用OpenGL可以很方便的绘制直线: 用GL_LINES作为glBegin()的参数。为了绘制(40, 100)和(202, 96)两点间的直线,可以使用如下代码: glBegin (GL_LINES); //这里使用常数GL_ LINES glVertex2i(40,100); glvertex2i(202,96); glEnd();为了方便,可以将这个代码置于例程drawLine()中: void drawLine (GLint xl, GLint yl, GLint x2, GLint y2) { glBegin(GL_LINES); glVertex2i(xl,yl); glVertex2i(x2,y2); glEnd(); }另一个例程drawLineFloat()也可以类似地实现(想想如何实现). 如果在glBegin(GL_LINES)和glEnd()之间指定了多个顶点,它们将会被成对的取出,并且在每对之间画出一条单独的线。图2.19(a)所示的tic-tac-toe底板可以用下面的命令画出: glBegin(GL_LINES); glVertex2i(10,20); //第一条水平线 glVertex2i(40,20); glVertex2i(20,10); //第一条垂直线 glVertex2i(20,40); (在此的另外两条线还需调用4次glVertex2i()) glEnd(); glFlush();图2.19 4条线构成的简单图形 OpenGL提供了设定线条属性的工具。线条颜色的设定方式和点颜色的设定方式相同,都是使用glColor3f()。图2.19(b)所示的粗线条使用glLineWidth (4.0)设置,默认宽度是1.0. 图2.19(c)展示了点划线(点和破折号). 2.3.1 绘制折线和多边形 第1章介绍过,折线就是一系列头尾相连的线段的集合。它由一个有序点列表来描述,如: p0=(x0, y0), p1=(x1,y1),…,pn=(xn, yn)(2.2) 在OpenGL中,折线称为线带,通过在glBegin (GL_LINE_STRIP)和glEnd()之间依次指定顶点对: p0到p1,p2到p3等来绘制。例如下面的代码: glBegin(GL_LINE_STRIP); //画一条开放式折线 glVertex2i(20,10); glVertex2i(50,10); glVertex2i(20,80); glVertex2i(50,80); glEnd(); glFlush();产生如图2.20(a)所示的折线。折线的颜色、宽度及点划线等属性的设置和单条线段一样。如果需将最后一点和第一点相连,把折线变为多边形,只需用GL_LINE_LOOP替换GL_LINE_ STRIP即可,其多边形如图2.20(b)所示。 用GL_LINE_LOOP绘制的多边形不能填充颜色或图案。要想绘制填充的多边形,必须使用glBegin(GL_POLYGON),这将在后面介绍。 例2.3.1 画线图 在例2.2.3中,我们看到的是在一系列(xi, f(xi))位置处画点来绘制函数f(x)相对x的曲线。线图是这种概念的延伸,只不过将这些点简单地用线段连在一起以形成折线。图2.21是一个基于下列函数的例子:f(x)=300-100cos(2πx/100)+30cos(4πx/100)+6cos(6πx/100) 其中,x步长为3,共100步。 图2.20 折线和多边形 图2.21 一个数学公式图 线图的处理过程几乎与点图一样,只需将图2.17稍微改动。象点图需做缩放和平移一样,为了将图放置在窗口中的合适位置,线图也需要做变换。常数A、B、C和D的计算方法与以前一样(参见等式(2.1))。图2.22 给出了myDisplay函数中内部绘制循环必须做的修改。<计算常数A,B,C,D用于缩放和平移> glBegin(GL_LINE_STRIP); for(x=0; x<=300; x+=3) glVertex2d(Ax+B, Cf(x)+D); glEnd(); glFlush();图2.22 线图函数代码片断例2.3.2 绘制存储在文件中的折线 大多数有趣的图形都是由包含很多线段的折线构成。将折线存储在文件中,使用起来比较方便。这样的图形可以随时根据需要进行绘制。 编写一个绘制存储在文件中的折线的程序并不难。图2.23就是一个这样的例子。 图2.23 绘制存储在文件中的折线 假定文件dino.dat按下列格式包含一系列折线 (其中注释部分不包含在文件内). 21 文件中折线的数量 4 第一条折线的点数 169 118 第一条折线的第一个点 174 120 第一条折线的第二个点 179 124 178 126 5 第二条折线的点数 298 86 第二条折线的第一个点 304 92 310 104 314 114 314 119 29 32 435 10 439 … 等等 图2.24给出了一个用C++编写的完整的函数drawPolyLineFile(),该函数用于绘制存储在文件中的图形。读入文件的文件名(由字符串fileName指定),再依次读入每条折线并进行绘制。像myDisplay()一样,程序使用函数drawPolyLineFile()作为重新绘制事件的回调函数。必须确定A、B、C和D的值,以便正确地缩放这些折线。第3章将开发一种一般性的方法来完成这个任务。 这个版本的drawPolyLineFile()只做了很少的错误检查。如果文件不能打开--可能是把错误的名字传送给该函数了--这个例程就简单地返回一个错误。如果文件中有坏数据,比如在需要整数的地方却出现了实数,其结果是不可预料的。现在只能把该例程当做开发更强版本例程的一个起点。这个函数的一些改进可以是:当每次调用这个特殊的函数时,不需要再重读文件,而是将折线数据存在内存里(在这里,这些数据将立刻用于绘制,用{ fstream inStream; inStream.open(fileName, ios ::in); //打开文件 if(inStream.fail()) return; glClear(GL_COLOR_BUFFER_BIT); //清屏 GLint numpolys, numLines, x ,y; inStream>>numpolys; //读折线数量 for(int j=0; j>numLines; glBegin(GL_LINE_STRIP); //画下一条折线 for (int i=0; i>x>>y; //读下一对x,y glVertex2i(x, y); } glEnd(); } // 结束 j 循环 glFlush(); inStream.close(); } //结束drawPolyLineFile() 图2.24 画存储在文件中的折线函数后就丢弃了). 例2.3.3 参数化图形 图2.25展示了一个由几条折线组成的简易房子。它可以用图2.26中的部分代码绘制出来。但对于绘制门和窗,什么代码才合适呢?图2.25 一栋房子 图2.26不是一个灵活的方法。每个端点的位置都被强制写入代码中,所以hardwirededHouse()只能绘制出相同大小和位置的房子。但如果我们把这个图参数化,并将参数值传送到例程中,就有更好的灵活性。用这种方法,可以绘制出有着不同参数值的物体簇。图2.27给出了这种方法,其中用参数指定房屋顶点的位置以及房子的宽度和高度。绘制烟囱、门和窗等细节留给读者作为练习。void hardwiredHouse(void) { glBegin(GL_LINE_LOOP); glVertex2i(40, 40); //画房子的外形 glVertex2i(40, 90); glVertex2i(70, 120); glVertex2i(100, 90); glVertex2i(100, 40); glEnd(); glBegin(GL_LINE_STRIP); glVertex2i(50, 100); //画烟囱 glVertex2i(50, 120); glVertex2i(60, 120); glVertex2i(60, 110); glEnd(); //画门 //画窗户 } 图2.26 用固定尺寸画房子void parameterizedHouse(GLintPoint peak, GLint width, GLint height) //房子的顶点: peak, 给定房子大小: width,height { glBegin(GL_LINE_LOOP); glVertex2i(peak.x, peak.y); //画房子的外形 glVertex2i(peak.x+width/2, peak.y-3height/8); glVertex2i(peak.x+width/2 peak.y-height); glVertex2i(peak.x-width/2, peak.y-height); glVertex2i(peak.x-width/2, peak.y-3  height/8); glEnd(); //用同样的方式画烟囱 //画门 //画窗户 } 图2.27 画一个参数化的房子这个例程可以用于绘制图2.28所示的村庄,用不同的参数值多次调用 parameterizedHouse()就可以了(房子是如何翻转的?图中所有的房子都能用所给出的例程绘制吗). 图2.28 用parameterizedHouse()画的村庄 例2.3.4 构造一个折线绘图器 我们将会看到,一些应用程序计算并用一个列表存储折线的顶点。所以自然会想到,应该往不断扩大的例程工具箱中加入一个接受列表作为输入参数并绘制相应折线的函数。这个列表可能是数组或者链表的形式,在此使用数组形式,并定义图2.29中所示的类来保存数组。图中的struct GLintPoint将在下面的章节中讨论。对于那些想处理大量数据(可能超过10 000个数据)的程序员来说,C++ STL(标准模板库)容器可能是一个安全的数据结构。struct GLintPoint { Glint x, y; }; class GlintPointArray { const int MAX_NUM=10000; // 使用符号常数MAX_NUM方便以后改动 public: int num; GLintPoint pt\; }; 图2.29 顶点数组的数据类型图2.30给出了一个绘制折线的例程。这个例程采用了参数closed: 如果closed非0,则折线中最后一个顶点就会与第一个顶点相连,使图形闭合成为一个多边形。closed的值决定了glBegin()的参数。这个例程只是简单地将折线的每个顶点送给OpenGL. void drawPolyLine(GlintPointArray poly, int closed) { if(closed) glBegin(GL_LINE_LOOP); //此情况下画闭合的折线 else glBegin(GL_LINE_STRIP);//此情况下画开放的折线 for(int i=0; i>>>>>>>>>>>>> void moveTo(GLint x, GLint y) { CP.x=x; CP.y=y; //更新CP } //<<<<<<<<<<<< lineTo >>>>>>>>>>>>>>>>> void lineTo(GLint x, GLint y) { glBegin(GL_LINES); //画线 glVertex2i(CP.x, CP.y); glVertex2i(x, y); glEnd(); glFlush(); CP.x=x; CP.y=y; //更新CP } 图2.31 用OpenGL定义moveTo()和lineTo()注意: 变量CP是全局变量,要小心处理。 2.3.3 绘制边校正的矩形 多边形的一个特例是边校正的矩形。之所以叫这个名字,是因为它的边与坐标轴平行。我们可以创建函数来绘制一个边校正的矩形,不过OpenGL已提供了现成的函数glRecti(). glRecti(GLint x1,GLint y1,GLint x2,GLint y2); //以(x1,y2)和(x2,y2)为对角点绘制矩形 该命令基于两个给定点绘制一个边校正矩形,并以当前颜色填充矩形。图2.32展示了用下面所示代码绘制的图形。gIClearColor(1.0,1.0,1.0,0.0); //设置白色背景 glClear(GL_COLOR_BUFFER_BIT); //清除窗口 glColor3f(0.6,0.6,0.6); //浅灰 glRecti (20,20,100,70) ; glColor3f(0.2,0.2,0.2); //深灰 glRecti(70,70,150,130); glFlush();注意第二个矩形画在了第一个的上面。 图2.33展示了两个更加复杂的例子。图2.33(a)是随机生成的矩形雪花,图2.33(b)是人们很熟悉的具有不同灰度级的棋盘。如何生成这样的图形,留给读者作为练习。 图2.32 两个颜色填充的边校正矩形 图 2.33 (a) 随机的矩形雪花; (b) 棋盘 2.3.4 边校正矩形的长宽比 边校正矩形的主要特性是它的尺寸、位置、颜色以及形状。它的形状可以用长宽比来描述,矩形的长宽比定义为宽度与高度的比值注: 一些作者将它定义为高/宽。: 长宽比=宽高(2.3)不同长宽比的矩形如图2.34所示。 矩形A是一个8.5英寸×11英寸的纸张形状,即所谓的山水画定位(也就是说,矩形的长大于高)。矩形A的长宽比是1.294. 图2.34 边校正矩形长宽比的例子 矩形B具有电视屏幕的长宽比,即比例为4/3。而C是著名的黄金分割矩形(将在案例2.3中具体描述),它的长宽比接近= 1.618034。长方形D是一个长宽比为1的正方形。E是一个标准肖像纸张的形状,高宽比是0.7727。最后,F是一个又高又细的长方形,高宽比为1/。注意,矩形只有在边校正的情形下,谈论长宽比才有意义。当长宽比为r的边校正矩形旋转,使其一边与一相邻边平行时,长宽比变为1/r. 练习 练习2.3.1 画棋盘 (在看答案之前,请先动手试试)写出例程checkerboard (int size),用来绘制图2.33(b)中所示的棋盘。将该棋盘的左下角置于(0, 0)处。这64个正方形的边长都设为size个像素。为这些正方形选择两种好看的颜色。 解: 第ij个正方形的左下角位于(isize, jsize) 处(i=0,…,7; j=0,…7)。使用下面的代码,可以将颜色在(r1 , g1 , b1)和(r2 , g2 , b2)间变换。if((i+j)%2==0) //如果i+j是偶数 glColor3f(r1, g1, b1); else glColor3f(r2, g2, b2); 练习2.3.2 指定矩形的其他方法 除了指定两个相对的角点外,还有其他描述边校正矩形的方法。其中两种可能的方法是: (1) 它的中心、高和宽; (2) 它的左上角、宽以及长宽比。 写出函数drawRectangleCenter()和drawRectangleCornersize()传送这些指定的参数。 练习2.3.3 不同的长宽比 写一小程序,用它绘制长宽比为R的矩形,R由用户指定。假定显示窗口的绘图空间为400×400。想办法使这个矩形尽可能的大。也就是说,如果R>1, 该矩形就会在横向上撑满绘图空间;如果R<1,它就会在纵向上撑满绘图空间。 练习2.3.4 绘制参数化的房子 细化图2.27所示的函数parameterizedHouse(),以便在给定的高度和宽度下,按照适当的比例绘制门、窗户和烟囱。 练习2.3.5 使用参数缩放和平移图形 写出函数void drawDiamond (GLintPoint center, int size),以绘制图2.35中所示的简单菱形,其中心位于center处,大小为size. 使用该函数绘制出图2.36所示的菱型雪花。 图2.35 一个简单菱形 图2.36 一些菱形 2.3.5 填充多边形 到目前为止,我们已经能够用OpenGL绘制非填充多边形了,也可以绘制单色填充的边校正矩形。OpenGL同样支持用图案或彩色填充更一般的多边形,前提是这些多边形必须是凸的。 凸多边形: 如果多边形中任意两点的连线完全位于多边形内部,那么这个多边形是凸的。 图2.37显示了几个多边形,其中只有D、E和F是凸的(按凸多边形的定义检查一下). D肯定是凸的,所有的三角形都是凸的。A甚至算不上是简单图,所以它不可能是凸的。B和C都在某一点“向内弯曲”(可以在B上找出两个点,它们之间的连线不完全位于B内部). 图2.37 凸多边形和非凸多边形 要基于顶点绘制一个凸多边形,可以使用顶点列表(x0, y0), (x1, y1),…,(xn,yn),但是要把它们放在glBegin(GL_POLYGON)和glEnd()之间: glBegin(GL_POLYGON); glVertex2f(x0, y0); glVertex2f(x1, y1); … glVertex2f(xn, yn); glEnd(); 该多边形以当前的颜色进行填充,也可以用点划线图案来填充(参见案例2.5)。以后我们将会把一些图像贴在多边形上,作为应用纹理的一部分。 图2.38显示了一些填充的凸多边形。第9章将考查一个用于填充任意多边形(无论凹凸) 的算法。 图2.38 几个填充的凸多边形 2.3.6 OpenGL中的其他图形元素 除了点、线和多边形,OpenGL还支持绘制五种其他元素,图2.39显示了这些元素的例子。为了绘制其中某个元素,需要在使用glBegin()时指定下列常数。 图2.39 其他几何图形元素 GL TRIANGLES: 一次使用3个顶点,每次绘制一个独立的三角形。  GL_QUADS: 一次使用4个顶点,每次绘制一个独立的四边形。  GL_TRIANGLE_STRIP: 基于3个一组的顶点v0, v1, v2;然后是v1, v2, v3;接着是v2, v3, v4。依此递推,绘制一系列三角形(按同样的顺序,即逆时针方向,遍历所有的三角形).  GL_TRIANGLE_FAN: 基于3个一组的顶点v0, v1, v2;然后是v0, v2, v3;接着是v0, v3, v4。依此递推,绘制一系列与v0相连的三角形。  GL_QUAD_STRIP: 基于4个一组的顶点v0, v1, v3, v2;然后是v2, v3, v5, v4;接着是v4, v5, v7, v6。依此递推,绘制一系列四边形(按同样的顺序,遍历所有的四边形,如逆时针方向). 2.4 与鼠标和键盘的交互 交互式应用程序富有成效的和激动人心的性质之一是用户能用人类自然的动作(如用鼠标指示和单击鼠标、按下键盘上不同的键等)来控制程序的流程。单击鼠标时鼠标的位置、按下的键盘上键的特性对于应用程序来说是有用的,需要被恰当的处理。 当用户按下或释放鼠标按钮、按下按钮时移动鼠标或者按下和松开键盘按键时,就会产生一个相关事件。程序员可以用每类事件注册一个回调函数,例如使用如下函数:  glutMouseFunc(myMouse): 利用按下或释放鼠标按钮时发生的事件来注册myMouse().  glutMotionFunc(myMovedMouse): 利用按下按钮同时移动鼠标的事件来注册 myMovedMouse().  glutKeyboardFunc (myKeyboard): 利用按下和松开键盘按键的事件来注册myKeyBoard(). 人们希望计算机对于人类自然的输入,例如说话和手势,能有更多的反应。对于这点,今天的计算机还有更长的路要走。通过移动鼠标来控制计算机,虽然只是一个笨拙的辅助方式,但与那些没有多少人懂的、晦涩神秘的输入符号序列相比,已经自然许多了。在本节中我们将学习在程序中如何使用鼠标和键盘。 2.4.1 用鼠标交互 与鼠标有关的数据怎样发送给应用程序呢?这是在设计将鼠标注册到glutMouseFunc的回调函数时必须回答的问题。回调函数的名字可以任意取(例如用myMouse),但是,它一定要带4个int 参数,其原型如下: void myMouse(int button, int state, int x, int y);当鼠标事件发生时,系统就会调用已注册的函数,并向其提供这些参数值。Button值可能是下面明显含义中的一个: GLUT_LEFT _BUTTON、GLUT_MIDDLE_BUTTON和GLUT_RIGHT_BUTTON。而state的值可能是GLUT_UP和GLUT_DOWN中的一个。x和y的值指明事件发生时鼠标的位置(但需注意: x值是距离窗口左边的像素数,这在意料之中,但y值是距离窗口顶端的像素数). 记住,事件处理器本身并不引起到屏幕的重新绘制事件。因此,为了看到鼠标事件的效果,鼠标处理器应该调用glutPostRedisplay(). 例2.4.1 用鼠标放置点 我们从一个初级的但也是很重要的例子开始。用户每次按下鼠标左键时,就会在屏幕窗口上鼠标所在的位置绘制出一个点。如果用户按下右键,就改变窗口背景颜色。下面的 myMouse()版本就可以完成这项工作。由于鼠标位置的y值是距离屏幕窗口顶端的像素数,所以不应该在(x, y)上绘制该点,而在(x, screenHeight-y)上绘制,其中screenHeight是假设的窗口高度,以像素来计量。void myMouse(int button,int state, int x, int y) { if(state==GLUT_DOWN) { if(button==GLUT_LEFT_BUTTON) { drawDot(x. screenHeight -y); glFlush(); } else if (button==GLUT_RIGHT_BUTTON) { glClearColor(1.0f,0.0f,0.0f,0.0f); //红色 glClear(GL_COLOR_BUFFER_BIT); glFlush(); } } return; }例2.4.2 使用鼠标指定一个矩形 这里,我们想让用户画尺寸大小由鼠标输入的长方形。用户在两个点上单击鼠标,指定边校正矩形的两个角点,就可画出该矩形。每个边校正矩形的数据不需要保留: 新的矩形会取代前一个矩形,用户右击鼠标即可清屏。 图2.40所示的例程在静态(static)数组corner\中存储一对角点,该数组设置成静态,所以在两次调用例程之间数组仍会被保存。变量numCorners跟踪己添加的角点数,当这个数达到2时,就会绘制出矩形,然后将numCorners重置为0. 例2.4.3 用鼠标控制Sierpinski垫片 可以很容易地扩充前述的Sierpinski垫片例程,使用户可以用鼠标指定初始三角形的3 个顶点。可以使用与前例相同的办法: 将3个顶点一起置于数组comers\中,当这3个点可用时,就绘出了Sierpinski垫片。具体的myMouse()例程内容如下: static GLintPoint corners\; static int numCorners=0; if(button==GLUT_LEFT_BUTTON && state==GLUT_DOWN) { corner \.x=x; corner \.y=screenHeight -y; //翻转y坐标 if(++nurnCorners==3) { Sierpinski(corners); //绘制垫片 numCorners=0; //返回0个角点 } } glFlush(); void myMouse(int button, int state, int x, int y) { static GLintPoint corner\; //创建一个数组 static int numCorners=0; //初始值为 0 if(state==GLUT_DOWN) { if(button==GLUT_LEFT_BUTTON) { corner\.x=x; corner\.y=screenHeight-y; //翻转 y 坐标 if(++numCorners==2) { glRecti(corner\.x, corner\.y, corner\.x, corner\.y); numCorners=0; //回到0个角点 } } else if(button==GLUT_RIGHT_BUTTON) { glClear(GL_COLOR_BUFFER_BIT); glFlush(); } } 图2.40 画鼠标控制大小的矩形回调函数这里,Sierpinski ()与图2.13一样,不同之处是该三角形的3个顶点是被当作参数传送的。 鼠标运动 鼠标移动时产生另一个鼠标事件。回调函数,记为myMovedMouse(),通过使用下列函数之一注册该事件: glutMotionFunc(myMovedMouse); glutPassiveMotionFunc(myMovedMouse);第一个函数在按下一个或几个鼠标按钮(但不松开)鼠标在窗口内移动时调用。第二个函数是在没有按下鼠标按钮的情况下,鼠标在窗口内移动时调用。该回调函数必须有两个参数,且须为原型void myMovedMouse (int x, int y). x和y值都是事件发生时鼠标所在的位置。 使用glutPassiveMotionFunc()的一个例子是绘制并显示橡皮矩形: 随着用户移动鼠标,矩形将相应地变大或变小。在下面的程序中,用户单击鼠标建立矩形的一个角点,然后在不按下鼠标按钮的情况下任意移动鼠标。用这种方式移动鼠标将产生一个事件,它调用myPassiveMotion(int x, int y),并且,当前鼠标的位置决定了橡皮矩形的第二个角点。 这里,为了使动画显示更加平滑,我们设置GLUT使用双缓冲渲染模式(这种机制将在第3章中详细介绍)。尽管在前面的例子中,有一些绘制发生在myDisplay函数之外,这些绘制还是应该尽量避免的。因此,我们设置了几个全局变量,每次调用glutPostRedisplay()渲染矩形时,就相当于强制GLUT调用我们自己的绘制函数myDisplay(). /允许用户画橡皮矩形程序: 长方形大小随鼠标变化/ //包括所有合适的 include 文件 struct GLintPoint { GLint x, y; }; //全局变量 GLintPoint corner\; bool selected=false; int screenWidth=640, screenHeight=480; void myDisplay() { glClear(GL_COLOR_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadldentity(); glColor3f(1.0f, 1.0f, 1.0f); if(selected) { glBegin(GL_QUADS); glVertex2i(corner\.x, corner\.y); //画一个长方形 glVertex2i(corner\.x, corner\.y); glVertex2i(corner\.x, corner\.y); glVertex2i(corner\.x, corner\.y); glEnd(); } glutSwapBuffers(); } void myMouse(int button\] int state, int x, int y) { if(button==GLUT_LEFT_BUTTON && state==GLUT_DOWN) { corner\.x=x; corner\.y=screenHeight-y; selected=true; } glutPostRedisplay(); } void myPassiveMotion(int x, int y) { corner\.x=x; corner\.y=screenHeight -y; glutPostRedisplay(); } int main(int argc, char argv) { glutlnit(&argc, argv); //初始化窗口 glutlnitWindowSize(screenWidth, screenHeight); glutlnitWindowPosition(0, 0); 图2.41 画橡皮矩形的完整程序 glutlnitDisplayMode(GLUT_RGB I GLUT_DOUBLE); //创建窗口 glutCreateWindow("Rubber Rect Demo"); //设置投影矩阵 glMatrix问ode( GL_PROJECTION ); glLoadldentity(); gluOrtho2D(0, screenWidth, 0, screenHeight); glMatrixMode(GL_MODELVIEW); //清渲染面 glClearColor(0.0f ,0.0f, 0.0f, 0.0f); //背景为黑色 glViewport(0 , 0, screenWidth, screenHeight); glutMouseFunc(myMouse); glutDisplayFunc(myDisplay); glutPassiveMotionFunc(myPassiveMotion); glutMainLoop(); retun(0); } 图2.41 (续)2.4.2 键盘交互 按下键盘上的某个键就会产生一个键盘事件,并放入消息队列中。回调函数myKeyboard()就是通过glutKeyboardFunc(myKeyboard)来注册这种事件。该函数必须有如下的原型: void myKeyboard(unsigned int key, int x, int y); 按键的值就是所按键的ASCII值ASCII(American Standard Code for Information Interchange)代表美国标准信息交换码,ASCII表可从因特网得到。。数值x和y指示事件发生时鼠标所在的位置(如前所述,y值是距离窗口顶端的像素数). 程序员可以利用键盘上的按键为用户提供许多快捷键,它们可在程序中的任意位置被激活。大多数myKeyboard ()的实现方法都由很长的switch语句组成,一个case对应一个感兴趣的键。注意: 需要记住,switch语句会给程序带来很大的危险,一定不要忘记用break结束每个语句。当然,你有可能忘记。在编程历史上,这曾导致过巨大的灾难,包括AT&T在1990年1月长距离电话网络事故中发生的大错。如果想更多地了解这个故事,可以查找以下这个资料: 《Expert C programming: Deep C Secret》(Peter van der Linden 著)【van der Linden 94】。图2.42显示了这样一种可能的方案: 按下p键就会在鼠标位置绘制一个点,按下E键就会退出程序。注意,如果用户一直按着p键并移动鼠标,就会产生一个快速点序列,这就模拟了“徒手”绘图。 void myKeyboard(unsigned char theKey, int mouseX, int mouseY) { GLint x=mouseX; GLint y=screenHeight-mouseY; switch(theKey) { case 'P' drawDot(x, y); //在鼠标位置画点 break; case 'E': exit(-1); //终止程序 default: break; //什么也不做 } } 图2.42 一个键盘回调函数的例子2.5 程序中的菜单设计与使用 如果一个图形程序能够提供菜单功能,对用户来说,使用就方便多了。正如在第1章简要说明的一样,程序运行期间,会在适当的时刻出现一个菜单。菜单出现在程序已经生成的图形前面,给用户提供许多选项。像我们所看到的一样,选项有不同的类型。然后程序会等待用户做出一个或多个选择。当用户做出选择后(通常是用鼠标或键盘),菜单消失,重新显示刚才的图形(菜单出现前的图形),接着,程序按用户选择的路线运行(设计合适的菜单对用户来说方便易用,无需太多的解释). GLUT 和GLUI菜单 这里通过介绍两种不同的OpenGL库,GLUT和GLUI库,来学习两个级别的菜单管理。虽然GLUT菜单(回顾一下图2.3)比GLUI菜单简单,但仍然很有用。下面首先介绍一个较好使用GLUT菜单的应用程序。 实例 我们要构建一个的菜单,允许用户在程序运行中,改变一个正在旋转的三角形的颜色。用户在OpenGL窗口里右击鼠标激活菜单,如图2.43所示,菜单提供了4种颜色供选择。 用户在选项位置左击鼠标来选择需要的颜色。当然除此之外,还有许多其他的控制方式,例如,键盘的按键控制。 设计代码和使用GLUT菜单 即使设计十分简单的菜单,对程序员来说,在程序中编写代码来指定菜单的形状、位置、条目和动作似乎仍是一个艰难的任务。从编程角度来看,需要考虑将菜单画成一个正方形,用合适的方式定位和标记菜单的选项,图2.43 一个激活的GLUT菜单 允许点击鼠标右键激活菜单(使其可视)和用鼠标选择其中一个选项。选择完后,菜单消失。对程序员来说,这似乎是一个使人畏缩的任务,但是,事实上,GLUT为我们全都处理了: 只需调用几个GLUT函数,就很容易完成所有这一切。 首先,必须创建菜单,使用void glutCreateMenu()初始化菜单句柄。除了glutCreateMenu(),还需要定义实际的菜单条目。我们使用带有一个参数ProcessMenuEvents的函数glutCreateMenu(ProcessMenuEvents)来定义。我们必须编写ProcessMenuEvents函数,处理根据菜单选项来执行的动作。ProcessMenuEvents是一个典型的switch语句,它根据选择的菜单条目,在应用程序中设置一些值。 使用函数glutAddMenuEntry(),可在菜单中添加选项,这是设计菜单本身必要的一步。对于上述的这个例子,我们添加如下的条目(为方便起见,可使用枚举数据类型或defines功能)glutAddMenuEntry("Red", RED); glutAddMenuEntry("Blue", BLUE); glutAddMenuEntry("Green", GREEN); glutAddMenuEntry("White", WHITE);程序员可自由地添加菜单条目,不需要预先告诉GLUT有多少条目,当用户单击鼠标做出选择时,每个菜单条目都能显示一串字符,并产生一个与此相关的值。一旦菜单设计好,可以用glutAttachMenu(GLUT_RIGHT_BOTTOM)建立与鼠标按钮的联系(可用的鼠标按钮有左、中或右)。这就是GLUT菜单处理的所有要求。注意,不需要单独的鼠标句柄。图2.44提供了一个完整的含有GLUT菜单的工作程序。#include #include #include #define RED 1 #define GREEN 2 #define BLUE 3 #define WHITE 4图2.44 一个演示使用GLUT菜单的完整程序 float angle=0.0; //三角形旋转角 float red=1.0,blue=1.0,green=1.0; //可能的三角形颜色 void renderScene(void) { //画三角形的回调函数 glClear(GL_COLOR_BUFFER-BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); glRotatef(angle,0.0,1.0,0.0); //一点点旋转三角形 glColor3f(red,green,blue); //改变三角形的颜色 glBegin(GL_TRANGLES); //画三角形 glVertex3f(-0.5,-0.5,0.0); glVertex3f(0.5, 0.0,0.0); glVertex3f(0.0,0.5,0.0); glEnd(); angle++; glutSwapBuffers(); } void processMenuEvents(int option) { //鼠标的选择选择颜色 switch(option) { case RED: red=1.0; green=0.0; blue=0.0; break; case GREEN: red=0.0; green=1.0; blue=0.0; break; case BLUE: red=0.0; green=0.0; blue=1.0; break; case WHITE: red=1.0; green=1.0; blue=1.0; break; } } //----------------------MAIN-------------------------------------------- void main(int argc, char argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); glutInitWindowPosition(100,100); glutInitWindowSize(320,320); glutCreateWindow("Menu Test"); //打开一个OpenGL窗口 glutDisplayFunc(renderScene); //注册显示函数 glutIdleFunc(renderScene); //调用函数创建菜单 //右击鼠标激活函数 //创建菜单并关联菜单事件 glutCreateMenu(processMenuEvents); glutAddMenuEntry("Red",RED); glutAddMenuEntry("Blue",BLUE); glutAddMenuEntry("Green",GREEN); glutAddMenuEntry("White",WHITE); glutAttachMenu(GLUT_RIGHT_BUTTON); //系在右鼠标按钮上 glutMainLoop(); } 图2.44 (续) GLUI菜单 GLUT给用户提供了很好的、对程序流程的高级控制能力。但是,有时用户想对逻辑输入设备进行更好的控制和更广泛的设置(回顾第1章)。这里,我们学习GLUI库(GL User Interface,GL 用户接口),GLUI提供不同种类的控制。图2.45显示了一个使用GLUI的相当复杂的菜单,以下描述每个条目时都参考这个图(下面的条目有些经常出现,有些则不太明显).  Buttons(按钮)--例如用于OK和Cancel的按钮,在图中Button的例子为“Button、Another Button等。  CheckBoxes(检查盒) --on/off(开/关)选择,例如线框图与实体图之间切换观看,图中的例子为CheckBox1.  Radio Buttons(单选按钮) --选择设备;在某个时刻,只能从单选按钮组中选择一个按钮。图中标记了Radio Button 1、Radio Button 2、Radio Button 3等。  Static Text(静态文本)--显示一串短消息,帮助用户管理程序。用户不能改变静态文本,所以它不是一个输入设备。图中标记为Static Text.  Editable Text areas(可编辑文本区)--字符串设备;用户能输入字符串,空格,并且改变输入的字符。  Spinners(微调控制项)--数值输入设备;当用户用鼠标单击上或下箭头时,数值快速地增加或减少。 图2.45 GLUI接口样本 Panels(面板) --一组接口元件,为了方便观看,能最大化、最小化。图中显示了一个标记为Rollout(open)的例子,当按下“-”时,面板收缩到标记为Rollout(closed)的单个对象。不要奇怪,单击“+”会展开被收缩的接口对象(例如Hi There和123),并且允许它们中的每个都成为一个输入设备。  List Boxes(列表盒) --一组下拉式子菜单,提供一个可选的输入设备(图中标记为ListBox 1和ListBox 2).  Rotation and Translation devices(旋转和平移设备) --像弓形球的设备,提供数值的输入,用户用鼠标连续地移动弓形球的上部或箭头的头部,可以看到目标在平移或旋转。  Separators(隔离器)--程序的可视化助手(不是输入设备),能帮助管理屏幕上的许多选项和元件。例如,Static Text下面的线就是一个隔离器。 注意,GLUI菜单比GLUT更加复杂,我们能想象得到,定义和管理GLUI菜单的代码也必然更加复杂。像GLUT一样,GLUI本身处理了许多困难的任务。一个完整的、经P.RademacherPaul Rademacher 与Nigel Stewart 做GLUI方面的工作,GLUI项目的主页是http://glui.sourceforge/net. 授权的、演示GLUI控制能力的工作程序,可以从本书的网站上得到。 本章小结 编写图形应用程序中最难的部分是开头: 如何在程序中将硬件和软件结合在一起,生成最初的几个基本图形。OpenGL应用程序接口对此提供了很大帮助,它提供了功能强大却简单易用的例程集来进行绘图。OpenGL的最大优点之一是设备无关,这样在一种图形环境中写出来的程序,就能在不做修改的情况下应用到其他环境中。 现在所编写的大多数图形应用程序都是基于窗口环境。这种程序在屏幕上打开一个用户可以移动并调整大小的窗口,还可以对单击鼠标和敲击键盘做出反应。我们已经学习了怎样使用OpenGL函数便捷地创建这样的程序。 基本元素绘制例程用于绘制由点、线、折线和多边形组成的图形,并且和更加强大的例程结合在一起,构成个人图形工具箱的基础。本章列举了一些例子阐述这些工具的使用,并且介绍了在程序中使用键盘和鼠标进行交互的方法。我们还介绍了moveTo()和lineTo()方法以便于绘制直线,并特别强调了使用OpenGL函数glRecti()画边校正矩形。 下面介绍的案例进一步提供了编程实例,帮助读者更深入地理解前面讨论过的主题,或者引申出有趣的相关主题。 案例分析 为了巩固所学知识,最好的方法是尝试使用这些介绍过的知识。在学习最初几章时更要如此,因为一开始学习计算机图形学,不了解程序的编写是必须克服的障碍。为了强调这一点,每章都以案例结尾,其中介绍的编程项目不仅本身有趣,而且浓缩了该章中所提到的知识。 有些案例只是简单的练习,只需要搞懂本章中给出的伪代码并按步骤运行程序就可以了。有些案例则很有挑战性,并且可以作为课程中大型编程项目的基础。很难判断某人完成某个项目需要多少时间,每一个案例的难度只是一个大致的粗略猜测。难易程度 I: 简单的练习,可以在课上完成。 II: 较难任务。可能需要几天才能完成。 III: 复杂任务。可能需大约两周时间。 案例2.1 伪随机点云 (难度: II)每调用一次随机数生成器(RNG)rand()(该函数是标准C++库函数),就产生一个0~N-1之间的数。每个数值似乎都是随机的,与前一个数没有任何关系。 在一些随机概率起很大作用的游戏中,如纸牌和掷骰游戏,随机数有着广泛的应用。 事实上,rand()所产生的一系列数字根本不是随机产生的,而是通过一个很有规律的机制产生的: 每个数ni都是由它前一个数ni-1,通过一个特殊的等式来决定。典型的等式是: ni=(ni-1A+B)%N(2.4) 其中,A、B和N都是适当选择的常数。对ni-1的操作看上去像把ni-1二进制表示中不同的位杂乱无章的拼凑在一起形成了新的ni值。但实际上每个数都是由前一个数独一无二的确定。基于式(2.4)的一组有效的数字可以是A=1103515245、B=12345、N=32767。将ni-1乘A再加上B构成了一个很大的数值,模运算将这个数限制在0~N-1范围内。这个过程从设置n0的种子(seed)数开始。 因为这些数字只是表面上看是随机的,所以将它们称为伪随机数。选择A、B和N的数值很重要,这些值稍有差异,生成的数列就会出现极不相同的特征。更详细的介绍可以在【Knuth, Weiss98】中找到。 散乱点图 有些实验产生了一些由许多数字对(ai , bi)组成的数据,目的是可视地推理出a值和b值是怎样相关的。例如,对许多人进行测量,想了解他们的身高和体重之间是否有紧密联系。 散乱点图可以给出这些数据的直观视觉效果。每个人的(身高、体重)数据都可当作点绘制出来,所以只需使用工具drawDot()。图2.46显示了一个例子。它表明: 人的身高和体重之间大概是线性关系,尽管有些人特殊,比如A有点瘦。 图2.46 人的身高体重散点图 这里用散乱点图来可视化地检测随机数生成器的性质。每调用一次函数rand(N),它就会返回一个0, …, N-1之间的数,好像是随机挑选的,与先前rand()产生的数毫无关系。但是这些数字真的不相干吗? 做个简单的试验,根据rand()所返回的数构建散乱点图,下面这个例程依次调用rand()两次,并且绘制第一个坐标值对第二个坐标值的图(每点由一对坐标值确定)。可以用drawDot()来实现: for(int i=O; i #include #include #include #include #include const float pi=3.14159265358979; // 用这个常量近似pi // ---- setWindow ---- void setWindow(GLdouble left, GLdouble right, GLdouble buttom, GLdouble top) { //定义我们自己的函数设置窗口(后面会详细解释) glMatrixModel(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(left, right, bottom, top); } // ---- setViewport ---- void setViewport(GLint left, GLint right, GLint bottom, GLint top) { // 定义我们自己的函数设置视口(后面会详细解释) glViewport(left, bottom, right-left, top-bottom); } // ---- myDisplay ---- void myDisplay(void) //用世界坐标绘制sinc 函数 { glClear(GL_COLOR_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glBegin(GL_LINE_STRIP); / 自动使用定义好的窗口和视口,正确的裁剪和映射/ for (float x=-4.0; x<4.0; x+=0.1) // 绘制图像 { if (x==0.0) glVertex2f(0.0, 1.0); else glVertex2f(x, sin(3.14159x)/(3.14159x)); } glEnd(); glFlush(); } 图3.3 显示sinc函数的完整程序 // ---- myInit ---- void myInit(void) { glClearColor(1.0, 1.0, 1.0, 0.0); //白色背景 glColor3f(0.0f, 0.0f, 1.0f); //蓝色线条 glLineWidth(2.0); //线宽是2 } // ---- main ---- void main(int argc, char argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(640, 480); glutInitWindowPosition(100, 150); glutCreateWindow("The Famous Sinc Function"); glutDisplayFunc(myDisplay); myInit(); setWindow(-5.0, 5.0, -0.3, 1.0); / 调用函数设置窗口/ setViewport(0, 640, 0, 480); / 调用函数设置视口/ glutMainLoop(); } 图3.3 (续)图3.4 世界窗口和视口 我们不仅想描述如何让OpenGL来做这件事--这很容易,还想描述它在内部是如何操作的,以便解释所用的底层算法。这一章我们只考虑二维的版本,但在之后(第5章)我们会看到这些想法如何被自然地应用于相机看到的三维世界。 3.2.1 窗口到视口的映射 图3.5显示了世界窗口和视口的更多细节。世界窗口由它的左、上、右、下的边界描述,分别是W.l、W.t、W.r和W.b。视口在屏幕窗口坐标系中(从屏幕的某个位置展开),被同样的描述,使用V.l、V.t、V.r和V.b,单位是像素为了简明,我们在数学等式中用“l”表示“左边”, "t”表示“上边”,等等。. 图3.5 指定(a)窗口和(b)视口 世界窗口必须是对齐的矩形,但它可以有任意大小以及出现在任何位置。类似的,视口也可以是任意对齐的矩形,虽然它常常被选成完全位于屏幕窗口内。此外,世界窗口和视口并不需要有同样的纵横比,当然不同的纵横比会带来图像的变形。如图3.6所示,出现变形是因为窗口中的图像必须被拉伸以适合视口。随后我们将看到如何设置视口的纵横比,使之总是和窗口匹配,即使用户改变了屏幕窗口的大小。 图3.6 一张图从窗口映射到视口。这里产生了一些变形 给定窗口和视口的描述,我们可以得到一个映射或变换,即窗口到视口的映射。这个映射是基于一个等式,它对每一个在世界坐标下的点(x, y),产生屏幕坐标系中的一个点(sx,sy)。我们希望这个映射是“保持比例”的映射,也就是说,例如x是位于窗口中距离左边界40%处,则sx应该位于视口中距离左边界40%处。同样,对于某个比例f,如果y在窗口中距离下边界1/f高的位置,则sy应该是在视口中距离下边界同样的1/f高的位置。 保持比例的性质使得这个映射有线性形式: sx=Ax+C sy=By+D(3.2)其中A、B、C、D是常数。常数A、B缩放x坐标和y坐标,而C、D平移它们。 如何确定A、B、C、D呢?首先考虑x的映射,如图3.7,保持比例的性质说明sx-V.l与视口宽度V.r-V.l的比例,必须等于x-W.l与窗口宽度W.r-W.l的比例。 图3.7 x到sx映射的保持比例的性质 基本上,如果你位于视口的中间,那么你必须也位于窗口的中间,并且对于其他比例也是类似的。所以: sx-V.lV.r-V.l=x-W.lW.r-W.l 或者,经过一些代数变换后: sx=V.r-V.lW.r-W.lx+V.l-V.r-V.lW.r-W.lW.l. 现在,把A看作放大x的部分,而C看作常数,我们有: A=V.r-V.lW.r-W.l, C=V.l-A·W.l 类似的,y方向上的保持比例性质规定: sy-V.bV.t-V.b=y-W.bW.t-W.b 把sy写成By+D,得到: B=V.t-V.bW.t-W.b, C=V.b-B·W.b 现在对窗口到视口的变换做一下总结: sx=Ax+C, sy=By+D(3.3)和A=V.r-V.lW.r-W.l, C=V.l-A·W.l B=V.t-V.bW.t-W.b, C=V.b-B·W.b 警告: 请仔细检查这一部分! 这个映射可用于任意点(x, y),不管它是否在窗口之中。在窗口中的点映射到视口中的点,而窗口外的点映射到视口外的点。 (重要!)用等式(3.3)仔细检查映射的下列性质。 a. 如果x是在窗口的左边界: x=W.l,则sx是在视口的左边界: sx=V.l. b. 如果x是在窗口的右边界,则sx是在视口的右边界。 c. 对于某个比例f,如果x位于窗口宽度1/f处,则sx位于视口宽度1/f处。 d. 如果x在窗口左边界外(x #include #include #include #include #include //一个基于正边行绘制花环的程序 class GLintPoint { public: GLint x, y; }; //point 2 类 class Point2 { public: float x, y; void set(float dx, float dy) {x=dx; y=dy;} void set(Point2& p) {x=p.x; y=p.y;} Point2(float xx, float yy) {x=xx, y=yy;} Point2() {x=y=0;} }; 图3.27 一个绘制花环的完整程序 Point2 currPos; Point2 CP; void moveTo(Point2 p) { CP.set(p); } void lineTo(Point2 p) { glBegin(GL_LINES); glVertex2f(CP.x, CP.y); glVertex2f(p.x, p.y); glEnd(); glFlush(0); CP.set(p); } void myInit(void) { glClear(GL_COLOR_BUFFER_BIT); glClearColor(1.0, 0.0, 0.0, 0.0); //背景是红的 glColor3f(0.0, 0.0, 1.0); //用蓝色绘制 } void rosette(int N, float radius) { Point2 pointlist=new Point2\; GLfloat theta=(2.0f  3.1415926536) / N; for (int c=0; c0 对所有在曲线外的 (x, y) F(x,y)<0 对所有在曲线内的 (x, y)(3.9) 有一些曲线对x来说是单值的。如果g(x)是单值的,那对于每一个x,都只有一个函数值。事实上,只有单值的函数是“合法”的函数。对于这些曲线,函数的隐式形式可以写成F(x,y)=y-g(x)。另外一些函数对y来说是单值的,因此存在函数h(),使得曲线上所有点满足x=h(y)。还有一些函数对x和y来说都不是单值的: F(x, y)=0不能写成y=g(x)或者x=h(y)的形式。例如圆,可以表示成: y=±R2-x2(3.10) 但这里有两个函数,而不是一个。其中一个函数用正号,另一个函数用负号。 3.5.1 曲线的参数形式 曲线的参数形式在参数取不同值的时候,会产生出曲线上不同的点。参数形式可以用于更广的一类曲线,因此推荐使用这种形式,特别是当人们想绘制或者分析曲线的时候。参数形式使人想起一个点随着时间运动,我们可以把它转换成一支笔绘制曲线的运动。粒子沿曲线运动的路径由两个函数x()和y()确定,如果是三维的情况,有三个函数: x()、y()和z(),它们确定粒子在时刻t的位置。参数t通常被看作是时间。而曲线本身就是粒子随着时间在某一个区间内的变化所经过的点。对于任一曲线,如果我们可以设计出合适的函数x()和y()或者x()、y()和z(),则他们可以简明而精确的表示出这条曲线。 图3.38所示的是经典的Etch-A-SketchEtch-A-Sketch是Ohio Art的商标。设备,它提供了一个形象的二维模拟。当旋转按钮时,藏在盒子内部的笔会在屏幕上划出一条可见的细线。一个按钮控制笔水平方向位置,另一个按钮控制垂直方向位置。图3.38 Etch-A-Sketch绘制参数曲线 如果根据x(t)和y(t)来旋转按钮,就能绘制出这个参数的曲线。复杂的曲线需要足够灵巧的手工操作。关于设计曲线和曲面的全面分析见第10章。 直线和椭圆 等式(3.8)所示的直线通过A、B这两个点。我们选择一个参数形式,在t=0时访问A点,在t=1时访问B点,从而得到: x(t)=Ax+(Bx-Ax)t y(t)=Ay+(By-Ay)t(3.11) 因此,当t在0~1之间变化时,P(t)=(x(t), y(t))会扫过直线上在A与B之间的所有点(请仔细检查此处). 另外一个典型的例子是椭圆,它是圆的一个轻微的泛化。它可以通过参数形式描述: x(t)=Wcos(t) y(t)=Hsin(t), 0≤t≤2π(3.12) 这里,W是椭圆的“半宽”,H是椭圆的“半高”。椭圆的一些几何性质会在练习中探讨。当W和H相等时,椭圆就是一个半径为W的圆。图3.39显示了一个椭圆,以及它的x(.)和y(.). 图3.39 用参数方式表示椭圆 当t从0到2π变化时,点P(t)=(x(t), y(t))沿着椭圆移动一周,起点(也是终点)是(W, 0)。这个图片显示了在不同时刻点的位置。在Etch-A-Sketch上观察绘制一个椭圆是有用的。按照波动的方式来回转动按钮,一个模仿Wcos(t),另一个模仿Hsin(t)(这个动作很难手工完成). 从参数形式求隐式形式--“隐式化” 假定我们想检查等式(3.12)中的参数形式是否真的表示椭圆。怎么才能通过参数形式得到隐式形式?基本的步骤是联合x(t)和y(t)两个方程,再设法消去变量t。这提供了一个对任意t都成立的关系。进行这个步骤并不总是那么容易--没有一个适用于所有参数形式的简单的步骤。但对于这个椭圆,我们可以对x/W和y/H取平方,并使用众所周知的关系cos(t)2+sin(t)2=1得到下面这个大家所熟悉的椭圆方程: xW2+yH2=1(3.13) 下面的练习将探讨椭圆和其他一些经典曲线的性质。它们还指出了圆锥曲线的一些有用的性质,以后我们会用到这些性质。阅读下面的练习,即使你不停下来解每一道题目。 练习 练习3.5.1 椭圆的几何特征 椭圆是到两个焦点的距离和为常数的点的集合。图3.39中的(c,0)是其中一个焦点,(-c,0)是另一个焦点。证明H、W、c满足: W2=H2+c2. 练习3.5.2 离心率 椭圆的离心率,e=c/W,用来衡量椭圆和圆的差别。圆的离心率是0。举个有趣的例子,太阳系中的行星都有近似圆的轨道,e从1/143(金星)变化1/4(冥王星)。地球的轨道离心率是1/60。当离心率接近于1,那么这个椭圆就会变成一条直线。在变成直线前,e会非常接近1。当椭圆的e=0.99时,它的高宽比H/W是多少? 练习3.5.3 其他的圆锥曲线 椭圆是三种圆锥曲线中的一种。如图3.40,圆锥曲线是用一个平面来切开一个圆锥时所形成的。这些圆锥曲线如下。图3.40 经典的圆锥曲线  椭圆: 平面沿着圆锥的等分半圆锥切开。  抛物线: 平面平行于圆锥的边。  双曲线: 平面切开圆锥的两个等分半平面。 抛物线和双曲线都有有趣且有用的几何特性。他们都有简单的隐式形式和参数形式。 证明下面的参数形式和隐式形式的表达是一致的。  抛物线: 隐式形式为y2-4ax=0 参数形式x(t)=at2 y(t)=2at(3.14)  双曲线: 隐式形式为(x/a)2-(y/b)2=1 参数形式x(t)=asec(t) y(t)=btan(t)(3.15) 用于扫描双曲线的t的范围是什么?注意: 双曲线定义为到两个固定焦点的距离的差的值是常数的点的集合。如果两个焦点的坐标分别是(-c, 0)和(+c, 0),证明a、b满足c2=a2+b2. 3.5.2 绘制参数曲线 当某个曲线有参数表达形式可以使用时,可以直接绘制出这条曲线。这是参数形式相对于隐式形式的主要优势。如图3.41(a)所示,假定一个曲线C有参数形式P(t)=(x(t), y(t)),其中t从0变化到T。我们只需用非常紧凑的间隔采集P(t)的样本,从而仅使用直线段来很好的近似曲线进行绘制。选择一个时间的序列{ti},对每一个ti,可以计算出曲线上的位置Pi=P(ti)=(x(ti), y(ti))。如图3.41(b)所示,曲线P(t)可以由基于点列Pi的折线来近似。 图3.41 用折线近似曲线 图3.42是在已知采样时间数组t\时,绘制曲线(x(t),y (t))的一段代码。// 使用t\, ..., t\1\] 中的采样时间, // 绘制曲线(x(t), y(t)) glBegin(GL_LINES); for(int i=0; i1时,是一个双曲线。 对数螺旋线 对数螺旋线(或“等角螺旋线”)f(θ)=Keaθ,如图3.47(a)所示,也是一种很重要的形状【Coxeter61】。这个曲线在一个常数角度α处切割所有径向线,其中a=cot (α)。这是唯一一种在任意的尺度变换下具有相同形状的螺旋线。将这种螺旋线进行任意放大后,再经过适当的旋转,可以和原螺旋线重合。因此,它也被称为是自相似的。我们在以前看到过自相似这个术语,之后我们还会看到它与Mandelbrot集合(见附录4)的联系。类似的,旋转等角螺旋线,会让人觉得它变大了或者变小了【Steinhaus69】Descartes在1938第一次描述了这个曲线。Jacob Bernoulli(1654-1705)对之非常着迷,甚至在他位于瑞士巴塞尔的墓碑上都刻着这条曲线,并题为Eadem mutate resuiggo:“虽然某些状况改变了,我却保持不变。" . 图 3.47 (a) 对数螺旋线; (b) 鹦鹉螺 这种保持形状的特性似乎被一些动物所采用,例如鹦鹉螺这种软体动物(见图3.47(b))。这种动物生长时,它的壳会沿着一条对数螺旋线增长,从而保持同样的形状【Gardner61】。做一个迷人的边注: 鹦鹉螺相邻的两个腔的体积比是黄金分割率! 其他类型的曲线会在练习和案例中讨论。在【Yates46, Seggern90, Shikin95】中,可以看到一个值得关注的曲线特征的详细列表。 本章小结 在这一章里,我们开发了几个工具,它们可以使应用程序的程序员使用最方便的世界坐标系,思考并直接解决手头的问题。对象通过高精度的实数坐标系来定义和建模,而不需要考虑对象将显示在屏幕的哪个位置,以及生成的图像有多大。这些考虑都推迟到随后对窗口和视口的选择上--手工的或自动的--它们决定了对象哪些部分被显示,以及如何显示在屏幕上。这种方法将建模的阶段和视图阶段分开,允许程序员或者用户在各阶段集中解决相关问题,而不需为显示设备的细节而分心。 窗口的使用,使放大或者缩小场景,以及漫游场景的不同部分变得非常容易。从日常使用照相机的经验可以熟悉这些操作。视口的使用,使程序员能够将图像或者图像集放在显示器上的期望的位置,从而合成最终的图像。我们讨论了保证窗口和视口的纵横比不变的技术,从而避免图像变形。 我们还开发了一些附加的工具,用于创建正多边形,圆弧和圆。我们还介绍了曲线的参数形式,这是一种非常自然的描述曲线的方法。它使得绘制曲线变得很容易,即使是那些多值的,相互交叉的,或者在某些地方是垂直的曲线。 案例分析案例3.1 学习逻辑图和混沌的模拟 (难度: II)第2章结尾,我们讨论了迭代函数系统(IFS)。另一种IFS提供了对混沌世界的有趣的观察(见【Gleick87, Hofs85】)。它需要适当地设置窗口和视口。通过重复应用称为逻辑图的函数f(.),可以生成一系列的数值。这个函数通过下面的方程描述一条抛物线: f(x)=4λx(1-x)(3.20)(检查它确实描述了一条抛物线),其中λ是一个0到1之间的常数。从一个在0~1之间的起始点x0开始,迭代应用函数f(.),可以得到轨迹(回忆在第2章的定义): xk-1=f[k](x0) 也就是说,x1=f(x0), x2=f(f(x0)), x3=f(f(f(x0)))…,以此类推。 下面我们研究一下这个序列是如何得到的。通过函数在x上的值得到一个新的值y(一个垂直的移动从x轴到(x,f(x)),然后再把y的值当作下一个x的值(一个水平的移动从(x,f(x))到(f(x),f(x))),重复这个过程,就可以得到轨迹。一个复杂的世界潜伏在这里。 图3.48显示了当x在0~1之间变化时,抛物线y=4λx(1-x)的形状。其中λ=0.7. (在x取多少的时候,这个曲线达到它的最大值?)图3.48 λ=0.7时的逻辑图 这里我们选择起点x0=0.1(一个任意值),在x轴的这一点上,向抛物线方向画一条垂线,它将终止在 0.252(检查这个终止点)。在图像上,这就是绘制一条从(0.1, 0)到(0.1, 0.252)的线。为了找到曲线上的下一个点,我们从(0.1, 0.252)绘制一条水平线到y=x这条线,它会在(0.252, 0.252)处终止,因此x1=0.252。下面我们计算函数在x1=0.252上的值。如图3.48所示,这在视觉上这就是移动到y=x这条线上。接下来在新的值上计算f(.),就是向抛物线绘制一条垂直的线。像其他IFS一样,一直重复这个过程。从上一个点(xk-1,xk),绘制一条水平线到(xk,xk),再绘制一条垂直线到(xk,xk+1)。这个图显示了当λ=0.7时,这些数值很快就收敛到个稳定的“吸引点”上,这是一个满足f(x)=x的固定点(当λ=0.7时,这个数值是多少?)这个“吸引点”不依赖于起始点;这个序列总是很快的收敛到一个最终的数值。 如果λ是一个很小的值,这个动作就会更加简单: 只有一个“吸引点”在x=0处。但是当λ增大时,就会发生一些奇怪的事情。图3.49(a)显示了λ=0.85时的情况。这个序列的轨迹进入一个无限的循环,而不会到一个最终值。这里有几个吸引点,图中展示了在这个有限循环中,位于每条垂线上的吸引点。当λ增加到临界值λ=0.892486418…时,这个过程变成了一个真正的混沌。 图 3.49 (a)λ=0.85和(b)λ=0.9时的逻辑图 图3.49(b)是λ=0.9的例子。对大多数起始点,轨迹都是周期的,周期间的轨迹数目变得非常的大。其他一些点产生非周期的运动,并且起始点很小的改动会导致非常不同的行为。在1975年,Mitchell Feigenbaum首先发现这种现象的显著特征之前,大多数研究人员都认为,系统的微小改变(在这个例子中,将起始点做微小改变,或者将λ在0.85~0.9之间变化)应当导致行为上的微小改变,并且像这种简单的系统不会表现出极其复杂的行为。 Feigenbaum的工作开拓了一个新的领域来研究复杂的非线性系统的本性,也被称为混沌理论【Gleick87】。用这种逻辑图做实验非常有趣。 写一个程序,允许用户研究逻辑图的重复迭代行为,就像图3.49所示的那样。用户设置一个合适的窗口和视口,使得可以清晰的看到整个逻辑图。进一步的,当用户给定x0和λ的值,程序可以绘制出这个系统产生的有限循环。 案例3.2 在C/C++中实现Cohen-Sutherland算法 (难度II)在3.3.2节中描述了Cohen-Sutherland算法的基本流程。这里,我们将讨论并充实一些在C/C++中的实现细节,研究这些语言提供的高效的,底层的位操作。 如图3.19所示,我们首先要形成一个“内部/外部”码字,它说明P点与窗口的相对位置。一个8位二进制的code就够了: 其中4位二进制用于捕获4个信息。依次测试P点与各个窗口边界,如果它在边界外,对应的位被设置为1,用来表示真。图3.50显示了这是怎么做到的。code被初始化为0,然后每一独立位用OR位操作来设置为合适的值。数字8、4、2和1是简单的掩码。例如,因为8的二进制是00001000,对一个数字和8进行OR位操作,会讲这个数字从右数的第4位设置为1. unsigned char code=0; //初始所有位都是0 ... if (P.xwindow.t) code|=4; //设置第2位 if (P.x>window.r) code|=2; //设置第1位 if (P.y