Unity Shader入门(一)
本人是一个初学Shader的小白,写此博客为了记录学习Shader过程中的一些概念以及问题,主要参考书籍《Unity Shader入门精要》如果有哪些不对的地方欢迎各位大佬指导!!!
渲染流水线
1、什么是渲染流水线
通俗来说,计算机需要从一系列的顶点数据等信息出发,把这些信息最终转换成一张人眼可见的图像,这一过程为渲染流水线。
渲染流水线包括三个阶段:应用阶段、几何阶段、光栅化阶段。(注意:每个阶段本身也是一个流水线系统,即包含了子流水线阶段)
2、CPU流水线(应用阶段)
应用阶段 (程序员拥有绝对的控制权):
1)准备好场景中的数据(摄像机的位置、场景的模型、光源等),不可见剔除(把场景中不可见的物体剔除出去,这样就不需要交给几何阶段进行处理了),
把数据加载到显存中:
所有渲染所需要的数据都要从硬盘加载到系统内存中,然后,从系统内存中将网格和纹理等数据加载到显存(显卡上的存储空间)中,因为显卡对显存的访问速度更快,而且大多数显卡对系统内存没有直接的访问权力。
当把数据加载到显存中后,系统内存中的数据就可以移除了。但对一些数据来说,CPU仍然需要访问它们(例如:CPU访问网格数据来进行碰撞检测),我们可能就不希望这些数据被移除,因为从硬盘加载到系统内存是非常耗时的。
2)设置渲染状态(例如模型使用材质,纹理、Shader等)
3)调用DrawCall
DrawCall是一个命令,发起方是CPU,接收方是GPU,这个命令仅仅会指向一个需要被渲染的图元列表。
应用阶段最重要的是输出渲染所需的几何信息,即渲染图元(通俗来讲:渲染图元可以是点、线、三角面等),将这些信息传递到下一个阶段——几何阶段*
3、GPU流水线(几何阶段和光栅化阶段)
图中绿色表示该流水线阶段是完全可编程控制的
黄色表示该流水线阶段可以配置但不是可编程的
蓝色表示该流水线阶段是由GPU固定实现的,开发者没有任何可控制权
实线表示该Shader必须由开发者编程实现,虚线表示该Shader是可选的
几何阶段:
主要任务是把顶点坐标变换到屏幕空间中,再交给光栅化器处理。几何阶段用于处理从上一阶段接收到的待绘制物体的几何数据,与每个渲染图元打交道,进行逐顶点、逐多边形的操作,通过对输入的图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标,每个顶点对应的深度值,着色等相关信息。
- 顶点着色器:
顶点着色器的处理单位是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点预定点之间的关系。正是因为这样的独立性,GPU可以利用本身的特性并行化处理每个顶点,这意味着这一阶段的处理速度会很快。
顶点着色器完成的工作主要有:坐标变换和逐顶点光照,输出后续阶段需要的数据
如图:顶点着色器必须进行顶点的坐标变换,需要时还可以计算和输出顶点的颜色。例如我们可能需要进行逐顶点的光照
坐标变换:对顶点的坐标进行某种变换。顶点着色器可以在这一步中改变顶点的位置,这在顶点的动画中非常有用,无论我们在顶点着色器中怎样改变顶点位置,一个最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间,接着通常再由硬件做透视除法后,最终得到归一化的设备坐标(NDC)。
- 裁剪:
裁剪的目的是将那些不在摄像机视野范围内顶点裁剪掉,并剔除某些三角图元的面片(面片通常是由一个一个更小的图元来构成的)。
一个图元和摄像机有三种关系:完全在视野内,部分在视野内,完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元不会向下传递,因为他们不需要被渲染。而那些部分在视野内的图元需要进行一个处理,这就是裁剪。
- 屏幕映射:
这一步输入的坐标仍然是三维坐标系下的坐标(范围在单位立方体内)。屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面分辨率有很大关系。
如图,屏幕映射将下x,y坐标从(-1,1)范围转换到屏幕坐标系中,这实际上是一个缩放的过程屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。
屏幕映射不会对输入的z坐标做任何处理,实际上,屏幕坐标系和z坐标一起构成了一个坐标系,叫做窗口坐标系,这些值会一起被传递带光栅化阶段。
有一个需要注意的地方是,屏幕坐标系在OpenGL和DirectX之间的差异问题。OpenGL把屏幕的左下角当成最小的窗口坐标值,而DirectX把屏幕左上角当成最小的窗口坐标值(开发过程需注意这个问题)。如下图
光栅化阶段:
这一阶段将会使用上个阶段输出的屏幕坐标系下的顶点位置以及和他们相关的额外信息,并渲染出最终的图像。主要任务有两个:计算每个图元覆盖了哪些像素,以及为这些像素计算他们的颜色。
- 三角形设置:
这个阶段会光栅化一个三角网格所需的信息。具体来说,上一阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点,但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们需要得到三角形边界的表达式。这样一个计算三角网格表示数据的过程叫做三角形设置,它的输出是为了给下一阶段做准备。
最终目的确定一个三角网格边界像素坐标
- 三角形遍历:
三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换。
三角形遍历阶段会根据上一个阶段(三角形设置确定的三角形边界像素的信息)的计算结果来判断一个三角形网格覆盖了哪些像素,并对这些像素进行插值。
如图:根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置,对应像素会生成一个片元,而片元中的状态是对3个顶点的信息进行插值得到的。
这一步的输出就是得到一个片元序列。需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息、以及其他从几何阶段输出的顶点信息,例如:法线、纹理坐标等
最终目的获取一个片元序列
- 片元着色器
前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表示一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。
片元着色器输入的是上一个阶段对顶点信息插值得到的结果,具体的来说,是根据哪些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值(会影响到像素的显示)
这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
如图:根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色- 逐片元操作:
主要任务:
1)决定每个片元的可见性。涉及了很多测试工作,例如深度测试、模板测试等。
2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经储存在颜色缓冲区中的颜色进行合并,或者说是混合。
如图:逐片元操作阶段所做的所有操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后在写入颜色缓冲区中。
模板测试:
模板测试可以作为一种丢弃片元的辅助方法,与之相关的是模板缓冲。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片原位置的模板值,然后将该值和读取(使用读取掩码)到的参考值进行比较,这个比较函数可以由开发者指定,例如小于时舍弃该片元,或者大于等于时舍弃该片元。如果这个片元没有通过这个测试,这个片元将会被舍弃。不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作,例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。模板测试通常用于限制渲染的区域。
深度测试:
如果开启了深度测试,GPU会把该片元的深度值和已经存在与深度缓冲区中的深度值进行比较。这个比较函数也是由开发者设置的。通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于当前深度缓冲区中的值,那么就会舍弃它,因为我们总想只显示离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过深度测试,他就没有权利更改深度缓冲区的值(这与模板测试不同,不管通过没通过测试,均可以修改缓冲区的参考值)。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原来的深度值,这是通过开启/关闭深度写入来做到的。
混合:
为什么需要混合?渲染过程是一个物体接一个物体画到屏幕上的,而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。因此,当我们执行这次渲染时,颜色缓冲中往往已经有了上次渲染之后的颜色结果,这就需要进行对颜色缓冲区进行混合。
对于不透明物体,开发者可以关闭混合操作,这样片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲区中的像素值。但对于半透明物体,我们就需要使用混合操作来让这些物体看起来是透明的。
需要注意的是上面给出的测试顺序并不是唯一的,而且虽然从逻辑上来说这些测试是在片元着色器之后进行的,但对于大多数GPU来说,它们会尽可能在执行片元着色器之前就进行这些测试,这也是为了杜绝片元着色器计算出颜色值后经过测试舍弃该片元,造成计算颜色值这一过程的性能消耗。在Unity给出的渲染流水中,深度测试是在片元着色器之前的。还有一种情况,如果将这些测试提前的话,其检验结果可能与片元着色器中的一些操作冲突,因此,现代的GPU会判断片元着色器中的操作是否和提前测试发生冲突,如有冲突,就会禁用提前测试。
当模型的图元经过上面层层计算和测试后,就会显示到我们的屏幕上。我们屏幕上显示的就是颜色缓冲区中的值。但是,为了避免我们看到那些正在光栅化的图元,GPU会使用双重缓冲策略。这意味着,对场景中的渲染实在幕后发生的,即在后置缓冲中。一旦场景已经被渲染到后置缓冲中,GPU就会交换后置缓冲和前置缓冲的内容,而前置缓冲就是之前显示在屏幕上的图像。因此,保证了我们看到的图像总是连续的。