[图形学]Unity Sharder入门精要(一)

前言

这一片文章写的是关于Unity Sharder入门精要第二章的读书笔记。

知识点

流水线

就是把一整套大步骤分为多个小步骤,然后可以一起执行。

渲染流水线

就是把三维场景中的内容渲染成一张二维图像。由CPU和GPU共同完成的。
这分为三个阶段:应用阶段,几何阶段,光栅化阶段

应用阶段:

这个阶段是由我们应用程序所主导的,因此通常由CPU负责实现。所以在这个阶段,我们开发者是有绝对的控制权。
三个主要任务:

  1. 准备场景数据
  2. 粗粒度的剔除工作
  3. 设置渲染状态
    场景数据:包括但不限于:摄像机位置,摄像机的视椎体,场景中的模型,光源
    粗粒度剔除工作:把那些完全不可见的物体剔除出去,这样就不需要再发给几何阶段出来,减少GPU工作。
    渲染状态设置:包括但不限制于:模型使用的材质,使用的纹理,使用的shader。
    在这个阶段重要的输出是渲染图元也就是渲染所需要的几何信息。这些图元被传递到下一个阶段-几何阶段
    三个阶段
  4. 把数据加载到显存中。
  5. 设置渲染状态
  6. 调用Draw Call
    把数据加载到显存中:渲染所有的数据都需要从硬盘加载到系统内存中,然后再把网格和纹理等数据又被加载到显存中。因为显卡对于显存的访问速度更快, 数据被加载好了以后,这些数据就可以被移除了,但是如果我们还需要做一些cpu的检测,那可以暂时不移除。
    设置渲染状态:渲染状态就是用来定义网格是被怎样渲染出来的,比如用了哪个定点着色器,用了什么光源属性,用了什么材质等等,如果我们没有更改渲染状态,那么所有的网格都讲使用同一种方法来进行渲染。
    调用Draw Call:Draw Call就是一个命令,从cpu发送到GPU,用来告诉GPU渲染哪个图元,这里面不包含任何材质信息,因为我们在前几步就已经把数据加载到了显存中了。当我们调用了Draw Call以后,GPU就会根据设置的渲染状态,和定点数据来进行计算,输出需要显示的像素。

    几何阶段

    这个阶段主要用于处理所有和我们要绘制的几何相关的事情,把从应用阶段保存到显存中的图元,在收到Draw Call指定的图元后,进行逐顶点,逐多边形的操作,在这个阶段有个最重要的任务就是把顶点坐标变换到屏幕空间中,然后再把这个坐标交到光栅器进行最后的处理。
    五个关键步骤

  7. 顶点着色器处理
  8. 曲面细分着色器处理
  9. 几何着色器处理
  10. 裁剪
  11. 屏幕映射处理
    顶点着色器
    这步是完全可编程的。当Draw Call通知需要被处理的图元时,这里就开始这一步,把图元的顶点进行变化,和着色。在这步我们是不能创建和销毁任何一个顶点,并且不能得到顶点和顶点间的关系,但是就是因为这样的相互独立性,所有我们可以利用GPU的特性进行并行化处理每一个顶点,这就意味着,处理顶点的速度将会变得很快。
    坐标转换:
    顶点着色器在这步改变顶点的位置,这个在顶点动画中是个非常有用的。需要注意的一点是,无论我们在顶点着色器中怎么改变顶点的位置,我们都需要把顶点坐标从模型空间转换到齐次剪裁空间,然后再得到归一化的设备坐标。
    曲面细分着色器
    这是一个可选的着色器,用于细分图元。
    几何着色器
    这也是一个可选着色器,可以被用来执行逐图元的着色操作,或者被用于产生更多的图元。
    裁剪
    用于裁剪那些不在摄像机视野方位内的顶点,并且剔除某些三角图元的面片,这一步是可配置的。一些图元的点有一部分在摄像机的视野外面,这个时候,我们就需要把摄像机视野外的点个裁剪掉,在和裁剪接触面来新建几个点,用来替换视野外的点。
    屏幕映射
    这一步是不可编程,也不可以配置的。这步负责把每个图元的坐标转换到屏幕坐标系中。这一步输入进来的坐标其实还是一个三维坐标,而我们的屏幕坐标系是一个二维坐标,所以我们要把图元显示出来,就得在这步来进行一次坐标转换,把三维坐标映射到屏幕坐标中,因为屏幕的坐标和屏幕的分辨率有很大的关系,所以这里将会进行一次矩阵计算。这步计算得到了顶点对应屏幕上的哪些像素以及距离这个像素有多远。这步只处理图元坐标中的x,y,对于z我们将保留原始值。只包含x,y的叫屏幕坐标系,加上一个z就是窗口坐标系。

    光栅化阶段

    把上一个阶段传递过来的数据产生屏幕上的像素,并渲染出最终的图案。主要的任务就是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一个阶段得到的逐顶点数据进行插值,然后再进行逐像素处理。
    四个步骤

  12. 三角形设置
  13. 三角形遍历
  14. 片元着色器
  15. 逐片元操作
    三角形设置
    这个阶段计算光栅化一个三角网格所需要的所有信息。从上一个阶段输入进来的是三角网格的顶点,但是当我们要计算这个三角形所覆盖的像素情况时候,我们就需要计算每个边所对应的像素坐标。所以这步的最主要的功能就是为了能输出下一阶段所需要的三角网格的数据。
    三角形遍历
    这个阶段将会检查每个像素是否被一个三角网格所覆盖,如果被这个像素被覆盖了,那就生成一个片元,这样找到所有被三角网格所覆盖的过程就叫做三角形遍历(扫描变换)。使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。这样输出的就是一个片元序列。这个片元不是真正意义上的像素,这里面包含了很多状态,这些状态就是用于计算像素最终颜色的。
    片元着色器
    这是一个很重要的可编程着色器阶段。其中很重要的一个技术就是:纹理采样。为了方便在这个阶段进行纹理采样,我们在顶点着色器阶段输出了每个顶点对应的纹理坐标。然后经过对三角网格的三个顶点对应的纹理坐标进行插值以后,就可以得到覆盖在片元上的纹理坐标。这步虽然可以完成很多重要的效果,但是它只能影响到一个片元,在这步里是不可以将自己的任何结果发送给其他片元。但是有个例外就是可以访问导数信息。
    逐片元操作
    这一阶段的主要目的就是合并。这一步也是一个高度可配置性。
    两个重要任务
  16. 决定每个片元的可见性
  17. 进行颜色合并
    在做片元可见性时候,我们需要对片元做一系列的测试。只有通过了所有的测试才可以进行颜色合并。如果没有通过测试,那么这个片元在前面所有的处理都是白费的,因为这个片元会被舍弃掉。有两个最基本的测试-深度测试和模板测试。
    模板测试
    模板测试相关的就是模板缓冲。开启以后,GPU会首先读取模板缓冲区中该片元位置的模板值,然后将该值和读取到的参考值进行比较。这个比较函数可以由开发者指定。如果片元没有通过就会被舍弃。不管片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个操作也是由开发者决定的。
    深度测试
    如果开启了深度测试,那么gpu会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是开发者可以设置的。如果没有测试通过,那么这个片元就会被舍去。这里有个地方和模板测试不一样的,就是如果没有通过测试那么这个片元就没有权利更改深度缓冲区中的值。如果通过了测试还可以通过开启/关闭深度写入来决定是否要用到这个片元的深度值覆盖掉原有的深度值。
    合并
    我们所讨论的渲染过程是一个物体接着一个物体画到屏幕上的。而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。因此,当我们片元通过了所有测试将要被渲染出来的时候,颜色缓冲区中往往会有上一次渲染后的颜色结果,那么我们这次的片元的颜色将要怎么渲染呢?是覆盖还是其他处理,这里就需要的用合并来解决了。当如果是不透明的物体,我们就可以关闭混合,这样颜色值就会覆盖掉颜色缓冲区中的颜色。但是对于半透明的物体,我们就需要使用混合来让这个物体看起来是透明的,gpu会取出原颜色和目标颜色,将两种颜色进行混合,原颜色是从片元着色器中取出来的颜色值,目标颜色是存在与颜色缓冲区中的颜色值,之后使用一个混合函数来进行混合操作。这个混合函数通常跟透明通道息息相关。
    为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲的策略。对于场景的渲染是在幕后发生的,就是后置缓冲。当场景已经被全部渲染到后置缓冲中,GPU就会交换后置缓冲区和前置缓冲区的内容。前置缓冲区就是我们所看到的图像。

    优化知识点

    一般在谈到优化,我们基本上都会考虑优化Draw Call这个属性,但是在我们这章的内容上,我们了解到Draw Call只是一个命令集,但是为什么我们要优化Draw Call的数量呢?这里我们需要考虑在发送Draw Call的之前CPU还做了什么操作,一个是加载数据,一个是设置渲染状态,加载数据的话,有人说是通用在内存上的交互,因为在Unity中我们读取AB包的时候,数据以及在内存上了,然后再从内存加载到显存上,但是在操作系统层面上,内存和显存的块都是在一个地方的,所以这步加载是没有什么消耗的。那最大的消耗就是在设置渲染状态了,看了下Unity官方的文档,Draw Call是资源密集型操作,主要开销是Draw Call之间的状态变化(列如切换到不同材质),而这种情况会导致图形驱动程序中执行密集型验证和转换步骤。所以我们减少的Draw Call的数量就是为了减少cpu上的消耗,导致卡顿。