3D图形数学(3D Graphics Math)
1 图形管道概述
我们将讨论渲染一幅带有基本光照的单个图像的大体过程,这里不考虑动画和全局光照,如阴影和辐射度。
此外,注意这里只从概念上讲解通过图形管道的数据流,其顺序并不是固定的。实践中,我们也许会为了性能的优化而并行或乱序执行一些任务。比如,考虑到不同的渲染API,我们可能首先变换和照明所有顶点,然后才进一步的处理(进行裁剪和剔除),或者会并行处理二者,也可能在背面剔除之后再进行光照会得到更高效率。
还有一个我们将不详细讨论的要点,即工作负担如何在CPU与渲染硬件间分配。正确地组织渲染任务,以求得最大的并行效果对高效渲染是至关重要的。
考虑上述简化,就得到了图形管道中数据流的概况,如下所示:
- 建立场景:开始渲染之前,需要预先设定对整个场景有效的一些选项。比如,要建立摄像机位置,或者更具体些,要选择进行渲染的出发点---视点,渲染的输出---视图。还需要设定光照与雾化选项,同时准备z缓冲。
. - 可见性检测:选好了摄像机,就必须检测场景中哪些物体是可见的。可见性检测对实时渲染极为重要,因为我们不愿意浪费时间去渲染那些根本看不到的东西。
. - 设置物体级的渲染状态:一旦发现某物体潜在可见,就到了把它实际绘制出来的时候。每个物体的渲染设置可能是不同的,在渲染该物体的任何片元之前,首先要设置上述选项,最常见的此类选项是纹理映射。
. - 几何体的生成与提交:接着实际向API提交几何体,通常提交的数据是种种形式的三角形,或是独立的三角形,或是索引三角网格与三角带。此阶段,我们可能会应用LOD,或者渐进式生成几何体。
. - 变换与光照:一旦渲染API得到了三角形数据,由模型空间向摄像机空间的顶点坐标转换与顶点光照计算即开始。
. - 背面剔除与裁剪:然后,那些背对摄像机的三角形被去除("背面剔除");三角形在视椎外的部分也被去除,称作裁剪---这可能导致产生多于三个边的多边形。
. - 投影到屏幕空间:在3D裁剪空间中经裁剪产生的多边形,被投影到输出窗口的2D屏幕空间里。
. - 光栅化:当把裁剪后的多边形转换到屏幕空间后,就到了光栅化阶段。光栅化指计算应绘制三角形上的哪些像素的过程,并为接下来的像素着色阶段提供合理的插值参数(如光照和纹理映射坐标)。
. - 像素着色:最后,在管道的最后阶段计算三角形的色彩,此过程称作"着色"。接着把这些颜色写至屏幕,这是可能需要alpha混合与z缓冲。
下面的伪代码描述了渲染管道,为了达到概观的目的,大量细节被省去了。同时,由于渲染平台和API的不同,实践中会有许多不同的形式。
Listing 15.1: Pseudocode for the graphics pipeline
// First, figure how to view the scene
setupTheCamera();
// Clear the zbuffer
clearZBuffer();
// Setup environmental lighting and fog
setGlobalLightingAndFog();
// Get a list of objects that are potentially visible
potentiallyVisibleObjectList = highLevelVisibilityDetermination(scene);
// Render everything we found to be potentially visible
for (all objects in potentiallyVisibleObjectList)
{
// Perform lower-level VSD using bounding volume test
if (!object.isBoundingVolumeVisible())
continue;
// Fetch or procedurally generate the geometry
triMesh = object.getGeometry()
// Clip and render the faces
for (each triangle in the geometry)
{
// Transform the vertices to clip space, and perform vertex-level lighting
clipSpaceTriangle = transformAndLighting(triangle);
// Is the triangle backfacing?
if (clipSpaceTriangle.isBackFacing()) continue;
// Clip the triangle to the view volume
clippedTriangle = clipToViewVolume(clipSpaceTriangle);
if (clippedTriangle.isEmpty())
continue;
// Project the triangle onto screen space and rasterize
clippedTriangle.projectToScreenSpace();
for (each pixel in the triangle)
{
// Interpolate color, zbuffer value, and texture mapping coords
// Perform zbuffering and alpha test
if (!zbufferTest())
continue;
if (!alphaTest())
continue;
// Shade the pixel.
color = shadePixel();
// Write to the frame buffer and zbuffer
writePixel(color, interpolatedZ);
}
}
}
2 显示参数
2.1 设定视图参数
渲染场景之前,首先必须建立摄像机和输出窗口。即必须决定从哪个位置进行观察渲染(视点位置、方向、缩放)以及把渲染结果送到哪里(屏幕上的目标矩形区域)。上述二者中,输出窗口较为简单,故先讨论输出窗口。
2.2 指定输出窗口
我们不一定要把图像渲染到整个屏幕。比如,一个分屏的多人游戏,每个玩家只占据显示屏幕的一部分。输出窗口即指输出设备中图像将要渲染到的那部分,如图15.1所示:
窗口位置由左上角像素(winPosx,winPosy)给出,整数winResx、winResy是以像素为单位的窗口大小,如此定义,使用窗口大小而不是右下角的坐标,可避免整数像素坐标系带来一些麻烦。同时要注意窗口的实际物体大小和像素大小的区别。
要知道我们不一定在屏幕上渲染,也许只是将渲染结果保存到一个TGA文件里,或是AVI的一帧,也许只是渲染到一个纹理上---作为主渲染器的一个子过程而已,因此,名词"帧缓冲"一般指用来保存我们正渲染图像的那块内存。
2.3 像素纵横比
不管是渲染到屏幕还是缓冲区,我们必须知道像素的纵横比。它是像素高对宽的比值,一般为1("方形"像素),不过并非总是如此。下面给出其计算公式(公式5.1):
pixPhys指像素物理尺寸。一般来说,度量单位并无关系,比例才是重要的。devPhys是显示设备的物理高与宽比,尺寸可能是英寸、英尺、picas等,但也只有比例才是重要的。比如,标准的桌面显示器,尺寸各异但却拥有相同的比值4:3---视区宽大于高约33%。另一个常见比例是高清晰电视和DVD上的16:9。整数devResx和devResy是x、y方向的像素比,如640 x 480指devResx=640,devResy=480。
如前所述,比值为1的方形像素最为常见。如标准桌面显示器,有4:3的物理纵横比,而许多常见解析度:320 x 240,640 x 480,800 x 600,1024x 768,1600 x 1200也都是4:3,因此像素是方形的。
注意计算中未用到窗口的尺寸及位置,这是合理的,窗口性质不影响像素的物理属性。但是,窗口尺寸在视场问题中十分重要,而位置对摄像机到屏幕的映射是关键。
2.4 视锥
视锥是摄像机可见的空间体积,看上去像截掉顶部的金字塔,如图15.2所示:
视锥是由6个裁剪面围成的。构成视锥的4个侧面称为上、左、下、右面,它们对应着输出窗口的四边。为防止物体离摄像机过近,设置近剪面,从而去除金字塔形的顶端。同理,也设置了视野的远端,因为太远的物体实际上太小而不可见,故可有效而安全地去掉。
2.5 视场与缩放
摄像机同其他物体一样有位置和朝向,同时它还具有"视场"这一额外的属性。另一名词"缩放"你也许已经很熟悉,直观上,你早就知道放大和缩小。但拉近时,物体显大;拉远时,物体显小,这太常见了。
视场是视锥所截的角。实际上需要两个角:分别对应水平视场和垂直视场。这里只在2D中讨论其中一个,图15.3从上方显示了视锥,精确的展示了水平视场角,坐标轴的标记用的是摄像机空间。
缩放表示物体实际大小和物体在90。视场中显示大小的比。所以大比值表示放大,小比值表示缩小。比如,2.0的缩放表示物体在屏幕上比用90。视场时大两倍。缩放的几何解释如图15.4所示:
应用基本三角知识,就能推导出缩放和视场角之间的转换公式:
在3D中,需要两个缩放值,一个水平的,一个垂直的。可以随意给值,但如果二者比例不恰当,图像便像被拉伸过似的(好比宽银幕电影在电视上播出)。为了维持恰当的比例,缩放要和输出窗口的尺寸对应:
假设输出为正常比例,许多渲染引擎允许仅用一个视场角(或zoom值)设定摄像机,然后自动计算另一个。例如,可以指定水平视场角,自动计算垂直视场角,反之亦然;或者指定视场角中较大的一个,自动计算较小的。
3 坐标空间
3.1 模型与世界空间
物体最开始由物体空间(和物体相连的坐标空间)来描述,其中常见的信息包括顶点位置和表面法向量。物体空间又称作模型空间或局部空间。
可将坐标从模型空间中转换到世界空间中,此过程称作模型变换。通常,光照计算使用世界空间,但其实使用什么坐标空间无所谓,只要确保几何体与光线在同一空间即可。
3.2 摄像机空间
通过视变换,顶点从世界空间变换到摄像机空间,此空间也称作眼睛空间。摄像机空间是原点在投影中心的3D坐标系统,一个轴平行于摄像机拍摄方向且垂直于投影平面,另一个轴由上、下裁剪面相交得到,还有一轴由左、右裁剪面相交得到。如果我们考虑的是透视投影,那么一个轴可视为水平,另一个则可视为垂直的。
左手坐标系中,常约定摄像机朝向+z,而+x和+y指向右和上方向(透视投影情况下)。这是非常直观的,如图15.5所示。右手坐标则指定-z为摄像机朝向。
3.3 裁剪空间
从摄像机空间,顶点接着又被变换到裁剪空间,又名标准视体空间(the canonicalview volume space),该变换对应的矩阵称为裁剪矩阵。
目前为止,顶点还是"纯粹"的3D向量,即它们只有三个坐标值,或者加上第四个分量w,并且w总为1。裁剪矩阵改变了这个现状,它将有用信息放入w中,它主要做两件事:
- 为透视投影准备向量,由除以w来实现。
- 规格化x,y,z,使它们可以w比较,用于裁剪。
裁剪矩阵的第一个目的是为透视投影准备向量而将坐标值除以w,4D齐次向量由除以w而对应到3D向量:
裁剪矩阵的一个目的就是计算正确的w值,以得到正确的投影,我们已经知道如何投影到垂直于z轴且距原点为d的平面(形如z=d的平面)。投影平面在视锥内的矩形部分将映射到屏幕,如果改变d,投影平面将前后移动;在一个真正的摄像机中,这样变化焦距将产生放大、缩小的效果。但对计算机内的投影平面不会如此,增大焦距,像也会变大,但是"底片"(就是投影平面在视锥内的部分)也变大了。因为它们变化的比例一致,所以渲染出的图像不变。因此,计算机图形学中,缩放完全由视锥的形状空间,d值并不重要。所以,我们可以任意选择一个d值并一直使用它,对我们来说最方便的值是d=1。
如果这是裁剪矩阵唯一的目的,即计算正确的w值,那么它可简化如下:
将它乘以形如[x, y, z, 1]的向量再进行透视除法,得到:
现在已经知道如何用矩阵求得w的值。这里,你也许发现似乎只要除以z就可完成上述工作。没错,的确可以只用z而不涉及w,但4D坐标可以表达更多的摄像机要求,包括一些"奇异"的形式,比如投影面不垂直于摄像机指向;另一个原因是它使得z裁剪(近面和远面裁剪)和x、y裁剪形式一致,从而更好地使用硬件。一般来说,使用齐次坐标4 x 4矩阵更紧凑和优雅。无论如何,多数API都使用它,这才是最重要的。
裁剪矩阵的另一个目的是规格化x、y、z分量,使得6个裁剪面有一致的简单形式。符合下列简单不等式的点在视锥外:
bottom y < -w
top y > w
left x < -w
right x > w
near z < -w
far z > w
公式15.3 裁剪空间中的视锥
反之,视锥内的定满足下列不等式:
-w ≤ x ≤ w
-w ≤ y ≤ w
-w ≤ z ≤ w
任何不满足这些不等式的点都要被裁减掉。
我们用摄像机的缩放值对x、y进行缩放,从而使上、左、右、下4个剪切面处于正确位置。对于近、远两个剪切面,使得对近剪切面z/w = -1,远剪切面z/w = 1。
设zoomx、zoomy分别为水平、垂直缩放值,设n、f分别为近、远两个剪切面的距离。下面的矩阵可完成上述计算:
所谓"openGL 风格",是指近裁剪面到远裁剪面的z值在[-w, +w]之间,其他API(如DirextX)调整z值到区间[0, w]。换言之,如果满足下式,那么点在裁剪面外:
near z < 0
far z > w
而在视锥以内的点则满足:
0 ≤ z ≤ w
此时剪切矩阵稍有不同(公式15.5):
3.4 屏幕空间
一旦用视锥完成了几何体裁剪,即可向屏幕空间投影,从而对应于真正的屏幕像素。注意输出窗口不一定占有整个屏幕,只不过,通常情况下希望屏幕坐标系和渲染设备坐标系一致。
显然,屏幕空间是2D的,于是要进行一次3D到2D的映射以得到正确的2D坐标。下列公式概括这一过程:除以w,并调整x、y以映射到如图15.6所示的输出窗口:
注意y前面的负号,因为裁剪空间中+y向上,而屏幕空间的+y向下。
zscreen和wscreen呢?因为屏幕是2D的,它们并无意义。但也不能简单地丢弃它们,在z缓冲和透视校正中,它们还会有用。
4 光照模型
4.1 光照与雾化
标准光照模型是局部模型中的一种----即当处理一个物体时,不考虑其他物体的影响。物体也不向别的物体投下影子,实际上,物体自身无法生成影子,影子是使用全局光照模型生成的。
4.2 色彩的数学
计算机中的色彩常用RGB色彩模型表示,这里R表示红,G表示绿,B表示蓝,其精度因平台与渲染状态而异。我们视RGB为0至1间的值,并且也不考虑其中各分量到底占用多少二进制位。在计算机图形学中,色彩常被视为数学实体。我们用黑体的小写罗马字母表示色彩符号,如c,与向量符号相同,但由于二者上下文不同,所以不会混淆。
可以认为色彩存在于一个3D单位立方体"色彩空间"中,如图15.7所示:
黑、白两色十分重要,分别由RGB(0, 0, 0)和(1, 1, 1)表示,并且用特殊的符号0、1分别标识,灰色都在黑白色之间的那条连线上。
色彩可以加、减、乘以标量,和向量运算一样。两个色彩也可以作"按位乘"运算,运算记作。
色彩运算有时会使一个或多个RGB分量超出[0, 1]的界限(比如,计算光照时,出现极强的光亮),此时简单地加以截断。根据情况,也许在计算中每一步都进行截断,也许中间允许超界,而在最后截断
。
如果只有一个分量超界,截断运算可能会引起色彩混乱。比如,色彩(1, 2, 1)其实是(0.5, 1, 0.5)的一个更亮的表示,类似某种绿色。简单地加以截断后,绿色消失只剩余白色。更聪明的办法是除以最大色彩值以单位化。此例中,均除以最大分量值2。当然,开始时就防止越界(比如,调整光强以使越界不发生)才是最好的办法。
4.3 光源
为进行渲染,必须向图形API描述场景中的光。下面是一个光源类型的简单列表:
点光源,平行光,聚光灯,环境光。
我们将讨论大多数渲染API都支持的常见光源。
点光源是向四面八方发射光线的单点,又称全向光或球状光。点光源有方向和色彩,此色彩同时表示色调与亮度,点光源还有一个辐射衰减半径,控制照亮的范围。图15.8展示了3DS如何表示点光源。
光强通常由光源中心向辐射前进方向不断衰减,最终为0。点光源可代表许多常见发光物,如灯泡、电灯、火把等。
平行光是从无限远处射来的点光源的光线,场景中所有光线皆为平行的。平行光源没有位置的概念,也无衰减。太阳是平行光的典型代表(目前还不考虑太阳的位置来渲染场景)。
聚光灯指从特定光源向特定方向射出的光,比如信号灯、车头灯等。它们有位置、方向、甚至还有辐射距离的概念,其照亮区域为圆锥形或金字塔形。
圆形聚光灯有一个圆形的"底",其宽度由辐射衰减角给出(注意与辐射衰减距离的区别)。并且,有一个描述高亮区的内角度,如图15.9:
方形聚光灯形成金字塔形状,而不是圆锥形。方形聚光灯非常有趣,因为常被用来投影图像,比如电影放映。
最后,环境光指不属于任何光源而照亮整个场景的光。不考虑环境光,则物体的影子将完全是黑的,因为它们不被任何光源照亮。现实中,这类物体常被间接照亮,环境光是积累这种间接光照最根本的方法。
4.4 标准光照方程----概述
光照方程定义了标准光照模型,用于计算单个像素的色彩。标准方程可简写为公式15.7:
Clit =
Cspec +
Cdiff +
Camb
公式15.7 标准光照方程。其中,
- Clit是打开光照情况下计算颜色值的结果,这与关闭光照(注意:相当于用最强光照照射)情况下计算的结果不同。和我们日常生活中的"光照"一词不同,计算机图形学中的"光照"是指取关闭光照情况下的纹理颜色值进行计算,一般情况下得到的结果比原纹理颜色值暗。
. - Cspec是镜面反射分量。
. - Cdiff是散射分量。
. - Camb是环境分量。
物体外观主要取决于以下四点因素:
- 物体表面的性质,即材质属性。
. - 表面的方位与朝向,朝向常用单位法向量表示。
. - 照射来的各光源性质。
. - 观察者位置。
方程中的三个组成部分,分别考虑了上述因子的不同组合。
4.5 镜面反射分量
标准光照方程的镜面反射分量指由光源直接经物体表面反射入眼睛的光线,如图 15.10:
- n为表面法向量
. - v指向观察者
. - l指向光源,对方向光源,l为定值
. - r为镜像向量,即l对n镜像之结果
. - θ为r和v的夹角,由r.v给出,描述镜像的方向性。
镜面反射使物体看上去有光泽,粗糙表面因反射率不高,所以缺乏此类效果。镜面反射的强度取决于物体、光源和观察者。
所有向量均为单位向量,如图15.11所示,r由2(n.l)n - l给出。
下列等式给出镜面反射的Phong模型:
mgls为材料的光泽度,也称作Phong指数,它控制亮斑的范围,小的mgls带来大而平滑的光斑,大的mgls带来小而亮的光斑。完全的反射面,如玻璃,有非常大的mgls----只有反射光进入眼睛,不完全光反射面,如苹果的表面,有较大的亮斑。
另一个有关亮度的值为mspec,即材料的反射颜色,对整个材料来说一般是一个不变的灰度值。mgls控制光斑的大小,mspec控制光斑的强度。强反射面有大的mspec值,粗糙些的表面则有较小的mspec值。如果你愿意,可用一个光泽图控制物体的反射,如同纹理控制物体颜色一样。
sspec是光源的镜面反射颜色,控制光本身的色彩与强度。对于方形聚光灯,此值可能来自投影光照面。sspec常等于光的漫反射颜色sdiff。
图15.12显示了mgls和mspec如何影响物体的镜面反射。图中,mspec从最左列到最右列由黑至白变化,而指数mgls在上方第一行最大,向下递减。注意到,最左面一列的头像看上去一样,因为镜面反射强度为0,对光照没有任何贡献。
如果观察者离物体的距离远大于物体的尺寸,可以仅计算v一次,然后认为它对整个物体是一个常量,同样道理对光源和l也适用。(其实对平行光源,l本身就是固定的。)但由于n是变化的,仍需计算r----这是一个应该尽量避免的计算。Blinn模型通过计算一个稍微不同的角度来避免这个计算。如图15.13所示:
Blinn模型使用h,表示v、l的中间量,由标准化v、l的平均值求得:
h= (
v+
l) / ||
v+
l||
公式15.9 Blinn模型中间向量h的计算
Blinn模型和Phong模型一样,只不过θ表示n与h的夹角:
此方程便于硬件的实现,特别是当光源与观察者均远离物体时,此时h被视为常数仅需计算一次。我们忽略的一个细节是有时cosθ小于0,此时简单地令镜面反射为0即可。
4.6 漫反射分量
与镜面反射类似,漫反射分量也刻画直接照射物体的光线。漫反射反映的是散开的随机方向上的反射,这是由物体表面的粗糙引起的,相反镜面反射则反映良好的反射。图15.14比较了良好反射表面和粗糙表面。
漫反射不依赖于视点位置,因为它本身就是随机,光源与物体的相对位置反而显得更重要。例如:若假定光线射入眼睛的几率一定,则由于垂直于光线的面在单位面积受光多于一个斜射的面,因为入射眼睛的光线更多,如图15.15所示:
注意上述两种情况中,光线间的垂直距离都相等。(由于错觉,右边的也许显得远些,但量一下,你就发觉实际情况了。)然而,注意右面的图,其命中目标时分得更开些,那么单位面积接受的光线将少些。从图上的示点线可看出左边受光9个单位,而右边只有6个单位,尽管二者面积一样。这正是赤道气候比两极暖和的原因,因为地球是圆的,太阳光在赤道有较直接的照射。
漫反射光服从Lambert法则:反射光强正比于法向量与光线夹角的余弦,用点乘计算余弦,如下:
n为表面法向量,l为指向光源的单位向量,mdiff为材料的散射色,即多数人认同的材料颜色。材质多来自纹理图,sdiff为光源散射色,一般和光源镜面色sspec一致。与镜面反射类似,这里也要防止点积出现负值,免得物体从背后透光。
4.7 环境光分量
镜面反射和漫反射都是刻画光源经物体反射后进入眼中的光线,但现实世界中,光线也经常在经历多于一次的反射后进入眼睛。好比你在黑暗的厨房打开冰箱,整个房间都会变得亮些,尽管箱门(或你的身体)挡住了大部分直线光。
描述这类反射,我们可用环境光,环境光取决于材质和全局环境光,此时没有涉及任何光源。下面的公式计算环境光:
mamb为材质的环境光分量,它总是等于漫反射分量----由纹理图定义,gamb为整个场景的环境光值。
4.8 光的衰减
光随距离衰减,所以远离光源的物体会变暗一些。现实世界里,光强度反比于物体和光源距离的平方。
i1 / i2 = d22 / d12
公式15.12 实际光线衰减反比于距离的平方
此处i为光强,d为距离。
实践中,公式15.12并不方便。我们常用另一个简单的基于辐射衰减距离的模型替代,在辐射衰减距离之外,光线将完全衰减为0。通常,可在光线有效射程内使用线性插值表现光随距离d的衰减:
如上,实际有两个辐射衰减距离。在dmin内,光强不衰减;dmin至dmax,光强有1减至0;超出dmax,光强一律为0。dmin控制开始衰减的距离,常设为0,表示光一旦射出即开始衰减;dmax是真正的衰减距离,此距离之外,光完全失效。
距离衰减也适用于点光源和聚光灯(平行光无衰减),聚光灯还多出一个Hotspot辐射衰减半径,表示光亮在光锥边上的衰减。一旦计算出衰减系数i,即可将它乘以镜面反射分量和漫反射分量。记住环境光是不衰减的,这很显然。
4.9 光照方程----合成
前面分别讨论了光照方程的各分量,现在是把它们合成到一起的时候了:
图15.16显示了当光分量独立存在时,各分量的视觉效果:
有几点需要注意:耳朵和鼻子一样亮,其实它本应该在头部的影子中。这是采用局部光照的结果,要计算阴影,必须考虑其他高级技术,影子涉及全局光照。前两幅图中,因为没有环境光,头背向光源的部分为全黑。若想照亮物体的背向部分,必须使用环境光,或者在场景中设置更多的光源,使得所有面都能被直接照亮。当只有环境光时,只能看出轮廓。光照是使物体呈现3D外观的重要武器,为了避免这种卡通效果,我们可以使用足够多的光源使场景中的所有表面都被直接照亮。
当有多个光源时,光照方程如何工作?对所有光源求和即可。若Sj表示第j个光源,j从1...n,n是光源个数,光照公式如下:
当然因为环境光只有一个,所以不做求和。
5 基本着色
5.1 雾化
现实中,光线被空气中无数粒子反射与折射。如果单位体积内粒子的浓度足够,则它们是可见的,例如烟、灰尘、雾等。计算机图形学中,上述现象都是通过雾化技术加以模拟的。想象我们正在注视远处的物体,眼睛与物体间的光线受到大量粒子的扰动。一些原来直线传播无法进入眼睛的光线,被那些粒子反射而进入眼睛,这就是我们"看到"空气中粒子的原因。最后的视觉效果上,物体的颜色向雾的颜色偏移,粒子越多,偏移越大。
雾浓度在[0, 1]间取值,控制雾化程度。浓度0表示无雾化,浓度1表示完全雾化,这时像素呈现雾的颜色,最终的颜色值由物体颜色和雾颜色线性插值求得。如何计算雾浓度?前面提到,大气中粒子越多,雾化越显著。然而,如何知道粒子数并转化为雾浓度呢?幸运的是,不必确切知道粒子数,我们用另一个值模拟这个数。粒子数依赖两个因素:场景中的全局武浓度和眼睛与物体间的距离。
眼睛与物体间的距离容易得到。于是,剩下的就是根据这个距离由雾浓度求得像素颜色。如何定义雾的浓度和单位呢?我们不直接定义,而是用一个简化系统。雾浓度由两个距离dmin和dmax控制,像素和眼睛距离小于dmin时无雾化,随着距离的增大,雾化逐渐加重,当距离大于dmax时完全雾化。如公式15.15所示:
有两点注意事项:
- 该公式假设雾是空间均匀的,但实际情况并不总是如此。例如,现实世界中,雾常在下方较浓,此模型不能表达这个现象。
. - 距离的定义是可变的。当然,可以用欧式距离,得到球状雾效果,但需要做开方运算。有一种简化是以摄像机空间深度z为距离,从而得到线性雾。它的优点是速度快,但有一个恼人的副作用,某一点雾浓度可能因摄像机朝向的不同而改变,现实世界中这是不可能的。
一旦得到[0, 1]间的雾浓度,像素颜色就可以用如下线性插值公式计算:
.
cfogged =
clit + f(
gfog -
clit)
.
其中:
- clit为计算光照后的物体表面颜色
.- f为公式15.15得出的雾浓度
.- gfog为全局雾颜色
.- cfogged为最终效果
.
为了在场景中得到雾化效果,必须向API说明雾的性质。常需要下列三种信息:
- 雾化开关,如果要得到雾化效果,必须打开。
.- 雾的颜色,即上式的gfog。
.- 雾化距离,dmin和dmax。
5.2 flat着色和Gourand着色
若渲染速度并不重要,我们可以逐像素地计算光照和雾化。(对于光照,这项技术称作Phong着色模型----不要和镜面反射的Phong模型混淆。)然而这样做计算量过于巨大,所以我们折中并减少计算的频率。有两个选择:逐多边形或逐顶点计算,这两项技术分别称作flat着色和Gourand着色。
使用flat着色,对整个三角形只计算一次光照值。通常计算光照的位置为三角形中心,表面法向量为三角形法向量。如图15.17所示,使用flat着色,物体由多边形构成的本质表露无遗,没有任何光泽可言。
Gourand着色,又称作顶点着色或插值着色。在顶点级计算光照和雾,然后这些值被线性插值用于整个多边形面,图15.18是和图15.17同样的茶壶,但采用Gourand着色。
显然,Gourand着色在保持物体的光滑性上做的较好。当被模拟的值本来就是线性时,Gourand着色能得到很好的效果。问题在于如果这些值不是线性变化,比如镜面高光。比较Gourand着色茶壶的高光部分和Phong着色茶壶的高光部分,如图15.19所示。Phong着色中,除去几何不连续的把手、壶嘴部分,高光的连续性很好,而Gourand着色中,由其高光面甚至可辨出各个小面元的分布。
线性插值的基本问题是内插值不可能大于较大的顶点值,所以高光只能在顶点出现,充分细化可解决这一问题。尽管有着自身的局限性,Gourand着色仍是现今硬件最常用的方法。
6 缓存
渲染涉及大量的缓存,这里缓存只是一个简单的存有像素数据的矩形内存块,最重要缓存是帧缓存和深度缓存。
帧缓存存储每个像素的色彩,即渲染后的图像。色彩可能有多种格式,但就当前的讨论来说,不考虑格式的差异。帧缓存常常在显存中,显卡不断读取该内存,并将二进制数据转化为CRT接收的合适信号。所谓双缓存技术,是为了防止图像在未完全渲染好之前就被显示。此时实际上使用了两个帧缓存,一个缓存存放当前显示的图像,另一个离线缓存存放正在渲染的图像。
一旦渲染完成并准备好显示即切换缓存,有两种方式:
- 如使用页切换技术,则命令显示卡开始从离线缓存读取数据,接着对调两个缓存的角色,现在的显示缓存变为离线缓存。
. - 也可以将离线缓存复制到显示缓存。
图15.20显示了双缓存的情况:
另一个用于渲染的重要缓存是深度缓存----也称作z-buffer。深度缓存不存储像素的颜色,而代之以像素的深度信息。存入缓存的深度信息有多种不同的变体,但它们基本上都反映物体到摄像机的距离。实践中通常保存的都是裁剪空间的z坐标,这就是z-buffer名称的由来。
深度缓存一般用于计算物体之间的遮挡,当光栅化三角形时,计算各像素的插值深度。在渲染像素之前,将这个深度值和深度缓存中该像素的深度值比较,如果新的深度比现有值离摄像机更远,则新的像素被丢弃;否则像素颜色被写到帧缓存,并用新的更近的值更新深度缓存。在开始进行新的渲染之前,记得要置z-buffer各值为无限远(在裁剪空间中,这个值为1.0),这样第一批像素才能通过深度测试,一般不对z-buffer设置双缓存。
7 纹理映射
物体外观不仅仅限于形状,不同物体的表面有着不同的颜色和图案,一个简单而有效的实现这种特性的方法是使用纹理映射。
纹理图是一个铺在物体表面上的位图,使用纹理图可以在"texel"(一个texel是纹理图中的一个像素)级别控制颜色,这优于在顶点或三角形级别控制物体颜色,也许你已经经过纹理映射的实例,不过还请看看下面的例子,它展示了纹理的强大威力。图15.21展示了一个纹理映射前和后的3D人物模型"Rayne"。
纹理图不过是一个贴在模型表面的位图,它是如何把整个物体包起来的?实际上,有多种不同方法,可以将网格用纹理图包起来。平面映射线性地将纹理投射于三角网,球形映射、柱形映射和立方体映射则有不同的投射方式,这里每项技术的细节并不重要,因为建模软件如3D Studio Max会处理它们。无论位图如何放置,每个顶点都要赋给一个纹理映射坐标,也就是位图上的2D笛卡尔坐标,通常称它们为u、v,以防止和渲染坐标x、y混淆。纹理映射通常按照位图尺寸单位化为[0, 1]之间的值,如图15.22,给出了图15.21模型中的一个纹理。
此纹理图为Rayne的头部所用,包围在头部的一半,从鼻子到后脑中线。艺术家特意使她的尺寸合适于模型,头部的另一半使用同样的纹理,一个镜像。
注意纹理不一定要连续地包围几何体。因为每个三角形都能独立映射,纹理的不同部分可以任意映射到物体的不同部分。比如上图中左上角的牙齿就是这样:Rayne有毒牙,嘴张开时可以看见。当然如果不使用连续映射,且纹理映射坐标存储在顶点级,纹理交界处对应的顶点坐标是双份的。注意这里对纹理使用了原点在左上角的坐标系,这类似于硬件访问的方式,更具学术性的文献则使用左下角为坐标原点。
如前所述,各顶点都有一个纹理的u、v坐标。用这种方式,纹理被钉在三角网上,要渲染三角形中的某个像素,可以先插值计算其u、v坐标(类似于Gourand着色),然后读取对应的纹理值。
8 几何体的生成与提交
一旦知道哪些物体可见(或至少潜在可见),即可将其生成并提交到图形处理器。该阶段完成以下任务:
- 细节层次(LOD)选择
. - 渐进式生成几何体
. - 向图形API提交数据
.
8.1 LOD选择和渐进式生成
一般我们希望以最大可能的三角形数量描绘物体以求得最佳的视觉效果,但不幸的是,较多的三角形一般意味着低帧率。我们必须在可接受的外表和帧率间做出折中选择。LOD在一定程度上可两全其美,基本思路是离摄像机远的物体只使用较少的多边形,此时并不降低视觉效果。
如何得到三角形数量较少的三角网?一种简单方法(从程序员角度)是让美工直接制作一个,然后根据物体离摄像机的远近(或屏幕分辨率大小)选用合适的LOD。问题是就在它由远及近改变的那一刻,这种方法会有一种跳动效果,当然我们希望把这种视觉上的不连续性降低到最低限度----好的三角网也许会有更大帮助。
一种克服跳动的方法是引入连续LOD。这种系统中,不同级别LOD包含的三角形数目几乎是连续的,我们可以产生任意多三角形的网格。渐进式网格技术就是一种这样的网格消减技术,但需要注意生成连续LOD的开支可能会很显著。而使用离散LOD,网格是现成可用的,渲染时可立即投送,我们所要做的就是决定用哪个网格。所以,即使实际的网格是用网格的消减技术生成的,离散LOD还是在实践中经常使用。
有时候几何体并非由美工创建,而是由计算机生成,这称为程序建模。分形地形图是程序建模的好例子,植物也可以自动创建,有时LOD也用在此类建模算法中。
8.2 向API投送几何体
多数API希望某种形式的三角网格输入,如单个三角形,索引三角网格,三角带或三角扇等。无论哪种形式,数据的核心都是顶点,三角形不过是顶点合适的连接方式。从另一方面说,API并不需要超过三角形级别的数据。
API根据操作的不同接受不同的数据格式。(当我们说“API如何时”,是指整个图形子系统,不论操作是由软件完成还是硬件完成的。)
在简化的情况下,顶点的数据一般分为三类:
- 位置:描述顶点的位置,可以是3D向量或者有深度信息的2D屏幕坐标。如果采用3D向量,还需要用模型、视图变换做向屏幕映射的工作。另一个骨骼动画中使用的高级技术是skinning,顶点坐标由若干骨骼给出。
. - 光照和雾化:为了渲染,顶点一般都带有色彩值,然后由这些值插值计算三角形中各点的颜色。我们可以指定这些值,或者让API计算合适的光照值。如果让API计算光照,通常要给出顶点法向量。无论如何,颜色均为RGB加alpha的元组。如果直接指定颜色,经常使用一个32位的ARGB值,每分量8位,或者为每个分量使用一个单独的值。如使用硬件雾化,还要指定各点的雾化强度,可以手动指定这个值,也可由API计算。
. - 纹理映射坐标:使用纹理映射时,每个顶点必须要有纹理映射坐标。最简单的情形下,只需要纹理图的2D坐标,常记为(u, v)。当使用多重纹理时,每个纹理都需要一个坐标。有时,可以阶段式生成纹理坐标(如向表面投射一道光线)。或者,可以阶段式地拷贝纹理坐标。在这种情况下,就可以不指定纹理坐标。
简单的说,投送顶点并没有一个简单的格式。事实上,存在许多变种,如DirectX有可变顶点格式的概念,可使你自定义格式,以最方便的顺序保存任何想要的信息。
有了这些之后,让我们给出几个C++结构,记录上面提到的常用格式。
最常见的是3D坐标,表面法向量和纹理映射坐标,需要API进行光照的静态纹理映射网格常使用这种格式。
// Untransformed, unlit vertex
struct RenderVertex
{
Vector3 p; // position
Vector3 n; // normal
float u,v; // texture mapping coordinates
};
另一种常用的格式,是用来显示2D物体或HUD(head up display)的,含有屏幕坐标和预定义的光照。虽然数据是2D的,但仍然带有某种形式的深度信息。
// Transformed and lit vertex
struct RenderVertexTL
{
Vector3 p; // screen space position and depth
float w; // 1/z
unsigned argb; // prelit diffuse color (8 bits per component – 0xAARRGGBB)
unsigned spec; // prelit specular color
float u,v; // texture mapping coordinates
};
最后一个例子是某种3D顶点,但不需要图形API的光照引擎照亮,它自带预定义的光照。这种格式经常用于特效,如爆炸、火焰、发光物等,以及调试用物体如包围盒、路点、标记等。
// Untransformed, lit vertex
struct RenderVertexL
{
Vector3 p; // position
unsigned argb; // prelit color (8 bits per component – 0xAARRGGBB)
unsigned spec; // prelit specular color
float u,v; // texture mapping coordinates
};
8.3 变换和光照
网格被提交到API之后,接下来的操作就是变换与光照(经常用T&L表示),图形管道的该阶段其实包含大量顶点级别的计算。基本上,所有顶点级别的计算都可以在本阶段进行,但最常见的操作有:
- 物体空间顶点位置变换到裁剪空间
. - 使用光照设置及法向量计算光照
. - 根据顶点位置计算顶点级雾浓度
. - 阶段式产生纹理映射坐标
. - 在骨骼动画中,用skinning技术计算顶点值
当然,根据不同的渲染上下文和提交的数据类型,某些操作不会执行。
当前图形API给予T&L阶段完全的灵活性。自第八版开始,DirectX支持顶点着色,其实就是运行在硬件上的小段代码。这些代码操作单个顶点,接受几何提交阶段发送来的任意多输入,并产生任意多输出到裁剪/光栅化阶段。典型的输入如顶点位置、法向量、光照前的颜色、纹理映射坐标等。可能的输出包括顶点坐标转换(摄像机空间或裁剪空间),Gourand着色,纹理坐标,雾浓度等。经常,输入只是简单地通过顶点着色,并映射成合适的输出(如纹理映射坐标,预计算的光照),或顶点着色执行一些运算产生全新的输出,如变换顶点位置、雾浓度、动态光照、或阶段式生成纹理映射坐标。
8.4 变换到裁剪空间
模型空间到裁剪空间的转换常以矩阵乘法实现。概念上,顶点经过一系列变换,如下所示:
- 模型转换到世界空间
. - 视图变换将世界空间转换到摄像机空间
. - 摄像机空间转换到裁剪空间
乘法顺序如下:
vclip =
vmodel(
Mmodel->world)(
Mworld->camera)(
Mcamera->clip)
实现中并没有做三步乘法,实际上,变换矩阵是连接好的,顶点的变换不需要做三次矩阵乘法。根据硬件的设计和光照方法,可以将所有矩阵连接成两个或一个矩阵。如果能够访问T&L硬件(如顶点着色),则可以直接施加精确的控制。如果不能,就必须依赖API让它作所有的优化。
8.5 顶点光照
理想的情况应该使用Phong着色,先对表面法向量插值而后像素点计算光照。实际上,我们却不得不多用Gourand着色,先计算顶点的光照而后插值生成多边形中各点的光照。
当在顶点级计算光照时,无法直接用公式15.14,因为mdiff不是一个顶点级材质属性,通常是由纹理定义这个值。为了使公式15.14更适合插值,必须进行变换以分离mdiff,同时,可以假设mamb等于mdiff。
用上面的光照方程,就可以在顶点级插值计算光照。对于每个顶点,我们计算两个值,vdiff和vspec。vdiff包含公式15.16的环境与散射分量,vspec包含镜面分量:
上述值都是逐顶点计算,然后对整个三角形插值。于是对每个像素,光照公式如下:
如前所述,mspec经常为常量,但也可以用光泽图定义。
应使用哪个坐标空间计算光照?可以在世界空间内进行。此时,顶点坐标、法向量都要转换到世界空间,以进行光照计算,接着顶点坐标转至裁剪空间。或者,可以将光照放到模型空间中计算,因为光照总比顶点较少,结果是总体减少了向量----矩阵乘法计算。第三种可能是在摄像机空间内计算。如果你不通过顶点着色直接控制T&L管道,API会为你做出这种选择。