Unity Shader精要读书笔记
第二章
2.1 渲染流程
应用阶段–>几何阶段–>光栅阶段
应用阶段:由我们的应用主导,通常由CPU实现,开发者具有绝对控制权。这一阶段最重要的的输出是渲染所需的几何信息,即渲染图元(点、线、三角面等)
在这一阶段,开发者有3个主要任务:
1.准备好场景数据,例如摄像机的位置、视锥体、场景中包含了哪些模型,使用了哪些光源等等
2.为了提高渲染性能,往往需要做一个粗粒度剔除工作,把那些不可见的物体剔除出去
3.设置好每个模型的渲染状态
几何阶段:用于处理所有和我们要绘制的几何相关的事情,通常在GPU上进行。例如(决定需要的绘制的图元是什么,怎样绘制它们,在哪里绘制它们)
几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段。
光栅化阶段:使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在GPU上运行。
光栅化的任务主要是决定每个渲染图元中的哪些像素因该被绘制在屏幕上。它需要对上一个阶段得到的逐顶点数据(例如纹理坐标,顶点颜色等)进行插值,然后再进行逐像素处理。
2.2 CPU和GPU之间的通信
渲染流水线的起点是CPU,即应用阶段,应用阶段可分为:
(1)把数据加载到显存中
(2)设置渲染状态
(3)调用DrawCall
2.2.1 把数据加载到显存中
所有渲染所需的数据都需从硬盘中加载到系统内存中。然后,网格和纹理等数据又被加载到显卡上的存储空间(显存)。因为显卡对显存访问速度更快,而且大部分显卡对于RAM没有直接的访问权利。
在这之后,开发者还需要通过CPU来设置渲染状态,从而指导GPU如何进行渲染工作。
2.2.2设置渲染状态
渲染状态定义了场景中的网格是怎样被渲染的。(例如使用哪个顶点着色器/片元着色器、光照属性、材质等),如果我们没有更改渲染状态,那么所有的网格都将使用相同一种渲染状态。
准备好上述所有工作后,CPU就需要调用一个渲染命令(DrawCall)来告诉GPU按照设置开始渲染。
2.2.3 调用Draw Call
Draw Call是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元列表,而不会再包含任何材质信息。当给定了一个Draw Call时,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的像素,计算过程就是GPU流水线。
2.3 GPU流水线
2.3.1 概述
几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些流水线阶段由GPU来实现,每个阶段GPU提供了不同的可配置性或可编程性。
2.3.2 顶点着色器
顶点着色器是流水线的第一个阶段,它的输入来自于CPU。顶点着色器的处理单位是顶点,它本身不可以创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系。
顶点着色器需要完成的工作主要有:
(1)坐标变换
(2)逐顶点光照
(3)输出后续阶段所需的数据
2.3.3 裁剪
一个图元和摄像机视野的关系有3种:完全在视野内、部分在视野内、完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元不会继续向下传递,因为它们不需要被渲染。而那些部分在视野内的图元需要进行一个处理,这就是裁剪。
这一步是不可编程的,是硬件上的固定操作,但可以自定义一个裁剪操作来对这一步进行配置。
2.3.4 屏幕映射
屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系下。
屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。
2.3.5 三角形设置
这个阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网络的顶点,即我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。
2.3.6 三角形遍历
三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换。
三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。
这一步的输出就是得到一个片元序列,一个片元序列不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息、例如法线、纹理坐标等。
2.3.7 片元着色器
光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表达一个三角网格是怎样覆盖每个像素的。而每个片元就负责储存这样一系列数据。真正会对像素产生影响的阶段是逐片元操作。
片元着色器的输入是上一个阶段对顶点信息插值得到的结果,是根据从顶点着色器中输出的数据插值得到的。它的输出是一个或多个颜色值。
为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
虽然片元着色器可以完成很多中药效果,但它的局限在于,它仅可以影响单个片元。当执行片元着色器时,它不可以将自己的任何结果直接发送给它的邻居们。有一个情况例外,就是片元着色器可以访问到导数信息。
2.3.8 逐片元操作
逐片元操作这一阶段的主要任务:
(1)决定每个片元的可见性,这涉及了很多测试工作,例如深度测试、模版测试等
(2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
逐片元操作阶段是高度可配置的,我们可以设置每一步的操作细节。
这个阶段首先需要解决每个片元的可见性问题。这需要进行一系列测试,通过所有测试才可以和颜色缓冲区进行合并,如果没有通过其中一个环节,这个片元会被舍弃掉。
模版测试:
模版测试与之相关的是模版缓冲,模版缓冲和我们经常听到的颜色缓冲、深度缓冲几乎是一类东西。如果开启了模版测试,GPU会首先读取模版缓冲区中该片元未知的模版值,然后将该值和读取到的参考值进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。如果这个片元没有通过这个测试,该片元就会被舍弃,不管一个片元有没有通过模版测试,我们都可以根据模版测试和下面的深度测试结果来修改模版缓冲区,这个修改操作也是开发者指定的。开发者可以设置不同结果下的修改操作,例如(在失败时模版缓冲区保持不变,通过时将模版缓冲区中对应位置的值加1等)模版测试通常用于限制渲染的区域,另外还有一些高级用法,如渲染阴影,轮廓渲染等。
深度测试:
深度测试也是可以高度配置的,如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是可由开发者设置的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。这是因为,我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过这个测试,该片元就会被舍弃。和模版测试不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。
对于不透明物体,开发者可以关闭混合操作。这样片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲区中的像素值。但对于半透明物体,我们就需要使用混合操作来让这物体看起来是透明的。
混合操作是可以高度配置的,开发者可以选择开启/关闭混合功能,如果没有开启混合功能,就会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色。如果开启了混合,GPU会取出源颜色和目标颜色。将两种颜色进行混合,源颜色指的是片元着色器得到的颜色值,而且目标颜色则是已经存在于颜色缓冲区中的颜色值。之后,就会使用一个混合函数来进行混合操作。