计算机图形渲染管线/光栅化-片元着色器【TA技术美术必修基础知识篇】第三期/共四期...
TA教程渲染Game艺视界原创 15526 2
实名

通过了实名认证的内容创造者

发布于 2022-3-23 15:18:34

您需要 登录 才可以下载或查看,没有账号?注册

x
b64e78dc188da7f93a16111abbb1eb9.png

注:
01-目录下高亮显示为本期内容
02-有些概念不同文章解释有些不同本质一样,所以同时记录两种解释为:理解二
目  录
1.1综述
1.1什么是渲染流水线(概念流水线)1.2渲染的流程
2.2渲染流水线概览2.2.1应用阶段=CPU读取数据并放入显存------准备场景数据、粗粒度剔除、数据加载到缓存
(延伸阅读):什么是前向渲染、什么是延迟渲染
(延伸阅读):什么是Draw Call
                    如何优化Draw Call
2.2.2几何阶段=GPU读取数据并逐顶点变换到屏幕空间中
------顶点着色器:(坐标变换、正交投影、透视投影、着色)
------几何着色器
*2.3.3片元着色器Fragment Shader
------Blinn-Phong光照模型
------锯齿和抗锯齿(超级采样、多重采样)
*2.3.4逐片元操作(测试和混合阶段)
------Alpha测试
------模板测试
------隐藏面消除
------入口剔除
------Alpha混合
------颜色空间
------抖动处理
文章正文
2.3光栅化阶段
这一阶段会使用上个阶段传过来的数据来产生屏幕上的像素,并渲染出最终的图像。光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上个阶段得到的逐顶点数据进行插值,再逐像素操作。
三角形设置(Traingle Setup)和三角形遍历(Traingle Travel):这两个阶段是固定函数阶段。
  • 三角形设置
光栅化的第一个流水线阶段是三角形设置(Triangle Setup)。这个阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。它的输出是为了给下一个阶段做准备。
理解二:
三角形组装 (Triangle Setup)
三角形组装会对顶点的输入数据(比如,颜色、法线、纹理坐标)进行插值,得到各个片段对应的数据值,为后面的片段着色提供片段数据。下图所示就是根据顶点输入的颜色值和法线进行插值得到的各个片段的颜色和法线,用于后续的计算。这里对法线的插值进行了可视化处理。不过我们这里只是进行一个简单的示范,一般情况下,我们很少去插值颜色值,通常都是利用片段着色器对片段进行着色。

对每个顶点的属性进行插值以确定每个片段的属性。如果将颜色分配给顶点,则它们将在整个三角形中进行插值。下图显示了颜色内插三角形示例。

60993148ba281e5d3986cae834b62ba3.png


然而,实际上,每个顶点的属性很少包括颜色。一般来说,顶点法线和纹理坐标是内插的。下图显示了插值法线,其中每个顶点法线(蓝色)首先沿三角形边缘(绿色)进行插值,然后沿扫描线(红色)进行插值。这些每个片段的属性被传递给片段着色器。
  • 三角形遍历
三角形遍历(Triangle Traversal)阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。
三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。图2.12展示了三角形遍历阶段的简化计算过程。
【图2.12三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。例如,对图2.12中三个顶点的深度进行插值得到其重心位置对应的片元的深度值为-10.0】
这一步的输出就是得到一个片元序列。需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。
理解二:
三角形遍历(Triangle Traversal)
其实三角形遍历的操作我们在前面基本都说过了,通过屏幕空间的坐标组装三角形后,我们遍历这些三角图元覆盖了哪些片段的采样点,随后得到该图元所对应的片元。接下来我们通过顶点的输入数据插值获取片段的数据属性,包括颜色、法线、纹理坐标、深度等信息。对于透射投影,我们需要用到透射校正插值(Perspetive-Correct-Interpolation)来正确插值片段的颜色、纹理等信息。
延伸阅读
     线段扫描转换
这部分我们讨论在屏幕绘制线段的方法。线段是最简单的图元,是绘制其他复杂图元的基础。我们从最简单的数字微分画线算法DDA开始讨论扫描转换的方法。然后介绍更加高效绘制线段的Bresenham光栅化算法。讨论完线段的绘制方法后,我们简单介绍多边形填充算法。这里主要介绍最简单最常见的扫描线填充算法!
  • 数字微分画线算法DDA (Digital Differential Analyzer)

假定一条线段的两个端点分别是(x1,y1)和(x2,y2),斜率m满足:0<=m <= 1。其实这里假定斜率m只是为了方便讨论,我们会在Bresenham算法中讨论如何对一般情况下的m进行处理。当x从x1变化到x2时,y从y1变化到y2。也就是说XY满足如下的增量关系:
假定x的增量为1,那么y的增量为m。虽然增量为整数,但是由于斜率m为浮点数,所以导致y的值不是整数,为了找到合适的像素位置,我们需要对y的值进行取整操作。DDA算法的伪代码如下:
我们把斜率最大限制为1的原因可以从下面左边的图看到。DDA算法的基本思想是对于每个x值,算出一个最佳位置的y值。对于斜率大于1的线段,由于两个像素点之间的间隔过大,生成近似的线段产生了较大的误差。对于斜率大于1的线段,我们可以通过交换x和y,即该算法变为:对于每个y值,计算一个最佳的x值,我们可以从下面右边的图看到对应的结果。

  • Bresenham光栅化算法

DDA算法虽然很简单,编码也非常容易实现,但是由于每生成一个像素都需要一次浮点数的加法运算,效率较低。我们接下来介绍的Bresenham算法可以避免浮点数的运算,只需要通过整数的加法、减法和移位操作,效率很高,所以它已经成为硬件和软件光栅处理器的标准算法。Bresenhan算法与DDA算法的主要区别在于构造了一个新的判定变量d来避免浮点数的运算,即:

  • 多边形填充算法

介绍完线段扫描算法后,我们接下来看多边形的填充算法,相关的算法有很多,比如:奇偶填充、种子填充和扫描线填充算法。我们这里介绍使用最多最常见的扫描线填充算法。·
  • 扫描线填充算法(Scanline Filling)

扫描线算法根据扫描线与多边形的交点来对多边形进行颜色填充。需要注意的是,当多边形的顶点与扫描线相交时,需要对交点进行取舍来保证正确的配对。我们可以通过检查与顶点相邻边的端点Y值来确定顶点的取舍,如下图(c)所示,扫描线y = e 与三角形的顶点P1相交。我们发现P1相邻边顶点PO和P2的Y值都大于P1的Y值,所以需要舍弃当前相交的顶点,保证顶点能够进行正确的两两配对!
在扫描线算法中,我们发现扫描线其实只与某些边存在交点,所以我们其实无需对每条边都进行求交的计算。通过使用活性边表(Active Edge Table,AET)这种数据结构可以对该算法进行优化。对于下面的多边形相交测试,我们可以使用该AET链表进行表示,其中每条边Edge的数据结构定义如下:







片段着色器(Fragment Shader) 可编程着色器阶段
片元着色器(Fragment Shader)是另一个非常重要的可编程着色器阶段。在DirectX中,片元着色器被称为像素着色器(Pixel Shader),但片元着色器是一个更合适的名字,因为此时的片元并不是一个真正意义上的像素。
前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作(Per-Fragment Operations)。我们随后就会讲到
片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值。图2.13显示了这样一个过程。
这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了
【图2.13-根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色】
虽然片元着色器可以完成很多重要效果,但它的局限在于,它仅可以影响单个片元。也就是说,当执行片元着色器时,它不可以将自己的任何结果直接发送给它的邻居们。有一个情况例外,就是片元着色器可以访问到导数信息(gradient,或者说是derivative)

phong光照模型
计算光照最常用的模型就是大名鼎鼎的phong模型了,该模型其实是经验模型,参数信息都是经验得到的,并没有实际的物理意义,所以利用Phong模型会出现违背物理规则的时候。Phong模型将物体光照分为三个部分进行计算,分别是:漫反射分量、镜面高光和环境光。其中,环境光分量是用来模拟全局光照效果的,其实就是在物体光照信息基础上叠加上一个较小的光照常量,用来表示场景中其他物体反射的间接光照

漫反射表示的是光线进入物体内部后重新散射出来的那部分光线,简单起见我们会认为重新散射出来的光线是均匀分布的,如下图所示。因此,无论观察者从哪个方向进行观察,漫反射效果其实是一样的,所以我们认为漫反射和观察位置是无关的。漫反射分量通常利用朗伯余弦定律(Lambert Consine Law)来计算,也就是说漫反射的大小取决于表面法线和光线的夹角。当夹角越大时,漫反射分量越小,当夹角接近90度时,我们认为漫反射几乎为零

说到朗伯余弦定律,我们不得不说下半朗伯模型(Half Lambert)。该光照模型是有Valve公司在制作半条命游戏时发明的,由于改进物体较暗区域的光照信息。如下图所示,右边的图是使用半朗伯模型得到的效果。我们可以明显的看到人物被照亮了!半朗伯模型的代码只需要在原来的代码加上float hLambert = difLight * 0.5 + 0.5;一行代码就可以了,其实该代码就是将之前的漫反射系数从[0,1]变到[0.5,1],所以提升了漫反射的亮度信息

镜面反射表示光线照射到物体表面后被重新反射的现象,镜面反射遵循反射定律。我们生活中发现金属表面会有高光的现象,就是由于金属对光线有较高的反射率,给人一种金属感,通过镜面反射我们可以模拟金属和非金属物质对光照的反射程度。我们在日常生活中其实也可以发现,高光跟我们观察的方向是有关系的,我们在描述高光性质时需要知道观察者位置信息

Blinn-Phong光照模型
下面我们介绍另一种光照模型:Blinn-Phong光照模型。Blinn-Phong模型是对我们上面讲到的Phong模型的改进,Phong模型在处理高光时会出现光照不连续的情况。我们知道高光跟观察位置密切相关,当观察方向和反射光线夹角大于90度时(如上图所示),Phong模型会出现镜面反射分量被消除的情况,所以出现高光不连续的想象,如下图的第一种图所示。我们可以通过Blinn-Phong模型来对它进行改进,下面两张图对比了这两种模型在处理高光时的差异

Blinn-Phong模型在处理镜面反射时不使用观察方向和反射光线的夹角来计算,而是引入了一个新的向量:半角向量(Halfway vector)。半角向量其实很简单,就是光线向量L和观察方向V的中间位置。Blinn-Phong模型计算的就是半角向量H和平面法线的夹角(当视线和反射向量对齐时,有最大的镜面反射),这样无论观察者在那个方向进行观看,半角向量和法线夹角都不会超过90度,不会出现上面说的高光不连续的问题,光照效果更佳真实

纹理贴图 (Textures)
纹理贴图也成为纹理映射,是将图像信息映射到三角形网格上的技术,以此来增加物体表面的细节,令物体更具有真实感。纹理技术有很多,最常见的是凹凸贴图(bump mapping)、法线贴图(normal mapping)、高度纹理(height mapping)、视差贴图(parallax mapping)、位移贴图(displacement mapping)、立方体贴图(cubemap)、阴影贴图(shadowmap)。凹凸贴图、法线贴图等技术都是利用光照信息的明暗对比来产生视觉错误。纹理贴图是片段着色器的主要操作,通过贴图技术可以实现很多高级的效果。我们将贴图上的每个像素成为纹素(texel,纹理像素texture pixel的意思,用于和像素进行区分),纹理映射其实就是进行纹素和像素对应的过程

我们一般使用一个二维的坐标(u,v)来表示纹理坐标,其中u是横坐标,v是纵坐标,因此纹理坐标一般也被称为UV坐标。UV坐标一般被归一化到[0,1]之间,但是如果UV超出这个范围,我们就需要指定纹理坐标的寻址方式,也叫作平铺方式。常见的寻址方式有:重复寻址(repeat)、边缘钳制寻址(clamp)和镜像寻址(mirror)。不同的寻址方式说明了UV超出[0,1]时如何访问纹理数据的。下图展示了Unity3d中纹理的重复寻址和钳制寻址方式。

除了寻址方式外,纹理的采样方式也会觉得最终的显示效果。由于纹素和像素通常不是一一对应的,所以我们需要决定像素所对应的纹素信息时,需要用到纹理的滤波方式。常见的滤波方式有点过滤(point)、线性过滤(linear)、最近领点过滤(nearest neighbor point)和双线性过滤(bilinear)。Unity中还有Trilinear滤波的技术,该滤波和Bilinear差不多,只不过会在多级渐近纹理(mipmapping)之间进行混合。纹理的多级渐进技术是为了解决纹理缩小时产生所谓的摩尔纹现象,由于远处的物体并不需要很高的精度,所以我们在对远处物体进行采样时,会使用分辨率更低的纹理贴图,这就是mipmapping的思想

  • 法线贴图 (Normal Mapping)
纹理常见的一种应用是凹凸映射(bumping mapping),该技术由Blinn引入的。凹凸映射一般有两种方式:一种是使用高度纹理(height map),利用高度图来修改表面的法线,这种方式也被称为高度映射(height mapping);还有另一种方式是使用法线贴图(normal map),这种方式一般称为法线映射(normal mapping),该技术由Peercy引入的,法线贴图直接存储的就是物体表面的法线,而高度贴图需要计算物体表面的法线扰动信息,所以说使用法线贴图会比使用高度贴图有更高的性能。尽管我们一般认为凹凸映射和法线映射是相同的技术,但是还是需要知道它们之间的区别

法线贴图一般有物体空间(object space)和切线空间(tangent space)两种。它们的区别在于法线存储的坐标空间不同。物体空间的法线贴图是相对于物体的坐标原点进行存储的;而切线空间的法线贴图是相对于顶点坐标进行存储计算的。我们可以发现:当物体进行旋转移动操作时,物体空间的法线贴图会得到错误的光照信息,也就是说该法线贴图不具备一般性,开发人员无法重复利用同一份贴图数据,所以一般采用的是切线空间进行存储计算。我们发现物体空间法线贴图偏向于五颜六色,因为发现相对于物体的朝向是随机的,而切线空间的法线贴图是淡蓝色的,因为Z轴总是朝向(0, 0, 1),经过映射后得到的RGB为浅蓝色
切线空间如上图所示。我们会根据顶点本身的法线(N)、切线(T)和副切线(B)三个信息来构建切线空间。其中,法线总是垂直于顶点所在表面向上,通过切线和法线我们可以获得副切线的信息。所以,一般我们都会使用纹理压缩技术来减少纹理的内存开销,常见的纹理压缩技术有:DXT1、DXT5和3Dc

此外,我们还需要考虑采样发现贴图后计算光照的坐标空间。我们既可以在世界空间中进行光照计算,也可以在切线空间中进行光照计算,那么我们一般在那个空间计算呢?如果我们在切线空间下计算光照,我们需要将光照方向和观察视角转换到切线空间;如果我们在世界空间下进行光照计算,我们需要将采样到的法线方向转变到世界空间下。从效率上来说,第一种方法比第二种方法要高,因为我们在顶点着色器就可以完成光照方向和观察方向的转换,而第二种方法需要我们将采样到的法线方法变换到世界空间,需要在片段着色器进行矩阵乘法。从通用性来说,第二种方法要优于第一种方法,因为我们有时候需要在世界空间下进行一些计算,例如在使用环境贴图映射时,我们需要使用世界空间下的反射方法对Cubemap进行采样。

锯齿和抗锯齿 (Aliasing and Anti-aliasing)
3D场景中的物体是连续的,而2D屏幕空间的像素点是离散的,当我们使用离散的像素点来表示连续的物体时,丢失了物体连续的信息,导致锯齿的产生。我们从前面光栅化阶段已经知道了一个片段是如何产生的,它是抗锯齿技术的基础,接下来主要介绍超级采样和多重采样抗锯齿两种技术。
  • 超级采样抗锯齿 (Super-Sampling Anti-aliasing)
最直接最好的抗锯齿方法就是SSAA,拿4xSSAA来说,假设屏幕分辨率为800x600,那么4xSSAA会将屏幕渲染到1600x1200的缓冲区上,然后在下采样到800x600,SSAA可以得到非常好的抗锯齿效果,不过SSAA需要的计算量是非常大的,光栅化和片段着色器都是原来的4倍,渲染缓存的大小也是原来的4倍。
  • 多重采样抗锯齿 (Multi-Sampling Anti-aliasing)
在前面的光栅化阶段,我们知道每个片段都有一个采样点,决定一个片段是否被三角面覆盖的方法就是看是否覆盖了该采样点。在MSAA中我们会使用多个采样点来决定覆盖率的问题(Coverage)。MSAA的原理其实和SSAA差不多,不过在光栅化阶段计算三角面是否覆盖了片段的每个采样点,得到采样点覆盖率的数值,接下来在片段着色器计算这个片段的颜色值(只计算一次),然后最终的颜色会乘上这个覆盖率。以下图第二个图的4xMSAA为例,我们发现三角形覆盖了4个采样点中的两个,所以最终片段得到的颜色值需要乘以采样点覆盖率0.5,得到的是浅红色的颜色。常见的采样模式(sampling schemes)如下图所示。MSAA的主要问题在于它跟延迟渲染(Deferred Rendering)并不兼容,因为延迟渲染需要Geometry和Lighting两个pass,在光照阶段无法通过GBuferf获取片段的覆盖率信息

上面讲到的MSAA计算方式在下面这种情况下会出现问题。我们这里使用的是4xMSAA,但是我们在片段着色器中利用的是片段的中心位置(图中的中央的圆点)作为采样点来决定最终的颜色,但是我们发现此时三角面是没有覆盖到该采样点,所以最终输出的颜色时错误的。此时我们会使用重心采样(centroid sampling)来解决这个问题。重心采样是由GPU自动执行的,GPU通过覆盖的采样点的来确定采样的重心点,以此避免上面提到的采样点位于三角面外部的问题。


除了我们上面提到的SSAA和MSAA之外,还有很多其他的抗锯齿技术,比如:覆盖采样抗锯齿(CSAA)、可编程过滤抗锯齿(CFAA)、快速近似抗锯齿(FXAA)、时间样本抗锯齿(TXAA)、多帧采样抗锯齿(MFAA)、形态抗锯齿(MLAA)、增强型抗锯齿(EQAA)、亚像素形态抗锯齿(SMAA)。

阴影 (Shadows)
阴影和光是这个世界的一体两面,有光的地方就会产生影子。阴影对于渲染真实感来说非常重要,它给出了物体在3D世界中的位置信息。对于静态的物体,我们可以使用Lightmap烘焙的方法来获取物体的影子,而对于动态的物体,一般采用的是Shadowmap的技术。该技术其实是一种渲染到纹理的技术,我们得到的这张贴图一般称作阴影贴图。Shadowmap的原理非常简单,首先是从光源的位置渲染一遍场景,将得到的深度信息写入到贴图到,然后再一次正常的渲染场景,利用我们得到的shadowmap来判断哪些片段落在了阴影中

在下面的左图中,我们从光源的角度看,像素p的深度值为d(p),但是从阴影贴图中我们发现离光源最近的像素深度值其实是s(p),而且d(p) > s(p)。所以我们可以得到像素p之前有物体挡住了它,因此p点落在了阴影当中。右图中,我们发现像素p的深度值d(p)=s(p),因此像素p没有落在阴影中

由于shadowmap精度的问题,阴影会出现阴影粉刺(shadow acen)。对于阴影粉刺我们可以通过偏移量对阴影贴图中的深度值进行调整,我们发现调整前平面会有某些像素深度比深度贴图大,某些像素深度比深度贴图小,所以导致了某些地方有阴影某些没有的粉刺现象,经过了偏移后,所有像素点的深度值都比深度贴图大,没有了粉刺现象。不过我们如果偏移太大的话,会出现阴影偏离物体的失真现象。偏移量的大小和平面的倾斜度有关,平面越倾斜的话,需要的偏移量就越大。

除了阴影粉刺之外,还有阴影走样(aliasing)的问题,如下图所示。虽然我们可以通过提升shadowmap的分辨率来降低走样的影响,但是同时也增大了显存的消耗。

所以通常使用百分比渐进过滤(Percentage Closer Filter, PCF)的方法来实现软阴影效果,如下图所示。我们在采样得到的结果进行插值,不过这里的插值和我们讨论的纹理采样插值有所不同,我们并非对采样得到的深度值的进行插值,而是对得到的阴影因子(shadow factor,不在阴影中为1,在阴影中为0)进行插值,从而使得阴影的边缘能够进行柔和的过渡,不会产生阴影到非阴影的突变。4-tap PCF的过滤方式如下图所示,我们以四个采样结果来计算对应的阴影颜色,得到的阴影因子为0.25,得到的是过渡的阴影颜色。

其实,shadowmap有非常多的变种,我们上面介绍的技术是最简单的一种,除了这种技术之外,还有PSSM(Parallel-Split Shadow Maps)、CSM(Cascaded Shadow Maps)、VSM(Variance Shadow Maps)、BSM(Bitmap Shadow Maps)、TSM(Trapezoidal Shadow Maps)等等



         

本帖被以下画板推荐:

微信公众号:Game艺视界
使用道具 <
金色的流星  发表于 2022-4-28 18:27:52  
2#
感谢分享,真的很有用
回复 收起回复
使用道具
金色的流星  发表于 2022-4-28 18:29:15  
3#
有可能的话,建议加精华
回复 收起回复
使用道具
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表