渲染流水线
渲染流水线的工作任务是:由一个三维场景触发 生成渲染一张二维图像,这个过程是由GPU与CPU共同完成的。一个渲染流程 可以分成三个部分:应用阶段 几何阶段以及光栅化阶段。这三个只是概念性阶段,每个阶段通常本身也是一个流水线系统,包含了子流水线阶段。
概念流水线阶段
应用阶段:由CPU负责实现,开发者具有这个阶段的绝对控制权。这一阶段的任务:首先需要准备好场景相关数据;其次为了提高渲染性能,一般需要做一个粗粒度的Culling,把那些不可见的物体剔除出去。最后需要设置每个模型的渲染状态,这些状态包括使用的材质纹理以及着色器。这一阶段最重要的是输出是渲染所需要的几何信息,即渲染图元。这些图元会被传递到下一个阶段几何阶段。
几何阶段:处理所有和绘制的几何相关事情。决定需要绘制的图元是什么,怎么样绘制他们,在哪里绘制,这一阶段通常在GPU进行。几何阶段负责和每个图元打交道,进行逐顶点逐多边形的操作。这一阶段可以进一步分为更小的流水线阶段。几何阶段的一个重要任务是把顶点坐标变换到屏幕空间中,再交给光栅器处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标,每个顶点对应的深度值 着色等信息,并传递给下一个阶段。
光栅化阶段: 使用上一阶段传递来的数据产生屏幕的像素,并渲染出最终的图像,这一阶段也是在GPU上进行的。这个阶段主要是决定每个渲染图元中哪些像素应该被绘制
在屏幕上。他需要对上一个阶段得到的逐顶点数据,纹理坐标 顶点颜色等进行插值,然后进行逐像素处理。 光栅化阶段也可以分为多个小的流水线。
CPU和GPU之间的通信
渲染流水线的起点是CPU,即应用阶段,主要有三个步骤
1.数据加载到显存
渲染数据需要从硬盘加载到系统内存,然后网格和纹理等数据又被加载到显卡上的存储空间,显卡对于显存的访问速度更快
2.设置渲染状态
渲染状态定义了场景中的网格是怎么样被渲染的。例如使用哪个顶点着色器/片元着色器,光源属性 材质等。如果没有更改渲染状态,那么所有网格将使用同一种渲染状态
3.调用Draw Call
准备好上面所做工作,CPU就需要调用一个渲染命令告诉GPU,数据准备好可以按照设置开始渲染,这个命令就是Draw Call。当给定一个Draw Call 时候,GPU就会根据渲染状态和所有的输入顶点数据来进行计算,最终输出屏幕上需要显示的像素。这一个计算过程,就是GPU流水线。
GPU流水线
概念阶段的后两个阶段,开发者没有绝对的控制权,实现载体是GPU。GPU通过实现流水线,大大的加快了渲染速度。虽然无法完全控制这两个阶段的细节,但是GPU向开发者开放了很多
控制权。几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些刘书香阶段由GPU实现,每个阶段GPU提供了不同的可配置性和可编程性。如下图
图中可以看出 GPU的渲染流水线接收顶点数据作为输出,顶点是在应用阶段加载到乡村中,再由Draw Call指定,随后被传递给顶点着色器。
顶点着色器:完全可编程的,通常用于实现顶点空间变换,顶点着色。
曲面细分着色器:可选着色器,用于细分图元。
几何着色器:可选,被用于执行逐图元的着色操作。
裁剪:将不在摄像机视野内的顶点截掉,并剔除某些三角图元的面片,可配置
屏幕映射:几何阶段的最后一个流水线阶段,不可配置不可编程,负责将每个图元的坐标转换到屏幕坐标
光栅化概念阶段中三角形设置和三角形遍历阶段都是固定函数的阶段。片元着色器,则是完全可编程,用以实现逐片元的着色操作。最后,逐片元操作阶段负责执行很多重要的操作
比如修改颜色,深度缓冲,进行混合等,不可编程但具有很高可配置。
顶点着色器:流水线第一个阶段,输入来自CPU。顶点着色器的处理单位是顶点。输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建和销毁任何顶点,
而且也无法得到顶点之间的关系。正是因为这样的独立性,GPU可以利用本身的特性并行化处理没一个顶点,这意味着每一个阶段的处理速度会很快。主要工作是:坐标转换和逐顶点光照,还可以输出后续阶段所需要的数据。
坐标转换,就是对顶点的位置进行变换。顶点着色器可以在这一步中改变顶点位置,这在顶点动画很有用,可以通过改变顶点位置模拟水面 布料等。顶点着色器必须完成一个
工作就是把顶点坐标从模型空间转换到其次剪裁空间
o.pos = mul(UNITY_MVP,v.position);
类似上句代码,就是把坐标转换到其次剪裁空间坐标系下,接着硬件做透视除法,最终得到归一化设备坐标NDC,下图是转换过程:
上图给出的坐标范围是Opengl同时也是Unity使用的NDC,z分量在[-1,1]而DX中NDC中z分量范围是[0,1]。
裁剪
目的:不在摄像机视野范围内的物体不需要被处理
图元和摄像机视野关系有三种:完全在视野内图元就继续传递给下一个流水线阶段,完全在是野外的图元不会继续向下传递,部分在视野内的图元需要进行一个处理就是裁剪。
视野外部的顶点应该是用一个新的顶点来代替,这个新的顶点位于物体与视野边界的交点处。由于我们已经知道在NDC下的顶点位置,因此裁剪就很简单,只需要把图元裁剪到单位立方体内,如下图
和顶点着色器不同,这不不可编程,可以自定义裁剪操作进行配置
屏幕映射
这一步输入的坐标还是三维坐标系下的坐标,主要任务是把每个图元的x y坐标转换到屏幕坐标系下。屏幕坐标系是一个二维坐标系,和我们用于显示画面的分辨率有很大关系。把场景渲染到一个窗口上,窗口的范围是从小的窗口坐标(x1,y1)到最大的窗口坐标(x2,y2),其中x1<x2 y1<y2.由于我们输入的坐标范围是-1-1因此这个过程实际是一个缩放的过程输入的z坐标不会做任何处理。屏幕坐标系和z坐标一起构成了窗口坐标系。这些值会被传递到光栅化阶段
屏幕映射得到的屏幕坐标决定了顶点对应屏幕上哪个像素以及距离这个像素多远
屏幕坐标系在Opengl和DirectX之间有一些差异:Opengl把屏幕左下角当做小的窗口坐标,而DirectX则是左上角
三角形设置
光栅化的第一个流水线阶段就是三角形设置。上一阶段输出信息是屏幕坐标系下的顶点位置以及他们的额外信息,如深度值,法线方向,视角方向。光栅化阶段两个目标:计算每个图元覆盖了哪些像素,以及为这些像素计算颜色。该阶段会计算光栅化一个三角网格所需要的信息,具体来说,上一个阶段输出的是三角网格的顶点,如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每个边的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样计算一个计算三角网格表示数据的过程就是三角形设置
输出是为了下一个阶段做准备
三角形遍历
该阶段会检查每个像素是bf瓯北一个三角网格覆盖,如果被覆盖,生成一个片元。
三角形遍历阶段会根据上一个极端的计算结果来判断一个三角网格覆盖了哪些元素,并使用三角网格的3个顶点的顶点信息对整个覆盖区域的像素进行插值 如下图
这一步的输出的到一个片元序列,需要注意的是,一个片元并不是真正意义的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了他的屏幕坐标,深度信息,以及其他从几何阶段输出的顶点信息,例如法线 纹理信息等。
片元着色器
非常重要的可编程着色器阶段。前面的光栅化阶段十几不会影响屏幕上每个像素的颜色值,而是产生一系列的数据信息,用来表述一个三角网格是怎么样覆盖每个像素的。而每个片元就负责存储这样一系列的数据。真正对像素产生影响的阶段是下一个流水线阶段逐片元操作。片元着色器的输入是上一个阶段对顶点信息插值得到的结果,是根据从顶点着色器中输出的数据插值得到的,他的输出是一个或者多个颜色值 如下图
这个阶段可以完成很多重要的渲染技术,最重要的技术之一就是纹理采样,为了进行纹理采样,通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到覆盖片元的纹理坐标。
逐片元操作
渲染流水线最后一步,逐片元是Opengl的说法,在DX中这一阶段称谓输出合并阶段。这一阶段的主要任务:决定每个片元的可见性,涉及到深度测试模板测试。如果一个片元通过
了所有的测试,需要把这偏远的颜色和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
逐片元高度可配置,可以设置每一步操作细节。下图是逐片元操作的操作流程
下图深度测试模板测试的简化流程图
模板测试:与之相关的是模板缓冲,如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲中该片元位置的模板值,然后将这个值跟读取到的参考值作比较,这个比较函数由开发者指定。如果没有通过测试,被舍弃,不管一个片元有没有通过模板测试,都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个操作是由开发者指定的。开发者可以设置不同结果下的修改操作,例如失败时候模板缓冲区保持不变,通过时将模板缓冲区中对应的位置的值加1.模板测试通常用于限制渲染的区域。另外模板测试还有一些更高级的用法,如渲染阴影,轮廓渲染。
深度测试:一个片元通过了模板测试之后,进入深度测试,高度可配置。如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个开发者设置的,小于时舍弃该片元,或者大于等于时候舍弃该片元。通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前的深度缓冲区中的值,那么就会舍弃。这是因为我们想显示离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕。如果一个偏远没有通过这个测试,该片元就会被丢弃。和模板测试不同的是,如果一个片元没有通过深度测试,就没有权利更改深度缓冲区中的值。而如果通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原来的深度值,这是通过开启/关闭深度
写入来做到的。
通过上面所有测试之后 来到合并
渲染过程是一个物体接一个物体画到屏幕上的,而每个像素的颜色信息被存储在一个名为颜色缓冲的地方,因此,当我们执行这次渲染时,颜色缓冲中往往有上次渲染之后的颜色,那么我们是使用这次渲染得到的颜色完全覆盖之前的结构,还是进行其他处理,这就是合并需要解决的。对于不透明物体,开发者可以关闭混合操作。这样片元着色器计算得到的颜色值就会直接覆盖颜色缓冲区中的像素值。但是对于半透明物体,我们就需要混合操作来让这个物体看起来透明
下面是简化的混合流程
混合操作是高度可配置的,开发者可以选择开启或者关闭混合功能。如果没有开启混合功能,就会直接使用片元颜色覆盖掉颜色缓冲区中的颜色。如果开启了混合,GPU会取出源颜色
和目标颜色,奖两种颜色混合。源颜色指的是片元着色器得到的颜色值,而目标颜色是已经存在与颜色缓冲区中的颜色值。之后使用一个混合函数来进行混合操作。
当模型的图元经过了上面计算和测试后,就会显示到我们的屏幕。我们的屏幕显示的就是颜色缓冲区中的颜色值,但是为了避免我们看到那些正在光栅化的图元。GPU会使用双缓冲
技术。对场景的渲染是在幕后发生的,即在后置缓冲。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲和前置缓冲中的内容,而前置缓冲区就是之前显示在屏幕上的图像,保证了
图像的连续性。