体绘制(Volume Rendering)概述

摘抄“GPU Programming And Cg Language Primer 1rd Edition” 中文名“GPU编程与CG语言之阳春白雪下里巴人”   

 

 

1982 年2 月,美国国家科学基金会在华盛顿召开了科学可视化技术的首次会议,会议认为“科学家不仅需要分析由计算机得出的计算数据,而且需要了解在计算过程中的数据变换,而这些都需要借助于计算机图形学以及图像处理技术”。

---- 《三维数据场可视化》1.1 节科学计算可视化概述

 

 

自 20 世纪 80 年代科学计算可视化( Visualization in Scientific Computing )被提出后,三维体数据的可视化逐渐称为发展的重点,并最终形成了体绘制技术领域。

一些文章,甚至是优秀硕博士论文库上的文章,解释体绘制概念时,通常都说 “ 体绘制技术是直接根据三维体数据场信息产生屏幕上的二维图像 ” ,这种说法太过含糊,如果根据三维体数据场信息随便产生一张图像,难道也是体绘制吗?我查找相关文献后,发现这种说法是国外一些文献的误译,例如, M.Levoy 在文章 “Display of surfaces from volume data”( 文献【 14 】 ) 中提到 “volume rendering describes a wide range of techniques for generating images from three-dimensional scalar data” ,翻译过来就是 “ 体绘制描述了一系列的 “ 根据三维标量数据产生二维图片 ” 的技术 ” 。注意,人家文章中用的是 “ 描述 ” ,而不是对体绘制下定义。老实说,老外的这种说法虽然挑不出毛病,但是我依然感觉没有落实到重点。

体绘制的核心在于 “ 展示体细节!而不是表面细节 ” 。我给出的定义是:依据三维体数据,将所有体细节同时展现在二维图片上的技术,称之为体绘制技术。利用体绘制技术,可以在一幅图像中显示多种物质的综合分布情况,并且可以通过不透明度的控制,反应等值面的情况。

例如, CT 图片中展示的是人体的肌肉和骨骼信息,而不是表面信息(那是照片)。所以理解体绘制和面绘制技术的区别的 , 一个很直观的比喻是:普通照相机照出的相片和 CT 仪器拍出的 CT 照片,虽然都是二维图片,但是展现的对象是不同的!

国外自上世纪 80 年代末以来,在体绘制技术方面已经取得了长足的进步,西门子、东芝、通用电器,都有对 GPU 编程领域以及体绘制技术进行研究,并将体绘制技术运用到医疗器材中。然而,体绘制技术在中国的发展,如果说还处于萌芽阶段,实不为过!我在学习和研究过程中,在国内网站上甚至只找到了一个可用的体数据,还是国外代码中附带的演示数据,而国内的 openGPU 网站上关于体绘制的论坛板块则是根本是空白。国外已经常用的医疗器材和算法,在中国还没有成形,这实在是一种悲哀。一个讽刺的现象是,外国公司从事体绘制算法研究的却不乏中国人,这更是一种悲哀。写到这里,作为一名以 server the people 为毕生理想的有志青年,我有点伤感,有些沮丧,所以,还是先洗洗睡了,明天再来写下面的章节。

14.1 体绘制与科学可视化

科学可视化技术是运用计算机图形学、图像处理、计算机视觉等方法,将科学、工程学、医学等计算、测量过程中的符号、数字信息转换为直观的图形图像,并在屏幕上显示的理论、技术和方法。

体绘制是科学可视化领域中的一个技术方向。如前所述,体绘制的目标是在一副图片上展示空间体细节。举例而言,你面前有一间房子,房子中有家具、家电,站在房子外面只能看到外部形状,无法观察到房子的布局或者房子中的物体;假设房子和房子中的物体都是半透明的,这样你就可以同时查看到所有的细节。这就是体绘制所要达到的效果。

14.2 体绘制应用领域

人类发展史上的重大技术带来的影响大致分为两种:其一,技术首先改变生活本身,然后改变人类对世界的看法,例如电视、电话等;还有一种技术,是首先改变人类对世界的看法,然后改变生活本身,例如伦琴射线、望远镜。

体绘制技术应该属于后者,通过改变所见,而改变生活。体绘制计算的重要意义,首先在于可以在医疗领域 server the people, 有助于疾病的诊断,这一点应该不用多说,计算机断层扫描( CT )已经广泛应用于疾病的诊断。医疗领域的巨大需求推动了体绘制技术的告诉发展,如果了解 CT 的工作原理,也就大致了解了体绘制技术原理和流程,所以本书在附录 B 给出了医学体绘制的有关文献,作为补充阅读资料,当您对体素、光线投射等术语缺乏感性认识时,可以参阅理解;其二,体绘制计算可以用于地质勘探、气象分析、分子模型构造等科学领域。我在工作期间承担的一个较大的项目便是有关 “ 三维气象可视化 ” ,气象数据通常非常庞大,完全可以号称海量数据,每一个气压面上都有温度、湿度、风力风向等格点数据,气象研究人员希望可以同时观察到很多气压面的情况,这时就可以采用体绘制技术,对每个切面(气压面)进行同时显示。

体绘制技术也能用于强化视觉效果,自然界中很多视觉效果是不规则的体,如流体、云、烟等,它们很难用常规的几何元素进行建模,使用粒子系统的模拟方法也不能尽善尽美,而使用体绘制可以达到较好的模拟效果。如 图 41 所示,这是使用体绘制技术进行烟的模拟效果。


14.3 体绘制与光照模型

尽管光照模型通常用于面绘制,但是并不意味着体绘制技术中不能使用光照模型。实际上 , 体绘制技术以物体对光的吸收原理为理论基础,在实现方式上,最终要基于透明度合成计算模型。此外,经典的光照模型,例如 phong 模型, cook-torrance 模型都可以做为体绘制技术的补充,完善体绘制效果,增强真实感。

往往有初学者会分不清 “ 体绘制技术 ” 以及 “ 透明光照模型 ” 之间的区别。这个问题很有意思。实际上,体绘制技术与透明光照模型在感性认识上十分类似,在很多教程中对体绘制技术的阐述也涉及到透明物体。但是,透明光照模型,一般侧重于分析光在透明介质中的传播方式(折射,发散,散射,衰减等),并对这种传播方式所带来的效果进行模拟;而体绘制技术偏重于物体内部层次细节的真实展现。举例而言,对于一个透明的三棱镜,使用透明光照模型的目的在于 “ 模拟光的散射,折射现象(彩虹) ” ;而对于地形切片数据或者人体数据,则需要使用体绘制技术观察到其中的组织结构。此外,在实现方式上,透明光照模型一般是跟踪光线的交互过程,并在一系列的交互过程中计算颜色值;而体绘制技术是在同一射线方向上对体数据进行采样,获取多个体素的颜色值,然后根据其透明度进行颜色的合成。

总的来说,透明光照模型侧重于光照效果展现,并偏向艺术化;而体绘制技术侧重展现物质内部细节,要求真实!

不过,现在体绘制技术实际上也可以用于艺术领域,因为体绘制技术所使用的方法,实际上具有很强的通用性,尤其是传统的 ray-cast 方法,完全可以应用到透明光照模型中(绘制烟雾等)。不同的技术之间会存在共融性,将技术和领域的关系近固化,是研究人员的大忌。科学史上很多前例都说明了一个事实:不同领域的交合点,往往会出现重大发现或发明。在爱因斯坦之前,又有谁知道时间、空间和质量之间的关系呢?

体数据( Volume Data )

学习任何一门技术,首先要弄清楚这项技术的起源以及数据来源。技术的起源也就是技术最原始的需求,最原始的发展动力,了解了这一点就了解了这项技术的价值。而了解一门技术的数据来源,就把握了技术的最初脉络,是 “ 持其牛耳 ” 的一种方法,正如软件工程中的数据流分析方法一般。

我很想说,体数据与面数据的区别,就好像一个实心的铁球和一个空心的兵乓球的区别。不过这个比喻很显然有点俗,很难让人相信作者(我)是一个专业人士。于是我决定还是将与体数据相关的专业术语都阐述一遍。

不过,在此之前,我需要先消除大家的恐惧感,研究表明,动物对于未知事物总是存在恐惧感,这也是阻碍进一步学习的关键所在。体数据不是什么特别高深的火星符号,它是对一种数据类型的描述,只要是包含了体细节的数据,都可以称之为体数据。举个例子,有一堆混凝土,其中包含了碳物质( C )若干,水分子( H20 )若干,还有不明化学成分的胶状物,你用这种混凝土建造了块方砖,如果存在一个三维数组,将方砖 X 、 Y 、 Z 方向上的物质分布表示出来,则该数组可以被称为体数据。不要小看上面这个比喻,体数据本质上就是按照这个原理进行组织的!

体数据一般有 2 种来源:

1.        科学计算的结果,如:有限元的计算和流体物理计算;

2.        仪器测量数据,如: CT 或 MRI 扫描数据、地震勘测数据、气象检测数据等。

与体数据相关的专业术语有:体素( Voxel )、体纹理( Volume Texture )。尤其要注意:所谓面数据,并不是说二维平面数据,而是说这个数据中只有表面细节,没有包含体细节,实际上体数据和面数据的本质区别,在于是否包含了体细节,而不是在维度方面。

14.4.2 体素( Voxel )

Wikipedia 中对体属 voxel 的介绍为:

A voxel (a portmanteau of the words volumetric and pixel) is a volume element, representing a value on a regular grid in three dimensional space. This is analogous to a pixel , which represents 2D image data in a bitmap 

即 “ 体素,是组成体数据的最小单元,一个体素表示体数据中三维空间某部分的值。体素相当于二维空间中像素的概念 ” 。 图 42 中每个小方块代表一个体素。体素不存在绝对空间位置的概念,只有在体空间中的相对位置,这一点和像素是一样的。

通常我们看到的体数据都会有一个体素分布的描述,即,该数据由 n*m*t 个体素组成,表示该体数据在 X 、 Y 、 Z 方向上分别有 n 、 m 、 t 个体素。在数据表达上,体素代表三维数组中的一个单元。假设一个体数据在三维空间上 256*256*256 个体素组成,则,如果用三维数组表示,就必须在每一维上分配 256 个空间。

在实际的仪器采样中,会给出体素相邻间隔的数据描述,单位是毫米( mm ),例如 0.412mm 表示该体数据中相邻体素的间隔为 0.412 毫米 。


14.4.1 体纹理( Volume Texture )

体数据最主要的文件格式是 “ 体纹理( volume texture ) ” !故而,非常有必要对体纹理的概念进行详细的阐述。

目前,学术性文章中关于体纹理的概念描述存在不小的混乱,很多书籍或者网页资料没有明确的区分 2d texture , 3d texture , volume texture 之间的区别。导致不少人认为 “ 只要是用于三维虚拟或仿真技术中的纹理都称之为 3d texture” 。这是一个误会。纹理上的 2 , 3 维之分本质上是根据其所描述的数据维数而定的,所谓 2d texture 指的是纹理只描述了空间的面数据,而 3d texture 则是描述了空间中的三维数据。 3d texture 另一个较为学术化的名称是: volume texture 。文献【 22 】上对体纹理的定义是:

3D texture (Three Dimensional Texture), also known as "volume texture," is a logical extension of the traditional (and better known) 2D texture. In this context, a texture is simply a bitmap image that is used to provide surface coloring for a 3D model. A 3D texture can be thought of as a number of thin pieces of texture used to generate a three dimensional image map. 3D textures are typically represented by 3 coordinates.

翻译成中文就是 “ 三维纹理,即体纹理,是传统 2D 纹理在逻辑上的扩展。二维纹理是一张简单的位图图片,用于为三维模型提供表面点的颜色值;而一个三维纹理,可以被认为由很多张 2D 纹理组成的,用于描述三维空间数据的图片。三维纹理通过三维纹理坐标进行访问 ” 。

从上面这句话,可以得到两点信息:

1.        三维纹理和体纹理是同一概念;三维纹理和二维纹理是不同的;

2.        三维纹理通过三维纹理坐标进行访问。

这时可能会有人提出问题了,图片都是平面的,怎么能表示三维数据?请注意,我们通常所看到的图片确实都是平面的,但是并不意味着 x,y 平面上的像素点不能存放三维数据,举一个例子:在高级语言编程中,我们完全可以用一维数组去存放三维数组中的数据,只要按照一定规则存放即可!

按照一定规则将三维数据存放在 XY 像素平面所得到的纹理,称之为 volume texture 。

体数据通常是由 CT 仪器进行扫描得到的,然后保存在图片的像素点上。目前国际上比较常用的体纹理格式有,基于 DirectX 的 .dds 格式和 .raw 格式。注意,很多人会将 .raw 格式当作摄像器材使用的那种格式,其实这两个格式的后缀虽然都是 .raw ,但是其数据组织形式是不同的。用于体纹理的 .raw 格式,存放的是三维数据,用于摄像器材的 .raw 格式只是普通的二维图片。 图 43 从左到右分别是 University of Tübingen ( Germany )、 Viatronix Inc.(USA) 、 Walter Reed Army Medical Center (USA) 三家机构的通过仪器扫描得到的体纹理数据的体绘制图片。


这三个体纹理数据的描述分别是: 256 x 320 x 128 /0.66, 0.66, 0.66 ; 512 x 512 x 174/0.8398, 0.8398, 3.2 ; 512 x 512 x 463/0.625, 0.625, 1.0 。

由于在国内的网站上很难找到体数据,所以下面我给出几个国外的网址,这些网址提供用于教学和研究只用的体纹理数据(只能用于教学和研究)。

http://wwwvis.informatik.uni-stuttgart.de/~engel/pre-integrated/data.html

http://www9.informatik.uni-erlangen.de/External/vollib/

http://www.volren.org/

 

14.5 体绘制算法

国际上留下的体绘制算法主要有:光线投射算法( Ray-casting )、错切 - 变形算法( Shear-warp )、频域体绘制算法( Frequency Domain )和抛雪球算法( Splatting )。其中又以光线投射算法最为重要和通用。

究其原因,无外乎有三点:其一,该算法在解决方案上基于射线扫描过程,符合人类生活常识,容易理解;其二,该算法可以达到较好的绘制效果;其三,该算法可以较为轻松的移植到 GPU 上进行实现,可以达到实时绘制的要求。

本书的第 15 章将重点阐述光线投射算法。

 

15.1 光线投射算法原理

光线投射方法是基于图像序列的直接体绘制算法。从图像的每一个像素,沿固定方向(通常是视线方向)发射一条光线,光线穿越整个图像序列,并在这个过程中,对图像序列进行采样获取颜色信息,同时依据光线吸收模型将颜色值进行累加,直至光线穿越整个图像序列,最后得到的颜色值就是渲染图像的颜色。

为什么在上面的定义是穿越 “ 图像序列 ” ,而不是直接使用 “ 体纹理 ” ?原因在于,体数据有多种组织形式,在基于 CPU 的高级语言编程中,有时并不使用体纹理,而是使用图像序列。在基于 GPU 的着色程序中,则必须使用体纹理。这里所说的图像序列,也可以理解为切片数据。

尤其要注意:光线投射算法是从视点到 “ 图像序列最表面的外层像素 ” 引射线穿越体数据,而不少教程中都是糊里糊涂的写到 “ 从屏幕像素出发 ” ,这种说法太过简单,而且很容易让人误解技术的实现途径,可以说这是一种以讹传讹的说法!从屏幕像素出发引出射线,是光线跟踪算法,不是光线投射算法。

体绘制中的光线投射方法与真实感渲染技术中的光线跟踪算法有些类似,即沿着光线的路径进行色彩的累计。但两者的具体操作不同。首先,光线投射方法中的光线是直线穿越数据场,而光线跟踪算法中需要计算光线的反射和折射现象。其次,光线投射算法是沿着光线路径进行采样,根据样点的色彩和透明度,用体绘制的色彩合成算子进行色彩的累计,而光线跟踪算法并不刻意进行色彩的累计,而只考虑光线和几何体相交处的情况;最后,光线跟踪算法中光线的方向是从视点到屏幕像素引射线,并要进行射线和场景实体的求交判断和计算,而光线投射算法,是从视点到物体上一点引射线( 16.1.2 节会进行详细阐述),不必进行射线和物体的求交判断。

上述文字,对于光线投射算法的描述可能太过简单,会引起一些疑惑,不过这是正常的,有了疑惑才会去思考解决之道,最怕看了以后没有任何疑惑,那只是浮光掠影似的一知半解,而不是真正的了然于胸。

15.1.1 吸收模型

几乎每一个直接体绘制算法都将体数据当作 “ 在某一密度条件下,光线穿越体时,每个体素对光线的吸收发射分布情况 ” 。这一思想来源于物理光学,并最终通过光学模型( Optical Models )进行分类描述。为了区别之前的光照渲染模型,下面统一将 Optical Model 翻译为光学模型。

文献【 15 】中对大多数在直接体绘制算法中使用的重要光学模型进行了描述,这里给出简要概述。

1.        吸收模型( Absorption only ):将体数据当作由冷、黑的体素组成,这些体素对光线只是吸收,本身既不发射光线,也不反射、透射光线;

2.        发射模型( Emission only ):体数据中的体素只是发射光线,不吸收光线;

3.        吸收和发射模型( Absorption plus emission ):这种光学模型使用最为广泛,体数据中的体素本身发射光线,并且可以吸收光线,但不对光线进行反射和透射。

4.        散射和阴影模型( Scattering and Shading/shadowing ):体素可以散射(反射和折射)外部光源的光线,并且由于体素之间的遮挡关系,可以产生阴影;

5.        多散射模型( Multiple Scattering ):光线在被眼睛观察之前,可以被多个体素散射。

通常我们使用 吸收和发射模型( Absorption plus emission )。为了增强真实感,也可以加上阴影(包括自阴影)计算。

15.2 光线投射算法若干细节之处

15.2.1 光线如何穿越体纹理

这一节中将阐述光线如何穿越体纹理。这是一个非常重要的细节知识点,很多人就是因为无法理解 “ 体纹理和光线投射的交互方式 ” 而放弃学习体绘制技术。

前面的章节似乎一直在暗示这一点:通过一个体纹理,就可以进行体渲染。我最初学习体绘制时,也被这种暗示迷惑了很久,后来查找到一个国外的软件,可以将体纹理渲染到立方体或者圆柱体中,这时我才恍然大悟:体纹理并不是空间的模型数据,空间体模型(通常是规则的立方体或圆柱体)和体纹理相互结合才能进行体渲染。

举例而言,我们要在电脑中看到一个纹理贴图效果,那么至少需要一张二维的纹理和一个面片,才能进行纹理贴图操作。这个面片实际上就是纹理的载体。

同理,在体绘制中同样需要一个三维模型作为体纹理的载体,体纹理通过纹理坐标(三维)和模型进行对应,然后由视点向模型上的点引射线,该射线穿越模型空间等价于射线穿越了体纹理。

通常使用普通的立方体或者圆柱体作为体绘制的空间模型。本章使用立方体作为体纹理的载体。

注意:体纹理通过纹理坐标和三维模型进行对应,考虑到 OpenGL 和 Direct3D 使用的体纹理坐标并不相同,所以写程序时请注意到这一点。


图 44 展示了体纹理坐标在立方体上的分布,经过测试,这种分布关系是基于 OpenGL 的。在宿主程序中确定立方体 8 个顶点的体纹理坐标,注意是三元向量,然后传入 GPU ,立方体 6 个面内部点的体纹理坐标会在 GPU 上自动插值得到。

根据视点和立方体表面点可以唯一确定一条射线,射线穿越整个立方体等价于穿越体数据,并在穿越过程中对体数据等距采样,对每次得到的采样数据按照光透公式进行反复累加。这个累加过程基于 11 章讲过的透明合成公式,不过之前只是进行了简单的讲解,在本章中将针对透明度,透明合成,以及排序关系做全面阐述。

15.2.2 透明度、合成

透明度本质上代表着光穿透物体的能力,光穿透一个物体会导致波长比例的变化,如果穿越多个物体,则这种变化是累加的。所以,透明物体的渲染,本质上是将透明物体的颜色和其后物体的颜色进行混合,这被称为 alpha 混合技术。图形硬件实现 alpha 混合技术,使用 over 操作符。 Alpha 混合技术的公式如下所示:

                              

其中,as 表示透明物体的透明度, cs表示透明物体的原本颜色, cd表示目标物体的原本颜色, co 则是通过透明物体观察目标物体所得到的颜色值。

如果有多个透明物体,通常需要对物体进行排序,除非所有物体的透明度都是一样的。在图形硬件中实现多个透明物体的绘制是依赖于 Z 缓冲区。在光线投射算法中,射线穿越体纹理的同时也就是透明度的排序过程。所以这里存在一个合成的顺序问题。可以将射线穿越纹理的过程作为采样合成过程,这是从前面到背面进行排序,也可以反过来从背面到前面排序,毫无疑问这两种方式得到的效果是不太一样的。

如果从前面到背面进行采样合成,则合成公式为:

                            

其中, Ci 和 Ai分别是在体纹理上采样所得到的颜色值和不透明度,其实也就是体素中蕴含的数据; deta Ci和 deta Ai表示累加的颜色值和不透明度。

注意,很多体纹理其实并没有包含透明度,所以有时是自己定义一个初始的透明度,然后进行累加。

如果从背面到前面进行采样合成,则公式为:

                              

15.2.3 沿射线进行采样


如 图 45 所示,假定光线从 F 点投射到立方体中,并从 L 点投出,在立方体中穿越的距离为 m 。当光线从 F 点投射到立方体中,穿越距离为 n ( n<m )时进行采样,则存在公式:

               

其中 Tstart表示立方体表面被投射点的体纹理坐标; d表示投射方向;detal 表示采样间隔,随着 n 的增加而递增;t 为求得的采样纹理坐标。通过求得的采样纹理坐标就可以在体纹理上查询体素数据。直到 n>m ,或者透明度累加超过 1 ,一条射线的采样过程才结束。

下面总结一下:首先需要一个确定了顶点纹理坐标的三维立方体,光线穿越立方体的过程,就是穿越体纹理的过程,在整个穿越过程中,计算采样体纹理坐标,并进行体纹理采样,这个采样过程直到光线投出立方体或者累加的透明度为 1 时结束。

我想这个过程应该不复杂,大家一定要记住:纹理坐标是联系三维模型和体纹理数据之间的桥梁,通过计算光线穿越三维模型,可以计算体纹理在光线穿越方向上的变化,这就是计算采样纹理坐标的方法。

高中时学习物理的力学部分,最初一直处于浑浑噩噩的状态,遇到应用题不知道从何处入手,后来看一本参考资料讲到 “ 加速度是联系力和运动状态的桥梁,遇到题目首先分析加速度的求法 ” ,由此举一反三,不再感觉物理难学。所以在此我也借用那句话,总结纹理坐标的作用。

现在还存在一个问题:如何知道光线投射出了立方体?这个问题等价于计算光线在立方体中穿越的距离 m 。在下一节中将进行阐述。

附:在 OpenGL 和 DirectX 中,体纹理坐标的分布规则是不一样的,所以要针对自己当前使用的 profile 来确定顶点体纹理坐标的设置。这也从侧面说明了, Cg 语言是基于 OpenGL 和 DirectX 的。

 

15.2.2 如何判断光线投射出体纹理

上一节阐述过:光线投射出体纹理,等价于光线投射出立方体。所以如何判断光线投射出体纹理,可以转换为判断光线投射出立方体。

首先计算光线在立方体中入射到出射的行进距离 m ,然后当每次采样体纹理时同时计算光线在立方体中的穿越距离 n ,如果 n>=m ,则说明光线射出立方体。给定光线方向,以及采样的距离间隔,就可以求出光线在立方体中的穿越距离 n 。

如果是在 CPU 上,距离 m 很容易通过解析几何的知识求得,直接求出光线和几何体的两个交点坐标,然后计算欧几里德距离即可。但是在 GPU 上计算光线和几何体的交点是一个老大难的问题,尤其在几何体不规则的情况下;此外,就算是规则的几何体,光线与其求交的过程也是非常消耗时间,所以通过求取交点然后计算距离的方法不予采用。

请思考一下,在 GPU 中确定点和点之间顺序关系的还有哪个量?深度值(我自问自答)。

在 GPU 中可以间接反应点和点之间关系的有两个量,一个是纹理坐标,另一个就是深度值。通常在渲染中会进行深度剔除,也就是只显示深度值小的片段。不过也存在另外一个深度剔除,将深度值小的片段剔除,而留下深度值最大的片段(深度值的剔除方法设置,在 OpenGL 和 Direct 中都有现成函数调用)。如果使用后者,则场景中渲染显示的是离视点最远的面片集合。

所以,计算距离 m 的方法如下:

1.        剔除深度值较大的片段(正常的渲染状态),渲染场景深度图 frontDepth (参阅第 14 章),此时 frontDepth 上的每个像素的颜色值都代表“某个方向上离视点最近的点的距离”;

2.        剔除深度值较小的片段,渲染场景深度图 backDepth , backDepth 上的每个像素的颜色值都代表“某个方向上离视点最远的点的距离”;

3.          将两张深度图上的数据进行相减,得到的值就是光线投射距离 m 。

如果认真实现过第 14 章讲的 shadow Map 算法,对这个过程应该不会感到太复杂。可能存在的问题是:背面渲染很多人没有接触过。这里对背面渲染的一些细微之处进行阐述,以免大家走弯路。

通常,背面的面片(不朝向视点的面片)是不会被渲染出来的,图形学基础比较好的同学应该知道,三个顶点通常按逆时针顺序组成一个三角面,这样做的好处是,背面面片的法向量与视线法向量的点积为负数,可以据此做面片剔除算法(光照模型实现中也常用到),所以只是改变深度值的比较方法还不够,还必须关闭按照逆 / 顺时针进行面片剔除功能,这样才能渲染出背面深度图。 图 46 是立方体的正面和背面深度图。


附:在很多教程上,都是将 frontDepth 和 backDepth 相减后的值,保存为另外一个纹理,称之为方向纹理,每个像素由 r 、 g 、 b 、 a 组成,前三个通道存储颜色值,最后的 a 通道存放距离值,我觉得这个过程稍微繁琐了些,此外由于方向向量可能存在负值,而颜色通道中只能保存正值,所以必须将方向向量归一化到【 0 , 1 】空间,这个过程有可能导致数据精度的损失。基于如上的考虑,我将方向向量的计算放到片段着色程序中,通过视点和顶点位置进行计算。

 

算法流程



图 47 展示了使用光线投射算法进行体绘制的实现流程。

首先要渲染出正向面深度图和背向面深度图,这是为了计算射线穿越的最大距离,做为循环采样控制的结束依据;然后在顶点着色程序中计算顶点位置和射线方向,射线方向由视线方向和点的世界坐标决定,其实射线方向也可以放在片段着色程序中进行计算。然后到了最关键的地方,就是循环纹理采样、合成。

每一次循环都要计算新的采样纹理坐标和采样距离,然后进行颜色合成和透明度累加,如果采样距离超过了最大穿越距离,或者透明度累加到 1 ,则循环结束。将合成得到的颜色值输出即可。

图 48 给出了使用光线投射算法进行体绘制的效果图:


15.4 光线投射算法实现

本节给出光线投射算法的着色程序实现代码。依然是分为三个部分:结构体、顶点着色程序和片段着色程序。

代码 22 光线投射算法结构体

 

 

struct VertexIn

{

  float4 position : POSITION;    

  float4 texCoord:  TEXCOORD;

};

 

struct VertexScreen

{

  float4 position   : POSITION; 

  float4 worldPos   : TEXCOORD0;

  float4 projPos    : TEXCOORD1;

  float4 texCoord   : TEXCOORD2;

};

代码 23 光线投射算法顶点着色程序

 

 

VertexScreen main_v(VertexIn posIn,

                        uniform float4x4 world,

                  uniform float4x4 worldViewProj,

                  uniform float4x4 texViewProj)

{

    VertexScreen posOut;

   

    posOut.position = mul(worldViewProj, posIn.position);

    posOut.worldPos = mul(world,posIn.position);

    posOut.projPos = mul(texViewProj, posOut.worldPos); 

    posOut.texCoord = posIn.texCoord;

 

    return posOut;

}

 

代码 24 光线投射算法片段着色程序

 

void main_f(VertexScreen posIn,

           uniform float3 eyePosition,

           uniform sampler3D volumeTex: register(s0),

           uniform sampler2D frontDepthTex: register(s1) ,

           uniform sampler2D backDepthTex: register(s2) ,

        out float4 result        : COLOR)

{

       // 根据视点和当前顶点世界坐标计算方向

       float3 dir = posIn.worldPos.xyz-eyePosition;

       dir = normalize(dir);

       float3 deltaDir = float3(0.0, 0.0, 0.0);

      

       // 获取当前顶点的三维纹理坐标

       float3 tex = posIn.texCoord.xyz;

       float2 uvDelta;

       uvDelta.x = 0.0;//ddx( tex ).x;

       uvDelta.y = 0.0;//ddy( tex ).y;

      

       // 取出深度间隔值 , 并设置采样间隔

       float2 uv= posIn.projPos.xy/posIn.projPos.w;

       float frontDis = tex2D(frontDepthTex,uv).x;

       float backDis = tex2D(backDepthTex,uv).x;

       float len = backDis-frontDis;

      

       // 初始化颜色值、采样值、透明度

  float3 norm_dir = normalize(dir);

  float stepsize = 0.01;

  float delta = stepsize;

  float3 delta_dir = norm_dir * delta;

  float delta_dir_len = length(delta_dir);

  float3 vec = posIn.texCoord.xyz;

  float4 col_acc = float4(0,0,0,0);

  float alpha_acc = 0;

  float length_acc = 0;

  float4 color_sample;

  float alpha_sample;

 

  for(int i = 0; i < 800; i++){

       color_sample = tex3D(volumeTex,vec);

      alpha_sample = color_sample.a * stepsize;

      col_acc   += (1.0 - alpha_acc) * color_sample * alpha_sample * 3;

      alpha_acc += alpha_sample;

      vec += delta_dir;

      length_acc += delta_dir_len;

      if(length_acc >= len || alpha_acc > 1.0) break; // 采样循环控制条件

 }

       result.xyz = col_acc.xyz*2.0+float3(0.2,0.2,0.2);

       result.w = col_acc.w;  

}

15.5 本章小结

本书的第14 、15 章阐述了体绘制中光线投射算法的基本原理和实现流程。实际上,在此基础上可以对光线投射算法加以扩展,例如将光线投射算法和阴影绘制算法相结合,可以渲染出真实感更强的图像。

此外,有些体数据是中间是空的,在射线方向上进行采样时需要跳过空区域,这其中也需要额外的算法处理,在英文中称为“Object-Order Empty Space Skipping ”。

目前我所发现关于体绘制以及光线投射算法最好的教材是Markus Hadwiger 等人所写的“Advanced Illumination Techniques for GPU-Based Volume Raycasting ”。此书发表在SIGGRAPH ASIA2008 上,是目前所能找到最新也是非常权威的教材,共166 页。英文阅读能力比较好的同学可以尝试着看一下。

本章已经是此书的最后一章,最后希望中国的计算机科学可以真正上升到科学研究的层次,而不是一直在混沌中热衷做泥瓦匠的工作。


From: http://blog.csdn.net/pizi0475/article/details/6650855

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 以下是一个基于DICOM序列图像的VTK绘制的C代码示例: ``` #include <vtkSmartPointer.h> #include <vtkDICOMImageReader.h> #include <vtkVolume.h> #include <vtkVolumeProperty.h> #include <vtkColorTransferFunction.h> #include <vtkPiecewiseFunction.h> #include <vtkFixedPointVolumeRayCastMapper.h> #include <vtkVolumeRayCastCompositeFunction.h> #include <vtkRenderer.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> int main() { // 创建DICOM图像读取器 vtkSmartPointer<vtkDICOMImageReader> reader = vtkSmartPointer<vtkDICOMImageReader>::New(); reader->SetDirectoryName("DICOM序列图像文件夹路径"); reader->Update(); // 创建颜色传递函数 vtkSmartPointer<vtkColorTransferFunction> colorTransferFunction = vtkSmartPointer<vtkColorTransferFunction>::New(); colorTransferFunction->AddRGBPoint(0, 0.0, 0.0, 0.0); colorTransferFunction->AddRGBPoint(255, 1.0, 1.0, 1.0); // 创建灰度传递函数 vtkSmartPointer<vtkPiecewiseFunction> opacityTransferFunction = vtkSmartPointer<vtkPiecewiseFunction>::New(); opacityTransferFunction->AddPoint(0, 0.0); opacityTransferFunction->AddPoint(255, 1.0); // 创建组合传递函数 vtkSmartPointer<vtkVolumeProperty> volumeProperty = vtkSmartPointer<vtkVolumeProperty>::New(); volumeProperty->SetColor(colorTransferFunction); volumeProperty->SetScalarOpacity(opacityTransferFunction); volumeProperty->ShadeOff(); volumeProperty->SetInterpolationTypeToLinear(); // 创建数据映射器 vtkSmartPointer<vtkFixedPointVolumeRayCastMapper> volumeMapper = vtkSmartPointer<vtkFixedPointVolumeRayCastMapper>::New(); volumeMapper->SetInputConnection(reader->GetOutputPort()); // 创建积 vtkSmartPointer<vtkVolume> volume = vtkSmartPointer<vtkVolume>::New(); volume->SetMapper(volumeMapper); volume->SetProperty(volumeProperty); // 创建渲染器并添加积 vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New(); renderer->AddVolume(volume); // 创建渲染窗口并设置渲染器 vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New(); renderWindow->AddRenderer(renderer); // 创建交互式操作器并设置渲染窗口 vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New(); interactor->SetRenderWindow(renderWindow); // 开始渲染 interactor->Initialize(); interactor->Start(); return 0; } ``` 该代码使用VTK库创建了一个DICOM图像的绘制的demo。首先,通过vtkDICOMImageReader读取DICOM序列图像的文件夹路径。然后,创建颜色传递函数和灰度传递函数用于对数据进行颜色和透明度的控制。接下来,使用vtkFixedPointVolumeRayCastMapper将DICOM图像数据映射为数据,并设置相应的传递函数。再创建一个vtkVolume对象来包含数据和属性。然后,创建一个vtkRenderer并将积添加到其中。接下来,创建一个vtkRenderWindow并将渲染器添加到其中。最后,创建一个vtkRenderWindowInteractor,并将渲染窗口设置给它,并开始渲染。 ### 回答2: 以下是一个基于DICOM序列图像的C代码示例,用于vtk绘制和三维重建的演示。 ```c #include <vtkSmartPointer.h> #include <vtkDICOMImageReader.h> #include <vtkFixedPointVolumeRayCastMapper.h> #include <vtkColorTransferFunction.h> #include <vtkPiecewiseFunction.h> #include <vtkVolumeProperty.h> #include <vtkVolume.h> #include <vtkRenderWindow.h> #include <vtkRenderer.h> #include <vtkRenderWindowInteractor.h> int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " <DICOM Directory>" << std::endl; return EXIT_FAILURE; } // 创建DICOM图像阅读器 vtkSmartPointer<vtkDICOMImageReader> reader = vtkSmartPointer<vtkDICOMImageReader>::New(); reader->SetDirectoryName(argv[1]); reader->Update(); // 创建渲染Mapper vtkSmartPointer<vtkFixedPointVolumeRayCastMapper> mapper = vtkSmartPointer<vtkFixedPointVolumeRayCastMapper>::New(); mapper->SetInputConnection(reader->GetOutputPort()); // 创建颜色转换函数 vtkSmartPointer<vtkColorTransferFunction> colorFun = vtkSmartPointer<vtkColorTransferFunction>::New(); colorFun->AddRGBPoint(0, 0.0, 0.0, 0.0); colorFun->AddRGBPoint(255, 1.0, 1.0, 1.0); // 创建灰度转换函数 vtkSmartPointer<vtkPiecewiseFunction> opacityFun = vtkSmartPointer<vtkPiecewiseFunction>::New(); opacityFun->AddPoint(0, 0.0); opacityFun->AddPoint(255, 1.0); // 创建属性 vtkSmartPointer<vtkVolumeProperty> volumeProperty = vtkSmartPointer<vtkVolumeProperty>::New(); volumeProperty->SetColor(colorFun); volumeProperty->SetScalarOpacity(opacityFun); volumeProperty->ShadeOn(); volumeProperty->SetInterpolationTypeToLinear(); // 创建 vtkSmartPointer<vtkVolume> volume = vtkSmartPointer<vtkVolume>::New(); volume->SetMapper(mapper); volume->SetProperty(volumeProperty); // 创建渲染器和窗口 vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New(); vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New(); renderWindow->AddRenderer(renderer); vtkSmartPointer<vtkRenderWindowInteractor> renderWindowInteractor = vtkSmartPointer<vtkRenderWindowInteractor>::New(); renderWindowInteractor->SetRenderWindow(renderWindow); // 将添加到渲染器中 renderer->AddViewProp(volume); // 渲染和开始交互 renderWindow->Render(); renderWindowInteractor->Start(); return EXIT_SUCCESS; } ``` 这个示例代码使用vtkDICOMImageReader加载DICOM序列图像,然后通过vtkFixedPointVolumeRayCastMapper创建渲染映射器。然后,使用vtkColorTransferFunction设置颜色转换函数和vtkPiecewiseFunction设置灰度转换函数来定义的颜色和透明度。创建一个vtkVolumeProperty,并将之前创建的转换函数和其他属性设置给它,然后创建一个vtkVolume并将之前创建的映射器和属性设置给它。最后,将该添加到vtkRenderer中,并通过创建vtkRenderWindow和vtkRenderWindowInteractor来显示渲染结果并启动交互。 ### 回答3: 以下是一个基于DICOM序列图像的C++代码示例,用于绘制vtk绘制(volume rendering)或三维重建的demo: ```c++ #include <vtkSmartPointer.h> #include <vtkDICOMImageReader.h> #include <vtkVolume.h> #include <vtkVolumeMapper.h> #include <vtkVolumeProperty.h> #include <vtkFixedPointVolumeRayCastMapper.h> #include <vtkPiecewiseFunction.h> #include <vtkColorTransferFunction.h> #include <vtkRenderWindow.h> #include <vtkRenderer.h> #include <vtkRenderWindowInteractor.h> int main(int argc, char *argv[]) { // 创建DICOM图像阅读器 vtkSmartPointer<vtkDICOMImageReader> reader = vtkSmartPointer<vtkDICOMImageReader>::New(); reader->SetDirectoryName("DICOM文件夹路径"); // 修改为DICOM数据所在文件夹路径 reader->Update(); // 创建绘制的映射器和绘制属性 vtkSmartPointer<vtkFixedPointVolumeRayCastMapper> mapper = vtkSmartPointer<vtkFixedPointVolumeRayCastMapper>::New(); mapper->SetInputConnection(reader->GetOutputPort()); vtkSmartPointer<vtkVolumeProperty> volumeProperty = vtkSmartPointer<vtkVolumeProperty>::New(); volumeProperty->ShadeOff(); // 关闭阴影效果 // 创建透明度函数,并设置透明度映射 vtkSmartPointer<vtkPiecewiseFunction> opacityFunction = vtkSmartPointer<vtkPiecewiseFunction>::New(); opacityFunction->AddPoint(0, 0.0); opacityFunction->AddPoint(700, 0.1); opacityFunction->AddPoint(1400, 0.2); opacityFunction->AddPoint(2000, 0.4); volumeProperty->SetScalarOpacity(opacityFunction); // 创建颜色函数,并设置颜色映射 vtkSmartPointer<vtkColorTransferFunction> colorFunction = vtkSmartPointer<vtkColorTransferFunction>::New(); colorFunction->AddRGBPoint(0, 0.0, 0.0, 0.0); colorFunction->AddRGBPoint(700, 1.0, 0.5, 0.3); colorFunction->AddRGBPoint(1400, 1.0, 0.7, 0.5); colorFunction->AddRGBPoint(2000, 1.0, 1.0, 0.9); volumeProperty->SetColor(colorFunction); vtkSmartPointer<vtkVolume> volume = vtkSmartPointer<vtkVolume>::New(); volume->SetMapper(mapper); volume->SetProperty(volumeProperty); // 创建渲染器和渲染窗口 vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New(); vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New(); renderWindow->AddRenderer(renderer); // 创建交互器 vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New(); interactor->SetRenderWindow(renderWindow); // 将绘制添加到渲染器中 renderer->AddVolume(volume); renderer->SetBackground(1.0, 1.0, 1.0); // 设置背景颜色 // 启动交互器 interactor->Initialize(); interactor->Start(); return 0; } ``` 请将代码中的`"DICOM文件夹路径"`替换为实际的DICOM数据所在的文件夹路径,并确保已经正确安装和配置了VTK库。代码中的透明度函数和颜色函数是根据具需求进行设置,可以根据实际情况进行调整。运行该代码将显示一个包含绘制的渲染窗口,可以使用鼠标进行交互,可以使用鼠标滚轮进行缩放和旋转。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值