使用 POV-Ray 创建动画
Silicon Graphics 工作站带有一个名为 “mailbox” 的邮件通知程序,如果用户有新邮件,它会通知窗口系统用户。mailbox 程序不会像 xbiff 程序那样显示简单的位图,而是绘制一个真实的邮箱,当检测到新邮件时,邮箱的红色旗帜会平稳升起。当用户在窗口中单击鼠标时,邮箱的门会打开,露出空的空间或等待的信件。在后一种情况下,用户最喜欢的邮件程序会运行。
在本文中,我将追溯免费版本 mailbox 的部分开发过程——构建邮箱和制作动画。由于 SGI 工作站包含特殊的图形处理芯片,mailbox 生成的图像是即时计算的。在配置较低的机器上,解决方案是预先生成并存储帧,然后按顺序将它们写入屏幕。在大多数流行的浏览器以及许多网页中都可以找到类似动画的其他示例。
自然而然的选择是使用光线追踪程序。虽然使用绘图程序肯定可以完成这项工作,但要制作平滑的动画以及在非常简单的场景中保持各种对象大小的一致性,将需要大量的工作。Persistence Of Vision (POV) 光线追踪程序是最古老的免费光线追踪器之一。它的开发与 Linux 的开发类似,都是由一个庞大的、无偿的、分散的开发团队编写的。
光线追踪得名于它用来构建图像的方法。对于图像中的每个像素,都会在空间中沿光线路径向后计算一条线,以查看组成该像素的光线来自哪里,以及它必须是什么颜色。如果向后追踪的光线与镜面相交,则会追踪另一条光线以查看反射光线来自哪里。如果光线来自实体对象,则会进行另一次计算,以查看该点如何被周围的光源照亮。
从这个简单的描述中,可以看出光线追踪对于渲染的每个像素都涉及许多数学运算。在 486DX 系统及更高版本中发现的数学协处理器将性能提高约三十倍。POV 以 X 和 SVGALIB 版本分发。在 Linux 下运行 POV 而不是在其他操作系统(DOS、Windows 或 Macintosh)之一上运行的一个巨大优势是能够在等待完成图像的同时做其他事情。在 XFree86 下运行 POV 时,使用以下命令以 16 位颜色模式启动服务器
startx -- -bpp 16
您不会后悔的。如果您正在运行另一个使用大量颜色的程序,8 位模式最终会看起来非常糟糕。
POV 通过解释一种简单的编程语言来生成图像。由于其唯一目的是定义对象和灯光的各种属性,因此没有子程序、循环或 if 测试。虽然 POV 语言的稀疏性可能看起来很局限,但它具有使程序的设计专注于光线追踪而不是其扩展语言的效果。存在各种各样的第三方程序,它们的最终输出是 POV 可读的文本文件。
本文介绍的是 POV 3.0 版本,尽管在撰写本文时仍处于 beta 测试阶段,但它具有使简单事情变得更容易的巧妙功能,并为动画提供了更多支持。POV 2 附带了一个很好的教程,在 3.0 中变得更好,现在甚至附带了 HTML 版本。我不会尝试超越 POV 教程,而只是展示用于构建邮箱动画的源代码。
不带参数运行 POV 会在标准错误输出中生成在线帮助——130 行,因此需要管道连接到 less。POV 的所有参数都可以在名为 povray.ini 的文件中指定。命令行参数会覆盖此初始化文件。参数前的 “+” 表示打开该选项,“-” 表示关闭该选项。最常用的参数是 D,它会在渲染图像时显示图像;P,它会在图像完成后暂停;以及 +Ifilename,它会导致 POV 解释 filename。POV 可以以 PPM、PNG 和其自身的 Targa 格式输出文件。这些格式足够常见,可以转换为其他格式。
POV 的工作原理是解释用户提供的文本文件。该文件描述了对象的位置和大小、它们的表面外观以及灯光和相机的位置。语言的结构是一个关键字,后跟用花括号括起来的修饰符。有些关键字需要特定的修饰符,而另一些则不需要任何修饰符。有时,修饰符本身也会有修饰符,也用花括号 ({ }) 括起来。
构建图像所需的计划量与图像的复杂性直接相关。复杂的图像应该以与复杂程序相同的方式构建——首先获得一个大致的轮廓,然后填充细节。在我们的例子中,邮箱相当简单。我们将 z 方向定义为高于地面的高度,地面位于 z=0。邮箱所靠的柱子将位于 x=0 和 y=0,以便于操作。有关邮箱的简单计划,请参见图 1。
邮箱程序的第一部分包含以下几行
#include "colors.inc" #include "shapes.inc" #include "textures.inc" declare POST_WIDTH = 1.5 declare POST_HEIGHT = 10.0 declare MB_WIDTH = 3.0 declare MB_LENGTH = 6.0 declare MB_HEIGHT = 3.0
如果您使用 C 语言编程,您就会知道 #include 后跟文件名会导致该文件插入到 #include 的位置。POV 附带了许多包含文件,这些文件提供了预定义的内容。在本例中,我们正在加载带有易于使用的符号名称的颜色定义。我们包含 shapes 文件,因为它包含我们邮箱顶部所需的圆柱体的定义,以及 texture 文件,它允许我们指定表面外观。
包含文件主要由注释和 declare 指令组成,这些指令将符号名称附加到 POV 构造。我们目前使用它们的唯一目的是定义几个常量,这些常量给出了邮箱在 POV 三维空间中的尺寸。这使得更改邮箱的尺寸非常容易。稍后,我们将声明邮箱的侧面和末端,以便我们可以轻松地重用它们。
接下来我们定义灯光和相机
light_source { <10.0, 0.0, 25.0> color White } light_source { <0.0, 0.0, POST_HEIGHT+0.95*MB_HEIGHT> Gray50 } camera { location <7.0, 7.0, 13.0> sky z look_at <0.0, 0.0, POST_HEIGHT+MB_HEIGHT/2> }
在我们的图像中,我们有两个灯光。第一个灯光位于邮箱上方,并偏向一侧,与相机的位置相同。light_source 关键字后跟灯光的位置和颜色。第二个灯光实际上位于邮箱内部,靠近顶部。由于唯一其他的灯光没有照进箱子内部,除非我们在那里放一个灯光,否则内部最终会变成一个黑色的洞穴。由于我们想要一个非常小的灯光,我们选择了颜色 Gray50,它是黑色和白色之间的一半 (50%)。
指定相机的两个主要参数是其位置和视角。在我们的例子中,相机与箱子的高度大致相同,并且偏向一个角度,以便我们可以同时看到末端和侧面。sky 关键字指定哪个方向是向上,即图像的顶部。默认情况下,POV 假定 y 方向是向上。这里的 z 实际上是指定向量 <0,0,1> 的简写方式——您可以将任何方向定义为向上。
现在灯光和相机已经定义好了,我们可以开始将东西放入我们的小世界了
background { color SkyBlue } plane { z,0 color Green }
background 关键字用于指定如果光线最终没有与任何对象相交,则生成的颜色是什么。我们将其定义为名为 SkyBlue 的颜色。POV 能够生成更复杂的背景,具有不同深浅的蓝色、云彩和雾效果。
plane 关键字指定一个无限平面——在我们的例子中,是地面。平面的方向可以通过所谓的法向量来指定。法向量是垂直于平面伸出的方向,对于这张图片来说是 z 方向。第二个参数是一个数字,它指定平面与原点(点 <0,0,0>)的距离。由于地面位于 z=0,因此此参数为零。
从地面向上构建,第一个对象是木柱
box { < -POST_WIDTH/2.0, -POST_WIDTH/2.0, 0.0 >, < POST_WIDTH/2.0, POST_WIDTH/2.0, POST_HEIGHT > pigment { DMFWood4 } }
一个盒子由两个对角顶点的两个位置指定。盒子始终与 x、y 和 z 轴对齐——要获得其他方向,必须旋转盒子。为了获得木质表面,我们指定了 textures.inc 文件中预定义的颜料。表面的颜色在技术上是颜料的一部分,颜料包含有关表面的更多信息,但当仅指定颜色时,POV 允许我们省略 pigment 关键字。
现在我们已经到达邮箱本身,我们稍微移动平面以使事情更容易。我们不是在柱子顶部 (z=POST_HEIGHT) 构建邮箱,而是在地面 (z=0) 构建邮箱,然后将完成的箱子移动到柱子顶部。
declare mb_side = polygon { 5, < 0.0, -MB_LENGTH/2.0, 0.0 >, < 0.0, MB_LENGTH/2.0, 0.0 >, < 0.0, MB_LENGTH/2.0, MB_HEIGHT/2.0 >, < 0.0, -MB_LENGTH/2.0, MB_HEIGHT/2.0 >, < 0.0, -MB_LENGTH/2.0, 0.0 > } declare mb_bottom = polygon { 5, < -MB_WIDTH/2.0, -MB_LENGTH/2.0, 0.0 >, < MB_WIDTH/2.0, -MB_LENGTH/2.0, 0.0 >, < MB_WIDTH/2.0, MB_LENGTH/2.0, 0.0 >, < -MB_WIDTH/2.0, MB_LENGTH/2.0, 0.0 >, < -MB_WIDTH/2.0, -MB_LENGTH/2.0, 0.0 > }
正如我们前面提到的,我们正在声明这些部件,目的是稍后将它们组装在一起。Polygon 是一个希腊词,意思是“多边形”。第一个数字告诉 POV 有多少个空间点指定多边形。由于某种原因,POV 要求第一个点等于最后一个点。如果不是,POV 将自动闭合多边形并发出警告。
细心的读者会注意到我们遗漏了颜料/颜色规范。它将在最后添加,以便所有侧面都获得相同的颜料,并且我们不必重新输入相同的规范。
现在我们已经完成了一些简单的形状,我们可以做一些更复杂的事情,例如邮箱的末端
declare mb_end = union { polygon { 5, < -MB_WIDTH/2.0, 0.0, 0.0 >, < MB_WIDTH/2.0, 0.0, 0.0 >, < MB_WIDTH/2.0, 0.0, MB_HEIGHT/2.0 >, < -MB_WIDTH/2.0, 0.0, MB_HEIGHT/2.0 >, < -MB_WIDTH/2.0, 0.0, 0.0 > } disc { < 0.0, 0.0, MB_HEIGHT/2.0 >, y, MB_HEIGHT/2.0 } }
POV union 是一个或多个对象的集合,它们被绑定在一起成为一个对象。在邮箱末端的情况下,我们有一个矩形和一个相互重叠的圆盘。圆盘的位置由其中心的位置指定。方向由其法向量决定(可以将其视为轴的方向)。最后一个参数是圆盘的半径。由于它们完美重叠,圆盘和矩形无缝地结合在一起。
最复杂的构建对象是构成邮箱顶部的半圆柱体
declare mb_top = intersection { box { < -2,-1, 0 > < 2, 1, 2 > pigment { color red 1.0 green 1.0 blue 1.0 filter 1.0 } } Cylinder_Y scale <MB_WIDTH/2.0, MB_LENGTH/2.0, MB_WIDTH/2.0> }
intersection 的工作方式与 union 类似,不同之处在于最终结果由该部分所有元素共有的表面组成。如果我们在邮箱末端说 intersection 而不是 union,我们将最终得到一个半圆盘。在当前问题中,我们想要半个圆柱体。
Cylinder_Y 实际上是在 shapes.inc 中根据 POV quadric 原语定义的。结果是一个半径为 1.0 的沿 y 轴的无限圆柱体。我们通过将圆柱体与盒子相交来将其截断。除了最后一个细节外,这都有效——结果是一个实心的半圆柱体,而不是外表面。不透明的平面来自我们的边界框。
这个问题的解决方案只能在计算机上实现——我们通过给盒子一个特殊的颜色使其不可见。我们没有使用 color 关键字后面的预定义颜色名称,而是指定了要使用的红色、蓝色和绿色的量。最后一个关键字 filter 反转了颜色的整个解释,因此表面不是反射光,而是透射光。如果我们将命令更改为 color red 1.0 filter 1.0,我们将最终得到一个仅透射红光的表面。由于我们的颜色具有红色、绿色和蓝色的最大值,因此所有光线都会透射。
当最终场景在门打开的情况下渲染时,仔细检查会发现结果略有不足。如果您正在寻找它,圆柱体的末端比下部更暗。造成这种情况的原因是 POV 在速度和精度之间做出的权衡。
回想一下,像素的颜色是通过从观察者那里反弹光线直到它击中光源来计算的。在盒子内部,会发生大量的反弹,并且在某个时候,POV 不得不放弃击中光源,因为每次反弹都会减少正在传输的光量。问题在于我们的不可见表面仍然被视为一个表面,POV 过早放弃。通过将 max_trace_level 全局变量设置得更高可以补救这一点。最佳值可以通过设置一个非常高的值来确定,然后查看 POV 在运行结束时打印的统计信息。在当前场景中,需要最多 16 次反弹,因此我们将其设置为 20,使用以下行
global_settings { max_trace_level 20 }
在复杂的半圆柱体之后,邮箱的旗帜相比之下显得微不足道。旗帜各个部分的尺寸也在图 2 中。

图 2. 邮箱旗帜
declare STAFF_HEIGHT = 3.0 declare STAFF_WIDTH = 0.30 declare FLAG_HEIGHT = 1.0 declare FLAG_WIDTH = 1.5 declare mb_flag = polygon { 7, < 0.0, 0.0, 0.0 >, < 0.0, -STAFF_HEIGHT, 0.0 >, < 0.0, -STAFF_HEIGHT, -FLAG_WIDTH+STAFF_WIDTH >, < 0.0, -STAFF_HEIGHT+FLAG_HEIGHT, -FLAG_WIDTH+STAFF_WIDTH >, < 0.0, -STAFF_HEIGHT+FLAG_HEIGHT, -STAFF_WIDTH >, < 0.0, 0.0, -STAFF_WIDTH >, < 0.0, 0.0, 0.0 > pigment { color Red } finish { ambient 0.85 } }邮箱旗帜的 finish 与邮箱的其余部分不同,因此我们在此处指定它。finish 决定了光线如何从所讨论的表面反弹。特别是,ambient 参数指定了要反射到相机的环境光量。环境光是一个全局参数,默认为白光。0.85 指定“大量”,因此我们最终得到一个荧光红色的旗帜,即使所有其他灯都关闭,它也会发光。
邮箱的最后一部分是顶部的标志,上面写着地址。
declare sign = union { text { ttf "timrom.ttf" "Andy" 0.05, 0.0 pigment { Red } finish { ambient 0.5 diffuse 0.5 } translate <0.32, 0.175, 0.0> } polygon { 5, < 0.0, 0.0, 0.0 >, < 3.0, 0.0, 0.0 >, < 3.0, 1.0, 0.0 >, < 0.0, 1.0, 0.0 >, < 0.0, 0.0, 0.0 > pigment { White } } }
同样,我们在地面上构建标志,稍后再将其移动到位。这样做的主要原因是文本对象在固定位置渲染,该位置在我们的地面上是平坦的 (z=0)。在 POV 3.0 之前,文本必须费力地从多边形的联合体构建。POV 3.0 能够读取和显示 Adobe TrueType 字体。大多数字母大约一个单位高,大约半个单位宽。由于 POV 的世界是一个三维世界,因此文本对象具有厚度,我们将其设置为 0.05。这足够大,可以使字母从背景矩形中突出,但又足够小,以至于厚度不明显。我们设置为零的最后一个参数是一个数字,它指定每个字符之间超出通常间距的偏移量。
文本对象的缺点是居中和确定文本应有多大是反复试验的问题。translate 关键字通过在特定方向上移动对象来修改对象,在本例中是移动到以下矩形的中心。
最后,我们将整个邮箱组装在一起。我们将所有内容放在一个联合体中,以便我们可以将整个结构平移到柱子的顶部
union { object { mb_bottom } object { mb_side translate < -MB_WIDTH/2.0, 0.0, 0.0> } object { mb_side translate < MB_WIDTH/2.0, 0.0, 0.0> } object { mb_end translate < 0.0, -0.5*MB_LENGTH, 0.0 > } object { mb_end rotate < -90.0*clock, 0.0, 0.0 > translate < 0.0, 0.475*MB_LENGTH, 0.0 > }
联合体的第一部分将底部、侧面和末端放在一起。本节中唯一的新内容是 rotate 关键字,它旋转邮箱的前端。rotate 关键字后的向量给出了绕 x、y 和 z 轴的旋转角度(以度为单位)。clock 变量是零到一之间的数字,当渲染多帧动画时使用。不是渲染单帧,而是在命令行中给出要绘制的帧数,并且变量 clock 对于第一帧为零,对于最后一帧为一。结果是一系列图像,门平稳地旋转打开。对于单帧,clock 设置为零。
object { mb_top translate < 0.0, 0.0, MB_HEIGHT/2.0 > } object { sign scale 0.9 rotate <90, 0, -90> translate <0, 1.5, MB_HEIGHT> } object { mb_flag rotate <-90*clock, 0.0, 0.0> translate < 0.01+MB_WIDTH/2.0, MB_LENGTH/2.95, MB_HEIGHT/2.25 > } texture { pigment { color Silver } normal { bumps 0.1 scale 0.01 } finish { ambient 0.2 brilliance 6.0 reflection 0.5 } } translate < 0.0, 0.0, POST_HEIGHT> }最后一部分将邮箱的顶部、标志和旗帜放置到位。最后的 texture 关键字用于为所有尚未具有纹理的表面提供纹理——已经具有纹理的表面不受影响。brilliance 参数控制光线如何作为光线照射表面角度的函数从表面反射。较高的值使表面看起来更具金属感,这正是我们想要的。reflection 关键字导致反射发生,其中 1.0 是镜面,0.0 是黑色非反射表面。
当然,最后要做的事情实际上是运行 POV 来渲染图像。在动画模式下运行 POV 涉及添加命令行开关 +KFFn,其中 n 是要生成的帧数。输出文件名附加 01、02 等,对应于帧号。对于长动画,可以轻松完成整个序列的子集。
虽然真正的项目尚未完成,但查看输出的真实效果会有所帮助。为了方便起见,提供了一个简短的 Tcl/Tk 脚本 (列表 1),它将在鼠标单击时向前和向后显示简单的动画。由于是解释型的,所以速度不快,但编写速度很快并且可以工作。此脚本的替代方案是使用 GIFMerge 程序将单独的 GIF 图像合并为单个 GIF,该 GIF 可以由主要的 Web 浏览器之一或 XAmin 显示。POV 不写入 GIF 文件,但有许多转换器可用。

图 3:完成的邮箱

图 4:邮箱的分解视图
Andy Vaught 目前是亚利桑那州立大学计算物理学的博士候选人,并且自 1.1 版本以来一直在运行 Linux。他喜欢与民用航空巡逻队一起飞行以及滑雪。可以通过 andy@maxwell.la.asu.edu 与他联系。