GPU和Shader技术的基础知识(全8回)
http://www.opengpu.org/forum.php?mod=viewthread&tid=7376&extra=page%3D1
http://www.opengpu.org/bbs/forum.php?mod=viewthread&tid=7550&extra=page%3D1
3D图形的概念和渲染管线(Render Pipeline)
前面介绍了3D图形历史,接下来要解说的是3D图形的处理流程。
3D图形管线的流程图
图1是3D图形的流程模型。这个虽然是对应DirectX 10/SM4的GPU流程模型,不过部分流程会根据GPU的不同,有时会有更细致的处理,有时也会做一些简略,这点敬请谅解。
首先,介绍一下3D图形的处理为什么会变成这样的根本原因。会变成这个样子,是由于在漫长又短暂的实时3D图形历史中,这个部分需要进行最顺畅的处理,更重要的是因为这样设计GPU会更容易实现的原因。这个流程不管是在Direct3D还是在OpenGL里都没有太大的差异。
图1 GPU内部的渲染流程
CPU负责的3D图形处理部分 = 游戏引擎?
图1的[1]和[2]的部分,主要是在CPU里进行的处理。
配置3D物体或是移动后再设置,因为这两种很类似,所以把这些在系统中处理的部分全部称作[游戏引擎]。
在游戏引擎中,遵从键盘输入、鼠标输入、游戏控制输入让3D角色移动,或是进行开枪击中敌人的命中时的碰撞检测,根据碰撞的结果,虽然要进行把3D角色们击飞的物理模拟,但这些是游戏逻辑的部分,某种意味上,是和[1][2]相同的部分。
另外,在[2]中如果有对应DirectX 10/SM4.0的GPU,要是能利用Geometry Shader,那么也可以在GPU上进行了,例如关于particle或billboard这样的point sprite,把创建和销毁用Geometry Shader来处理,就可以实现让GPU介入的处理。虽然如此,一般的3D游戏处理中,这部分还是由CPU来处理的部分。
Vertex Pipeline和Vertex Shader的坐标系是什么?
图中红线的[3][4][5][6]的部分,是进行顶点相关处理的顶点管线。
通常从这里开始是在GPU内部进行处理的部分。但是,为了简化内部逻辑以降低成本,所以要让图形机能整合,就是所谓的[统一芯片组],把这个顶点管线转移到CPU的(仿真)系统也是存在的。
那么,直到稍早之前,很多时候把这个顶点管线称为[几何体处理]。所谓几何体(Geometry)就是是[几何学]。在高中,虽然就会在数学或是[代数•几何]的课程中,学到[向量(Vector)运算]和[线性映射(Linear Map)或线性变换(Linear Transformation)],不过这就是那个世界的事了。说句闲话,NVIDIA的GPU,GeForce系列的名字是由「Geometric Force(几何学的力量)」的缩写所造的名词,可以说有着[G-Force重力]的双关语。
把话题转回来,说回3D图形上的[3次元向量]的概念,简单而言想成是[三次元空间上的”方向”]就可以了。这些”方向”被x,y,z的3个轴的坐标值来表示,以这些”方向”为基准的被称之为[坐标系]。
「局部坐标系」,如果具体的描述就是对于某个3D角色来说,设置为基准的坐标系,3D角色的方向就是这个3D角色的基准坐标系,通过处理[朝向是哪里],控制的人会很轻松,所以利用了局部坐标系这个概念。
对了,一般的3D角色上虽然有很多是带有手臂和脚,考虑到那些手脚在关节处弯曲的情况,如果用关节为基准的局部坐标系进行控制会更容易理解,但是这样考虑的话,就要把局部坐标系做成多层的结构,最终的处理会变得不容易理解。
接下来,支配3D全局空间的整体坐标系就很有必要了,这就是[世界坐标系]。在处理3D图形的顶点管线中的顶点单位时,从局部坐标系向世界坐标系的变换会多次发生。
这样生成的顶点要根据shader程序进行坐标系的变换处理,就是[3][顶点着色语言][Vertex Shader]。通过着色器编程,就可以进行独特而特殊的坐标系变换。
图2 坐标系的概念图
Vertex Shader的另一项工作:顶点空间的阴影处理。
图1到3的Vertex Shader的工作不光是坐标变换,另一个重要的工作就是顶点空间的阴影处理和光照处理(lighting)。
「坐标变换」是「数学上的」的感觉,所谓“计算”的印象容易想象并理解。可是,在计算机中做光照,也就是说“光的照射”,这样的印象就很难想象是怎么回事。当然GPU因为不是照相机而是计算机,不可能直接拍出光照射后的效果照片,所以需要通过计算来获得。
光一旦照射到物体上,就会在那里反射/扩展和被吸收。如果这个物体有颜色或图案,那么也许就可以看到这个颜色,如果照射的光有颜色,那么就可以看到和物体颜色或图案合成后的颜色。寻求这样的计算处理,就是计算机图形的基本想法。
怎样才能把这个处理放入计算机所擅长的计算中呢?实际也是使用向量运算。
显示光的方向的[光源向量]和显示视线方向的[视线向量],还有显示构成光照射到的多边形的顶点方向的[法线向量],一共用到了这3个向量。再把这些向量对应到通过光和视线方向来表示反射的反射方程里来计算。
这个反射方程,可以表现出想要的各种类型的材质,这个反射方程在程序上的表现就是Vertex Shader程序。并且,实际执行这个顶点单位反射方程的程序,就是Vertex Shader.
在Vertex Shader中,不仅可以进行顶点空间的阴影处理,还可以进行多边形的纹理(Texture)坐标计算。纹理坐标的计算,就是计算出在怎样的多边形上,把怎样的材质纹理通过什么样的方式贴上去。还有,实际做纹理映射(Texture Mapping)是在[8][9]的Piexl Shader的时候,这里只是纹理映射的准备工作。
Vertex Shader工作的例子, 利用Vertex Shader来做折射表现。
Geometry Shader可以增减顶点的厉害东西
在DirectX 9/SM3.0之前,GPU并不包含Geometry Shader,3D模型的顶点信息是通过CPU这边预先准备好之后,再输入到GPU中,在GPU这里不能随意的进行增减。
为了打碎这个“原则性限制”,拥有可以自由增减顶点功能的着色器就是[Geometry Shader]。
通过Shader程序可以指定Geometry Shader对顶点信息进行增减。还有,因为实际增减的是复数的顶点,所以对各种的线段、多边形、粒子等图元也可以进行增减。
利用Geometry Shader的各种方法被创造出来,因为可以自由的生成多边形,那么就可以在地面上生长出草的多边形,或者让3D角色生长出毛发等是最基本的使用方法。在游戏中,还可以把不需要做逻辑交互处理的例如火花等特效的表现,使用Geometry Shader来生成。
【千里马肝注:Geometry Shader并不如想像中,或者说是宣传中那么好。可能是由于成本或是其他的什么原因,Geometry Shader通常是在display driver中实现的,也就是说其实是由CPU负责计算,当重新返回GPU的VS时,对流水线的影响很大,所以Geometry Shader的实际效能并不高,甚至是非常低。】
大致上就是可以做到这些吧。
用Geometry Shader生成的顶点,因为还可以再返回到Vertex Shader,所以可以对返回的顶点重新进行处理。例如普通方法不能实现的,把低多边形的3D模型,在Geometry Shader里通过插值生成更平滑的高多边形,理论上也是可行。
顶点管线处理的最后
[5][6]是顶点管线最后的处理部分,负责在描绘之前做最后的准备工作。
将世界坐标系中的坐标,进一步在[5]上进行变换到照相机视点坐标系的处理。这个相当于照相机在拍摄照片时构图和镜头的部分,这一连串的处理总的来说是[透视变换处理]。
因为3D图形只需要描画出视野内捕捉到的影像就可以了,一旦[5]的处理结束后,就转移到以视野空间为主体来考虑。
[6]是通过判断,将不需要描绘的多边形,在进入实际描绘处理的像素管线之前,进行剔除的处理(也称为Culling处理)。
[Clipping处理],是把完全在视野范围以外的3D模型的多边形进行剔除,如果3D模型的多边形只有一部分在视野内,那么会对视野范围内的多边形进行分割处理。
【千里马肝注:为了避免昂贵的View Frustum Clipping,一旦出现下图的这种情况,其代价为:
Extra vertices produced,costing more bandwidth
CPU cost for interpolation of x,y,z, u,v,color,specular,alpha and fog
Breaking up of strips and fans
Poor vertex locality of new vertices,which hurts CPU and vertex cache coherency
请参考:Guard Band。】
「背面剔除」,即没有朝向视点方向的顶点,理论上从视点应该把看不到的多边型进行剔除。但如果涉及到透明物体的情况,进行这样的处理有时会出现不协调的问题。
【千里马肝:即Independent Transparency,从视点观察时模型时,可能出现由于顶点传入顺序的原因,背面的顶点在前面的顶点之前就绘制的问题,于是Alpha Blend就不正确了。】
负责分解和发送像素单位工作的光栅器(rasterrizer)
当变换到视野空间,无用多边形都剔除后,在[7]上进行的是,把到没有实际形态的多边形,绘制为对应在画面上的像素的处理。另外,在最新的3D图形上,并不只是绘制显示帧,也有把场景渲染到纹理的情况,这个时候[7]会进行多边形和纹理像素的处理。
在[7]上的处理,实质上是将从顶点管线上把成为顶点单位(多边形单位)输出的计算结果,作为像素单位进行分解,持续的向像素管线发送任务,可以说是起到中介的作用。
这个[7]的处理被称作[triangle setup],或者是[rasterise处理],因为是个规范化的处理,所以从1990年代初期的GPU里就作为固定功能实现在GPU里,到现在也没有太大的进化。
通常,一个多边形因为要用多个像素来绘制,所以多边形通过光栅化被分解成大量的Pixel Task。在GPU上Pixel Shader处理单元的数量如此之多,是由于需要处理的像素是如此繁多的原故。
图5:Rasterise也可以说成是生成piexl shader的任务书,还有,一个多边型会生成多个pixel task.
进行像素单位阴影处理的Pixel Shader,纹理(Texture)不只是图像而已
把由[7]的[rasterise处理]而生成的像素单位进行阴影处理工作的是[8][9]所显示的Pixel Shader部分,还有把包含Render Backend的框架整体统称为[像素管线(Pixel Pipeline)]。
由于GPU的实现形态多种多样,不仅有把[8]中进行像素单位的阴影处理的机能模块称为[Pixel Shader]的情况,也有将后面的纹理单元[Texture Unit]的[9]统称为Pixel Shader的情况。
那么,虽然在[8]的Pixel Shader上进行计算,只不过实际的单位是像素,进行处理的内容本身与Vertex Shader非常相似。
在像素单位上,也要用光源向量、射线向量,还有这个像素的法线向量计算反射方程式。通过计算求出这个像素的颜色就是[Pixel Shader的光照]。
比起用顶点单位的照明结果插值出来的光照效果,像素单位的结果会有更平滑的阴影和漂亮的高光,这种处理被特别得称作[Per Pixel Lighting]。
通过在Vertex Shader上得到的纹理坐标,负责从原来的纹理(Texture)读出纹素(Texel)的就是[9]纹理单元(Texture Unit)。
通过纹理单元取出的像素颜色,和之前纹理单位的阴影处理算出的像素颜色一同处理,就得到了最终的像素颜色。
通过这样得到像素单位照明的方法就是Pixel Shader用的Shader程序。
话说通常的[纹理],虽然可以联想为在多边形上贴图像,但现在的可编程shader时代,把这种应用做了扩展,在纹理上除了可以存放普通图像外,还可以存放数学(或物理)意义的各种数据的利用兴起了。用Pixel Shader在像素单位上进行阴影处理时,从这样的数值纹理上读出数据的方法被利用在计算中。
因为和PC画面的像素ARGB各8bit组成一样,所以纹理也是由ARGB 4个颜色要素组成的。比如是32位色的纹理就分配为A(透明度)8Bit,R(红)8Bit,G(绿)8Bit,B(蓝)8Bit。当需要把数值存放入纹理的时候,ARGB意味着可以存放最大4个元素的向量或矩阵,如果是3维向量的数据,只需将XYZ的3个元素的数值放入到ARGB的RGB中。
在实际的Pixel Shader处理中,通过在纹理中存放向量数据,让视线向量,光源向量,法线向量组合起来解特殊的反射方程式,这种方式可以实现特殊的材质表现。
图6中示范了把法线向量放入到纹理的[normal map]以实现凹凸贴图(bump map)的例子。这个因为在后面的连载会详细解说,在这里只用了解[Pixel Shader的工作是这样的感觉]就行了。
图6: Pixel Shader的工作图例,完成凹凸贴图流程的概念图。从heightmap到normal map变换在Pixel Shader进行。法线贴图存放了法线向量,每1个texel对应1个XYZ表示的三维法线向量值(也有存放XY,计算求出Z的方法)。
渲染的后端 Render BackEnd
Pixel Shader的输出,简单的说就是[场景中构成多边形的像素,会在这里确定最终的颜色],虽然可以想象就是按原样向显存里写入[Pixel的绘制处理完成了],但还有一些工作要做。
那个就是[10]的Render BackEnd,另外NVIDIA称呼这个部分为ROP Unit。ROP Unit到底是Rendering Output Pipeline还是Raster Operation的简称并不是太清楚,在本连载中将前者作为正确的解释。
尽管如此,这里是做Pixel Shader输出时[是不是可以写入的验证],以及写入时[决定如何写入]等,这些是向显存写入的控制部分。Pixel Shader自己可以从纹理中读取,但不能直接向显存写入,所
以这个处理也成为极为重要的部分。
可是,因为DirectX 9/SM2.0以前的GPU上Pixel Shader数量和ROP Unit数量通常是一致的,于是有着Pixel Shader与ROP Unit是一一对应关系的印象。但是从DirectX 9/SM3.0的GPU开始,伴随着Pixel
Shader程序的高度复杂化,Pixel Shader的数量被作为重点进行增加,结果,ROP Unit的数量一般都比Pixel Shader的数量要少。
最新的GPU一般都是Pixel Shader > ROP Unit的结构。上图是GeForce 7800 GTX的结构图。中间是4X6 = 24个Pixel Shader,下面是16个ROP Unit。
[是不是可以写入的验证]的部分为[Alpha Test]、[Stencil Test]和[Depth Test]。
Alpha Test是对输出的Pixel颜色是否完全透明的测试。α元素为0时为透明不需要绘制,意味着将放弃对应Pixel的绘制。
Alpha Test只绘制不透明部分的Pixel。
Stencil Test能够以多种方式进行检测,以Stencil Buffer的内容为参照来计算Frame Buffer,对于设定好的条件如果不能通过,就会把那个Pixel放弃绘制。应用之一,作为Stencil Shadow Volume
的影子生成技术。
Stencil Test参考Stencil Buffer的内容,满足预设的测试条件就进行描绘,图中是[Stencil Buffer 的内容只在A的部分进行绘制]的案例.
深度测试是检查所有要绘制的可见像素中,哪些是从视点来看最近的像素。和绘制像素一一对应的用来存放深度值的称作Z Buffer,从这里读出的深度值和要绘制的像素的深度值做比较,就是[深度测
试的实际状态],深度值由Pixel Shader计算获得。
另外,在绘制半透明3D对象的像素时,也有不进行这个深度测试的情况。
深度测试,空间位置前后顺序不同的物体在描绘时,深度测试变得尤其重要。如果仅仅根据物体绘制的顺序来决定画面效果,则会出现物体的前后次序混乱的情况。
对于Z Buffer中深度值从未写入过的的地方要无条件的写入,而对于已经写入过深度值的位置要进行深度测试,通过检测所需描绘的像素是不是在最前面,再决定是否进行绘制。
[是否可以写入的验证]的变化还有[Alpha Blending]和[Fog]。
α混合,并不能将像素直接进行绘制,而需要和当前位置已经写入的像素做半透明计算后再写回的处理。从渲染对象的Frame Buffer中读出像素颜色,因为需要读取显存的处理,所以进行Alpha
Blending相比普通的像素处理是需要更多的负荷。那些3D测试软件都是以连续绘制重叠的半透明来作为性能评价的。
Alpha Blending因为要将已经渲染的结果读出后,做进一步合成计算再绘制出来,所以绘制的负荷很高。
Fog(雾),是根据绘制像素的深度值,调整预先设定的雾的混合颜色来进行处理。越远的地方的像素颜色越接近白色,用作表现场景深处可以看到朦胧感的空气。
Fog,深度值越远就有越模糊的像素值的空气远近表现法。
当然,不处理Alpha Blending和Fog的时候,就可以直接把像素颜色写入显存。并且在写入的时候,为了下一次像素的深度测试作准备,同时进行深度值的更新。
另外,画面中的像素是以格子型排列的,为了减少画面的锯齿感,要在渲染的BackEn