第二章 Shader和渲染管线
2.1 综述
要学会怎么使用 Shader, 我们首先要了解 Shader是怎么工作的。 实际上, Shader仅仅是渲染流水线中的一个环节,想要让我们的 Shader发挥出它的作用,我们就需要知道它在渲染流水线中扮演了怎样的角色。而本节会给出简化后的渲染流水线的工作流程。
2.1.1 什么是流水线
2.1.2 什么是渲染流水线
《Real-Time Rendering, Third Edition》[I] 一书中将一个渲染流程分成3个阶段: 应用阶段 (Application Stage)、 几何阶段(Geometry Stage)、 光栅化阶段(Rasterizer Stage)。
注意, 这里仅仅是概念性阶段, 每个阶段本身通常也是一个流水线系统, 即包含了子流水线阶段。
应用阶段
- 顾名思义,此阶段是由开发者主导的.
这个阶段最重要的输出是渲染所需的几何信息, 即渲染图元(rendering primitives).
渲染图元可以 是点、 线、 三角面等。 这些渲染图元将会被传递给下一个阶段 —— 几何阶段.
这个阶段的三个主要任务:
- 准备场景数据
- 粗粒度剔除(可以剔除不可见物体,提升渲染性能)
- 输出渲染图元.
几何阶段
这一阶段通常在 GPU 上进行,决定绘制的图元,怎么绘制,在哪里绘制.
几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作.
光栅化阶段
- 这一阶段通常在GPU上进行,使用上个阶段传递的数据来产生像素,渲染最终图像。
光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕 上。它需要对上一个阶段得到 的逐顶点数据 (例如纹理坐标 、顶点颜色等)进行插值,然后再进 行逐像素处理。
读者需要把上面的 3个流水线阶段和我们将要讲到的GPU流水线阶段区分开来。
这里的流水线均是概念流水线,是我们为了给一个渲染流程进行基本的功能划分而提出来的。下面要介绍的GPU流水线, 则是硬件真正用于实现上述概念的流水线。
2.2 CPU和GPU之间的通信
渲染流水线的起点是CPU, 即应用阶段。应用阶段大致可分为下面 3 个阶段 :
- 把数据加载到显存中。
- 设置渲染状态。
- 调用 Draw Call (在本章的最后我们还会继续讨论它).
2.2.1 把数据加载到显存中
所有渲染所需的数据都需要从硬盘 (Hard Disk Drive, HDD) 中, 加载到系统内存(Random AccessMemory, RAM)中,然后网格和纹理等数据又被加载到显存(Video Random Access Memory, VRAM)中。因为显卡对于显存的访问速度更快,且对于RAM无直接访问权限。
需要注意的是, 真实渲染中需要加载到显存中的数据往往比上图所示复杂许多。 例如, 顶点的位悝信息、 法线方向、 顶点颜色、 纹理坐标等。
当把数据加载到显存中后, RAM 中的数据就可以移除了。 但对于一些数据来说, CPU 仍然需要访问它们(例如, 我们希望 CPU 可以访问网格数据来进行碰撞检测), 那么我们可能就不希望这些数据被移除, 因为从硬盘加载到 RAM 的过程是十分耗时的。
在这之后, 开发者还需要通过 CPU 来设置渲染状态, 从而 “ 指导 “GPU 如何进行渲染工作。
2.2.2 设置渲染状态
通过CPU设置渲染状态,指导GPU如何进行渲染工作。
渲染状态定义了场景中的网格是怎么被渲染的,如片元着色器(Fragment Shader),顶点着色器(Vertex Shader),光源属性,材质等。
当没有更改渲染状态的时候,所有网格都使用一种渲染状态,看起来像是同一种材质。
在准备好上述所有工作后, CPU 就需要调用 一个渲染命令来告诉 GPU: “嘿!老兄, 我都帮 你把数据准备好啦, 你可以按照我的设置来开始渲染啦!” 而这个渲染命令就是 Draw Call。
2.2.3 调用Draw Call
Draw Call是一个命令,发起方为CPU,接收方GPU。一次DC命令指向一个需要被渲染的图元列表(primitives)。而不会再包含任何材质信息–这是因为我们已经在上一个阶段中完成了.
GPU会根据渲染状态和所有输入的顶点数据进行计算,生成像素。这就是GPU流水线。
当给定了一个DrawCall时,GPU就会根据渲染状态(例如材质、 纹理、 若色器等)和所有输入的顶点数据来进行计算, 最终输出成屏器上显示的那些漂亮的像素。 而这个计算过程, 就是下一节要讲的GPU 流水线。
2.3 GPU流水线
当 GPU 从 CPU 那里得到渲染命令后,就会进行一系列流水线操作,最终把图元渲染到屏幕上。
2.3.1 概述
GPU渲染过程就是GPU流水线。
对几何阶段和光栅化阶段开发者无法拥有绝对控制权,实现载体为GPU。
几何阶段和光栅化阶段可以分为若干的流水线阶段,由GPU实现,每个阶段都提供了不同的可配置性或可编程性。
GPU的渲染流水线实现, 颜色表示了不同阶段的可配置性或可编程性, 上图解释如下:
- 绿色表示该流水线阶段是完全可编程控制的.
- 黄色表示该流水线阶段可以配置但不是可编程的.
- 蓝色表示该流水线阶段是由 GPU 固定实现的,开发者没有任何控制权.
- 实线表示该 Shader 必须由开发者编程实现.
- 虚线表示该 Shader 是可选的.
从图中可以看出 , GPU 的渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由 Draw Call 指定的。这些数据随后被传递给顶点着色器。
几何阶段
- 顶点着色器 (Vertex Shader) 是完全可编程的,它通常用于实现顶点的空间变换 、 顶点着色等功能。
-
曲面细分着色器 (Tessellation Shader) 是一个可选的着色器,它用于细分图元。
-
几何着色器 (Geometry Shader) 同样是一个可选的着色器,它可以被用于执行逐图元 (Per-Primitive) 的着色操作,或者被用于产生更多的图元。
-
裁剪 (Clipping), 这一阶段的目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片。这个阶段是可配置的。 例如,我们可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪 三角图元的正 面还是背面。
-
屏幕映射 (ScreenMapping)。这一阶段是不可配置和编程的,它负责把每个胆元的坐标转换到屏幕坐标系中。
光栅化阶段
- 光栅化概念阶段中的三角形设置 (Triangle Setup) 和三角形遍历 (Triangle Traversal) 阶段 也都是固定函数 (Fixed-Function) 的阶段。接下来的片元着色器 (FragmentShader), 则是完全可编程的,它用于实现逐片元 (Per-Fragment) 的着色操作。最后,逐片元操作 (Per-Fragment Operations) 阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它不是可编程的,但具有很高的可配置性。
接下来,我们会对其中主要的流水线阶段进行更加详细的解释。
2.3.2 顶点着色器
顶点着色器 (Vertex Shader) 是流水线的第一个阶段,它的输入来自于 CPU。顶点着色器的处理单位是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系。例如,我们无法得知两个顶 点是否属于同一个三角网格。但正是因为这样的相互独立性, GPU 可以利用本身的特性并行化处理每一个顶点 ,这意味着这一阶段的处理速度会很快。
顶点着色器需要完成的工作主要有:
-
坐标变换
-
逐顶点光照
-
除了这两个主要任务外,顶点着色器还可以输出后续阶段所需的数据。
坐标变换
- 顾名思义,就是对顶点的坐标(即位置)进行某种变换。顶点着色器可以在这 一步 中改变顶点的位置,这在顶点动画中是非常有用的。可以通过改变顶点位 置来模拟水面、布料等。
顶点着色器最基本必须完成的一个工作
-
把顶点坐标从模型空间转换到齐次裁剪空间. o .pos = mul(UNITY_MVP, v.position); 在由硬件做透视除法后最终得到归一化的设备坐标 (NormalizedDevice Coordinates , NDC)
上图给出的坐标范围是 OpenGL 同时也是 Unity 使用的 NDC, 范围在 [-1, 1]之间,而在 DirectX 中, NDC 的 z 分量范围是 [0, 1]。
2.3.3 裁剪
2.3.4 屏幕映射
2.3.5 三角形设置
2.3.6 三角形遍历
2.3.7 片元着色器
2.3.8 逐片元操作
2.3.9 总结
2.4 一些容易困惑的地方
2.4.1 什么是OpenGL/DirectX
2.4.2 什么是HLSL、GLSL、CG
2.4.3 什么是Draw Call
2.4.4 什么是固定管线渲染