图形学基础知识
前言
可供学习用的文章和书记
电子书《《Real-Time Rendering 3rd》提炼总结》
游戏引擎架构(第二版)
渲染管线
参考自以下文章。本文会在此次数上在进行一次提炼
【《Real-Time Rendering 3rd》 提炼总结】(二) 第二章 · 图形渲染管线 The Graphics Rendering Pipeline
Real-Time-Rendering中将渲染管线划分为应用阶段,几何阶段以及光栅化阶段
应用阶段
应用阶段是软件控制的阶段,是开发者可以完全控制的阶段。在此阶段可以实现各种加速算法来为GPU减轻计算负担,其中一种是层次视锥体裁剪。应用阶段末端会通过一次DrawCall将绘制图元的信息发送给GPU,之后的几何阶段以及光栅化阶段将会在GPU上执行
CPU准备绘制所需要的顶点,纹理等信息发送给GPU并通知其进行绘制的过程,称作一次DrawCall
几何阶段
在RTR中,几何阶段又可以划分为以下几个步骤
-
模型视点变换:Movel and View Transform。Model将顶点从局部坐标系转换为世界坐标系;ViewTransform将相机放在原点上,并调整相机朝向,使其看向-Z方向,Y轴指向上方
-
顶点着色:着色阶段可不仅仅只会在片元着色器中进行
-
投影:分为正交投影和透视投影(具有近大远小的效果)两种,此步骤只会进行投影矩阵变换,不会进行透视除法
-
裁剪:此裁剪为小粒度的裁剪,裁剪的结果将会决定哪些顶点会进入光栅化阶段。裁剪阶段会生成新的顶点(裁剪是在3D坐标系中进行的)
-
屏幕映射:将裁剪坐标转变为屏幕坐标,完成了3D到2D的转化。尽管此时物体都被映射到同一个平面上,但此时物体并没有前后的关系,具体的遮挡剔除需要等到深度测试时再执行。屏幕映射所执行的操作可以简单理解为:
- 忽略Z轴坐标
- 通过拉伸将单位正方体变换成长宽为屏幕窗口大小的矩形
光栅化阶段
目前经过屏幕映射,GPU拥有多个处在屏幕空间坐标系中的顶点,同时还拥有这些顶点的深度信息(Z值)和各种着色信息,那么光栅化的目的就是完成这些顶点到屏幕上像素的转化
-
三角形设定:在硬件上执行,不可控。该阶段计算了三角形的各种重要数据(三角形边的方程,深度值等等),供三角形遍历阶段使用
-
三角形遍历:在硬件上执行,不可控。功能是找出有哪些采样点在三角形中,并对这些采样点进行各种属性的插值(透视投影矫正插值就是在该阶段执行的)。由采样频率导致的走样问题可以用抗锯齿技术解决,如常见的MSAA
-
片元着色器:完全可编程,纹理贴图,光照模型,阴影处理等都是在此阶段进行的
-
逐片元操作/测试合并阶段:可配置的阶段。各种混合和测试操作都是在这一阶段进行的,如裁剪测试,透明测试,模板测试,深度测试和色彩混合等
可编程性
对一个最基础的Vertex Shader来说,会完成几何阶段中的模型视点变换和投影两个操作,然后硬件会对变换后的结果进行裁剪和屏幕映射
各个坐标空间
Unity Shader入门精要:P73
在图形学中,坐标空间分为以下几种
- 模型空间:即Local Space
- 世界空间:即World Space
- 观察空间:Unity一个以摄像机为原点,摄像机朝向为-Z,上方为+Y,右方为+X的方向的坐标空间
- 裁剪空间:用于执行这个变换的矩阵叫做投影矩阵。但是为什么要叫做裁剪空间呢,因为在完成投影变换后执行的裁剪操作,就是在这个坐标空间下进行的
- 屏幕空间:经过屏幕空间变换后,我们将能够得到顶点在屏幕上的位置
世界坐标系转换为观察坐标系
假设在Unity中,摄像机的位置是
- Position(0, 10, -10)
- Rotation(30, 0, 0)
那么为了将摄像机变换到原点,需要进行“逆”变换,即先将相机平移至原点,然后进行旋转
对向量进行一次变换矩阵的矩阵乘法,即可得到位于观察坐标系下的坐标
观察坐标系转换为裁剪坐标系
一个比较直观而且简便的理解方式
如果对这种简便的理解方式不满意,具体的数学计算流程可以参照
Unity Shader入门精要:P77
裁剪坐标系转化为屏幕空间坐标系
首先非常重要的一点:真正的投影操作是在完成裁剪操作后执行的
透视除法
所以为了将顶点从裁剪空间转换到屏幕空间,我们第一步要进行透视除法。其实就是将w归一,用齐次坐标系的w分量去除x,y,z分量,得到的结果我们称作归一化的设备坐标(NDC)。在OpenGL中,这一步会将顶点们变换到一个xyz分量范围都是[-1, 1]的立方体中
对于透视投影来说,经过变换后的坐标位置长这样
对于正交投影来说,经过变换后的坐标位置长这样
平移和拉伸
然后第二步就是进行平移和拉伸操作,与Games101课程中提到的一致。这一步仅仅会对xy分量做变换操作,z值保持不变,它会被用于深度缓存,而w分量则会被用于透视矫正插值中
至此我们就能获得每个顶点对应到屏幕上的位置了
渲染前CPU和渲染中GPU的裁剪和剔除(待更新)
可能由于翻译或是其他问题,导致剔除和裁剪这两个词常会被混淆,因此我们只需要分清这些操作是CPU执行还是GPU执行,是在哪个阶段执行的即可
应用阶段CPU会进行一次剔除,叫做视锥体剔除,英文名叫做Culling。这一阶段CPU将不与视锥体交叉的物体剔除掉,不会传递至几何阶段。注意此时剔除的粒度大,是在Object层面的,会对物体的包围盒进行检验。如下图中的AD会被剔除。BCE会被传入到GPU中进行下一步操作
![](https://img2020.cnblogs.com/blog/2162360/202112/2162360-20211201221800507-567780297.png)
透视校正插值
当经过屏幕空间变换后,3D空间中的物体被映射到2D的屏幕上,之后就可以在光栅化阶段前期根据重心坐标进行属性的插值
这里以深度插值为例,假设对A'和B’而言有一个采样点位于中点位置,那么该采样点的值就应该是A‘与B’的平均。但是在3D空间中,它们所对应的点却明显不是中点位置,而是深度应该更“近”一些,那么普通的插值结果就是不准确的(假设摄像机采用的是透视投影)
解决方案1:逆变换
由Games101中提到的,重心坐标不能确保透视投影后保持不变(正交投影重心坐标保持不变),因此可以对2D平面上的物体执行一次逆变换,然后在3D平面上求重心坐标,最后进行插值
解决方案2:透视矫正插值
对于深度插值来说,推导如下。本推导仅参照数学过程,忽略摄像机空间的-Z朝向
因此在光栅化阶段,只需要取出顶点对应的深度信息(即ZA或ZB),然后取倒数,最后利用重心坐标进行插值就能得到正确的结果
那么能将深度插值到正确的结果后,其他属性的插值也能在此基础上推导出来了,以颜色为例
裁剪测试与模板测试
Alpha-Test
Alpha-Test发生在什么时间?Alpha-Test执行什么样的工作?
Z-Test和Early-Z
GPU的渲染管线:
深度测试发生在fragment shader之后,因此可能会出现在fragment shader中耗时耗力进行的着色,被深度测试抛弃的情况。因此可以采用Early-Z技术,在fragment shader之前就先初步筛选一次
Early-Z(Early-Z-Test)的步骤与Z-Test是相同的,也是会有一个Early-Z-Buffer然后进行深度比较操作,是硬件厂商实现的技术
但是Early-Z也有不小的局限性
-
如果手动修改GPU插值后的深度值,或是开启了Alpha Test,那么在这种情况下用Early-Z技术是不准确的,必须老老实实用Z-Test,即等到光栅化和着色后才决定是否抛弃像素
-
Early-Z是不稳定的,当渲染顺序是从远往近处渲染时,Early-Z将不会带来任何优化效果。
众所周知,绘制的顺序并不会影响深度测试的结果,但是对于Early-Z而言,如果CPU给GPU传递的顶点信息恰好是从远往近排列的,那么Early-Z对最远处的fragment进行测试,结果是通过,直到最近的fragment,测试的结果一直都会是通过,它们都将会执行片元着色器,但是最后进行Z-Test的时候只有最近的fragment会被保留,此时Early-Z将没有任何优化效果
相反的,如果此时渲染的顺序是从近到远,那么Early-Z的优化效果将会达到最大
那么如何让渲染顺序从近到远排列呢,在CPU中进行排序然后再发送给GPU显然是不现实的,这样不仅会消耗大量的CPU性能,而且也无法进行合批优化
因此Z-Prepass就派上了用场,Z-Prepass要搭配Early-Z使用,以Unity Shader为例。可以在Shader中设置两个Pass。第一个Pass仅写入深度(其实就是生成了一张Z-Buffer),不进行像素着色(但是仍然会跑vertex shader);第二个Pass不写入深度(即不生成Z-Buffer),但是会在fragment shader之前利用第一个Pass产生的Z-Buffer进行深度测试,由于之前生成的Z-Buffer中记录的已经是最近的深度值,因此只需要进行一个相等的比较,让通过的fragment执行fragment shader
参考自:
Unity Shader (三)深度测试(depth test)