学习目标
了解现代GPU渲染流水线的实现流程,为后面理解shader的工作原理打好基础
渲染流水线概览
一般会将整个渲染流水线分为如上图的三个阶段:
一、应用阶段 Application Stage (CPU)
主要任务:准备场景数据,设置渲染状态,输出渲染图元,为下一阶段提供几何信息
其他任务:粗粒度剔除(culling,剔除不可见物体)
图元:渲染的基本图形,如顶点,线段,三角面
输出:渲染图元
渲染所需数据加载到显存中的流程:
- 所有渲染所需数据:硬盘Hard Disk Drive → 系统内存Random Access Memory
- 网格和纹理等数据:硬盘 → 系统内存 → 显存Video Random Access Memory(显卡对于显存的访问速度更快,且大多数显卡没有直接访问RAM的权利)
具体事务:
- 准备场景数据:摄像机位置、视锥体、场景中包含的模型、光源等
- 设置渲染状态:如材质,纹理,shader等
- 调用Draw Call命令:这个命令仅仅指向一个需要被渲染的图元列表,不包含任何材质信息(刚刚设置过)
当CPU给定了一个Draw Call时(应用阶段的最后阶段),GPU就会根据渲染状态和所有输入的顶点数据来进行计算(几何阶段、光栅化阶段),最终输出成屏幕上显示的像素。
二、几何阶段 Geometry Stage (GPU)
主要任务:输出屏幕空间的顶点信息
重要任务:把顶点坐标变换到屏幕空间中,再交给光栅器进行处理
输入:模型空间的渲染图元(待绘制物体的几何数据如各种坐标)
操作对象:每个渲染图元(逐顶点、逐多边形)
输出:屏幕空间的二维顶点信息(包括深度值、着色等,也可包括UV等)
三、光栅化阶段 Rasterizer Stage (GPU)
主要任务:决定每个渲染图元中的哪些像素应该被绘制在屏幕上
输入:屏幕空间的顶点信息(位置,颜色,UV等)
操作:逐顶点数据进行插值,然后进行逐像素处理
输出:屏幕上的像素和图像
对于几何阶段和光栅化阶段,举个例子,应用阶段输出了一个三角形图元,那么接下来:
几何阶段:只得到图元顶点的相关信息,如对于三角形图元,得到的是三个顶点的坐标、颜色等信息
光栅化阶段:根据这三个点,计算出三角形覆盖了哪些像素(扫描),并插值计算出颜色(填充)
GPU流水线展开
首先大致了解一下GPU流水线的全貌:
曲面细分着色器:可用于细分图元,如将三角面细分成更小的三角面来添加几何细节
几何着色器:可决定输出的图元类型和个数,当输出的图元减少时,实际上起到了裁剪的作用;当输出的图元增多或类型改变时,起到了产生或改变图元的作用
可配置:不可以修改具体实现的方法,但是可以对一些可选项进行选择(如是混合模式是选择相加还是相减、深度测试的值满足什么条件算是通过等)
几何阶段 Geometry Stage
顶点着色器 Vertex Shader:完全可编程
处理单位:顶点
必做工作:坐标变换(MCS - WCS - VCS - CS)
可选工作:顶点坐标改变(顶点动画)、逐顶点光照、颜色计算
输入:模型空间的渲染图元(Rendering Primitives)
输出:齐次裁剪空间(Clip Space)的渲染图元(见下图)
输入元素间相互独立:无法得知点与点之间的关系(如两个点是否属于同一个三角形网格),所以GPU可以放心的大批量并行处理顶点,本阶段速度很快
处理速度:很快(点与点之间相互独立,关系不可知,因此不需考虑过多可并行化处理每一个顶点)
注:顶点着色器本身不可以创建或者销毁任何顶点
上图为OpenGL的NDC,一个[-1, 1][-1, 1][-1, 1]的空间
裁剪 Culling:可配置不可编程
首先应该明确图元和摄像机视野的关系有三种:完全在内、部分在内、完全不在内
主要任务:
1. 丢弃不在摄像机视野内的图元(Culling,直接扔)
2. 裁剪不完全在视野范围内的图元(Clipping,需生成新顶点构成新图元)
屏幕映射 Projection:
输入:三维坐标系下的坐标(范围在单位立方体内)
输出:屏幕坐标系下的xy坐标(二维) + 其他额外信息(深度值z、法线、视角方向等)
未经改变的z坐标 = 窗口坐标系下的xyz坐标(三维)
主要任务:把每个图元的x和y坐标转换到屏幕坐标系下(缩放)
注:屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远
光栅化阶段 Rasterizer Stage
三角形设置 Triangle Setting:完全不可控
输入:三角网格的顶点
输出:三角形网格边界表达式
三角形遍历 Triangle Traversal(扫描变换):完全不可控
任务:
1. 检查每个像素是否被一个三角形网格所覆盖,覆盖则生成一个片元Fragment
2. 对整个覆盖区域的像素进行插值(如深度信息、纹理坐标、法线、颜色等),得到片元
输出:片元序列
片元:每个像素都会生成一个片元,但片元 ≠ 像素,片元是包含了很多用于计算像素最终颜色的状态集合,包括屏幕坐标、纹理坐标、深度值、法线等信息
片元着色器 Fragment Shader:完全可编程
任务:逐片元计算颜色值
操作:
1. 纹理采样(需在顶点着色器输出UV、在扫描变换时插值UV)
2. 逐片元颜色计算与着色
输出:一个或者多个颜色值(即计算该片元对应像素的颜色,但不是最终颜色)
局限:仅可以影响单个片元,不可以将自己的任何结果直接发送给它的邻居们(导数信息除外)
传递导数的作用目前还没有学习到,插眼待补充
逐片元操作:可配置不可编程
操作:颜色填充,深度缓冲,颜色混合等
主要任务:
- 决定每个片元的可见性(各种测试):深度测试,模板测试、Alpha测试等
- 片元通过了所有的测试,就将它的颜色值和已经存储在颜色缓冲区中的颜色进行合并/混合
模板测试
作用:通常用于限制渲染的区域,可作为一种丢弃片元的辅助方法,与之相关的是模板缓冲。
高级用法:渲染阴影、轮廓渲染等
- 若开启模板测试,GPU会首先读取模板缓冲区中该片元位置的模板值,然后将该值和读取到的参考值进行比较,这个比较函数可由开发者指定(例如小于时舍弃该片元,或者大于等于时舍弃)
- 若这个片元没有通过这个测试,该片元就会被舍弃
- 不管一个片元有没有通过模板测试,都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的,开发者可以设置不同结果下的修改操作(例如在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等)
深度测试
任务:舍弃被遮挡的片元(深度值≥当前深度缓冲区的值)
若开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值比较。比较函数由开发者设置(大于、小于等)
如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值(注意与模板测试的差异)
若片元通过了测试,开发者可以指定是否要用它的深度值覆盖掉原有的深度值(开启/关闭深度写入)
混合
为什么需要混合:渲染过程是一个物体接着一个物体画到屏幕上的,而每个像素的颜色信息被存储在颜色缓冲区。因此,当我们执行这次渲染时,颜色缓冲区往往已经有了上次渲染之后的颜色,是使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其他处理,就是混合需要解决的问题
对于不透明物体,开发者可以关闭混合操作
对于透明物体,需要使用混合操作来让这个物体看起来是透明的
混合函数:通常和透明通道息息相关,如根据透明通道的值进行相加,相减,相乘等
需要注意的是,上面给出的测试顺序并不是唯一的,对于大多数GPU来说,它们会尽可能在执行片元着色器之前就进行这些测试(Early-Z)。但是,如果将这些测试提前的话,其检验结果可能会与片元着色器中的一些操作冲突,导致GPU无法提前执行各种测试。现代的GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突,就会禁用提前测试。但是,这样也会造成性能上的下降,因为有更多片元需要被处理了。这也是透明度测试会导致性能下降的原因。
总结
还是这张图
-
应用阶段:CPU读取数据并放入显存
准备场景数据:场景中的所有模型、相机参数等。
设置渲染状态:shader、材质、纹理、灯光等。
输出渲染图元:点、线、面。每个顶点包含位置、颜色、UV、法向量等信息。
总结:完全不用程序员操心,了解一下即可。
-
几何阶段:GPU读取数据并逐顶点变换到屏幕空间中
顶点着色器Vertex Shader:顶点变换、空间变换、光照计算、UV计算等
裁剪与消隐:GPU自动进行
总结:进行坐标变换,输出顶点信息
-
光栅化阶段:GPU经过一系列计算与测试后向屏幕输出最终颜色
片元着色器Fragment Shader:纹理采样、颜色计算、片元着色等
逐片元操作:GPU自动进行,但程序员可自行配置执行的项目(模板测试、深度测试、Apha测试、混合等)
总结:获得经过处理的顶点信息,决定最终输出到屏幕的像素颜色
傻傻分不清
光栅化 和 光栅化阶段
光栅化 ≠ 光栅化阶段
光栅化 = 三角形设置 + 三角形遍历(扫描)= 决定每个图元中的哪些像素应该被绘制在屏幕上
光栅化阶段 = 光栅化 + 片元着色器处理 + 逐片元操作 + 颜色呈现
流水线中的着色器 和 着色器代码
简单来讲,一个是原本就有的着色器,一个是我们写出来的用来配置着色器的代码,它会被传递给流水线中相应的着色器
顶点着色器 Vertex Shader
- 输入:模型空间中顶点的属性信息(空间坐标、法向量、UV坐标、颜色)、顶点shader代码
- 要点:想要处理坐标信息(空间坐标系变换、操作各种UV坐标)、计算光照颜色,把代码甩给顶点shader!
片元着色器 Fragment Shader
-
输入:必须是从顶点着色器中输出得到的(除了一些常量参数)、片元shader代码
-
要点:想要计算最终的颜色和透明度信息(RGBA),把代码甩给片元shader!如果需要借助纹理贴图,就提前把输出纹理坐标uv的代码插入顶点shader!
参考资料
解析顶点着色器和片元着色器可以更详细的了解顶点片元着色器的输入、输出、处理内容