概述
在渲染管线中,最慢的阶段决定整个渲染的速度。
我们一般使用吞吐量(throughput)来描述一个阶段的处理速度,而不是帧率。
因为帧率会受到设备更新的限制而导致实际速度比帧率所标示的更慢。
一个例子:
假设一个设备为60赫兹,这意味着这个设备16.666666ms刷新一次,这时恰好有一管线阶段花费了62.5ms执行完成,由于63大于16.666666*3,小于16.666666*4,所以他实际上想要最终完成工作必须等待下一次设备刷新。所以真实的执行时间折算下来其实不止62.5ms而是趋近于66ms。当然,如果关闭了垂直同步就另当别论了。
一个渲染管线可以按照执行顺序分成四大阶段:
应用阶段
几何阶段
光栅阶段
像素处理阶段
这些阶段又可以细分为更多的子阶段,注意这些都是功能性的分配,在实现上为了效率等因素往往会合并一些阶段或者拆分一些阶段等等。
下面是一张图:
一、应用阶段 Application stage
开发者对application stage发生的事有绝对的控制权,因为这个阶段是在CPU上执行的。例如,一个application stage的算法或设置可以减少被渲染的三角形的数量。
但是一些application work也可以通过GPU完成,这要用到计算着色器(compute shader)。compute shader把GPU当成一个高度并行的通用处理器(highly parallel general processor),而忽略了它渲染图形的专门功能。
在application stage的末尾,要被渲染的geometry被提供给geometry processing stage。这些是rendering primitives,例如,points, lines,和triangles,这些最终可能会出现在屏幕上(或者别的任何输出设备)。这是application stage最重要的任务
二、几何处理阶段 Geometry processing stage
Geometry processing stage负责 (GPU上)大部分per-triangle(逐三角形的)、per-vertex(逐顶点的)操作。每个阶段又可以进一步分成以下几个功能性阶段(functional stages): vertex shading, projection, clipping, and screen mapping。如下图:
Vertex Shading
Vertex Shading有两个主要的任务,即:
1.计算顶点(vertex)的位置(postition)
2.计算任何程序员可能想要的顶点输出数据(vertex output data),例如法线、纹理坐标。
传统上,大部分物体着色(shade of an object)是通过对每个顶点位置和法线应用光照并把产生的颜色存储在顶点(vertex)中来计算的。这些颜色将会在每一个三角形内部插值。因为这个原因,这个可编程的顶点处理单元被命名为vertex shader。随着现代GPU的出现,随着部分或全部的逐像素着色(per-pixel shading)的出现,vertex shading stage功能变得更加泛化,可能根本不进行任何着色(或者说不计算任何着色方程,shading equation),这取决于程序员的意图。现在vertex shader是一个更通用的单元,用于设置和每个顶点相关联的数据。
在显示到屏幕的过程中,一个模型(model)被变换到好几个不同的空间(spaces)或者坐标系统(coordinate systems)。最初一个模型存在于它自己的模型空间(model space),每一个模型都有变化属性(model transform),这样它就可以被定位或定向。也有的是单个模型(single model)与多个model transforms相关联。也允许在同一场景中,相同模型(same model)的多个实例有不同的位置、朝向或尺寸,而且不需要复制的基本几何体(basic geometry)。
受model transform变换影响的是模型(model)的顶点(vertices)和法线(normals)。模型内的坐标叫作模型坐标(model coordinates),模型加载到场景后,模型就处于世界坐标(world coordinates)或世界空间(world space)。world space是唯一的,所有模型经过空间转换后,就都处于同一个空间/坐标系了。
在渲染时,只有被camera(或observer)看到的模型(models)才会被渲染。Camera在世界空间有一个位置(location)和方向(direction),用于放置和对准相机。为了方便投影(projection)和裁剪(clipping),相机和所有模型(models)会经历view transform。View transform的目的是把camera放置在原点(origin)上,并使得camera看向负z轴(negative z-axis),y 轴朝上(pointing upward),x 轴朝右。本书使用z轴朝内的约定(右手坐标系)。模型经view transform变换后就处于camera space了,或者叫view space,eye space。下图是一个view transform影响camera和models的例子。
为了产生一个具有真实感的场景,仅仅渲染物体的形状和位置是不够的,也要渲染他们的“外观”(appearance)。这个描述包含每个物体的材质和照射到物体的光源的效果。材质和光源多种方式建模,从简单的颜色到复杂的物理描述。
决定灯光在材质上的效果的操作被称为着色(shading)。它涉及到在物体各个点上计算着色方程shading equation。通常,这些计算的一部分是在模型顶点(vertices)的几何处理阶段完成的,而另一些是在per-pixel处理时完成的。各种各样的材质数据可以被存储在每个顶点(vertex)上,如:点的位置,法线 ,颜色或者任何别的用于计算着色方程的数值信息。Vertex shading的结果(可能是colors, vectors, texture coordinates,以及别的数据)被送到rasterization和pixel processing阶段,被插值,被用于计算表面着色(shading of the surface)。
作为vertex shading的一部分,渲染系统完成projection(投影)和随后的clipping(裁剪),这会把视景体(view volume)转换成一个单位立方体(unit cube),它的顶点坐标范围为(-1, -1, -1)和(1, 1, 1)。这个单位立方体被称为标准视景体(canonical view volume)。首先完成投影(Projection),在GPU上是通过vertex shader完成的。有两种常见的投影方法,即正交投影(orthographic projection),也称作平行投影(parallel projection),和透视投影(perspective projection)。另外几种投影方式,如:oblique和axonometric投影。
正交投影的视景体(view volume)通常是一个的长方体,而正交投影会把这个长文体变换成一个单位立方体(unit cube)。正交投影的主要特点是变换后平行线仍然保持平行。这个变换是平移和绽放的组合。
透视投影更复杂点。在透视投影中,物体离像机越远,物体在投影后看起来越小。另外,平行线可能会相交。投视变换模仿我们感知物体尺寸的方式。在几何学上,透视投影的视景体(view volume)被称为视锥体(frustum),即一个截头锥体。视锥体也会被变换成一个单位立方体(unit cube)。正交变换和透视变换都可以用4×44×4矩阵来构造,经过两者中任何一种变换后,都说模型处于裁剪空间或裁剪坐标(clip coordinates)。这些实际上是齐次坐标(homogeneous coordinates),出现在被w除之前。为了使下一个功能阶段,clipping(裁剪),正确工作,GPU的vertex shader必须总是输出这种类型的坐标。
尽管这些矩阵把一个volume(视景体)变换成另一个(unit cube),他们被称投影(projections),因为显示后(after display),zz坐标不再存储在生成的图像中,而是存储在z-buffer中。以这种方式,模型被从三维空间投影到了二维空间。
Optional Vertex Processing
上面讲的vertex processing是每个渲染管线都有的。一旦这个过程完成后,还有一些在GPU上发生的可选的阶段(stages),按此顺序:tessellation,geometry shading和流输出(stream output)。是否使用由硬件的能力(并不是所有的GPUs都支持)和程序员的意愿决定。它们之间相互独立,通常用不到它们。
第一个可选可选的阶段是曲面细分/表面细分(tessellation)。设想你有一个弹球,如果你用一个单独的三角形集合(a single set of triangles)来表示它,你可能会遇到质量或性能问题。你的球可能在5米远处看起来挺好的,但是一旦靠近,就会看到一个个三角形。如果你使用更多的三角形来提高球的渲染质量,当球离相机远只覆盖屏幕上一点点像素的时候,你可能会浪费相当大的处理时间和内存。通过曲面细分(tessellation)可以使用适量个数的三角形来生成一个弯曲表面(curved surface)。
下一个可选阶段是Geometry shader。Geometry shader在时间上先于表面细分(tessellation shader),而且相比之下,在GPUs上更常见。它和tessellation shader相似,因为它接收各种类型的基本体(primitives,比如点、线、面),然后可以产生新的顶点(vertices)。想像一下模拟烟花爆炸。每个火球可以用一个点(a single vertex)来表示。Geometry shader可以接收每一个点,把它转变成一个面向观察者、覆盖一些像素的正方形(由两个三角形组成),提供了一个能令人更加信服的用于着色的基本体。
最后可选一个阶段被叫做stream output。这个阶段允许把GPU处理一个几何引擎(geometry engine)。这个阶段不是把处理过的顶点数据沿着渲染管线继续向下传送渲染到屏幕上,而是可以选择(optionally)把这些数据输出到一个数组中用于进一步处理。这些数据可以被CPU或GPU使用。这个stage通常被用于粒子模拟,如烟花的例子。
这三个阶段按这样的顺序执行: tessellation, geometry shading, stream output,而且每一个都是可选的。每哪一个被使用了,如果我们沿着渲染管线走,我们得到的齐次坐标下的顶点集,这将会被用于检测相机是否可以看到它们。
Clipping
Primitives(点、线、三角形,这里裁剪是对这些基本体进行的)只有在完全或部分出现在视景体(view volume)内部时,才能被传递到光栅化阶段(rasterization stage)以及随后的像素处理阶段(pixel processing stage),才会被绘制到屏幕上。一个完全落在视景体内的primitive会按它原有的样子传递到下一个阶段。完全处于视景体外的Primitives不会被进一步传递到下一个阶段,因为它们不会被渲染。只有那些部分落在视景体内的物体才会被裁剪,这样会使得视景体外的顶点被新的primitive与视景体相交的点替代。由于使用了投影矩阵,这意味着这些经投影变换后的primitives是相对于一个单位立方体进行裁剪的。在裁剪(clipping)之前,先进行view transformation (从世界空间变换到view space)和投影(projection)的优势是使得裁剪问题具有一致性,所有primitives总是在一个单位立方体内进行的。Clipping过程如下图所示。
裁剪这一步(clipping step)使用由投影产生的有四个分量(4-value)的齐次坐标来完成。透视空间中(perspective space)中,坐标不值不是在三角形中进行普通的线性插值。为了在使用透视投影时,数据能够被合理地插值和裁剪,才需要用到第四个坐标值。最后一步是透视除法(perspective division),这会把三角形放到一个三维的标准化设备坐标(normalized device coordinates)中。像之前提到的那样,这个视景体的坐标值变化范围是(−1,−1,−1)(−1,−1,−1)到(1,1,1)(1,1,1)。在几何阶段(geometry stage)的最后一步是把标准化设备坐标(空间)标转化到窗口坐标(window coordinates)。
Screen Mapping
只有经裁剪后仍然在视景体内部的primitives才会被传递到屏幕映射阶段(screen mapping stage),且刚进入这一段时的坐标仍然是三维的。每个primitive的x,y坐标通过变换成为屏幕坐标(screen coordinates)。屏幕坐标和z坐标一起被称为窗口坐标(window coordinates)。
三、光栅阶段 Rasterization stage
给定变换和投影后的带有着色数据(shading data)的顶点(vertices)(都来自几何处理阶段),下一个阶段的目标是找到所有在基本体(primitive,如:点、线、三角形)内部的像素(pixels)。我们称这个过程为光栅化(rasterization),它被分为两个功能子阶段:三角形设置(triangle setup,也称为图元装配,primitive assembly)和三角形遍历(triangle traversal),如下图所示。这些过程即适用于点(points)、线(lines),也适用于三角形(trangles),但三角形是最常见的,所以了阶段用“三角形”来称这些基本体。
光栅化,也叫作扫描转换(scan conversion),是从屏幕空间的具有z值(即深度值,depth value)和其它着色信息的二维顶点(vertices)到屏幕像素的转换。也可以认为光栅化是几何处理和像素处理的一个同步点(synchronization point),因为由三个顶点(vertices,除了位置坐标,还包含别的信息,纹理坐标,法线)构成的三角形是在这里被发送到像素处理阶段的。
三角形是否覆盖某个像素,由你怎么设置GPU的渲染管线决定。例如,你可能使用点采样来决定是否在三角形内部("insideness")。最简单的情形是使用在每个像素中心的单点样本,这样如果这个中心点在三角形内,则认为对应的像素也在三角形内。对于一个像素,你可能使用多于一个采样点(使用supersampline或multisampling反锯齿技术)。另外一种方法是使用保守的光栅化,只要一个像素至少一部分与三角形重叠,就认为此像素在三角形内("inside")。
Triangle Setup 三角形设置
在这个阶段differentials, edge equations和此三角形的别的数据被计算了。这些数据可能用于接下来的三角形遍历(triangle traversal),和几何阶段产生的各种着色数据(shading data)的插值。使用固定功能的硬件来完成这个任务。
Triangle Traversal 三角形遍历
这里是检测每个像素的中心点(或采样点)是否被三角形覆盖了的地方,并且为被三角形覆盖的部分产生一个fragment。之后,在第五章会介绍更多的精细复杂的采样方法。查找哪些采样点或像素在三角形内部的过程通常称为三角形遍历(triangle traversal)。三角形的每一个fragment的属性是通过在三角形三个顶点之间进行数据插值产生的。这些属性包括fragment的深度和所有从几何阶段得到的着色数据。这里也是透视校正插值(perspective-correct interpolation)完成的地主。所有在一个primitive(点、线、三角形)内部的像素或采样点被送到像素处理阶段(pixel processing stage,见下方)。
四、像素处理阶段Pixel Processing
像素处理阶段是一个最耗时,但是也是能使你的渲染效果品质更高的地方,像素最终的样子在这一阶段决定,可以进行纹理映射、逐像素光照、像素附加处理等,做纹理混合、模糊、扩散等效果。这也是可编程管线中使用shader控制的另一个控制过程。
Pixel Shading (像素着色)
所有逐像素的着色计算都是在这里完成的,使插值了的着色数据作为输入。最终结果是一个或多个将被传入到下一个阶段的颜色。Pixel Shading用可编程的GPU cores完成。为此,程序员需要为pixel shader(OpenGL中称为fragment shader)提供一个程序(里面可以包含任意想要的计算)。在这儿,各种各样的技术可以被使用,其中最重要的一个是texturing。简单地讲,texturing一个物体意味着把一个或多个图像(images)“融合”("gluing")一物体上。用到的图像可以是一维的、二维的,甚至三维的,其中二维的最常见。最终的产出是每个fragment的像素值,这些数据会被传递到下一个子阶段。
Merging 合并/融合
alpha测试、深度测试、全局混合等。当像素经过像素处理阶段后,并不能都有机会输出到屏幕上,因为他们还要经过深度(也有一些比较优化的渲染管线将深度测试提到像素处理之前)和模板测试、alpha测试,经过这些测试后,还要进行一次alpha混合,这次与目标缓冲区的混合,能够实现半透明的效果。虚拟世界中的五光十色就是因为这个半透明效果而生动。