前言
在过完games101课程后仍然觉得自己还有许多地方不懂与遗漏,以此来补充与复习一些其中的知识。
参考:Games101、《Unity Shader 入门精要》
GPU渲染流水线(GPU Rendering Pipeline)
----注:Games101课程中所展示渲染流程与书中有所不同,大体相同,细节展示有所不同,这点请看图
这里以《Unity Shader入门精要》中为主介绍,如下图:
补充:绿色表示完全可编程控制,黄色表示可以配置但不是可编程,蓝色表示由CPU固定实现。实线表示该Shader必须由开发者编程实现,虚线表示该Shader是可选的。
----------------------下面说明各阶段:
Application(应用阶段):
渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为以下3个阶段:
(1)把数据加载到显存中
(2)设置渲染状态
(3)调用Draw Call
> 把数据加载到显存中:
渲染所需要的数据需要从硬盘传输到系统内存中(RAM),网格和纹理等数据被加载到显存中。(真实渲染中需要加载到显存中的数据还有顶点的位置信息、法线方向、顶点颜色、纹理坐标等)。
> 设置渲染状态:
渲染状态可以理解为场景中的网格是怎样被渲染的,即使用哪个顶点着色器/片元着色器、光源属性、材质等。如果没有更改渲染状态的话那么所有网格都将使用同一种渲染状态。
下图为同一状态下渲染3个网格。
接下来,就是调用Draw Call啦!
> 调用Draw Call:
其实Draw Call本质上就是一个命令,它的发起方是CPU,接收方是GPU。当我们给定了一个Draw Call时,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算。
----------关于Draw Call的一些补充----------
Draw Call本身的含义很简单,就是CPU调用图像编程接口,入OpenGL中的glDrawElements命令。有一个常见的误区时Draw Call中造成性能问题的元凶时GPU,认为GPU上的状态切换是耗时的,其实真正“拖后腿”的是CPU。
问题一:CPU和GPU是如何实现并行工作的?
若没有流水线化,则CPU需要等GPU完成上一个渲染任务才能再次发送渲染命令,这导致效率低下。而解决方法就是使用一个“命令缓冲区”
命令缓冲区包含了一个命令队列,CPU向其中添加命令,GPU从中读取命令,CPU与GPU互不干扰。如下图:
(黄色方框内的命令就是Draw Call,而红色方框内的命令用于改变渲染状态。使用红色方框来表示改变渲染状态的命令,是因为这些命令往往更加耗时)
问题二:为什么Draw Call多了会影响帧率
在每次调用Draw Call之前,CPU需要向GPU发送很多内容,包括数据、状态和命令等。在这一阶段,CPU需要完成很多工作,例如检查渲染状态等。当CPU完成这些准备工作之后GPU就可以开始本次渲染了。但是由于GPU的渲染能力很强,会导致GPU的渲染速度大于CPU提交命令的速度。如果Draw Call的数量太多,CPU就会把大量时间花费在提交Draw Call上,造成CPU的过载。如下图:
(虚线框表示GPU已经完成的命令。此时命令缓冲区已没有可执行的命令,GPU处于空闲状态,而CPU还没有准备好下一个渲染命令。)
问题三:如何减少Draw Call
这里仅提一种方法,叫做“批处理” 方法。
提交大量很小的Draw Call会造成CPU的性能瓶颈,即CPU把时间都花费在准备Draw Call的工作上了。批处理的思想是把很多小的Draw Call合并成一个大的Draw Call。 (批处理技术更适合于那些静态的物体。)
Geometry Processing(几何阶段):
几何阶段又细分为以下几个阶段:
(1)顶点着色器
(2)曲面细分着色器
(3)几何着色器
(4)裁剪
(5)屏幕映射
>顶点着色器:
顶点着色器(Vertex Shader)是完全可编程的,顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系。顶点着色器的主要工作:坐标变换和逐顶点光照。如下图
- 坐标变换:在顶点着色器中完成MVP变换,把顶点坐标从模型空间转换到齐次裁剪空间。接着通常再由硬件做透视除法,最终得到NDC。
>曲面细分着色器:
一个可选着色器,用于细分图元。
>几何着色器:
一个可选着色器,用于逐图元的着色操作,或者用于产生更多的图元。
>裁剪:
裁剪提出的目的是要为了处理掉那些不在摄像机视野范围内的物体。由于我们已知在NDC下的顶点位置,即顶点位置在一个立方体内,则只需要将图元裁剪到单位立方体内,如下图
图片补充:和单位立方体相交的图元会被裁剪,新的顶点会被生成,原来在外部的顶点会被舍弃。
>屏幕映射:
屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系下。这个过程实际是一个缩放的过程,但是屏幕映射不会对输入的z坐标做任何处理。过程如下图:
一些补充:在OpenGL中,屏幕左下角为最小的窗口坐标值,在DirectX中,屏幕的左上角为最小的窗口坐标值。
Rasterization(光栅化阶段):
光栅化阶段细分为以下几个阶段:
(1)三角形设置
(2)三角形遍历
(3)片元着色器
(4)逐片元操作
>三角形设置:
这是光栅化的第一个流水线阶段。上一个阶段输出的都是三角网格的顶点,为了得到整个三角网格对像素的覆盖情况,我们还需计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。
>三角形遍历:
这个阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖,则生成一个片元。这个阶段也被称为扫描变换
这一阶段还会根据三角网格3个顶点信息对整个覆盖区域的像素进行插值(利用重心坐标)。如下图(一个简化的计算过程):
这一步的输出得到一个片元序列。但是一个片元并不是真正意义上的像素,而是包含了很多状态的集合,用于计算每个像素的最终颜色。
>片元着色器:
片元着色器(Fragment Shader)是另一个可编程着色器阶段,又叫做像素着色器(Pixel Shader)。片元着色器的输入是上一个阶段对顶点信息插值得到的结果,而它的输出是一个或者多个颜色值。
前面的光栅化戒定慧产生一系列的数据信息,用来表述一个三角网格是怎天覆盖每个像素的。而每个片元就负责存储这样一系列数据。
如下图:
这一阶段可以完成许多重要的渲染技术如纹理采样。
局限:它仅可以影响单个片元,当执行片元着色器时,它不可以将自己的任何结果直接发送给它的邻居们。
>逐片元操作:
这是真正会对像素产生影响的一个流水线阶段。
这一阶段会有几个主要任务
<1>进行测试工作,如深度测试、模板测试等。
<2>如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。下图是简化后的逐片元操作所做的操作。
在逐片元操作中的后面的合并操作时,对于不透明物体,开发者可以关闭**混合(Blend)**操作。这样片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲区的像素值。对于半透明物体,就需要使用混合操作来让这个物体看起来是透明的。
《Unity Shader入门精要》中前面部分对渲染管线的介绍到此结束。下面是一些额外补充:
- 不同的文章对渲染管线的阶段分配介绍各有不同,清楚大体过程即可。
- 系统为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)也就是说,对场景的渲染发生在幕后,即后置缓冲(Back Buffering)中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲和前置缓冲(Front Buffering)。
- 鉴于自己总是弄混顶点着色器的作用,在此再次总结一下:顶点着色器中完成MVP变换将顶点变换至齐次裁剪空间中,再由硬件做透视除法得到NDC。接着再由系统做裁剪以及屏幕映射。
下面再放上一些在Games101中渲染管线的图方便作对比:
写在最后:尽管写了不少,中间仍然可能会出现错误,后面继续学习发现错误后加以改正。
初次写于2023.10.7