1. 渲染流水线三个概念阶段
- 渲染流程的三个阶段以及它们之间的联系:
1-1 应用阶段
- 通常由CPU负责实现,开发者具有这个阶段的绝对控制权。
- 开发者有3个主要任务:
1. 准备好场景数据;
2. 粗粒度剔除(culling)工作;
3. 设置好每个模型的渲染状态。 - 渲染图元(renderingprimitives):是这一阶段最重要的输出。
1-2 几何阶段
- 通常在GPU上进行。
- 几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。
- 几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。
1-3 光栅化阶段
- 将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。
- 在GPU上运行。
- 光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。
2. CPU和GPU之间的通信(应用阶段)
- 渲染流水线的起点是CPU,即应用阶段。
应用阶段大致可分为下面3个阶段:
(1)把数据加载到显存中。
(2)设置渲染状态。
(3)调用Draw Call。
渲染状态
渲染状态定义了场景中的网格是怎样被渲染的。
Draw Call
Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元(primitives)列表,而不会再包含任何材质信息——这是因为我们已经在上一个阶段中完成了!。
3. GPU流水线(几何阶段、光栅化阶段)
绿色表示完全可编程控制,
黄色表示可配置,
蓝色表示由GPU固定实现,不可修改。
实线表示必须由开发者编程实现,
虚线表示该Shader是可选的。
4. 几何阶段
4-1 顶点着色器(Vertex Shader)
- 特征:
- 顶点着色器(Vertex Shader)是流水线的第一个阶段,它的输入来自于CPU。
- 顶点着色器的处理单位是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器。
- 是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。
- 主要工作:坐标变换和逐顶点光照。
4-2 曲面细分着色器(Tessellation Shader)
是一个可选的着色器,它用于细分图元。
4-3 几何着色器(Geometry Shader)
是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。
4-4 裁剪(Clipping)
- 部分在视野内的图元需要进行一个处理就是裁剪。
- 裁剪是可配置,不可编程的;
4-5 屏幕映射(Screen Mapping)
- 是不可配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。
- 屏幕映射(ScreenMapping)的任务是把每个图元的x和y坐标转换到屏幕坐标系下。
5. 光栅化阶段
5-1 三角形设置
- 是固定函数(Fixed-Function)的阶段
- 三角形设置会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。
- 一个计算三角网格表示数据的过程就叫做三角形设置。
它的输出是为了给下一个阶段做准备。
5-2 三角形遍历
- 是固定函数(Fixed-Function)的阶段
- 三角形遍历(Triangle Traversal)阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(ScanConversion)。
5-3 片元着色器(Fragment Shader)
- 是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作。在DirectX中,片元着色器被称为像素着色器(Pixel Shader)。
- 纹理采样
为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。 - 局限:它仅可以影响单个片元。
5-4 逐片元操作(Per-Fragment Operations)
- 它不是可编程的,但具有很高的可配置性。
- 逐片元操作(Per-Fragment Operations)是OpenGL中的说法,在DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。
- 主要任务:
1)决定每个片元的可见性。
2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
模板测试(Stencil Test)
- 如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值(reference value)进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。如果这个片元没有通过这个测试,该片元就会被舍弃。
- 不管一个片元有没有通过模板测试,我们都可以根据模板测试和下-面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。
- 开发者可以设置不同结果下的修改操作,例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。
- 模板测试通常用于限制渲染的区域。另外,模板测试还有一些更高级的用法,如渲染阴影、轮廓渲染等。
深度测试(Depth Test)
- 如果一个片元幸运地通过了模板测试,那么它会进行下一个测试——深度测试(Depth Test)。
- 如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是可由开发者设置的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。这是因为,我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过这个测试,该片元就会被舍弃。
- 和模板测试有些不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。
合并
- 如果一个幸运的片元通过了上面的所有测试,它就可以自豪地来到合并功能的面前。
双重缓冲(Double Buffering)
- 为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略。
- 一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲(FrontBuffer)中的内容,而前置缓冲区是之前显示在屏幕上的图像。由此,保证了我们看到的图像总是连续的。