使用 POV-Ray 进行科学可视化
我是一名在中央密歇根大学的气象学家,与伊利诺伊大学的合作者一起研究超级单体雷暴的行为,这是一种寿命很长的旋转怪物,每年春天都会在美国大平原上肆虐。我研究这些可怕风暴行为的主要工具是一个名为 NCOMMAS 的数值模型,这是一个用 FORTRAN 90 编写的计算机应用程序,它使用物理方程来模拟大气随时间推移的三维状态。即使使用有损压缩历史文件格式,这个模型在一个四小时的雷暴模拟过程中也会产生大量数据,大约 200GB。我在研究中面临的最大挑战之一是找到可视化这些数据的方法,以便提供对模拟风暴物理性质的科学见解。
可视化 3-D 数据的一种方法是使用光线追踪器,这是一种计算机应用程序,它模拟光与三维虚拟对象交互的行为,以创建位图图像(图 1)。这个位图图像可以显示在计算机屏幕上,和/或以 PPM 或 TIFF 等图像格式保存到磁盘。持久性视觉光线追踪器,简称 POV-Ray,是一个流行的开源光线追踪器,我在 1990 年代中期在威斯康星大学攻读博士论文时引起了我的注意。当时,我正在寻找软件来可视化我的微暴流 3-D 模型数据,微暴流是有时从雷暴云层下降的强烈下沉气流。由于习惯了学术界的共享源代码性质,并且作为一个贫穷的研究生,我正在寻找以源代码形式分发的免费软件,我可以下载并修改以适应我自己的特定需求。POV-Ray 当时是我的逻辑选择,并且在今天仍然适合我创建研究数据的光线追踪表示的需求。
然而,渲染科学数据并不是 POV-Ray 设计的任务,很少有研究人员使用 POV-Ray 渲染科学数据。存在其他面向研究人员的光线追踪软件包,用于处理数值模型,但它们是专有的且价格昂贵。在本文中,我概述了修改 POV-Ray 的过程,以便可以本地渲染 3-D 科学数据的等值面。
尽管 POV-Ray 以二进制形式分发用于 Linux、Mac OS 和 Microsoft Windows,但您需要获取源代码才能应用补丁并进行进一步的自定义。我正在使用截至本文撰写时可用的最新版本的 POV-Ray,版本 3.5。您需要从 POV-Ray 下载页面选择 Unix/Linux/通用源代码选项。此外,您还需要获取 Ryouichi Suzuki 的密度文件扩展补丁(请参阅在线资源),它实际上是一个 Zip 文件,其中包含少量 POV-Ray 文件的替换源代码。文件 pov35dfjs.zip 应该解压到 povray-3.50c/src 目录中,其中 13 个文件将被覆盖。
POV-Ray 的工作原理是读取场景描述文件,该文件包含创建位图图像所需的所有信息。POV-Ray 有自己的场景描述语言,POV-Ray 网站上有详细的文档。如果您以前从未使用过光线追踪器,我建议在修改源代码之前,先熟悉光线追踪器的基础知识和 POV-Ray 的场景描述文件。
在 POV-Ray 中渲染的项目称为对象。对象的示例包括 Box、Sphere、Torus 和 Plane。用户指定对象在场景中存在的位置、在创建对象时要使用的参数、要使对象呈现的颜色、照明参数等等。这些规范在场景描述文件中指定,有时称为 pov 文件,因为其后缀为 .pov,这是一个纯文本文件,由 POV-Ray 在运行时解析。
一种通用的对象是等值面,这是一种 3-D 形状,其表面表示函数值恒定的点。函数的恒定值由用户选择。POV-Ray 包含许多预定义的对象,实际上可以表示为等值面。例如,以下场景描述文件中的部分将渲染一个半径为 0.7 单位、中心位于原点(即笛卡尔网格点 (0,0,0))的灰色球体
sphere { <0,0,0>, 0.7 pigment {rgb .5} }
相同的对象可以按以下方式渲染为等值面对象
#declare R = 0.7 isosurface { function {x*x + y*y + z*z - R*R} pigment {rgb .5} }
这是可行的,因为描述半径为 R 的球体的数学公式是
x2 + y2 + z2 - R2 = 0
等值面对象的这种通用性使其成为我雷暴图像的首选对象。
在球体示例中,使用数学函数来计算等值面值。我的雷暴数值模型数据不能表示为数学函数;相反,它表示为三维浮点数组,其中包含模型变量,例如每个网格位置的温度、风速和云浓度(图 2)。
POV-Ray 3.5 具有一个称为密度文件的功能,允许映射表示为网格点值的函数。POV-Ray 文档将密度文件描述如下:“density_file 模式是一个 3-D 位图模式,它占据从位置 <0,0,0> 到 <1,1,1> 的单位立方体。数据文件是为 POV-Ray 创建的原始二进制文件格式,称为 df3 格式。”
密度文件可以用作传递给等值面对象的参数的函数。以下是使用密度文件进行等值面渲染的示例
#declare DENSFUNC=function { pattern { density_file df3 "cloud.df3" interpolate 1 } } isosurface {function { 0.1 - DENSFUNC(x,y,z) }
在上面的示例中,将使用三线性插值方案(稍后详细介绍插值)从 cloud.df3 文件创建值为 0.1 的等值面。
密度文件格式很严格,数据值表示为 8 位数据(无符号短整数,范围从 0 到 255),在内部缩放到范围从 0.0 到 1.0。由于我的雷暴数据是 32 位浮点数据,因此使用股票 POV-Ray 3.5 的密度文件格式是不可行的。
Ryouichi Suzuki 在 1996 年以来一直为 POV-Ray 提供非官方的附加代码。他为 POV-Ray 3.0 提供了第一个补丁,其中引入了等值面对象,现在是 3.5 版中的标准对象。Suzuki 的代码包含在上面引用的 Zip 文件中,其中包含扩展 POV-Ray 密度文件功能的例程,包括渲染浮点密度文件数据的选项。
当使用密度文件作为函数时,必须考虑到,尽管数学函数是一个连续表达式——它为 x、y 和 z 空间坐标的任何浮点值定义——但密度文件是由整数数组索引引用的离散数据集。在渲染场景时,必须对网格点之间的空间进行插值。可用的两种插值方法是三线性插值和三立方样条插值。三线性插值速度快,但通常不如三立方样条插值产生平滑的插值。
在使用 Suzuki 的密度文件代码修补 POV-Ray 3.5 代码后,如果您遵守 df3 格式或 Suzuki 的扩展格式,则可以渲染浮点等值面。在我的案例中,我有数百千兆字节的 HDF(分层数据格式)模型数据,这是专门为数值模型设计的。因为我不仅对生成一些等值面图像感兴趣,而且对从数百甚至数千个这些图像制作动画感兴趣,所以为每个文件编写 HDF 到 df3 转换器不是一个可行的选择。因此,我开始仔细研究处理密度文件数据的 POV-Ray 例程,希望我可以修改代码以读取 HDF 数据。
对我来说重要的是,我对 POV-Ray 所做的修改不会导致功能丧失或破坏与未修补版本的兼容性。我通过添加一些新的对象来实现这个目标,这些对象在场景描述文件中引用,可以由我的修补版本解析和渲染,同时保持所有其他对象不变。
我修改的主要代码部分位于 pattern.cpp 中,其中包含 Read_Density_File 例程。正如您可能猜到的那样,此例程将密度文件数据读取到三维数组中。使用此例程作为模板,我创建了一个新的例程 Read_Hdf_File,以将我的历史文件数据读取到 POV-Ray 中。这是 POV-Ray 代码的大部分修改需要完成的地方,以符合您自己的数据格式。Read_Hdf_file 的缩写版本如清单 1 所示。
清单 1. 来自 pattern.cpp 的缩写 Read_Hdf_File 清单
void Read_Hdf_File (DENSITY_FILE * df) { Locate_Density_File(df->Data->Name); df->Data->Type = 1; //floating point data Open_HDF_File(df->Data->Name); //povray needs array dimensions Read_HDF_File_Geometry(nx,ny,nz); df->Data->Sx = nx; df->Data->Sy = ny; df->Data->Sz = nz; //this array will contain density file data Allocate_Memory(mapF,nx,ny,nz); //read variable into mapF array Get_HDF_File_Data(Var,mapF,nx,ny,nz); //density file pointer now points to model data df->Data->DensityF = mapF; }
Read_Hdf_file 的功能是将 HDF 浮点数据读取到 mapF 中,mapF 是一个浮点数的 3-D 数组,现在它已准备好作为密度文件进行操作。我编写了一段单独的代码 history.c,其中包含 pattern.cpp 中引用的所有 HDF I/O 例程。您的数据文件格式将需要其自己的特定格式代码才能将您的 3-D 数据读取到 POV-Ray 中。
为了使 POV-Ray 本地识别新的 HDF 文件格式并允许渲染每个场景的多个模型变量,还需要修改一些文件。表 1 包含修改文件的列表以及对每个文件所做操作的简要说明。
表 1. 修改文件列表,以将模型数据纳入 POV-Ray,以及对所做操作的简要说明。
文件 | 修改 |
---|---|
pattern.cp | 添加了 Get_HDF_File_Data 例程,该例程将模型数据读取到内存中。 |
pattern.h | 添加 Read_Hdf_File 的声明。 |
parstxtr.cpp | 为 HDF_TOKEN 添加 case 语句块。 |
tokenize.cpp | 将 HDF_TOKEN 添加到 Reserved_Words 数组。 |
frame.h | 将 char *Var1 添加到 Density_file_Data_Struct 结构。 |
parse.h | 将 HDF_TOKEN 添加到 TOKEN_IDS。 |
HDF 文件格式允许在每个文件中存储多个变量,这与 density_file 格式不同。在我的案例中,每个 HDF 文件都是模型状态在给定时间的快照,并且每个文件包含 12 个 3-D 变量。在一个场景中一起查看多个变量(例如云、雨、冰雹和雪)通常具有启发意义。我通过创建一个新的令牌来表示 HDF 文件格式(称为 HDF_TOKEN,与表示原始 df3 格式的 DF3_TOKEN 不同)来实现这一点,并在结构 Density_file_Data_Struct 中添加了一个新的字符数组 Var。Var 在场景描述文件中分配,并作为参数传递给 HDF 例程,以指定要选择的模型变量。为了解析变量名(表示为字符串),我在 parstxtr.cpp 中的 Parse_PatternFunction 例程中添加了一个额外的 case 语句(清单 2)。请注意添加了 Parse_Comma 和 Parse_C_String,它们获取要读取的变量。
清单 2. HDF_TOKEN case 需要额外的解析,以允许指定要光线追踪的变量。此代码片段位于 parstxtr.cpp 中的 Parse_PatternFunction 例程中。
EXPECT CASE (DF3_TOKEN) New->Vals.Density_File->Data->Name = Parse_C_String(true); Read_Density_File(New->Vals.Density_File); EXIT END_CASE CASE (HDF_TOKEN) New->Vals.Density_File->Data->Name = Parse_C_String(true); Parse_Comma(); New->Vals.Density_File->Data->Var = Parse_C_String(true); Read_Hdf_File(New->Vals.Density_File); EXIT END_CASE
现在所有部分都已就位,可以构建一个场景描述文件,供 POV-Ray 解释。我使用了 Suzuki 的密度文件扩展 Web 页面上的示例作为模板,并对其进行了修改以适应我的需求。清单 3 包含完整的场景描述文件,用于渲染云浓度等值面,背景为天蓝色,表面为平铺,如图 1 所示。从顶部开始,需要 #version 语句才能使这个非官方版本的 POV-Ray 正常运行。以下九个 #declare 语句指定了包含等值面的框的笛卡尔坐标,以及缩放因子。
清单 3. cloud.pov
#version unofficial dfe 3.5; #include "colors.inc" #declare x0 = 0.0; #declare x1 = 700.0; #declare y0 = 0.0; #declare y1 = 600.0; #declare z0 = 0.0; #declare z1 = 80; #declare scalex = (x1-x0+1); #declare scaley = (y1-y0+1); #declare scalez = (z1-z0+1); #declare R = 0.7; #declare G = 0.7; #declare B = 0.7; #declare AMBIENT = 0.5; #declare DIFFUSE = 1.1; #declare SPECULAR = 0.3; #declare ROUGHNESS = 0.01; #declare BRILLIANCE = 1.0; camera { up <0,0,1> sky <0,0,1> right <3.0,0,0> direction <1.0,0,0> location <420,70,70> look_at <370,300,90> } light_source {<100,100,100> color Gray25 shadowless} light_source {<400,200,30> color Gray20 } light_source {<1000,-500,150> color Gray25 } light_source {<-400,-500,150> color Gray25 } #declare QCFUNC = function { pattern{ density_file hdf "supercell.ck10990.hdf","QC" interpolate 2 //tricubic spline frequency 0 scale <scalex,scaley,scalez> } } #macro QCISOSFC(iso,trans) isosurface{ function{ -QCFUNC(x,y,z) } threshold -iso max_gradient 0.0002 contained_by{box{<x0,y0,z0>,<x1,y1,z1>}} texture{ pigment{color rgbt<R,G,B,trans>} finish{ambient AMBIENT diffuse DIFFUSE specular SPECULAR roughness ROUGHNESS brilliance BRILLIANCE} } no_shadow } #end QCISOSFC(0.0002,0.0) // render cloud box { <x0,y0,z0> <x1,y1,z0> // tiles 5km square pigment {checker color NewTan, color .90*NewTan scale 50} finish {ambient 0.5 diffuse 0.5} } background {SkyBlue} // what else?
继续浏览场景描述文件,声明了颜色和完成参数,并设置了相机和照明参数。以下行包含创建等值面的重要位。QCFUNC 被声明为一个函数,它使用 HDF 文件 supercell.ck10990.hdf 作为数据源;它选择文件中的变量 QC(表示云浓度)进行渲染。选择了三立方样条插值,并且整个域被缩放,以便所有空间索引(例如相机位置和光照位置)都与数据的数组索引值一致。默认情况下,POV-Ray 的域在所有三个方向上的范围都是 0.0 到 1.0。
我创建了一个名为 QCISOSFC 的宏,它将我想要渲染的等值面的值和等值面的透明度级别作为参数。当渲染两个等值面时,透明度是一个有用的等值面属性,其中一个等值面位于另一个等值面内部。例如,渲染包含冰雹浓度等值面的透明云非常有用,因为冰雹通常包含在雷暴云中。上面定义的 QCFUNC 被选为要渲染的等值面函数。选择的等值面绑定了云浓度大于所选等值面值 0.0002 的体积。
max_gradient 参数基本上告诉 POV-Ray 它需要做多少工作来找到等值面。从技术上讲,它告诉 POV-Ray 表示等值面数据的函数在所选等值面附近包含的最大梯度(距离上的最大变化)。这是一个必须仔细选择的数字。值太低会导致等值面出现孔洞或根本无法渲染;值太大会导致 POV-Ray 运行的时间比必要的要长得多。需要进行一些实验才能找到合适的 max_gradient 值。我选择的值为 0.0002,这看起来可能很小;但是,云浓度范围从 0.0 到大约 0.01。POV-Ray 在渲染 max_gradient 值过大或过小的等值面后会警告您,并在渲染后建议它认为合适的值。
为了使用您的更改进行编译,您可能需要对 src/Makefile 进行一些小的修改,该文件在您运行configure从顶层 POV-Ray 目录开始。如果您正在使用外部库进行历史文件读取例程,或者如果您编写了一段单独的代码来处理文件 I/O,则情况就是如此。
编译完成后,您可以从命令行调用 POV-Ray。以下命令将从 cloud.pov 读取并创建一个 600×400 抗锯齿 PPM 文件,并在渲染时显示到屏幕
/home/orf/povray-3.50c-orf/src/povray +D \ Input_File_Name=cloud Width=600 Height=400 \ Antialias=on Output_File_Type=P
一旦您的数据使用 POV-Ray 成功渲染,您就可以使用 POV-Ray 广泛的可配置选项集来完全按照您想要的方式渲染场景。如果您的数据随时间变化,则制作动画既简单又令人满意。我编写了 Python 脚本,以便在我的小型渲染场上的每个处理器上调用 POV-Ray 的实例,其中每个处理器同时处理不同的模型时间。然后将生成的 PPM 文件连接在一起以制作动画,使用 mjpegtools。请参阅我的研究页面以获取一些动画。我还创建了立体图像和动画,用于在我们部门的 GeoWall 系统上显示。立体对生成对于 POV-Ray 来说是微不足道的,并且确实可以为您提供关于数据的全新视角。让 POV-Ray 与我的模型数据一起工作为我打开了许多令人兴奋的可能性,我希望它也能为您做到这一点。
本文资源: /article/7754。
Leigh Orf 是中央密歇根大学大气科学助理教授,也是一位长期的 Linux 用户。他的研究兴趣包括使用大规模并行 Linux 集群对雷暴进行逼真的模拟和可视化。在不工作的时候,他喜欢自己酿啤酒,通过业余无线电进行通信,演奏萨克斯管,并与他的妻子 Annie 一起去露营旅行。可以通过 leigh.orf@cmich.edu 联系到他。