1.1综述
渲染流水线的最终目的在于生成或者说是渲染一张二维纹理,即我们在电脑屏幕上看到的所有效果。它的输入是一个虚拟摄像机,一些光源,一些shader(着色器)以及纹理。
什么是渲染流水线?
渲染流水线的工作任务在于有一个三维场景出发,生产(或者是渲染)一张二维图像。换句话说,计算机需要从一系列的顶点数据,纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像,这个工作是由CPU和GPU共同完成的。
3个阶段:应用阶段,几何阶段,光栅化阶段
应用阶段:开发者有绝对控制权,由CPU负责。
主要任务:输出渲染图元(即渲染所需要的几何信息)
1.准备好场景数据。
2.剔除不可见的物体。
3.设置每个物体的渲染状态。
几何阶段:通常在GPU上进行。
负责和每个渲染图元打交道,进行逐顶点逐多边形的操作。重要任务是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。输出屏幕空间的二维顶点坐标,每个顶点对应的深度值,着色等。
光栅化阶段:GPU上进行。
使用几何阶段传递来的数据来产生屏幕上的像素,渲染出最终图像。光栅化的任务主要是决定每个渲染图元中哪些像素应该被绘制在屏幕上。需要对几何阶段得出的逐顶点数据(纹理坐标,顶点颜色)进行插值,然后做逐像素处理。
1.2 CPU与GPU之间的通信
渲染流水线的起点是CPU,即应用阶段,大致可分为以下三个阶段:
1)把数据加载到显存中。
2)设置渲染状态。
3)调用Draw Call。
把数据加载到显存中
所有数据从硬盘加载到系统内存》》网格,纹理数据等加载到显存(顶点位置信息,法线方向,顶点颜色,纹理坐标等)》》数据加载到显存中,RAM中的数据就可以移除了(除了一些数据,例如希望CPU可以访问网格数据来检测碰撞)》》设置渲染状态,指导GPU如何进行渲染工作。
设置渲染状态
渲染状态:定义场景中的网格怎样被渲染。例如,顶点着色器/片元着色器,光源属性,材质等。
PS:如果我们没有更改渲染状态,那么所有的网格都将使用同一种渲染状态。
调用Draw Call
之前的工作都准备好之后,CPU需要调用一个渲染命令来告诉GPU可以开始渲染了,这个命令就是Draw Call !
发起方是CPU,接收方是GPU,会指向一个需要被渲染的图元列表,不包括任何材质信息.
1.3 GPU流水线
当收到一个Draw Call时,GPU就会根据渲染状态(材质,纹理,着色器)和所有的顶点数据进行计算,最终输出屏幕上显示的那些漂亮的像素,这个计算过程就是GPU流水线.
几何阶段
顶点着色器(Vertex Shader):
完全可编程,用于实现顶点的空间变换,顶点着色.
曲面细分着色器(Tessellation Shader):
可选的着色器,用于细分图元.
几何着色器(Geometry Shader):
可选的着色器,可以被用于执行逐图元(Per-Prmitive),或者被用于产生更多图元.
剪裁(Clipping):
可配置的,目的是将那些不在摄像机视野内的顶点剪裁掉,并剔除某些三角图元的面片.
(我们可以自定义剪裁平面,也可以通过指令控制剪裁三角图元的正面还是背面)
屏幕映射(Screen Mapping):
不可配置与编程,把每个图元坐标转换到屏幕坐标系中.
光栅化阶段:
三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)阶段也都是固定函数的阶段.
片元着色器(Fragment Shader):
完全可编程,实现逐片元(Per-Fragment)的着色操作.
逐片元操作阶段(Per-Fragment Operations):
不可编程,具有很高的配置性.负责很多重要的操作,例如修改颜色,深度缓冲,进行混合等.
几何阶段
顶点着色器(Vertex Shader)
流水线第一阶段,输入来自CPU,处理单位是顶点,输入进来的每个顶点都会调用一次顶点着色器.顶点着色器本身不可以创建或者销毁任何顶点,而且无法取得顶点与顶点之间的关系.(无法得知两个顶点是否属于同一个三角网格)
需要完成的任务:坐标变换和逐顶点光照,以及输出后续阶段所需的数据.
坐标变换:对顶点的坐标进行某种变换.例如,我们可以通过改变顶点位置来模拟水面,布料等
无论我们在顶点着色器中怎样改变顶点的位置,一个最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模拟空间转换到齐次裁剪空间.
齐次裁剪空间:Unity3D引擎之渲染技术(三):齐次裁剪空间-腾讯游戏学院 (qq.com)
齐次的定义:
顶点着色器经常会看到以下代码:
o.pos = mul(UNITY_MVP, v.position);
类似上面这句代码的功能就是把顶点坐标转换到齐次坐标系下,接着通常再有硬件做透视除法后,最终得到归一化的设备坐标(Normalized Device Coordinates, NDC)
撒谎给你图示OpenGl也是Unity使用的NDC,它的z分量范围在[-1,1],而在DirectX中,NDC的z分量范围是[0,1].顶点着色器可以有不同的输出方式.最常见的输出路径是经光栅化后交给片元着色器进行处理.
裁剪(Clipping)
由于我们的场景可能会很大,摄像机的视野范围很有可能不会覆盖所有的场景物体,一个很自然的想法是,不在摄像机视野范围的物体不需要被处理.这就是裁剪的目的.
图元与摄像机视野的关系主要有三种:
1)完全在视野内 >> 传给下一个流水线
2)部分在视野内 >> 需要裁剪处理
3)完全在视野外 >>不需向下传递
已知在NDC下顶点位置,即顶点位置在一个立方体内,裁剪就变得简单:只需要将图元裁剪到单位立方体内.
屏幕映射(Screen Mapping)
这一步输入的坐标仍然是三维坐标系下的坐标(单位立方体内).屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordinates)下,屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系.
输入的坐标z不会被做任何处理,屏幕坐标系和z坐标一起构成了一个坐标系,叫窗口坐标系(Windows Coordinates),这些值会一起传递到光栅化阶段.
屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上那个像素以及距离这个像素多远.
光栅化阶段
从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和他们相关的额外信息,深度值(z坐标),法线方向,视角方向.光栅化阶段有两个最重要的目标:计算每个图元覆盖了哪些像素和为这些像素计算它们的颜色.
光栅化(Rasterization)是把顶点数据转换为片元的过程,具有将图转化为一个个栅格组成的图象的作用,特点是每个元素对应帧缓冲区中的一像素。把物体的数学描述以及与物体相关的颜色信息转换为屏幕上的像素,这个过程称为光栅化
三角形设置
计算光栅化一个三角网格所需要的信息,具体来说,上一个阶段输出的都是三角网格的顶点,及我们得到的是三角网格每条边的两个端点.但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标.为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方法.这样一个计算三角网格表示数据的过程就叫做三角形设置.他的输出是为了给下一个阶段做准备.
三角形遍历(Triangle Traversal)
三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖,如果被覆盖的话,就会生成一个片元(fragment).而这样一个找到那些像素北三角网格覆盖的过程就是三角遍历,这个阶段也被称为扫描变换(Scan Conversion)
三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了那些像素,并使用三角网格3个顶点的定点信息对整个覆盖区域的像素进行插值.
这一步的输出就是得到了一个片元序列,但是这个片元并不是真正意义上的像素,而是包含了许多状态的集合,这些状态被应用于计算每个像素的最终颜色,这些状态包括它的屏幕坐标,深度信息,以及其他从几何阶段输出的顶点信息,法线,纹理信息等.
片元着色器(Fragment Shader)
前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的,每个片元就负责存储这样一系列数据,真正对像素产生影响的阶段是下一个流水线阶段.
片元着色器的输入是上一个阶段对顶点信息插值的结果,更具体说,是根据那些从顶点着色器中输出的数据插值得到的。而他的输出是一个或者多个颜色值。
这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
虽然片元着色器可以完成很多重要效果,但它的局限在于,它尽可以影响单个片元。也就是说,当执行片元着色器时,他不可以将自己的任何结果直接发送给他的邻居们。
逐片元操作
高度可配置性
OpenGL>>逐片元操作(Per_FragmentOperations)
DirectX>>输出合并阶段(Output_Merger)
主要任务:
1)决定每个片元的可见性。很多测试工作:深度测试,模板测试。
2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。