Unity shader学习笔记(一)—— 渲染流程

渲染流水线

为了渲染一个模型,我们需要知道这个模型的每个顶点信息,有可能还需要知道由几个顶点构成的每个片元的颜色,这个颜色可能是直接指定的顶点颜色然后做的插值,也可能是用一张纹理“贴”上去,然后还要经过顶点变换,裁剪,等等操作,最后才渲染到屏幕上。为了加快这些步骤的执行,我们希望能够将这些步骤拆分成具体的几步,参考CPU指令流水线的思想,用流水线的思想加快执行。
在这些步骤中,可以分成两个部分。一部分在CPU上执行,一部分在GPU上执行。一开始模型的信息都存储在硬盘上,然后CPU要将其读取到内存中,之后设置一些渲染的状态(比如使用哪张纹理,顶点变换规则,颜色计算规则等等),然后将渲染状态和模型的顶点信息发送到显存,再发送一个指向该渲染状态的渲染命令Draw Call,通知GPU读取该渲染状态和模型顶点信息,开启第二部分的步骤绘制模型。而在第二部分中,又可以拆分出来两部分。总共三部分的步骤,分别如下:

应用阶段
几何阶段
光栅化阶段

其中应用阶段在CPU上执行,几何阶段和光栅化阶段在GPU上执行。这三个部分里面的十几个步骤(下文提及)最终构成了整个渲染流水线的元素。
注意上文提到的渲染命令,CPU有可能会发送多个,这些命令都会发送到一个命令缓存中,再由GPU一一读取。

OpenGL 和 DirectX

在这里插入图片描述
这张图是《Untiy shader 入门精要》下的一张图,非常清晰地解释了CPU,GPU,OpenGl/DirectX之间的关系。CPU会执行应用程序的代码,里面可能会包含一些渲染指令(即所谓的Draw Call),这些渲染指令其实并不是真正地直接发送到命令缓存中,而是调用了OpenGL或者DirectX提供的接口,由这两个计算机图形处理库去发送真正的渲染命令。渲染命令由显卡驱动去翻译成GPU能够识别的语言,然后GPU读取显存中由渲染命令指定的数据,最后开始渲染,在此之前,CPU已经提前将数据从内存中拷贝到了显存里。
一个游戏引擎,为了能够跨平台开发,跨平台发布,需要同时支持OpenGL和DirectX两个图形处理库。一般来说,我们在游戏引擎中写的代码,就是上图中的应用程序一项。不必直接接触OpenGL和DirectX提供的接口,因为游戏引擎一般都已经提供了调用这两个库的接口,并且做了很好适配和封装。但是有时候,我们仍然需要手动地去为这两个图形处理库的不同做一些处理。

GPU做的事情

第一张图中,有两个部分是GPU做的。一个是几何阶段,一个是光栅化阶段。这其中又细分为好几个步骤,以下是《Unity shader 入门精要》的一张图:
在这里插入图片描述
其中,顶点着色器,曲面细分着色器,几何着色器和片元着色器是可编程的。在应用阶段,CPU设置的渲染状态时,可以设置分别使用哪几个着色器。实际在Unity上实践时,这部分是通过在shader中编写着色器代码(每个着色器对应一个函数)来实现的。
几何阶段从一开始接收物体的顶点数据,到最后的屏幕映射,输出的是经过座标变换后的窗口座标系下的座标(关于具体的座标变换,可以参考关键词 MVP座标变换)。这些新的顶点数据会传给光栅化阶段做下一步的处理。
在光栅化阶段的片元着色器会对每个片元进行颜色填充或者纹理映射,计算片元的输出颜色。

顶点着色器

顶点着色器接收来自模型的顶点数据,这些数据包括每个顶点原始的座标、纹理座标、顶点法线和顶点颜色等顶点属性。我们可以在顶点着色器中进行一些光照计算,然而这样的光照计算很粗糙,的出来的结果不是特别自然,所以一般不会这么做。顶点着色器最主要的工作就是进行顶点座标变换(MVP矩阵变换),或者你可以指定顶点随时间的变化规律,实现诸如水的波纹这样的复杂的效果。一个顶点着色器必须输出顶点在齐次裁剪空间座标系下的顶点座标以及各个顶点颜色。

曲面细分着色器

曲面细分着色器出现得比较晚,是渲染管线中可选的部分,有些渲染管线的相关书籍里面并没有对这方面进行介绍。曲面细分利用镶嵌化技术对三角面进行细分,从而增加物体表面的三角面的数量。在这里我们可以实现一些细节层次相关的技术,让近处的物体具备更加丰富的细节,而远处的物体减少细节上的渲染,优化性能。

几何着色器

几何着色器也是一个可选的阶段。不像顶点着色器每次只能处理一个顶点,而且不能破坏和创造顶点。几何着色器的优点是能够输入多个顶点进行处理,并且破坏和创建顶点。几何着色器最常见的应用是将一个顶点拓展成一个三角面或四方形。这使得几何着色器能够完成许多绚丽的效果。
注:尽管几何着色器能够一次处理多于一个的顶点数据,但是官方并不建议几何着色器处理太多顶点,否则会对性能产生较大的影响。

裁剪

经过顶点着色器的座标变换之后,我们就可以判断物体在经过最后的投影变换后哪些部分被投影在屏幕之外。一个物体如果完全在屏幕之外,那么直接放弃对它的渲染,如果一个物体一部分在屏幕外,一部分在屏幕内,就需要进行裁剪操作。

屏幕映射

下图来自https://blog.csdn.net/wangdingqiaoit/article/details/51589825
在这里插入图片描述
上面讲到,顶点座标系主要完成的是前面三个变化(Modeling Transformation, View Transformation, Porjection Transformation,统称MVP矩阵变换),经过这三个步骤之后,物体的座标被转化成虚拟摄像机的胶片上的二维座标。你可以设置虚拟相机的观察范围和胶片大小,在Unity中,这些一般跟四个参数有关:Field of View, Clipping Planes, Viewport Rect以及Game视图的预定义屏幕分辨率(可以设置是比例,也可以设置绝对值)。你可以在Unity上调节这四个参数看看效果。之后,在屏幕映射阶段还会做两个操作:透视除法和视口变换。屏幕座标系以整数为单位,具体座标范围却跟设备分辨率息息相关。为了规避设备相关性,需要定义一个与具体设备无关的座标系,这个座标系座标范围一般从0到1,或者-1到1 。之后再根据视口变换转换到具体设备的屏幕座标系上去。

三角形设置

这一步骤接收上一个部分处理好的顶点,根据顶点序列产生三角形。在一些渲染管线的教程中,这一部分又叫图元装配,可能更贴切一点。它将输入的顶点组装成基本的图元供片元着色器处理。一般来说,这些图元就是三角形——最简单的多边形。

三角形遍历

这一阶段其实是一个光栅化处理。顶点的座标是浮点类型的,但是屏幕像素是离散的,整型的。这一阶段会遍历每个三角形,判断每个三角形覆盖了哪些像素,然后输出片元序列。你可以将片元暂时跟像素一一对应,但它不是像素。再经过片元着色器的处理之后,片元才会变成真正的像素信息。

片元着色器

在编写Unity Shader时,除了顶点着色器,另一个可编程且必须指明的着色器就是片元着色器(Fragment Shader)。片段着色器在DirectX中也被称为像素着色器(Pixel Shader)。片段着色器用来决定屏幕上像素的最终颜色。在这个阶段会逐片元地进行光照计算,阴影处理甚至一些颜色变换,从而产生一些高级的效果。

逐片元操作

终于来到最后的阶段。这个阶段亦即测试混合阶段,包括裁切测试、Alpha测试、模板测试和深度测试。没有经过测试的片元都将被丢弃,而不会被显示在屏幕中。比如两个片元都占据同一个像素,但是最终只会显示深度信息高的像素(在3D场景中,深度一般意味着离相机更近)。

纹理座标

再提一下纹理座标这个概念,它将一张纹理的每个像素映射到一个座标系中,这个座标系的原点(OpenGL)(-1,-1)为纹理的左下角,而纹理的右上角固定为座标(1,1)。即每个像素的横座标映射到(-1,1)的某一点,纵座标同理。不过在DirectX中,原点是在纹理的左上角。然后,纹理映射将纹理的纹理座标映射到模型的每个点,以此在模型上显示出纹理。由于纹理座标水平方向上一般叫U座标,竖直方向上一般叫V座标,故纹理座标又称UV座标。在输入到GPU渲染管线的顶点信息中,每个顶点都会包含各自的纹理座标(如果你使用了纹理的话),GPU会将纹理上对应座标的像素颜色赋给顶点,然后在光栅化阶段对顶点颜色进行插值产生每个三角形的颜色。

总结

最后,总结一下一个模型是如何被渲染到屏幕上。渲染一个模型需要什么信息?首先是模型的网格,即顶点信息,它决定一个模型具体长什么样,其次,由顶点构成的面元具体如何被绘制?一种可以在顶点信息中包含每个顶点的颜色,然后每个面元由构成它的顶点的颜色插值得到。另一种,指定一张纹理给它,通过纹理映射技术将纹理“贴”在模型表面。那我们定好了形状的物体以什么样的姿态呈现在屏幕上的(比如你可以指定它总是以哈哈镜的效果呈现在屏幕上),然后颜色如何插值,纹理具体怎么“贴”,那就需要自己书写顶点着色器,片元着色器等,然后指定使用你写的着色器。这些东西一并打包发送至显存中,然后发送一条渲染命令,通知GPU渲染模型。

欢迎大佬批评指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章