渲染引擎

采用深度缓冲的三角形光栅化基础

三维场景渲染的本质涉及的基本步骤:

  • 描述一个虚拟场景。一般是以某数学形式表示的三维表面。
  • 定位及定向一个虚拟摄像机,为场景取景。
  • 设置光源。
  • 描述场景中物体表面的视觉特效。
  • 对每个位于影像矩形内的像素,渲染引擎会找出经过该像素而聚焦于虚拟摄像机焦点的(一条或多条)光线,并计算其颜色及强度。该过程称为求解渲染方程,或称着色方程。

游戏图形一般是以照相写实主义为主要目标。为了产生运动的错觉,需要以每秒30/50或60帧的速度显示渲染的影像,因此实时渲染引擎需要在33.3ms以内产生一幅影像。

场景描述

渲染不透明的物体时,只需要考虑其表面;而游戏引擎渲染透明和半透明的物体时,只会采用和渲染不透明的物体差不多的方法。虽然这样可能会有一些异常(物体离摄像机较远的一面渲染的不正确),但是看上去足够真实。因此,大多数游戏引擎主要着重于渲染物体表面。

高端渲染软件所用的方法

数学中的公式不能为任意形状建模。高端电影渲染引擎采用细分曲面定义任意形状;每个表面由控制多边形网格表示表面,但是这些多边形会使用Catmull-Clark算法逐步细分成更小的多边形。这样无论摄像机离表面多近,多能细分多边形,使轮廓显得圆滑。

三角形网格

游戏开发者,使用三角形网格逼近一个表面;使用三角形的优点:

  • 三角形是最简单的多边形;
  • 三角形必然是平坦的。因为单点确定一个平面。
  • 三角形经多种转换之后仍然维持是三角形,这对于仿射转换和透视转换也成立。
  • 几乎所有商用图形加速硬件都是为三角形光栅化而设计的。

镶嵌:把表面分割为一组离散多边形的过程,这些多边形通常是三角形或四边形。三角化是指把表面镶嵌为三角形。

使用细分曲面能够实现无论物体远近,都能有一致的三角形对像素密度。表面能根据摄像机的距离来进行镶嵌,使每个三角形的尺寸都少于一个像素。 游戏开发者常使用一串不同版本的三角形网格来逼近理想的三角形对像素密度,每一个版本称为一个层次细节。当摄像机靠近或远离时,就可以相应的切换层次细节。

有些引擎会使用动态镶嵌的技术到可扩展的网格上。此时,网格通常以高度场来表示,最接近摄像机的网格区域会以栅格的最高分辨率来镶嵌,距离较远的区域以较少的栅格镶嵌。

渐进网格:当物体很接近摄像机时采用单个高分辨率网格,当物体远离时,网格自动把某些棱收缩为点。这样该过程能形成半自动的层次细节链。

构造三角形网格

通过三角形的三个顶点可以用法平面表示三角形网格。然后使用缠绕顺序确定平面的正向。缠绕顺序有两种定义方式:顺时针方向(clockwise CW)、逆时针方向(counterclockwise CCW)。很多底层图形API提供缠绕顺序剔除背面三角形;这样就不需要浪费时间渲染看不到的三角形,渲染透明物体的背面还会异常。因此要保持一致的缠绕顺序。

三角形表

网格的三个顶点对应一个三角形,这些顶点构成三角新表;但是它有许多重复顶点。为了避免内存浪费,多数渲染引擎会采用索引化三角形表。它保证每个顶点仅列举一次,然后用轻量级的索引定义组成三角形的三个顶点。在DirectX中定点存储在顶点缓冲,索引存储在索引缓冲中。

游戏引擎有时还会用到两种特殊网格:三角形带三角形扇。它们都规定了顶点的次序;三角形带中,前3个顶点定义了第一个三角形,之后每个顶点和前面的两个顶点组成新的三角形。三角形扇中,前3个顶点定义了第一个三角形,之后每个顶点和前一个顶点及三角形扇的首个顶点组成新的三角形。

GPU处理三角形时,为了在光栅化阶段保持三角形的完整性,顶点必须按照其位于三角形中的次序处理。当顶点着色器处理每个顶点后,其结果会被缓存以便重复使用。因此,规定了次序的三角形带和三角形扇能改善GPU存取显存时的缓存一致性。还可以使用索引化三角形带和索引化三角形扇来结合两者的优点。除此之外也有优化索引化三角形表以提升缓存一致性而设计的顶点缓存优化器。

三角形网格的位置矢量,通常指定在一个局部坐标系,此坐标系称为模型空间、局部空间。多个网格公用的坐标系称为世界空间。一个网格从自己的模型空间转换到世界空间需要一个变换矩阵,该矩阵称为模型至世界矩阵。转换矩阵参考:

  RSM->W用来旋转和缩放模型空间顶点至世界空间,tM是模型空间到世界空间的位移

描述表面的视觉性质

表面的视觉性质:

  • 几何信息:表面上不同位置的法矢量等
  • 描述光和表面交互作用的方式:漫反射颜色、粗糙度等
  • 表面随时间变化的描述:动画的角色皮肤如何追踪其骨骼关节等

渲染照相写实影像关键是正确的模拟光和场景中物体交互作用时的行为。

要描述表面的视觉特性,最简单的方法是把这些特性记录在表面的离散点上。网格顶点是个好选择,这是称为顶点属性。每个顶点可能包含的属性:

  • 位置矢量:以局部空间的坐标表示;pi=[pix,piy,piz]
  • 顶点法矢量:顶点i的表面的单位矢量,可用于每顶点动态光照的计算;ni=[nix,niy,niz]
  • 顶点切线矢量:它ti=[tix,tiy,tiz]和顶点副切线矢量bi = [bix,biy,biz]和顶点法矢量都相互垂直;这3个矢量能一起定义切斜空间的坐标轴;此空间用于计算多种逐像素光照。
  • 漫反射颜色:漫反射颜色是一个四元素矢量,以RGB描述表面的颜色,不透明度alpha(A)。静态光照时,该颜色脱机计算;动态光照时,该颜色运行时计算;di = [dRi,dGi,dBi,dAi]
  • 镜面颜色:si = [sRi,sGi,sBi,sAi]
  • 纹理坐标:把二维的位置收缩包裹网格的表面,此过程称为纹理贴图;uij = [uij,vij]
  • 蒙皮权重:骨骼动画中,网格顶点依附在骨骼的个别关节上。此时,每个顶点要指明它依附的关节索引;若收到多个关节的影响,就要求它们的加权平均。

常见的顶点格式:

//最简单的顶点,只含位置。(可用于阴影体伸展、卡通渲染中的轮廓棱检测、z渲染)
struct Vertex1P{
    Vector3 m_p;//位置
};

//典型的顶点格式
struct Vertex1P1N1UV{
    Vector3 m_p;//位置
    Vector3 m_n;//顶点法矢量
    F32 m_uv[2];//(u,v)纹理坐标
};

//蒙皮用的顶点,含位置、漫反射颜色、镜面反射颜色及4个关节权重
struct Vertex1P1D1S2UV4J{
    Vector3 m_p;//位置
    Color4 m_d;//漫反射颜色及透明度
    Color4 m_s;//镜面反射颜色
    F32 m_uv0[2];//纹理坐标1
    F32 m_uv1[2];//纹理坐标2
    U8 m_k[4];//4个关节索引
    F32 m_w[4];//3个关节权重(第4个由其他3个求出)
};

属性插值和纹理贴图

三角形顶点的属性仅仅是整个表面的视觉特性的粗糙、离散近似值。渲染三角形时,重要的是三角形内点的视觉特性,这些内点最终成为屏幕上的像素。因此,我们需要知道每个像素的属性。

最简单的得到每个像素的属性的方法是对每个顶点的属性进行线性插值,把线性插值施于顶点颜色的方法成为高氏着色法。我们使用表面特性和入射光计算每个顶点的漫反射颜色di,此时会用到该顶点垂直于表面的法向量,即顶点法向量。因此顶点法向量的方向对网格外观有重要影响。

然而当三角形比较大时,使用顶点设置表面属性可能会太过粗糙,线性插值在某些情况下,误差会很明显。例如镜面高光。此时,可以使用纹理贴图的方式。纹理通常含有颜色信息,并且一般会投射在网格的三角形上。某些吐信硬件上,纹理位图的尺寸必须是2的幂;虽然多数硬件没有限制,但是一般纹理尺寸是256x256、51x512、1024x1024、2048x2048等。

最常见的是漫反射贴图:它的纹素存储了表面的漫反射颜色;同样还有法线贴图(每个纹素用来存储以RGB值编码后的法矢量),光泽贴图(在每个纹素上描述表面的光泽程度)等。实际上纹理贴图可以存储任何计算着色时需要的信息。

如何将纹理投射到网格上:首先定义一个称为纹理空间的二维坐标系。纹理坐标通常以两个归一化数值(u,v)表示。这些坐标的范围是从纹理的左下角(0,0)伸展至右上角(1,1)。这样归一化的好处:这些坐标不会受到纹理尺寸影响。然后把三角形映射到二维纹理,只需要在每个顶点i上设置纹理坐标(ui,vi)。

图形硬件可用以下的方式处理范围以外的纹理坐标,这些处理方式称为纹理寻址模式

  • 缠绕模式:纹理在各个方向重复;
  • 镜像模式:和缠绕模式类似,但是在u为奇数倍数上的纹理会在v轴方向形成镜像,在v为奇数倍数上的纹理会在u轴方向上形成镜像;
  • 截取模式:当纹理坐标在正常范围之外时,纹理的边缘纹素会简单的延伸;
  • 边缘颜色模式:用户指定一个颜色,作为纹理坐标在[0,1]以外时使用。

纹理位图可在磁盘上存储为任何格式,只要能够读取;现在多数显卡及图形API会支持压缩纹理。压缩纹理的优点:占用较少的内存;渲染更高效;缺点:压缩会导致一些失真。

纹素密度

纹素密度:纹素和像素之比;如果每个纹素对应一个屏幕像素,则纹素密度为1;当在较远距离观看该图形时,屏幕上的面积会变小,而纹理尺寸不变,该图形的纹素密度会大于1,即一个像素会受到多于1个纹素的影响;而当纹素密度远大于1时,会产生莫列波纹,更甚者,像素的颜色会闪烁。当较近距离观看该图形时,纹素密度会小于1;当纹素密度远小于1时,纹素的边缘就会被察觉到。

理想的无论远近纹素密度接近于1是最好的,但是这不可能,因此使用多级渐远纹理来逼近。即对每张纹理建立较低分辨率位图的序列,每张大小递减一半。

纹理过滤

当渲染纹理三角形上的像素时,图形硬件会计算像素中心落入纹理空间的位置,来对纹理贴图采样。由于像素中心可以落在纹理空间的任何位置,因此,图形硬件通常需要采样出多余一个纹素,并把采样结果混合以得出实际的采样纹素颜色。这个过程称为纹理过滤。

纹理过滤种类有:

  • 最近邻:挑选最接近像素中心的纹素。当使用多级渐远纹理时,它会挑选一个最接近但高于理想分辨率的渐远纹理级数。
  • 双线性:围绕像素中心的4个纹素采样,并计算该4个颜色的加权平均(权重是基于纹素和像素中心的距离)。当使用多级渐远纹理时,和最近邻相同。
  • 三线性:把双线性过滤法施于最接近的两个渐远纹理级数(一个高于、一个低于理想分辨率),然后把两个采样结果线性插值。这样就能消除渐远纹理级数之间的边界。
  • 各向异性:如果纹理表面是面对着摄像机,则前面的双线性和三线性合适;然而,表面倾斜于虚拟屏幕平面,可以使用各向异性过滤法会根据视角,对一个梯形范围内的纹理采样,借以提高非正对屏幕的纹理表面的视觉品质。

材质是网格视觉特性的完整描述。顶点属性是表面特性的一部分,但不是材质的一部分。网格-材质对有时称为渲染包。三维模型通常对应多种材质,一个网格通常会切割成子网格,每个子网格对应一个材质。

光照基础

着色通常是光照加上其他视觉效果的泛称。渲染引擎会使用多种数学模型来模拟光和表面/体积的交互作用,这些模型称为光传输模型。最简单的模型只考虑直接光照。光源发出的光碰到某物体后会反射,然后直接进入虚拟摄像机的虚拟平面。此模型又称为局部光照模型。

想要更好的效果,就要考虑到间接光照,这种模型称为全局光照模型。有些全局光照模型针对某种视觉现象,例如产生逼真的阴影,模拟反射性表面、模拟焦散等,或者模拟多种光学现象,例如光线追踪和辐射度算法。

全局光照模型能够完全由单一数学公式描述,此公式称为渲染方程着色方程

Phong氏光照模型(最常用的局部光照模型),它把从表面反射的光分解为3个独立项。

  • 环境项模拟场景中的整体光照水平。它是场景中间接反射光的粗略估计。间接反射不会使阴影部分变成全黑。
  • 漫反射项模拟直接光源在表面均匀的向各个方向反射。它逼近真光源照射至哑光表面的反射。例如布块。
  • 镜面反射项模拟在光滑表面会看到的光亮高光。

计算某点的Phong反射,需要输入参数:

  • 视线方向矢量V = [Vx  Vy  Vz]是从反射点到虚拟摄像机焦点的方向。
  • 3个颜色通道的环境光强度A = [AR  AG  AB]
  • 光线到达表面上哪一点的表面法线N = [Nx  Ny  Nz]
  • 表面的反射属性
    • 环境反射量kA
    • 漫反射量kD
    • 镜面反射量kS
    • 镜面的“光滑度”幂α
  • 每个光源i的属性
    • 光源的颜色及强度Ci = [CRi  CGi  CBi]
    • 从反射点至光源的方向矢量Li

在Phong氏模型中,从表面上某点反射的光强度I可以表示为下面的矢量方程:

分解后:

其中Ri = [Rxi  Ryi  Rzi]是光线方向Li对于表面法线的反射。Ri可以通过矢量运算求得。过程如下:

L = LT + LN(任何矢量都可以表示为切线分量和法线分量之和)

LNN·L有这样的关系:LN=(N·L)N

反射向量RL由同一个法线分量,有相反的切线分量(-LT),因此:R = LN - LT = LN - (L - LN)=2LN - L = 2(N·L)N - L

Blinn-Phong反射模型是Phong反射模型的变种。中间方向矢量H为视线方向矢量V和光线方向矢量L的中间。则Blinn-Phong模型的镜面分量为(N·H)而不是(N·L)。Blinn-Phong是以降低准确度来换取高效。

详细参考:http://en.wikipedia.org/wiki/Blinn-Phong_shading_model

BRDF是沿视线方向V的向外辐射和沿入射光线L的进入辐射之比;BRDF可以显示为一个半球图表,当中距原点的径向距离代表从该角度观察到的反射光强度。

光源模型

  • 静态光照:光照最好在游戏运行前计算。网格顶点预计算Phong反射,并把结果存储在顶点漫反射颜色属性中。也可以逐像素预计算光照,把结果存储于光照贴图中。为什么不把光照信息烘焙到漫反射纹理中呢?
    • 漫反射纹理通常会在场景中重复使用;
    • 光照贴图的分辨率通常低于漫反射纹理的分辨率;
    • 单纯的光照贴图通常比包含漫反射颜色信息的贴图更易压缩。
  • 环境光:对应Phong中的A颜色项,不含方向。
  • 平行光:模拟距表面无限远的光源,发出的光线平行;用光源的颜色C和方向L表示。
  • 点光:又称全向光,从游戏世界的特定位置向所有方向均匀辐射,光的强度以距光源的距离做平方衰减,超出预设最大有效半径就为0。由位置P、光源颜色/强度C及最大半径rmax表示。
  • 聚光:光线在一个圆锥范围内,如手电筒。分内外两个圆锥,内圆锥的光线以最高强度发射,内外之间强度衰减直至为0;圆锥内也会随距离衰减。由位置P、光源颜色C、中央方向矢量L、最大半径rmax、内外圆锥角θmin和θmax表示。
  • 面积光:现实中的光源几乎都有一定面积。工程师通常以一些技巧模拟它:要模拟半影,可以投下多个阴影,再把结果混合;以某种方式把锐利的阴影的边缘模糊化。
  • 发光物体:发光表面可以用放射光贴图来模拟,此纹理的颜色永远以完全强度发射,不受附近的光照环境所影响。

虚拟摄像机

观察空间:虚拟摄像机的焦点是观察空间或称为摄像机空间的坐标系原点。则观察空间到世界空间的转换矩阵:

而有时需要世界空间到观察空间的转换矩阵,称为观察矩阵MW->V = (MV->W)-1 = Mview 

渲染某些网格前,通常需要将它从模型空间转换到观察空间:MM->V = MM->WMW->V = Mmodel->vew 

投影:把三维场景渲染成二维影像;最常见的是透视投影,它使得物体远小近大,这种效果称为透视投影。有些会采用维持长度不变的正视投影,主要用于渲染三维模型或游戏关卡的平面图。

摄像机映射的范围称为观察体积,它是由6个面组成的平截头体。近平面对应于虚拟影像感光元件的表面,然后是上下左右4个平面对应虚拟屏幕边缘,远平面则用作渲染优化,远平面也作为深度缓冲的深度上限。6个平面可用6个4位向量(nxi,nyi,nzi,di)表示,其中n = (nx,ny,nz)为平面法线,d为平面和原点的垂直距离。也可以用点法式表示每个平面,即6个矢量(Qini),其中Q表示平面上的任意点,n为平面法向量。

齐次裁剪空间

投影能把点从观察空间变换到齐次裁剪空间的坐标系,裁剪空间把观察体积转换为标准的观察体积,使它独立于投影类型和屏幕的分辨率。齐次裁剪空间通常是左手坐标系。

从上图可以推导出OpenGL(OpenGL中裁剪空间的z轴的范围是[-1,1])的透视投影矩阵(把点的坐标从观察空间转换到齐次裁剪空间)(左边),和DirectX(DirectX中裁剪空间的z轴的范围是[0,1])的透视投影矩阵(右边)。

            

进行透视投影后,每个点的x和y坐标会除以z坐标,这样可以产生透视收缩。为何能够这样做呢?可以尝试把观察空间的点Pv以四维齐次坐标表示并乘以OpenGL的透视投影矩阵:

 于是可以得到右边的形式: 

然后按照齐次坐标转换为三维坐标时,把x、y、z分量除以w分量:

当以透视投影渲染场景时,进行属性差值要把透视收缩的影响计算在内。对于每个顶点属性A1及A2,它们之间t百分比的插值属性:

同样正射投影的矩阵是:  可以看出来,它只是一个放缩并平移的矩阵,放缩是非统一放缩。

在渲染之前,需要缩放及平移裁剪空间坐标,使这些坐标变换成在屏幕空间而非在单位正方形里,此缩放平移操作称为屏幕映射。

最终渲染后的影像会存储在一个名为帧缓冲的颜色位图缓冲里。渲染引擎通常会维护至少两个帧缓冲。但现显示硬件扫描一个帧缓冲时,渲染引擎则更新另一个帧缓冲,这称为双缓冲法。通过垂直消隐区间互换两个缓冲。有些引擎使用3个帧缓冲。

三角形光栅化及片段

要在屏幕上产生一个三角形影像,需要给该范围内的像素填充数据。此过程为光栅化。光栅化过程中,三角形表面会拆成名为片段的小块,每个片段对应三角形的一个细小区域,每个细小区域对应单个屏幕像素。一个片段写进帧缓冲前要通过许多测试,片段不能通过任意测试都会被丢弃;只有通过所有测试,才能写进帧缓冲或和已有颜色混合。

当光栅化一个三角形时,边缘可能出现像锯齿一样不圆滑的情况。这时需要抗锯齿,它的效果是三角形边缘和帧缓冲附近的颜色混合起来。

  • 全屏抗锯齿:影像渲染比实际屏幕宽、高一倍的帧缓冲里,然后把该帧缓冲缩减采样至所需分辨率。它很耗时,因为实际渲染了4倍的需要大小,且很耗空间。
  • 多重采样抗锯齿:把每个像素拆为多个片段,这些片段在管道最后阶段结合成单个像素。

当渲染重叠三角形时,需要保证一个在另一个上面,如果不穿插,可以按照三角形从后往前的顺序渲染,这称为画家算法

如果三角形互相穿插,渲染引擎使用深度缓冲(z-buffer)技术。深度缓冲是全屏缓冲,当中每个像素含16或24位的深度数据。每个片段含有一个z坐标度量它的深度。这样一个片段渲染一个已有片段渲染的像素时,会比较它的深度和已有的深度,若新片段深度较小就写入帧缓冲,否则丢弃。

但是由于深度值不够精确,可能出现两个足够接近的平面的深度值相同,此时就会造成深度冲突。并且当物体离摄像机越远越严重。为了克服这个问题,我们希望深度缓冲中存储的是观察空间的z坐标,而不是裁剪空间的z坐标。观察空间的z坐标随摄像机距离线性变化,因此能保证整个深度范围的均匀的精确度。此技术称为w缓冲(w-buffer)。w缓冲更耗时一点。

渲染管道

高级渲染步骤是由管道的软件架构实现的。并行在管道的个别阶段中实现。管道的吞吐量是指总体每秒可产生的数据量;管道的潜伏期是指:单个数据需要花多少时间才能走完整个管道。管道中可以分为下面的几个阶段:

  • 工具阶段(脱机):定义几何和表面特性(材质)。
  • 资产调节阶段(脱机):资产调节管道处理几何和材质数据,生成引擎可用的格式。
  • 应用程序阶段(CPU):识别出潜在可视的网格实例,并把它们及其材质呈交至图形硬件渲染。
  • 几何阶段(GPU):把顶点变换、照明,然后投影至齐次裁剪空间。可选择用几盒着色器处理三角形,然后对三角形根据平截头体进行裁剪。
  • 光栅化阶段(GPU):把三角形转换为片段,并对片段着色。片段经过多种测试(深度测试、alpha测试、模板测试等)后,最终和帧缓冲混合。

工具阶段

三维建模师在数字内容创作(DCC)软件中制作三维模型,例如Maya、3ds Max等,这些模型可以有任何方便的表面描述方式定义,例如四边形、三角形等。但是,渲染前,都会镶嵌成三角形。网格的顶点也可以蒙皮,蒙皮需要把每个顶点关联到骨骼结构的一个或多个关节,每个顶点含对每个关联关节的影响权重。美术人员需要定义材质,包括为每个材质选择着色器,选取该着色器需要的纹理,以及设置着色器提供的配置参数和选项。制作材质时,通常使用材质编辑器。材质编辑器有时会以插件的方式整合到DCC工具里,也可以是独立程序。有些材质编辑器能连接至游戏,其他的提供脱机的三维预览视图。以图形化语言制作的着色器通常需要由图形工程是手工优化,因为图形化语言通常是以性能换取弹性、通用性、易用性。许多游戏团队建立材质库,从中给每个网格挑选合适的材质,让网格和材质维持松散的耦合。

资产调节阶段

资产调节阶段也是一个管道,有时称为资产调节管道(asset conditioning pipeline,ACP)。其工作是导出、处理、连接多个种类的资产,生成内聚的整体。几何和材质数据是由DCC软件抽取出来的,然后通常存储为拼台无关的中间格式。接着,根据引擎支持的目标平台种类,这些数据会被处理成一个或多个平台专用格式。理想上,此阶段生成的资产能直接载入内存,运行时,无需处理或只需少量的处理。ACP通常会顾及材质和着色器。例如着色器需要顶点法线、切线及副切线矢量,ACP自动生成它们。资产调节阶段可能会计算高级的场景图(scene graph)数据结构。耗时的光照计算通常也是在资产调节阶段进行的,这种情况是静态光照。也可以把每个像素的光照信息存于纹理中,称为光照贴图。它还可以生成预计算辐射传输(precomputed radiance transfer,PRT)的系数,这些通常是球谐(spherical harmonic)函数的系数。

GPU介绍

几乎所有的GPU都可以拆成下述的子阶段;每个阶段的各灰阶代表它的功能是可编程的、固定但可配置的、固定而不可配置的。

顶点着色器

此阶段是可编程的,顶点着色器负责变换及着色/光照顶点。此阶段的输入是单个顶点(虽然实际上会并行处理多个顶点)。顶点位置和法矢量通常会从模型空间或者世界空间变换到齐次裁剪空间,期间会进行透视投影、每个顶点的光照及纹理计算以及为动画角色蒙皮。

几何着色器

可选的几何着色阶段也是完全可编程的。几何着色器处理以齐次裁剪空间表示的整个图元(三角形、线段、点)。它能剔除或修改输入的图元,又能生成新的图元。例如阴影体积拉伸、渲染立方体贴图的6个面、动态镶嵌等。

流输出

现在的GPU允许把到达此阶段的数据写回内存,数据能从这里回到管道的起始做进一步的处理,该功能称为流输出。例如头发渲染,有了流输出,GPU可以在顶点着色器内在头发样条的控制点上进行物理模拟,在几何着色器中把样条镶嵌成线段,并用流输出功能把镶嵌后的顶点数据写回内存,最后这些线段重新流入GPU管道渲染。

裁剪

裁剪阶段把三角形在平截头体以外的部分切掉。原理是先判断那些顶点在平截头体之外,然后求出三角形和平截头体的交点,这些交点会成为裁剪后三角形的新顶点。此阶段是固定功能,但提供有限的配置。

屏幕映射

屏幕映射只是简单的放缩和平移顶点,使之从齐次裁剪空间变换到屏幕空间。此阶段是完全固定且不能配置的。

三角形建立

自该阶段开始,光栅化硬件开始迅速的把三角形转换成片段。此阶段不能配置。

三角形遍历

三角形遍历把三角形分解为片段。通常每个像素会产生一个片段,除非使用MSAA,每个像素会产生多个片段。三角形遍历也会对顶点属性进行插值,来产生每个片段的属性。有需要,也会使用透视校正插值。此阶段是固定且不能配置的。

提前深度测试

许多显卡能在此时检查片段深度,丢弃被遮挡的片段,这样被遮挡的片段不用浪费时间进入像素着色器阶段。

像素着色器

像素着色是完全可编程阶段,它会替每个像素着色(即光照及其他处理)。像素着色器会丢掉透明的片段。此阶段输入是一组每片段属性,输出是一个颜色矢量。

合并/光栅运算阶段

管道的最终阶段是合并阶段或混合阶段,NVIDIA称光栅运算阶段(raster operations stage,ROP)。此阶段不能编程,但能高度配置。此阶段执行多个片段测试,包括深度测试,alpha测试,以及模板测试。通过了所有测试的,其颜色就会与帧缓冲中原来的颜色进行混合,混合方式由alpha混合函数控制,它的结构是固定的,但可以配置它的参数和运算符。

Alpha混合最常用的混合函数来渲染半透明机和物体是:C`D = ASCS + (1 - AS)CD

S指“source”,D指“destination”。写进帧缓冲的颜色是C`D,目的帧缓冲内容CD,片段颜色CS,混合权重AS

要令alpha混合显示正常,必须先渲染场景中的不透明几何物体至帧缓冲,然后把半透明表面从后往前排序渲染。因为进行alpha混合后,新片段的深度会覆写原来被混合的像素深度。也就是说,深度缓冲会忽略透明度。如果要在不透明背景上渲染一堆半透明物体,那么理想的最终像素颜色要与那堆半透明物体的所有表面混合。若使用任何其他次序渲染,有些半透明片段深度测试会失败,导致那些片段被丢弃,造成最终不完整的混合。

通用的混合函数C`D = (wSCS) + (wDCD)

其中wSwD可由程序员设置。⊕根据前面的数据类型,可以使标量对矢量的乘法、矢量之间的Hadamard product。

可编程着色器

内存访问

着色器程序不能直接读写内存,它只能通过两种方法访问内存:寄存器和纹理贴图

着色寄存器

着色器可用寄存器间接的存取内存,所有GPU的寄存器是128位SIMD格式,矩阵可用一组3或4个寄存器表示。GPU寄存器可以保存单个32位标量,这时,通常会把该值复制到所有4个32位字段。有些GPU能在16位字段上计算,这种数据类型称为half

  • 输入寄存器:它是着色器的主要数据输入来源。在调用着色器之前,GPU会自动设置这些输入寄存器的值。
  • 常数寄存器:常数寄存器的值由应用程序设置,应用程序按照不同图元设置不同的值。所为常数是针对着色器的,常数寄存器是着色器的另一个输入。一般包含模型观察矩阵、投影矩阵、光照参数等。
  • 临时寄存器:它只供着色器程序内部使用,通常用于存储中间计算结果。
  • 输出寄存器:它的内容由着色器填充,作为着色器仅有的输出形式。在顶点着色器中,输出顶点属性;在像素着色器中,输出正在着色的片段的最终颜色。

当程序执行完成,GPU会把输出寄存器的内存写入显存。GPU通常会把输出的数据存储到缓存中,使这些数据能重用。

纹理

着色器能够直接读取纹理贴图。纹理数据是以纹理坐标寻值得,而不是绝对内存地址。GPU的纹理采样器会自动过滤纹理数据,适当的混合相邻纹素及相邻渐远纹理级数的值。也可关闭纹理过滤,直接存取某纹理的值。

着色器只能用间接方法写数据进纹理,即把场景渲染至屏幕外帧缓冲,再在后续的渲染阶段把该帧缓冲当做纹理贴图使用。此功能称为渲染到纹理(render to texture,RTT)。

高级着色器语言入门

高级着色器语言例如Cg和GLSL仿照了C语言来制定。程序员能声明函数、定义简单的struct,以及做算术运算。然而,这些变量会有着色器编译器把他们直接映射到寄存器。

语义:在变量或struct成员后加入冒号和一个名为语义的关键词。语义告诉编译器如何把变量或数据成员绑定到个别顶点或片段属性。

struct VtxOut
{
        float4 pos   : POSITION;    //映射至位置属性
        float4 color : COLOR;       //映射至颜色属性
}

输入输出:编译器会根据个别变量或struct的上下文,判断它们应映射至输入或输出寄存器。若变量是以参数形式传入着色器的主函数,那么它会被当做输入;若变量是主函数的传回值,那么它会被当做输出。

VtxOut vshaderMain(VtxIn in) //in 映射至输入寄存器
{
        VtxOut out;
        // ...
        return out;          //out 映射至输出寄存器
}

uniform声明:要应用程序经常从寄存器取数据,可以再声明变量时加入uniform关键字。

//模型观察矩阵可用下面的方式传入顶点着色器
VtxOut vshaderMain(VtxIn in, uniform float4x4 modelViewMatrix)
{
        VtxOut out;
        // ...
        return out;
}
//把顶点位置乘以模型观察矩阵
VtxOut vshaderMain(VtxIn in, uniform float4x4 modelViewMatrix)
{
        VtxOut out;
        out.pos = mul(modelViewMatrix, in.pos);
        out.color = float4(0, 1, 0, 1);//RGBA 绿色
        return out;
}

要从纹理获取数据,需要调用特殊的内部函数,它会从指定的纹理坐标读取纹素的值。这些函数有很多变种,以供读取不同格式的一维、二维、三维纹理,并可选择是否使用过滤。也会提供特殊的寻址模式存取立方体贴图及阴影贴图。引用纹理需要使用特别的数据类型声明方法,称为纹理采样器。

struct FragmentOut
{
        float4 color:    COLOR;
}

//Cg像素着色器,把漫发射贴图贴于三角形上
FragmentOut pshaderMain(float2 uv : TEXCOORD0, uniform sampler2D texture)//sampler2D数据类型代表对二维纹理的引用
{
        FragmentOut out;
        out.color = tex2D(texture, uv);//查找位于(u,v)的纹素
        return out;
}

效果文件

GPU管道还需要一些额外的信息,才能为着色器程序听有意义的输入。例如:需要指定应用程序相关的参数如何应设置着色器程序中声明的uniform变量;有些视觉效果需要两个或以上的渲染步骤,但着色器只能描述单个渲染步骤内的运算等。为了把(多个)着色器结合成完整的视觉效果,我们可以使用名为效果文件(effect file)的文件格式。

不同渲染引擎效果文件实现不同,但通常会有以下的层次结构:

  • 在全局作用域定义struct、着色器程序和全局变量。
  • 定义一个或多个技术。每个技术代表渲染某视觉效果的方法,一个效果通常提供一个技术作为效果的最高品质实现,另外加上多个回退(fall back)技术,供较低级的图形硬件使用。
  • 每个技术内定义一个或多个步骤(pass)。每个步骤描述如何渲染一整帧影像。通常一个步骤包含顶点/几何/像素着色器程序的“主函数”引用、多个参数绑定及可选的渲染状态设置。

Cg着色器详细编程细节:http://developer.nvidia.com/object/cg_tutorial_home.html

应用程序阶段

本阶段有3个角色:

  • 可见性判别:应该仅把可见的物体提交GPU,以免浪费资源渲染不可见的东西。
  • 提交几何图元至GPU以供渲染:使用DirectX的DrawIndexedPrimitive()或OpenGL的glDrawArrays()之类的渲染调用把子网格材质对传送至GPU。另一个提交方法是建立GPU命令表。
  • 控制着色器参数及渲染状态:uniform参数通过常数寄存器传送至着色器时,应用程序阶段需按每个图元为单位进行配置。应用程序必须设置所有不可编程但可配置的管道阶段参数,以确保每个图元能正确的渲染。

可见性判断

平截头体剔除

平截头体剔除中,完全位于平截头体之外的物体便会排除在渲染表之外。通过测试物体的包围体积(一般是球体,因为容易计算)及6个平截头体平面。对于每个平截头体平面,我们把该平面往内移动球体半径的距离,然后判断球心是位于修改后平面的哪一方,如果球体在所有6个修改后平面的前方,球体就是在平截头体之内。这种方法可能有误判的情况但是不会错误剔除一些潜在可见的物体。

遮挡及潜在可见集

把可见表中完全被其他物体遮挡的物体移除,称为遮挡剔除。大型环境的总体遮挡剔除可以通过预计算潜在可见集(potentially visible set,PVS)实现。PVS会不准确,但不会错误剔除一些有用的物体。实现方法:把场景切割成某类型的区域,每个区域提供摄像机在该区域内能看见的其他区域列表,这些PVS可由美术人员或游戏设计师手工设置。也可以使用脱机工具。

入口

使用入口渲染时,游戏世界会被划分为半封闭的区域,这些区域以孔洞互相连接,例如门口、窗口等。这些孔洞称为入口,通常以其边界的多边形表示。要渲染一个含入口的场景时,首先渲染包含摄像机的区域。然后对于每个连接着该区域的入口,我们建立对应的、像平截头体的体积。该体积含多个平面,每个平面都是延伸字摄像机焦点及入口的包围多边形的棱。相邻区域的物体就利用该入口体积来进行剔除,方法和平截头体剔除一样。

遮挡体积(反入口)

锥体的体积也可用于描述某物体遮挡的区域,这些区域内的物体不会被看见。这种体积称为遮挡体积或反入口。找到遮挡物的每个边缘轮廓,并把平面自摄像机焦点延伸至这些棱,再测试较远的物体是否被遮挡。

提交图元

产生了可见的几何图元表后,调用DirectX的DrawIndexedPrimitive()或OpenGL的glDrawArrays()。把它们提交到GPU的管道。

渲染状态

可配置参数:

  • 世界观察矩阵
  • 光源方向矢量
  • 纹理绑定(即某材质或着色器用到的纹理)
  • 纹理寻址及过滤模式
  • 基于时间的纹理滚动及其他动画效果
  • 深度测试(启用或禁用)
  • alpha混合选项

GPU管道内所有可配置参数称为硬件状态或渲染状态。应用程序阶段有责任确保提交每个图元时,正确及完整的配置硬件状态。应用程序阶段的工作:遍历可见的网格实例列表,遍历每个子网格材质对,以材质规格设置渲染状态,并调用底层的提交函数。

状态泄露(忘记设置某方面的渲染状态,会影响到下一图元)的结果可能会是物体出现配错纹理或不正确的光照效果。

应用程序阶段实际上使用命令表和GPU进行沟通。这些命令包含交错的渲染状态设置及渲染几何图元的引用。有的时候调用提交函数等API对应用程序成本太高,为了优化性能,有些游戏引擎会手工建立GPU命令表,或调用底层的渲染API。

几何排序

渲染状态设置是全局的,它们对整个GPU都有效,因此,改变渲染状态时,GPU管道必须完成目前工作,才能换上新的设置。那么渲染状态改变次数越少越好,这样可以按材质来排序几何物体。但是这样会增加无意义的覆绘,浪费GPU的时间。

于是可以使用深度预渲染。深度预渲染基本概念是渲染场景两次:第一次尽快产生深度缓冲的内容,第二次才用完整的颜色填进帧缓冲(由于有第一次深度缓冲,这次不会有覆绘)。当关闭像素着色器并仅更新深度缓冲,GPU便会使用特设的双倍速度的渲染模式。此次渲染步骤中,不透明物体可按从前往后的顺序渲染,使深度渲染的写入次数变得最少。然后几何物体按材质重排,用最少的状态改变渲染颜色,是管道吞吐量最大化。但是渲染半透明几何物体的材质排序问题并无通用解决方案,必须从后至前的顺序渲染,才能得到正确的alpha混合结果。

场景图

游戏世界规模通常很大,使用平截头体剔除物体时,大量平截头体外的物体会消耗大量时间。于是希望能设计一些数据结构管理场景中的所有几何物体,并能迅速丢弃大量完全不接近摄像机平截头体的世界部分。这种数据结构通常称为场景图。这种数据结构通常是树,例如:四叉树、八叉树、BSF树、kd树、空间散列等。这样查找哪些区域在平截头体中可见时,如果树根不可见,该子树的所有节点都必然不可见。根据实际的游戏选择适合的方法。

  • 四叉树:以递归的方式把空间分割成象限。每层递归以四叉树的节点表示,每个节点有4个子节点,每个子节点代表一个象限。以树形结构存储这些划分的空间,可渲染的图元存储在树的叶节点。通常我们尽量令每个叶节点由均匀的图元数目。要实现这个目标,可基于区域内的图元数目来决定继续或终止细分区域。
  • 八叉树:四叉树的三维版,每次将空间分割成8个子区域。
  • 包围球树:把空间以层次结构分割成球状区域,包围球树的子节点含有场景中可渲染图元的包围球。我们首先把图元分成小组,计算每组的包围球,然后这些小组再结合称较大的组,重复直到包含整个场景。
  • BSP树:把空间递归分割一半,每个半空间里的物体符合某些预定条件。常用的BSP树分割方法是按照场景中某三角形的平面分割空间,这样所有的三角形都会分成两类,在分割平面之前或之后。任何平面相交的三角形会分成3个三角形。此时,该BSP树可以是三角形按从后往前或从前往后的严格次序排序。
  • kd树:是BSP树的特殊情况,kd树的分割平面会依次序与k维空间的轴对齐。

高级光照及全局光照

基于图像的光照

许多高级的光照以及着色技术都会使用大量影像数据,这些数据通常以二维纹理贴图形式表示。这些技术统称基于图像的光照算法。

法线贴图中,每个纹素代表表面法矢量的方向。利用它可以细致的描述表面的形状。法矢量通常会在纹理的RGB颜色通道中编码。由于RGB颜色通道必须为正数,而法矢量的分量可为负数,所以为法矢量编码时会加上合适的偏置。

高度贴图:用来编码高于或低于三角形表面的理想高度。因为每个纹理只需单个高度值,所以高度贴图通常编码为灰阶影像。高度贴图通常用于视差贴图法和浮雕贴图。它们都能是平面表面显出强烈的高度变化,制造出自格挡和自阴影的效果。

镜面/光泽贴图。前面可以知道镜面强度的数学形式为kS(R·V)α,其中kS为表面整体镜面反射率,α为镜面幂。许多镜面是不均匀光滑的,把非常细致的镜面信息编码至一张贴图中,此贴图称为镜面贴图。若把kS的值存进镜面贴图的纹素,就能控制每个纹素的位置能造成多少镜面反射。

环境贴图中最常见的两种格式是球面环境贴图和立方环境贴图。球面贴图中球心是要渲染物体的位置,它使用求坐标寻址,这样在赤道附近就有充足的分辨率,而两极只有单个纹素。立方贴图是从6个主要方向(上下前后左右)拍摄照片后再拼合而成。立方体中心是要渲染的物体。

三维纹理给定一个三维坐标,GPU可以对它进行寻址和过滤。它非常适合描述物体的体积特性。

高动态范围光照

显示设备智能产生有限的强度范围,所以帧缓冲里的色彩通道限于0~1范围。而真实世界不同。高动态范围(HDR)光照尝试捕捉大范围的光照强度。把影像显示于屏幕之前还需要进行色调映射处理,把影像的强度调整到显示设备所支持的范围。它可以仿造许多现实世界的效果。而HDR影像的表示方法可以使把红绿蓝通道存储为32位浮点数,而不使用8位整数;或者使用完全不同的色彩模型。

全局光照

全局光照是指考虑到光从光源传送到虚拟摄像机之间与多个物体互动的光照算法。它包含许多技巧。

阴影渲染

有两个流行的技巧:阴影体积和阴影贴图。它们都会把场景分成三类:投射阴影的物体、接收阴影的物体、完全被阴影渲染忽略的物体。光源可以标识为产生或不产生阴影,这种优化可以再生成场景中的阴影时,能限制所需处理的光源和物体组合数量。

阴影体积,它会产生阴影的光源位置观察每个投射阴影的物体,在那个视角判断物体的轮廓边缘。这些边缘沿光线方向伸出,产生一个几何立体,该几何立体代表着光线被投射阴影物体遮挡所造成的空间体积。阴影体积使用特殊的全屏缓冲产生阴影,此缓冲称为模板缓冲,它对应屏幕每个像素存储一个整数值。

阴影贴图,它实际上是进行没片段的深度测试,但该“深度”是指从光源视角计算的。使用阴影贴图需要把场景渲染两次。由于阴影贴图仅含深度信息,它的每个纹素记录从光源来说的深度,所以,通常它会使用硬件的双倍速模式,仅填充深度缓冲。

环境遮挡

它是一种用于渲染接触阴影的技术。接触阴影是指场景仅以环境光照明时所产生的软阴影。度量某点的AO(ambient occlusion,AO 环境遮挡)值的方法是,以该点为球心设一个非常大半径的半球体,然后计算从该点可见的半球表面面积百分比。

镜像

焦散,它是指强烈反射或折射所产生的光亮高光,通常出现在非常光滑的表面,当反射表面在移动时,焦散会产生闪烁及在投射表面上“摇曳”。

次表面散射

预计算辐射传输,它可以实时模拟基于辐射度算法的渲染方法。

延迟渲染

它主要的光照计算在屏幕空间进行。首先迅速的渲染不含光照的场景,此阶段,把所有将用于光照计算的信息存储在一个“深厚的”帧缓冲里,此缓冲称为几何缓冲。完成场景渲染后,就是用几何缓冲的信息来计算光照和着色。

视觉效果和覆盖层

 粒子效果

粒子渲染系统是为渲染无固定形状的物体,它和其他渲染的区别:

  • 粒子系统有大量相对简单的几何物体所组成。称为quad,每个quad由两三个三角形组成。
  • 几何物体通常是朝向摄像机的,引擎必须做相应的工作,确保面片的法向量总朝向摄像机的焦点。
  • 其材质几乎都是半透明的,粒子渲染系统渲染有严格次序。
  • 粒子以多种丰富方式表现动画。
  • 粒子通常会不断出生及湮灭。

真实的游戏引擎总会以专门的动画及渲染系统来实现粒子效果。

贴花

覆盖在场景中正常物体上,相对较小的几何物体,用于动态改变物体的表面外观。例如弹孔、脚印等。

环境效果

天空

简单的方式是先把帧缓冲填满天空的纹理,采取渲染三维几何图形。该天空纹理应该有接近1:1的纹理像素比,使纹理逼近屏幕的分辨率。天空纹理可根据游戏摄像机的移动而相应的旋转即卷动。在渲染天空时,我们必须确认把所有像素的深度设置为最大值。云通常也需要专门的渲染及动画系统实现。

地形

高度场地形是大型地形建模的流行之选。应为高度场地形通常存储为灰阶纹理贴图,其数据量相对较少。大多数给予高度场的地形系统中,会用规则的栅格模式来镶嵌水平面,然后以高度场纹理的采样决定地形高度。每个区域单元的三角形数量可以按摄像机距离来调整,是大尺度的地形特征能在远处观看,而同时能表现近距离地形的层次细节。

水体

大型的水体可能需要动态镶嵌或其他类似地形系统的LOD技术。水体系统有时会与游戏的刚体动力学系统互动,有时会与游戏性系统互动。水体效果通常由不同的渲染技术机子系统结合创造。

覆盖层

多数游戏都有平视显示器(HID)、游戏内图形用户界面和菜单系统。这些覆盖层通常是用二维或三维的图形直接渲染在观察空间或屏幕空间中的。覆盖层通常在主场景之后渲染,并关上深度测试,以确保它们会显示在三维场景之上。

归一化屏幕坐标:二维覆盖层的坐标可使用屏幕像素为单位,但是如果游戏要支持多个屏幕分辨率,就需要使用归一化屏幕坐标。归一化坐标中,一个轴的范围是0~1,而另一个根据屏幕的长宽比。例如长宽比是4:3时,另一个是0.0~1.333的范围。

屏幕相对坐标:要完善归一化屏幕坐标,应令它可以使用绝对或相对坐标。

文本和字体:游戏引擎的文本和字体系统通常实现为一种特殊的二维(或三维)的覆盖层。文本渲染系统需要按字符串显示一串文字字形,并以某种方向在屏幕上排列。字体通常以含有字形的纹理贴图实现。在保存一个字体描述文件,包含每个字形在纹理中的包围盒、字体布局信息。如字距调整等。

伽马校正

阴极射线管(CRT)显示屏往往有非线性的亮度响应曲线。即视觉上,较暗的区域显得比理论上还要暗。

一般CRT显示器的伽马响应曲线:Vout = Vinγ   其中γCRT > 1。要校正此情况,颜色传送值CRT显示器前,通常会进行一个逆变换。一般CRT显示器的γCRT值是2.2,校正值通常是γcorr = 1/2.2 = 0.455。

全屏后期处理效果

  • 动态模糊:渲染一个屏幕空间的速度矢量缓冲区,并使用此矢量场选择性的模糊已渲染的影像。产生模糊的方法是把一个卷积核施于影像。
  • 景深模糊:使用深度缓冲区的内容调整每像素的模糊程度
  • 晕影:通过降低屏幕四角的亮度和饱和度,产生类似电影的戏剧性效果。
  • 着色:可用后期处理效果以任意方式修改屏幕上的颜色。