转自:http://blog.csdn.net/liu_lin_xm/article/details/4850609
摘抄“GPU Programming And Cg Language Primer 1rd Edition” 中文 名“GPU编程与CG语言之阳春白雪下里巴人”
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 】空间,这个过程有可能导致数据精度的损失。基于如上的考虑,我将方向向量的计算放到片段着色程序中,通过视点和顶点位置进行计算。