一、固定管线渲染流程
1、应用阶段(CPU)
视椎体裁剪——>数据加载到显存——>设置渲染状态——>调用Draw Call
2、几何阶段(GPU)
模型坐标系——>世界坐标系——>裁剪坐标系——>背面消隐——>光照——>裁剪——>投影——>视口变换
——>光栅化
3、光栅化阶段(GPU)
纹理采样——>模板测试——>深度测试——>混合——>写入缓冲区
二、现代Shader Model渲染流程
1、应用阶段(CPU)
视椎体裁剪——>数据加载到显存——>设置渲染状态——>调用Draw Call
2、几何阶段(GPU)
输入数据——>顶点着色器——>曲面细分着色器——>几何着色器——>裁剪——>屏幕映射——>光栅化
3、光栅化阶段(GPU)
三角形设置——>三角形遍历——>片元着色器——>逐片元操作——>屏幕图像
可以发现在可编程管线下,顶点着色器替代了固定管线中顶点的变换和光照,片元着色器则替代了纹理操作
三、现代Shader Model各个阶段的作用
几何阶段(GPU)
1、顶点着色器(Vertex Shader)
它的输入来源于CPU,对顶点进行处理,每个输入的顶点都会经过顶点着色器的处理,而且每一个顶点无法和其他顶点进行交互,所以GPU可以并行的处理输入的顶点信息。
其主要工作:坐标更换,逐顶点光照,以及输出后续阶段所需要的数据。但是无论怎么做顶点坐标变换,其最终都是要将顶点从模型坐标系转换到齐次裁剪坐标系,输出数据,经硬件做透视除法后,得到归一化的设备坐标(NDC)。在OpenGL中NDC的Z坐标是(-1,1),DirectX中是(0,1)。
2、裁剪(Clipping)
裁剪的目的是把不在摄像机视野范围内的物体处理掉,在进行裁剪时,顶点坐标已经是NDC下的,所以要做的就是将不在立方体内的图元裁剪掉。这一步是不能编程的,由硬件完成,但是我们可以配置裁剪操作
3、屏幕映射(Screen Mapping)
屏幕映射的作用就是将每个顶点的X,Y坐标映射到屏幕坐标系下(Screen Coordinates),而屏幕坐标系是一个二维坐标(所以屏幕映射的输入还是三维坐标,并且是经过归一化的)。而屏幕分辨率不定,所以映射的过程其实是个缩放的过程。
得到的屏幕坐标决定了顶点对应屏幕中哪个像素,以及距离这个像素多远(映射的过程要注意OpenGL和DirectX坐标的不同,OpenGL是左下角为原点,右上角是最大点,DX是左上角为原点,右下角为最大点)。而缩放过程并不会对顶点的Z坐标进行操作,屏幕坐标系和Z坐标组成新的坐标系: 窗口坐标系(Windows Coordinates)。这些值会一同传递到光栅化阶段
光栅化阶段(GPU)
由这一步开就进入到了光栅化阶段,上一阶段输入的信息是屏幕坐标系下顶点的位置,以及一些附加信息:深度值(Z坐标),顶点法线方向等。而光栅化的目标就是,计算每个图元都覆盖了那些像素,以及计算这些被覆盖的像素的颜色。
4、三角形设置(Triangle Setup)
因为上一阶段输入的都是顶点,为了能计算这些顶点覆盖的像素,我们需要求解这些顶点连接起来的三角形的表达式,所以三角形设置这一阶段其实就是连线的过程,将顶点连线成真的三角形,得到三角形边界的表达式,然后传递个下一个阶段。
5、三角形遍历(Triangle Traversal)
因为上一步三角形设置,已经得到了三角形的表达式,所以这个阶段我们就能计算有哪些像素被这三角形覆盖,如果被覆盖的话就会生成一个片元(fragment)。
而找到哪些像素被三角形覆盖的过程就是三角形遍历(Triangle Traversal),这个阶段也被称为扫描变换(Scan Conversion)。
在得到被覆盖的像素后,会使用三角形3个顶点数据对覆盖在整个三角形中的像素数据进行插值,比如颜色,深度值,纹理UV坐标。
三角形遍历后会得到一个片元序列。而一个片元并不是真正意义上的一个像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括但不限于,它的屏幕坐标,深度信息,法线,纹理坐标等。
6、片元着色器(Fragment Shader)
Fragment Shader是一个可编程阶段,在DX中叫做PS(Pixel Shader),但是叫做片元着色器好像更合适一些,因为传入到片元着色器中的是一个片元的信息,也就是经过各种插值之后得到的数据,颜色,纹理坐标等。
而这一步最重要的渲染技术就是纹理采样。在得到插值之后的纹理坐标后,就可以根据纹理坐标进行采样,得到对应坐标下的纹理颜色,然后经过某种计算,输出最终的颜色。
FS可以完成很多效果,但是局限于它只能影响到单个片元,它不能访问别的片元的信息,也不能将自己的结果发给别的片元(据说有一个特殊情况,可以访问到导数信息)
7、逐片元操作(Per-Fragment Operations)
逐片元操作(Per-Fragment Operations)是OpenGL中的叫法,DX中叫输出合并阶段(Output-Merger)。逐片元操作才是真正对像素产生影响的阶段,这个阶段决定了哪些像素可以显示,哪些不能。
这一阶段有几个重要的任务:
(1)可见性测试:模板测试,深度测试等
(2)合并,混合:经过可见性测试之后,要对这个片元颜色和已经存储在颜色缓冲区进行合并(混合)
。
模板测试
深度测试
混合
实际的测试顺序
从逻辑上将,这些测试都是在片元着色器之后进行,但是对大多数GPU来说,他们会尽可能在片元着色器之前做这些测试。这样就可以提高性能,因为被测试掉的片元就不需要在进行计算 。
而在Unity给出的渲染流水线中,深度测试是在片元着色器之前的,这种将深度测试提前的执行的技术通常被称为“Early-Z”。但是把测试提前有时也会导致和片元着色器冲突的情况,引起性能下降,如透明度测试,当GPU发现和片元着色器操作冲突,就会关闭提前测试,这样会有很多没用的操作,导致性能下降。
双重缓冲区(Double Buffering)
我们看到的屏幕上显示的就是颜色缓冲区中的颜色,为了避免出现黑屏,GPU使用了双重缓冲区,在渲染场景的时候,会将颜色绘制在后置缓冲区(Back Buffer)中,一旦完成绘制,GPU会将前置缓冲区(Front Buffer)和后置缓冲区内容交换,这样保证了我们看到的换面是连续的,而不是会出现闪屏或是黑屏的现象。
四、到底什么是Shader
我认为Shader就是运行在GPU可编程阶段中,对顶点、像素进行变换,着色的代码片段