【RTR4笔记】 第三章 GPU与渲染管线

更深入的了解渲染管线

Posted Kongouuu's Blog on January 12, 2022

GPU与渲染管线

3.1数据并行的架构

我们的GPU是用并行的架构去运行代码的,意思就是每一个运行的处理器不需要知道其他处理器的信息也不用共享写入的片段。那么在讨论这一个架构前,先简单了解一下CPUCPU一般是以串行的方式运行代码,处理器和处理器之间可能会有大量交际,也会共享写入的片段。为了减少中间的停顿,会使用像分支预测等机制来提高整体运行的效率。为了达到这些效果,芯片的内部会含有大量的本地缓存。

GPU是采用的并行架构,所以比起采用缓存的方式,每个芯片里面会含有大量的更简易的的处理器,Shader Core。为了对吞吐量优化,比起CPU的大量缓存的设计方式,为了让处理器增加,所以每个芯片用于缓存的地方比较小,所以没办法进行向分支预测这样的减少等待的机制。

假设我们要在着色器内读取纹理数据,纹理数据是着色器外的数据,在读取的时间着色器会停止运行直到得到纹理的数据,这是一个弊端。为了解决这个问题,处理器会在等待的时候切换到下一个片段去处理(比如处理第二个像素先)。GPU同时采取单指令,多数据(SIMD)的运行方式。一般我们是把使用相同着色器的一批线程打包为一个warp进行共同处理。比如说32个处理器为一组,他们处理同一个warp,在他们需要读取贴图等待结果的时候,32个处理器会同时切换到另一组warp进行处理这样。

关于效率

  1. 使用过多的寄存器会减少可以用于处理的线程,也就是说会影响到整体运行的效率
  2. 使用了if的情况,如果所有处理器都得出同一个分支,那么没有效率的影响。如果有其中一个线程得出第二个分支,那所有线程都要一起执行那不需要的分支的结果。

3.2GPU管线概览

这里和上一章节讲的管线是十分相似但略有不同的。因为我们只考虑在GPU上面执行的步骤,并且也只考虑实际发生的不同阶段(不会把类似投影的阶段单独拿出,因为那是手法不是硬件阶段)。

这里的话绿色的跟着色器相关的阶段都是完全可编程的。黄色的屏幕映射和合并阶段是我们可以设置的,例如如何根据深度数据决定最后的像素。蓝色的裁剪和光栅化阶段目前来说是不可编程也不可设置的。

3.3可编程着色器

可编程的着色器发展到现在,大部分的图形上的计算都可以在着色器内部实现。因为有很多的数学相关的操作例如atan()等都会内置在着色器语言里,并且会在加载的时候展开成比较适合GPU处理的格式。

着色器同时会有每次Draw Call时统一使用的固定数据以及每个顶点拥有的各自的有变化的数据。处理着色器的虚拟机在为这些数据提供寄存器以外,还会有一些临时的寄存器来加速部分数据的读取。

这里要重新提一下就是说如果我们是使用的统一的数据(非顶点数据)去进行分支的展开,那么这并不会影响到整个着色器的渲染效率,因为第二个分支不会被运行。考虑到这一点会让我们的着色器的编写更加的泛用。

3.4顶点着色器

实际上顶点着色器在GPU里跟上一章讲解的没有太大的区别。这里的话顶点着色器本身是对自己的三角形相关的几何信息不了解的,单纯是用于处理各个顶点的变换。我们可以通过顶点着色器达到许多效果,例如动画和变形,不过那是之后的章节的内容了。

还有一点是逻辑上来看我们是先准备了原始的顶点,喂给了顶点着色器之后再传到裁剪阶段。不过就硬件层面来说顶点着色器的输入只是一些单纯的数据,实际顶点的生成实际上还是在顶点着色器内部进行的。

3.5曲面细分着色器

曲面细分实际上是分为三个详细步骤:

  1. Hull Shader 壳着色器(可编程): 壳着色器会得到一个比较特殊的图元(Patch Primitive)的输入,包含着细分所需要的控制点。他得到输入后会告诉细分器要细分出多少三角形,以及一些设一些配置。并且同时会去处理控制点的变换,以及增删控制点。
  2. Tessellator 细分器(可配置): 细分器会被通知应该细分出什么形状(三角形/四边形等),并且会被配置应该细分多少。
  3. Domain Shader 域着色器(可编程): 域着色器会使用壳着色器传过来的控制点去对细分器生成的顶点们进行操作。他的操作方式比较类似于在使用一个顶点着色器。并且最后会替代顶点着色器来生成要传入裁剪的数据。

3.6几何着色器

几何着色器阶段和曲面细分阶段的不同就是几何着色器可以添加面片外的顶点。一般来说我们的输入的图元会是(点、线、三角形、线带、三角形带),并且几何着色器就是接收这些输入,做一些更改,然后输出成相同或其他的图元。

几何着色器会保证输出的顺序跟输入的一样,这样在并行处理的情况下,如果一次调用复制或创建了过多顶点,会极大的影响整个渲染管线的效率。

有的手机设备会用别的软件处理几何着色,尽量不要使用。

3.7像素着色器

像素着色器阶段使用光栅化好的插值完的像素数据去进行光照的计算。原本像素着色器只能把信息传入合并阶段,不过现在的图形API都可以使用MRT(Multiple Render Target),也就是说在一次的像素着色器流程里同时把数据写入到多个渲染目标。

这个技术的有一个很典型的应用就是延迟渲染,我们延迟渲染需要先把画面的几何信息全部储存在G Buffer,里面包含了物体本身的材质(颜色、粗糙度、金属度),同时也得包含物体的世界坐标的位置。传统的情况下我们没办法一次性的去储存这么多的信息,不过如果可以一次渲染到多个渲染贴图,那么我们就完全可以在一个Pass中把所有的几何以及材质信息储存起来。

像素着色器普遍不能知道周围的像素的信息,不过只有在找插值的梯度信息的时候可以找周围2x2的像素的信息。这个梯度本身有很多的用法,可以在GPU上计算出我们的纹理应该采样什么个Mip Level,在DX12写阴影的时候,为了阻止阴影自遮挡也可以通过内置的梯度计算来选取Bias的偏移值。

3.8合并阶段

合并阶段算是可以配置的一个阶段。一般来说是使用我们的模板缓冲和我们的深度缓冲来判断一个象素可不可见。如果像素可见,那么我们再进行颜色的混合。

不过想到合并阶段会有大量的看不到的像素被抛弃,所以很多GPU允许一个叫Early-Z的操作,也就是在像素着色器开始运行之前去测试该深度的像素可不可见,如果不可见就直接抛弃,就不需要在像素着色器里计算。不过如果着色器里面有改深度或丢弃片元的操作,那么Early-Z无法被配置。

3.9计算着色器

计算着色器跟传统的其他着色器不一样,并不是固定在管线的任何一个部分。同时也不一定要用来做图形,也可以用来做机器学习等,比较类似CUDA

计算着色器的一个很大的特点是可以比较直接的访问GPU上的数据,也就是减少了很多时候从CPU传递数据到GPU的过程所开销的时间。这个特性可以让计算着色器在我们的传统渲染管线之上有更多的操作空间。拿粒子系统来说,如果我们想要粒子系统带有物理性质,那传统的做法我们多半来说得依靠CPU去计算粒子的运动。但如果使用GPU去计算,例如UE4Niagara,我们可以一次性模拟大量粒子的物理行为,甚至与环境交互。