"A good picture is equivalent to a good deed."—Vincent Van Gogh。(Patrick:看了几遍才看明白,主要是突然看到这句话的作者,梵高,这两年去欧洲看了不少梵高的作品。那么再看这句话就明白了,意思是,好的作品会说话,或者说是好的作品有力量。结合下面要说的不同风格的作品,就很好理解了。)

当绘制一幅3D图片的时候,不只是要把每个物件的几何形状表达清楚,还需要使用合适的外观。根据产品不同,可以使用写实风(几乎和实物照片完全相同的外观),也可以使用更有创造性的各种风格化外观。如图5.1所示

T05

上面的图片是来自UE渲染的真实风格场景,由Gokhan Karadayi提供。下面图片来自于Campo Santo的游戏Firewatch,使用的是风格化外观,由Campo Santo提供。

本章所介绍的内容同时适用于写实风和风格化渲染。第15章将介绍风格化渲染,然后本书第9-14章将集中介绍用于真实照片渲染的物理算法。

5.1 着色模型

首先,先要选择一个着色模型,用来描述对象的颜色如何根据表面方向normal、视图方向view和光照等因素的变化而变化。

我们以Gooch shading model(参考文献[561])作为例子来开始,它属于非真实渲染NPR,属于第15章的范畴。Gooch shading model旨在提高技术插图中细节的易读性。

Gooch shading的中心思想在于对比表面法线和光源位置。如果法线指向光源,则使用较暖色调为表面着色,如果法线远离光源,则使用较冷色调。当中间角度的时候,再基于用户提供的表面颜色,在冷暖之间做插值。在这个粒子中,我们增加了一个风格化的高光,是的曲面会有光泽外表。图5.2展示了着色模型。

T05

上图为一个结合了Gooch shading和高光效果的风格化找色模型。上面的图是一个使用了中立表面颜色的复杂物件。下面的图为使用不同表面颜色的球体。(中国龙的mesh由Computer Graphics Archive(参考文献[1172])提供,原始模型来自stanford 3D扫描库)

和大多数着色模型一样,此示例受到曲面方向normal、视图view、光照方向的影响。为了进行着色,这些方向通常表示为标准化(单位长度)向量,如图5.3所示。

T05

上图展示了用于着色模型的单位向量:表面法线normal、视图view、光照l。

这样的话,我们就定义了光照模型所需要的所有变量,下面为光照模型的公式:

T05

在这个公式中,我们使用了以下的中间计算

T05

该定义中的很多数学表达式也经常出现在其他着色模型中。clamp操作经常在着色中用于将结果限定在0到正无穷大,或者0到1之间。在这里,用于计算高光混合因子s的公式中可以看到-+符号,这个我们在1.2节介绍过,就是用于表示clapmp(0,1)。点积运算符出现了三次,是一种常见的用于计算两个单位向量夹角余弦的方式,它的结果很精确。比如在着色模型中计算normal和light夹角关系的时候经常会被使用。

还有一个经常被使用的着色运算是使用0-1之间的标量值在两个颜色之间进行线性插值。此操作的公式为tCa+(1-t)Cb,当t的值在0-1之间移动时,在Ca和Cb之间进行插值。该公式在着色模型中出现了两次,第一次是Cwarm和Ccool之间,第二次是上次插值的结果和Chightlight之间。线性插值经常在shading中被使用,所以它有一个内置函数lerp或mix。

公式r = 2(nl)n - l是用于计算光l相对于n的反射矢量。虽然不像前面两个那么常见,但是很多着色语言也会有对应的内置函数reflect。

通过将这些操作以各种不同的方式和参数结合,就得到了各种着色模型用于各种风格(风格化或者写实风)的渲染表面。

5.2 光源

在我们示例着色模型中的光照其实很简单,只是为着色提供了一个主方向。当然,现实世界中的照明可能相当复杂。可以同时存在多盏光源,每个光源都有自己的大小、形状、颜色和强度,如果考虑间接光照的话将会更加复杂。我们在第9章的基于物理的真实着色模型中,会使用到这些所有的因素。

相对比,风格化渲染可能会根据需要,以各种不同的方式使用照明。一些高度程式化的模型可能根本没有照明的概念,或者只使用它来提供一些简单的方向性(比如上面提到的Gooch shading)

光照中下一个比较复杂的地方是需要根据与光照的关系,选择不同的着色模型。这种表面在光下会有一个外观,在不受光的情况下会有另外一个外观。这也就意味着需要区分这两种情况的一些标准:与光源的距离、阴影(第七章)、表面是否背离光源(n和l的角度超过90度),或者是这些因素的合集。

或者换一种方式来思考,我们不再考虑光照是否存在的二元性,而是考虑光照强度的连续渐变。这样的话,就可以从光源完全不存在到光源完全存在进行插值,强度的范围可以用0-1表示。或者还有另外一种方式,将各种影响着色的因素相加,常见的方式是:将着色模型分解成亮部和不亮部,灯光强度Klight线性的影响光照部分:

T05

针对RGB的光照Clight可以将公式扩展为

T05

针对多个光源的情况为:

T05

不受光的部分funlit(n,v)相当于二元性着色模型中处理表面不受光影响的部分。根据游戏的风格,可以使用多种风格。比如,funlit()=(0,0,0)也就代表着不受光的表面,表现为黑色。其他的,比如也可以类似Gooch model中那样,使用冷色来表示表面背离光照。通常,着色模型中的这一部分表示某种形式的照明,这些照明不是直接来自显示放置的光源,而是来自比如天光或者周围对象反射的光。这些形式的照明将在第10、11章中讨论。

我们前面提到,如果光照方向l与法线n的角度超过90度,则光照不会影响到表面,也就是说光照来自表面下方。这可以被看作是光照对表面的影响的一种特殊情况。尽管这种情况是基于物理的,但其实对很多非物理的风格化着色模型也很有用。

光对表面的影响可以被看作是一组光线,照射到表面的光线密度与用于表面着色的光照强度相对应。如图5.4所示,该图显示了照明表面的横截面。沿着横截面照射到表面上的光线的间距与L和N之间夹角的余弦值成反比。因此,照射到表面上的光线的总比密度与L和N之间夹角的余弦值成正比,余弦值为L和N两个单位长度向量的点积。在这里我,我们也就可以看到为什么光向量L与实际光的方向相反,因为不然的话,必须先对它求反(Patrick:原来如此)

T05

更准确的说,光线密度(或者说光线对着色的贡献程度),当点积为正数时,与点积成正比。当点积为负数时,也就是说,光线是从表面后面发出来的,这样没有效果。所以,当用点积计算光照着色的时候,首先先clamp到0,。在这里可以使用1.2节的符号x+,将负值限制到0,也就是如下公式

T05

支持多个光源的着色模型,可以使用公式5.5(更通用),或者5.6(基于物理)。这些公式同样也可以用于风格化模型,可以帮助确保整体照明的一致性,特别是对于背光或者阴影的表面。然而还是有一些光照模型不适合这些结构,一些模型要用5.5公式。

最简单的光照模型是将其看成一个常数颜色

T05

也就得到如下着色模型

T05

这个模型被称为lambertian光照模型,是由Johann Heinrich Lambert(参考文献[967])于1760年发布。该模型适用于理想情况下的漫反射表面,也就是毫无光泽的表面。这里我们先简单的介绍一下它,然后再第9章会更详细的介绍。Lambert模型本身可以用于简单的着色,同时也是很多着色模型中的关键组成部分。

从上述公式中,我们可以看到,光源是通过两个参数影响光照模型:光照的方向l,以及光照的颜色Clight。所以,可以根据光源的这两个参数,将光源分成各种不同类型。

下面我们将来聊一些常见的光源,它们有个共同点:在给定的表面位置,每个光源仅从一个方向l照亮表面。换句话说,从阴影表面位置看,光源是一个小点。对于现实世界中的光源,这并非完全正确,但是大多数光源相对于被其照亮的表面的距离来说,都很小,所以也是一个合理的近似值。在7.1.2节和10.1节中,会讨论那些从各个方向对一个点产生影响的光源,面光源。

5.2.1 方向光

方向光是光源中最简单的类型。l和clight在场景中都是恒定的,clight可以通过阴影减弱。方向光没有位置。当然,实际光源肯定是有位置的,然而由于方向光是抽象的,当与灯光距离比较大的时候,比如,一个很远的灯光照亮桌面上的一个小物件的时候,可以看成是方向光。另外一个例子是使用太阳的场景,基本都会认为太阳是方向光,除非讨论的场景是整个太阳系。

方向光的概念可以稍微扩展一下,以允许在光方向l保持不变的情况下,改变clight的值。这通常是想借助光效,实现场景中的一些特殊效果。比如,将一个区域定义成两个嵌套的区域,在这两个区域过度的时候进行平滑插值得到clight。

5.2.2 精准光源

精准光源是指有位置的光源。与现实世界的光源不同,这种光源也没有尺寸和形状。Punctual来自拉丁语中的Punctus,是点的意思。用来表示单一的,有位置的所有类型的光源。点光源代表着向各个方向均匀发光的光源。聚光灯和点光源是两种不同类型的精准光源。光的方向l取决于当前正在绘制的表面点P0,以及精准光源的位置Plighting

T05

这个公式也是向量归一化的一个示例:向量除以向量的长度,以得到一个相同方向的单位长度向量。这也是一个常见的操作,在大多数着色语言中有对应的内置函数。但是,有时也需要此操作的中间结果,这样就需要把这个公式拆成好几步,如下所示:

T05

我们知道点积就是两个向量的长度相乘,然后乘以它们夹角的余弦值。而0°的余弦值为1,所以向量和自身的点积等于向量长度的平方。所以,为了计算出向量的长度,我们通常对其自身进行点积运算,然后再进行开方。

通常我们需要中间变量r,也就是光源和当前着色点之间的距离。因为我们有时候会用r距离作为衰减因子,这个将在后面进行讨论。

Point/Omni Lights

在所有方向均匀发光的精确光源被称为点光源或者Omni光源。对点光源来说,Clight的值随着距离r的变化而变化,唯一的变化源是上面提到的距离衰减。图5.5显示了这种变暗现象发生的原因,原理和图5.4的余弦系数差不多。但是与5.4的余弦系数不同,针对平行光,垂直于平行光方向移动物件,物件上的点与光源的夹角始终不变,但是点光源不一样。来自点光源的光线之间的间距与从表面到光源的距离成比例关系,所以该点与光源的夹角也在变化。(Patrick:原来如此)所以随着间距的增加,变化是分为两个维度的,所以光线密度与距离的平方成反比。这样,我们就可以通过r0点光源Clight0计算出最终的Clight

T05 T05

如上图所示,光线之间的距离随着距离r的变大而变大。所以光线密度/强度与r的平方成反比。

上面这个公式被称为inverse-square light attenuation。尽管技术上没毛病,但是在实际着色中可能会出问题。

首先,第一个问题出现在短距离的情况下。当r趋近于0的时候,clight将无限大。当r等于0的时候,就会遇到除0的问题。通常的解决方案是加一个很小的值做除0保护(参考文献[861]):

T05

这个很小的值的具体取值取决于应用程序,比如Unreal取极小值为1厘米。(参考文献[861])

在CryEngine(参考文献[1591])和Frostbit(参考文献[960])中,将r clamp到Rmin中:

T05

在这里,Rmin有一个物理意义:物理光源的半径,当r小于rmin的时候,物体表面在光源中,这不可能(Patrick:这个比较有道理,R0=Rmin,然后无限靠近光源的时候,Clight=Clight0)

相反,平方反比衰减的第二个问题在相对较大的距离上。问题并非在视觉上,而是性能上。因为尽管光强度随着距离的增加而减小,但它永远都不会到达0。而为了有效的渲染,我们希望灯光在某个有限距离达到0强度(详见第20章)。有许多不同的方法可以对平方反比方程进行修改来实现这一点。理想情况下,修改应当尽可能少。而为了避免在光的有效边界处出现尖锐的切口,在相同的距离处,修正函数的导数和值最好同时达到0。一种解决方案是用具有所需特性的开窗函数乘以平方反比方程。UE(参考文献[861])Frostbite(参考文献[960])都使用了这样的功能(参考文献[860])

T05

公式中的+2意思为,先将值clamp到0,然后再对其进行平方。图5.6中显示了平方反比曲线的例子,方程式5.14中窗函数的例子,以及它们相乘的结果

T05

上图中将公式中的Rmax设置为3。

需要根据应用程序的需要选择使用的方法。比如,当距离衰减函数以相对较低的空间频率(比如lightmap或顶点光源)采样时,在Rmax处导数等于0尤为重要。Cryengine不使用lightmap或顶点光照,因此它使用了更简单的调整,在0.8*Rmax到Rmax的范围内采用线性衰减。

在某些应用中,平方反比衰减并非最优选,而选择了其他的一些函数,所以我们将5.11-5.14概括成下述公式:

T05

其中Fdist(r)是距离函数。这种函数被称为距离衰减函数。在某些情况下,是由于性能原因而不使用平方反比衰减函数。比如Just Cause 2这个游戏就需要计算量比较少的光源。从而引入了一些计算量比较少,而同样足够平滑以避免顶点光照瑕疵的函数(参考文献[1379])

T05

还有一些别的情况,是为了实现一些比较有意思的效果,才选择不同的衰减函数。比如UE中就有两种衰减函数,用于真实和风格化的游戏:一个是平方反比模型,正如5.12公式中一样,另外一个是指数衰减函数用于创建各种各样的衰减曲线(参考文献[1802])。Tomb Raider古墓丽影(2013)的开发者就使用了spline-editing工具(参考文献[953]),从而对衰减曲线进行更强的控制

聚光灯

与点光源不同,真实世界中所有的光源的照明强度,都随距离和方向不同而不同。这种变换可以通过一个方向性衰减函数Fdir(l)表示,它与距离衰减函数相结合,来定义光强度的整体空间变化。

T05

不同的Fdir(l)会产生不同的光照效果。比如聚光灯,是将光线投射到一个圆形的圆锥体中。聚光灯的方向衰减函数围绕聚光灯方向向量s具有旋转对称性,可以通过s和光照方向l的夹角Θ的函数表示。这里的光矢量需要被颠倒,因为我们将光照方向l定义为物体表面到光源,而在这里,我们需要光源到物体表面的矢量。

大多数聚光灯函数会使用由Θs的余弦值组成的表达式。聚光灯通常由一个umbra angle Θu,意味着当Θu>Θs的时候,Fdir(l)=0。该角度可用于与前面看到的最大衰减距离Rmax类似的方式进行剔除。聚光灯还通常会有一个penumbra angleΘp,定义了一个inner cone,在该点光照是完全强度的。如图5.7

T05

如上图所示,Θs为光线方向-l与聚光灯方向s之间的夹角。Θp代表着penumbra,Θu代表着umbra angle。

聚光灯可以使用不同的方向性衰减函数,虽然它们大致相同。比如Frostbite(参考文献[960])使用了函数Fdirf(l),three.js浏览器图形库(参考文献[218])中用到了函数Fdirt(l):

T05

符号X-+代表着clamp到[0,1]。而smoothstep函数则是一个三元多项式,常被用于平滑插值,是大多数着色语言的内置函数。

T05

上图展示了刚才我们所说的光源类型。从左到右,方向光、没有衰减的点港元、平滑过度的聚光灯。注意,当灯光和表面之间的角度发生变化的时候,点光源会逐渐变暗。

其它精准光源

除此之外,还有许多其它的精准光源。

Fdir(l)将不仅限于上述的简单的聚光灯衰减函数。还可以通过测量真实世界中的光源得到的复杂列表模式,来表示Fdir(l)。照明协会Illuminating Engineering Society(IES)为此类测量定义了标准文件格式。可以从很多照明制造商处获取IES配置文件,而且已经被游戏KillZone:Shadow Fall(参考文献[379、380])所使用的,除此之外,还有Unreal(参考文献[861])和Frostbite(参考文献[960])游戏引擎等等都使用了该技术。Lagarde(参考文献[961])总结了如何解析和使用该文件格式。

游戏古墓丽影 Tomb Raider 2013(参考文献[953])有一种精准光源,它的衰减函数是根据世界坐标轴X、Y、Z轴不同区分的。在古墓丽影中,光的强度也可以随着时间变换而沿着一条曲线变化,比如,拾取一个火把。

在6.9节中,我们还会聊一下,如何通过纹理改变光的强度和颜色。

5.2.3 其它光源类型

方向光和精准光源主要是通过如何光照方向L来进行区分。除此之外,还可以使用其它方法来计算光照方向以定义不同类型的灯光。例如,Tomb Raider古墓丽影定义了胶囊灯,使用线段来作为光源,而非点(参考文献[953])。对于每个着色像素来说,光照方向为该点到光源线段中最近的点的矢量。

着色方程式还需要l和Clight作为参数,而这些参数可以通过很多方法得到。

以上聊到的光源都是抽象的。真实世界中的光源都是有尺寸和形状的,且针对物体表面上的照射是从多个方向的。在渲染行业,这种光源被称为面光源,在实时渲染领域,其使用频率也越来越高。面光源的使用主要集中在两个方面:模拟部分遮挡面光源导致的阴影边缘软化技术(详见7.1.2节),模拟面光源对表面着色效果的影响技术(详见10.1节)。其中第二个方面针对光滑的镜面表面影响最为明显,在这些表面上,光线的形状和大小都可以通过其反射清晰的辨别出来。方向光和精准光源虽然不像过去那样无处不在了,但是不太可能被废弃。目前,已经开发出了比较简单的公式模拟面光源的近似值,这种公式耗能低,所以可以得到更广泛的应用。GPU性能的提高也使得可以使用越来越复杂的技术。

5.3 实现着色模型

然后这些着色和光照方程需要在代码中实现。被接种,我们讲讨论一些设计和编写此类实现的关键注意事项。然后会介绍一个简单的实现示例。

5.3.1 运算频率

当设计一个着色模型的时候,需要先根据运算频率将一些运算进行区分。首先,针对在整个DC中,都保持不变的运算,可以将其由应用程序在CPU端执行,尽管GPU也比较擅长运算。然后将CPU运算的结果,通过图形API中的uniform传入。

即使这个类型的数据,也可能分为很多不同的可能性。最简单的例子是着色方程中的常量表达式,然而其实这些可以通过硬件配置或者安装选项等表示。这些计算应当在shader编译的时候就完成,这种情况下甚至不需要通过uniform传入。或者,也可以在离线预计算的时候、安装的时候或者应用程序加载的时候运算。(Patrick:这是指啥?静态分支?)

另一种情况是,着色运算的结果在应用程序运行期间会发生变化,但速度太慢,不需要每帧都更新。例如,光照取决于虚拟游戏世界中的时间。如果光照计算的成本很高,可以尝试通过多帧进行分摊(Patrick:不同频率的cascade shadowmap我明白,多帧一起算OC我也明白。但是光照运算也可以分摊?比如光线追踪中每帧发一条射线,然后多帧混合)

其它的还有,逐帧更新的运算,比如V和P矩阵。逐物件更新的运算,比如依赖位置运算的光照参数。逐DC更新的运算,比如材质球属性。按照频率对uniform进行分组可以有效的提高性能,通过最小化变量更新的方式降低GPU开销。

当某个被需要的运算在一个DC中会发生变化的时候,则不能通过uniform将该运算的结果传入shader,而是需要用到第3章中提到的着色器来计算,如果需要,还可以通过varying来进行不同着色器之间的数据传输。理论上,着色计算可以在任何可编程阶段进行,而每个阶段对应不同的运算频率:

  • Vertex Shader:逐pre-tesselation顶点运算。
  • Hull Shader:逐surface patch运算。
  • Domain Shader:逐post-tesselation顶点运算。
  • Geometry Shader:逐图元运算。
  • Pixel Shader:逐像素运算。

实际上,大多数着色计算是逐像素进行的。虽然这些通常在pixel shader中实现,但computer shader的使用也越来越多,在第20章会给出一些例子。其他阶段主要用于几何运算,比如transform和变形。为了理解为什么着色计算要逐像素计算,下面我们对比一下逐顶点和逐像素计算的结果。在旧的文本中,也被称为Gouraud shading(参考文献[578])和Phong shading(参考文献[1414]),这些术语现在已经不用了。这个比较中使用的着色模型类似公式5.1,但是经过修改,可以使用多个光源。等我们说完细节之后,会给出完整的模型。

下图5.9可以看出两种顶点密度的模型,分别使用逐顶点着色和逐像素着色的区别。对于龙的那个模型,顶点密度很高,两种着色的差别很小。而对于茶壶,使用顶点着色的话会出现明显的错误,比如高光部分。而对于两个三角形平面来说,顶点着色完全不正确。这些错误的原因是因为着色方程的某些部分,特别是高光部分,在网格表面呈非线性变化。这使得,如果使用顶点着色,以线性插值的方式得到结果的方式,结果是不对的。

T05

上图为逐顶点和逐像素,以5.19着色模型,针对三种不同顶点密度模型所得到的对比图。左侧为逐像素的结果,中间为逐顶点的结果,右侧为模型的wireframe,可以看到其顶点密度。(中国龙的模型来自Computer Graphics Archive(参考文献[1172]),而原始模型来自Stanford 3D Scanning Repository)

理论上,可以在PS中只计算高光部分,其他部分在VS中进行计算。这样的话效果没区别,性能上理论上还会有所节省。但实际上,这种混合方式往往并非最优解。着色模型中线性变换的部分,往往是计算量最小的部分,以这种方式拆分着色计算,往往会导致更多的开销,比如重复计算以及额外的varying变量,这些消耗甚至可能超过节省的部分。

如前所述,VS主要负责非着色部分,比如几何体transform和变形。将生成的几何曲面属性转换为适当的坐标系,由VS输出,在三角形中线性插值后,作为varying输入传给PS。这些属性通常包括位置、法线以及可选的切线(如果需要做法线映射)

需要注意的是,即使VS中对法线做了归一化处理,但是插值后的向量依然可能是非归一化的。如图5.10所示。所以在PS中还需要对法线进行重新归一化。而VS中的法线归一化依然不可缺少。因为如果顶点之间的法线长度变换很大,比如做顶点混合的时候,这个将会扭曲插值。这可以在图5.10的右侧看到。所以,需要在插值之前和之后,也就是在VS和PS中都进行归一化。

T05

在左侧,我们可以看到,单位法线在曲面上的线性插值会产生长度小于1的插值向量。在右侧,我们看到长度显著不同的法线的线性插值,会导致插值方向向两条法线中较长的方向倾斜。

与法线不同,指向特定位置的向量(比如view向量、精准光源的灯光向量)通常不进行插值。而在PS中通过插值得到的曲面位置来计算这些向量。这些计算中,只有必须在ps中进行的归一化之外,只需要一个向量减法即可,这个计算量很小。如果出于什么原因,比如将这个运算放到VS中,则不要对其进行归一化,否则将会出现不正确的结果,如图5.11所示。

T05

上图为对两个灯光向量进行插值。左图为,在插值之前先归一化,会导致插值后方向错误。右图可以看到,直接对非归一化的向量进行插值,结果是对的。(Patrick:这个挺有意思的)

前面我们提到VS将几何体转换到适当的坐标系中。摄像机和光源的位置由应用程序转换到相同的坐标系中。这样的话,将最大程度降低PS的计算量。但是哪个坐标系是合适的?可能是世界坐标系,也可能是相机的视图坐标系,甚至可能是模型的局部坐标系。具体使用哪个坐标系,通常是基于系统性考虑(比如性能、灵活性和简单性)。比如,如果渲染场景中包含大量灯光,则可以选择世界空间以避免过多的变化灯光位置。或者,可以使用相机坐标系,来更好的优化依赖view向量的PS操作,这样的话精度可能还会得到提高(详见16.6节)

尽管大多数着色器都是按照上述描述的原则,但是凡事均有意外。比如,一些应用程序使用逐图元的渲染以实现风格化的效果。这种风格被称为flat shading。图5.12显示了两个例子。

T05

以上是两个使用flat shading的风格化效果:上面是Kentucky Route Zero,下面是That Dragon,cancer(上图来自Cardboard Computer,下面来自Numinous Games)

理论上,flat shading可以在GS中实现,但是一般都是通过VS实现的。通过将每个图元的属性与其第一个顶点关联,并禁用顶点值插值来完成。禁用插值(可以分别对对每个顶点进行设置)会将第一个顶点的值传给图元中的每个像素。(Patrick:这个厉害了)

5.3.2 例子

下面我们将介绍一个着色模型实例。该实例的着色模型类似5.1公式的扩展Gooch model,但是经过了修改,可以接受多个光源。模型为:

T05 T05

该公式也符合多光源的5.6公式,如下所示

T05

其中光照部分和非光照部分为

T05

一般情况下,材质属性,比如Csurface的值被存储在顶点数据或者纹理中。但是在这里为了简单,我们假设Csurface是常量。

此实现会使用到shader的动态分支功能来循环所有光源。(Patrick:这个居然是动态分支?)虽然这种方式在简单场景效果不错,但是不能很好的扩展到具有许多光源的大型和几何复杂的场景。第20章将专门介绍处理大量光照的渲染技术。此外,为了简单起见,我们只支持一种光源:点光源。

着色模型并非单独实现,而是要在更大的渲染框架的上下文中才能实现。当前例子是在一个简单的WebGL 2 的应用程序中实现,它是由Tarek Sherif(参考文献[1623])的"Phong-Shaded Cube" WebGL 2模型改编而来,当然,该模型还适用于更复杂的框架中。

下面我们来讨论一些GLSL的shader code以及JavasScript的WebGL API。目的并非是学习WebGL API,而是展示一般的实现原则。我们将按照"由内到外"的顺序实现,先PS,然后VS,最后看图形API调用。

在看shader代码之前,先看下shader的输入和输出。正如3.3节所描述的那样,GLSL的输入分为两类。一类是uniform,由应用程序传入,在一个DC中保持常量。第二类是varying,这些变量可以在VS和PS之间传输。下面是PS中的varying输入,在GLSL中通过in来表示,对应的PS的输出由out表示

T05

该PS只有一个输出,就是最终的颜色。PS的输入与VS的输出相对应,并在光栅化的时候被插值。当前PS有两个varying输入,曲面位置和法线,都是位于世界空间坐标系中。uniform的数量就会多很多,为了简洁期间,我们就展示两个光源相关的uniform

T05

由于这些都是点光源,所以每个点光源的定义都包括位置和颜色。为了符合GLSL std140 data layout数据布局标准,它们被定义成了vec4,而非vec3。这样的话,虽然在std140 layout下会导致浪费一些空间,但是这样的话,就确保了CPU和GPU布局一致,避免了如果布局不一致可能会导致的性能消耗,这就是我们在本实例中使用vec4的原因。(Patrick:这个厉害了,而且UBO在metal和vulkan中布局的不是很好,见下图)。光源的结构数组是由一个uniform block定义,这是glsl的特性,这样就可以将一组uniform变量绑定成一个buffer,这样传输会更快一些。数组长度为光源的最大限制数量。这样的话在shader编译之前,会将shader源代码中的MAXLIGHTS替换成对应的数值(比如10)。而uLightCount这个uniform才是当前DC光源的真正数量。

T05

下面我们来看PS部分

T05

可以看到,有一个lit函数,被main函数调用。总的来说,上述是5.20和5.21两个公式的GLSL实现。注意,Funlit()和Cwarm是作为uniform常量被传入,因为在整个DC中,它们都保持常量,这样的话通过应用程序计算,可以节省一些GPU运算。

这个PS中使用到了一些内置的GLSL函数。reflect函数将一个向量(在这里是光照向量)根据另外一个向量表示的平面(在这里是表面法线)进行反射。由于我们希望光向量和反射向量都指向远离曲面的地方,所以在其传递到reflect函数之前,对前者取反。clamp函数有三个输入,其中两个定义了第三个变量将被clamp的范围。在大多数GPU中,当clamp到0到1之间内,都有对应的函数(比如hlsl中的saturate),这样的话可以快速处理,且基本是无消耗的,免费的。这也就是为什么我们在这里使用clamp的原因(虽然我们只需要将其clamp到0即可,我们知道它不会超过1)。函数mix有三个输入,将在其中两个输入之间进行线性插值,在本例中是对warm颜色和highlight高光颜色,基于第三个参数(范围0-1)进行线性插值。在HLSL中这个函数是lerp,用于线性插值。最后,normalize是用于归一化,实现方式就是将向量除以向量长度。

下面来看下VS。由于上面已经介绍了不少uniform,这里就不再赘述了。这里看下varying的输入和输出的定义

T05

正如前面所提到的那样,VS的输出与PS的varying输入相匹配。而VS的输入包含了数据在顶点数组中布局的指令。下面是VS的代码:

T05

这些都是VS中的一些常见操作。shader中先将位置和法线转换到世界空间,然后将其传给PS着色使用。然后,顶点位置被转换到clip空间,赋给了gl_Position,这是光栅化使用的一个特殊的内置变量。gl_Position是VS中必须有的一个输出变量。

另外,可以看到在VS中并没有对法线进行归一化。因为在原始mesh中存储的法线长度本身就为1,而在VS中并没有对其进行比如顶点混合、不均匀缩放等会改变其长度的操作。模型矩阵中有缩放因子可能会改变法线长度,但是是统一的改变所有法线的长度,所以不会出现5.10右侧图片导致的问题。

此例子中的应用程序使用WebGL的API进行渲染和着色设置。其中每个shader都会独立被设置,然后绑定到一个program object上。下面是PS的构建代码:

T05

这里使用到了fragment shader。这个属于被WebGL(和它所基于的OpenGL)所使用。正如本书前面提到的,尽管Pixel shader不太精确,但是使用面积比较广,所以本书使用这个术语。此代码中也用于将MAXLIGHTS 字符串替换成适当的数值。大多数渲染引擎都会执行类似预编译的着色操作。

应用程序还会执行更多的操作,比如设置uniform,初始化顶点数组,clear、draw等,这些都可以在program(参考文献[1623])中查看这些代码,有很多的API指南会去解释这些代码。我们这里的目标是解释shader是如何在它们的编程环境中被当做一个个单独的处理单元。因此,我们的演练到此结束。

5.3.3 材质系统

很少有渲染框架只实现一个shader的。通常,都需要一个专门的系统来处理应用程序所使用的各种材质、着色模型和shader。

正如前面所说,shader适用于GPU可编程着色。因此,它是一个底层的图形API资源,而不是艺术家直接交互的东西。相对应的,艺术家一般直接操作材质球。虽然材质球有时候还会描述一些非视觉方面的属性,比如碰撞等(Patrick:材质球还可以干这个事情?),但是本书中我们不对这一块进行讨论。

虽然材质球是通过shader实现的,但并非一一对应关系。在不同的渲染情况下,同一个材质可能使用不同的shader,而同一个shader也可以被多个材质球共享。最常见的情况就是参数化材质球。通常情况下,参数化材质球需要两种类型的材质:材质模板和材质实例。每个材质模板描述一类材质,并具有一组参数,这些参数可以根据参数类型指定数值、颜色、纹理等。每个材质实例对应一个材质模板以及一套默认值。一些渲染框架,比如UE(参考文献[1802])包含一套更复杂的层次结构,其中材质模板可以从多个级别的其他模板中派生。

参数可以通过uniform在运行时传入shader,也可以在编译前通过替换值的方式传入shader(Patrick:不知道Unity是替换的,还是通过uniform的,但无论是哪个,其实都是静态分支吧)。常见的编译时参数是布尔开关,用它来控制材质球中使用哪个分支。可以通过材质球中的UI界面设置,也可以由程序直接控制。比如,可以在视觉效果可以忽略不计的情况下,降低远处对象的着色运算量。

虽然材质参数可以与着色模型的参数一一对应,但是情况往往并非如此。比如可以将给定着色模型的某个参数(比如表面颜色)设置成常量值,这样将减少一个材质参数。相应的,着色模型的参数也可能会需要通过一系列复杂的操作来计算得到,这些操作以多个材质参数,比如插值得到的顶点参数或者纹理作为输入。而在某些情况下,比如表面位置、法线方向甚至时间等都会影响到着色计算。基于表面位置和法线方向的着色在地形材质中尤为常见。比如基于高度和法线可以用于控制雪的效果,在高海拔和几乎水平的表面混合白色的表面颜色。基于时间的着色在动画材质中很常见,比如闪烁的霓虹灯标志。

材质系统最重要的任务之一是将各种材质球函数划分为单独的元素,并控制这些元素的组合方式。在许多情况下,这种类型的组合是有用的,比如:

  • 组合表面着色和几何处理,比如刚体变换、顶点混合、morphing变形、表面细分、instancing、clip等。这些功能各不相同:表面着色取决于材质,几何处理取决于网络。因此,可以将它们分开编写,然后由材质系统组合。
  • 组合表面着色和合成操作,比如alpha test、alpha blend。这与移动GPU尤其相关,后者通常在PS中执行。通常需要将这些操作独立于表面着色所使用的材质。
  • 组合着色模型和得到着色模型参数的计算。这样的话,就可以只实现着色模型一次,然后与各种计算着色模型参数的方法结合使用。
  • 组合分支和shader的其他部分。这样可以将每个feature都独立实例。
  • 组合光照模型和根据光源得到其参数的计算:针对每个光源计算当前片元的Clight和L。延迟渲染(详见20章)等技术改变了这个结构。支持多种类似技术的渲染框架的复杂度更高了。

如果图形API提供这种类型的着色程序代码模块作为核心功能,将会非常方便。但是遗憾的是,与CPU不同,GPU shader不允许代码片段编译后链接。每个program都会被编译成一个单元。着色器阶段的分离可以实现一些有限的模块化,但这只是我们上面列表中的第一条:表面着色(通常在PS中执行)组合几何处理(通常在其他shader中执行)。但是由于每个材质球还需要执行其他操作,所以这样并不完美。考虑到这些限制,材质系统实现所有这些类型组合的唯一方法就是在源码级别。这里主要将设计字符串操作,比如连接和替代,通常通过C样式的预处理指令执行,比如include、if、define。

早期的渲染系统中shader变体很少,基本都是全部手写的。这样的好处是,可以根据每个shader的特点来优化每个变量。但是随着变体数量的增加,这种方法很快变的不切实际。当考虑到所有不同的部分和选项的时候,变体的数量是巨大的。这也就是为什么模块化和可组合型如此重要。

在设计处理shader变体的系统的时候,第一个要解决的问题是:是选择在运行时通过动态分支,还是在编译时通过条件预处理进行分支选择。在旧的硬件上,动态分支特别慢,基本不可能,所以不会选择动态分支。(Patrick:加入一个wave对应64个thread,有动态分支的时候,会有32个thread走其中一个分支,另外一个thread走另外一个分支。wave和thread的概念见下图)所有的变量将在编译时处理,甚至是不同类型不同数量的光(参考文献[1193])。

T05 T05 T05 T05 T05

相比之下,当前GPU可以很好的处理动态分支,尤其在当前DC下,针对所有像素都使用相同分支的时候。所以现在,比如灯的数量,都是在运行时处理的。(Patrick:灯的数量不是uniform么,这个不是常量是静态分支么?静态分支和直接替换shader源代码还不一样?)但是,向shader中添加大量的变量也会产生其他的一些成本:寄存器计数增加,占用率相应降低,从而降低性能。有关更多详细信息,详见18.4.5。因此编译时变量仍然有用,因为它可以避免一些永远不会执行到的复杂逻辑。

举个例子,假如一个应用程序中支持三种不同类型的灯。其中两种很简单:点光源和方向光。第三种类型是通用聚光灯,它支持表格照明模式和其他复杂功能,需要大量的代码实现。但是很少使用聚光灯。在过去,会根据这三种灯光的类型和计数做出各种排列组合编译不同的shader变体,以避免动态分支。虽然现在不这样做了,但是还是可以拆成两个变体,一个适用于有聚光灯的时候,另外一个适用于没有聚光灯的时候。这样第二种变体的代码比较简单,就可以得到更低的寄存器占用率,得到更高的性能。(Patrick:所以使用uniform的静态分支和shader源代码替换的唯一区别就是:使用uniform的话,某个分支虽然永远不走,但是其中用到的变量还是需要占用寄存器的。)

现代的材质系统同时使用运行时和编译时shader变体。虽然整个负担不再只在编译时处理,但是总体的复杂性和变化的数量在不断增加。因此,仍需要编译大量的shader变体。比如,在Destiny中:The Taken King中一帧中会超过9000个已编译的着色变体(参考文献[1750])。Unity中由接近1000亿个可能出现的变体。即使是只编译实际使用的变体,但依然要重新设计shader编译系统,来处理大量可能的变体(参考文献[1439])

材质系统设计师采样不同的策略来解决这些问题。尽管这些策略有时会互斥(参考文献[342]),但这些策略依然经常会在一个系统中被结合起来使用。这些策略包括:

  • 代码重用,在共享文件中实现函数,使用#include预处理指令,从任何需要它们的shader中访问这些函数。
  • Subtractive,一个被经常成为uber shader或者super shader的shader(参考文献[1170,1784])。使用编译器预处理条件和动态分支的组合来删除未使用的部分,并在互斥的分支之间切换,从而实现了大量的功能。
  • Additive。各种功能被定义为具有输入和输出连接器的节点,这些连接点可以被组合在一起。这个类似于代码重用策略,但更加结构化。节点的组合可以通过文本(参考文献[342])或可视化图形编辑器完成。后者旨在使非工程师(比如TA)更容易创造新的材质模板(参考文献[1750,1802]),而通常只有部分着色可以访问可视化图形创作。例如,在UE4中,蓝图编辑器只能用于操作着色模型输入的计算(参考文献[1802])(Patrick:真的诶。。UE4怎么改着色/光照模型呢。)。如下图5.13
  • T05

    上图为UE的材质编辑器。注意看到右侧的节点,此节点的输入连接器对应于渲染引擎使用的各种着色输入,包括所有着色模型参数。(材质样本由Epic Game提供)

  • Template-based。定义一个借口,然后将不同的实现插入其中。这比加法策略更正式,通常用于更大的功能块。这种方式最常见的例子,就是将着色模型参数的计算与着色模型本身的计算进行分离。UE(参考文献[1802])有两个材质domain,一个用于计算着色模型输入参数的,一个用于计算光源的Clight的(Patrick:这个我貌似没注意到过。)需要注意的是,延迟着色(详见20章)强制执行类似的代码,其中G-buffer为接口。

在WebGL Insights(参考文献[301])一书中的几章讨论了各种引擎如何控制器shader管线(Patrick:Shader管线是什么,好吧,应该就是管线,从应用程序传入数据阶段到depth/stencil test、dither等结束)。除此之外,现代材质系统还有一些其他重要的设计考虑,比如使用最少的代码支持多平台。这些不同包括不同平台性能上、功能上甚至语言上的不同。Destiny系统(参考文献[1750])就用来解决这种问题的典型解决方案。它使用一个专门的预处理层,这层接受自定义着色语言编写的shader。这样的话,就可以编写平台无关的shader,然后自动转成不同的shader和实现了。UE(参考文献[1802])和Unity(参考文献[1436])都有类似的系统。

材质系统也需要确保良好的性能。除了shader变体之外,还有一些其他优化。Destiny系统和UE都会自动检测当前DC那些参数可以被看成常量(比如前面实例中的冷暖计算),并将其移除shader。(Patrick:不知道Unity会不会这么做,这样的话会省寄存器吧)。还有就是,Destiny会判断更新频率(比如每帧、每个光照、逐物件),然后再适当的时候更新每组常量,以减少API开销。(Patrick:这就是SRPBatcher吧)

所以,实现一个shader是一个决定哪部分可以简化,计算各种表达式频率以及用户如何修改和控制外观的问题。shader管线的最终输出是颜色和blend信息。下面的AA、透明度、图像处理将说明如何组合和修改这些值,以显示到屏幕上。

5.4 抗锯齿

想象一下,一个大的黑色三角形在一个白色的背景板上缓慢的移动。可以认为屏幕上的一个像素被三角形部分覆盖,而在移动的过程中,该像素的强度应该是平滑的发生变化。但是,基本上在所有类型的渲染器中,实际上情况是,当像素的中心被覆盖的时候,像素颜色直接从白色变成黑色。即使是最标准的GPU渲染也不例外。参见图5.14最左列。

T05

上面的三张图片,显示了针对同一个三角形、线段、点采用不同级别抗锯齿的效果。下面一行是上面一行的放大图片。最左边的一列针对每个像素只使用了一个样本,这意味着不使用抗锯齿。中间的一列针对每个像素使用了四个样本,而最右边的一列针对每个像素使用了八个样本(在一个4*4的格子中,采取一半的数据)

三角形会以像素的形式展示,而像素只有两种可能性,要么在那里,要么不在那里。绘制的线条也有类似的问题。因此,边缘会有锯齿状的外观,这种视觉效果被称为“锯齿”,而动起来的时候,看起来像是像素在流动/爬行(Patrick:其实就是摩尔纹)。更正式的说法,这个问题被称为走样,而避免这个效果被称为反走样。

采样理论和数字滤波是一个很大的主题,大到可以填满一整本书(参考文献[559、1447、1729])。这是渲染行业的一个关键领域,所以我们在这里也会介绍一些采样和数字滤波的一些基本理论。我们将重点放在实时反走样技术。

5.4.1 采样和数字滤波理论

渲染图像本质上是一个采样的过程。这是因为图像的生成就是对三维场景进行采样以获得图像中每个像素(一组离散的像素数组)的颜色值的过程。如果使用纹理映射(详见第6章),就需要对纹素进行重新采样,以在不同条件下获得良好的效果。针对序列帧动画,通常以相同的时间间隔进行采样。本节介绍采样、重建和过滤的相关主题。为了简单起见,大多数材料都将使用一维方式进行呈现。当然这些概念同样适用于二维图片。

下图5.15展示了如何以均匀间隔(即离散化)对连续信号进行采样。这个抽样过程的目的是以数字方式表达信息,这样做可以减少信息量。但是需要对采样信号进行重构以恢复原始信号,这是通过过滤采样信号来实现的。(Patrick:这个蛮有意思的)

T05

上图为对连续信号(左)进行采样(中),然后通过重构(右)恢复原始信号。(Patrick:所以我在想,针对贴图的滤波方式,如果开启mipmap的话,其实只要用point就好了吧,因为理论上这个贴图的尺寸和屏幕上的像素数是一一对应的,当然用Bilinear效果更好,而且因为texture cache的原因,性能也ok,Trilinear应该就没必要了,性能也会有问题。)

采样后,可能会出现走样,为了消除这个效果,我们就需要进行反走样来生成效果更好的图像。在旧的西部片中,一个典型的走样的例子是由相机拍摄的旋转的马车轮。由于轮子的转速比摄像机记录的频率高,所以马车轮可能看起来旋转的非常慢(甚至有时向前有时向后),甚至可能看上去根本不像旋转。如下图5.16所示,这种效果的产生是因为车轮的图像是以一系列的时间戳拍摄的,可以被称为时间走样。

T05

最上面一行显示了一个旋转轮(原始信号)。而在第二行中,对其采样频率不够,导致看上去像是在向相反的方向移动。这样是由于采样率太低而导致的走样实例。而在第三排中,采样率的问题,导致完全不发确定车轮的旋转方向。这就是Nyquist极限。在第四排中,采样率高于前面两排,这样可以看到车轮正在朝着正确的方向旋转。

计算机图形学中最常见的走样的例子是由于以下三种情况产生的"锯齿":光栅化线段或者三角形的边缘,闪烁的高光,被压缩的具有棋盘格图案的纹理(详见6.2.2)(Patrick:后面这两种情况是什么)

当信号采样频率过低时,就会出现走样。如图5.17,为了正确采样(通过采样数据重建原始信号),采样频率必须是原始信号最大频率的两倍以上。这通常被称为采样定律,对应的采样频率被称为Nyquist rate(参考文献[1447])或者Nyquist极限,由瑞典科学家Harry Nyquist(1889-1976)提出。如图5.16也可以看出Nyquist极限。定律中使用了最大频率这一术语,代表着原始信号的最大频率。换句话说,相邻样本之间必须足够平滑。

T05

上图中蓝色实线为原始信号,红色圆圈为均匀间隔的采样点,绿色虚线为重构信号。上面一行的采样率过低,因此重构的信号频率会比较低,发生了走样。而下面一行的采样率刚好是原始信号频率的两倍,重建的信号在这里是一条水平线。可以看出来,如果采样率稍微增加,完全可能重建原始信号。

然而3D场景基本不会出现band-limited。三角形的边缘,阴影的边缘以及其他一些现象都可能会出现不连续变化的信号,从而产生无限的频率(参考文献[252])。所以,无论采样点多密,依然可能会由于重要信息比较小,导致采样不对。因此,使用点采样去渲染场景的时候,不可能完全避免走样,虽然我们基本都是使用点采样。然而,有时可能知道信号是否是band-limited。比如采样一个纹理应用到物件表面上。相对于像素的采样率,可以计算出纹理的采样率。如果低于Nyquist极限,则不需要特殊操作,如果过高,则需要使用各种算法(详见6.2.2)(Patrick:应该就是当没有mipmap的时候,如果纹理过大,所占屏幕面积过小,则需要用bilinear等过滤算法)

Reconstruction重建

下面我们将讨论,针对band-limited信号,如何从采样信号中重构原始信号。为此,必须使用滤波器。图5.18中显示了三种常见的滤波器。需要注意的是,滤波器的面积应该始终为1,否则重建的信号可能会增大或缩小。

T05

左上角的图为box滤波器,右上角的图为tent滤波器,下图为sinc滤波器(被clamp到X轴上)

如图5.19所示,box滤波器使用最近点用于重建信号。这是最糟糕的选择,因为这样的话还原的信号是不连续的阶梯。尽管如此,由于它很简单,所以经常被用于计算机图形学。如图所示,box滤波器将被放置在每个采样点上,然后进行缩放,使得滤波器最顶端与采样点重合。之后,这些被缩放后的框组合在一起,就得到了右侧所显示的重建信号

T05

如上图所示,左侧的采样信号通过box滤波器重建。将滤波器放置在每个采样点上,然后沿着Y方向缩放,使得滤波器的高度和采样点相同。有图为重建的信号。

box滤波器可以被其他任何滤波器取代。如图5.20所示,tent滤波器,也被称为三角形滤波器。该滤波器针对相邻采样点之间进行线性插值,因此比box滤波器更好,因为重建的信号是连续的。

T05

采样信号被tent滤波器重建,右侧为重建后的信号。

然而,tent滤波器重建信号的平滑度比较差,采样点会有比较突然的坡度变化。为了得到完美的重建,必须使用理想的低通滤波器。信号的频率分量是正弦波:sin(2πf),其中f为该分量的频率。(Patrick:这句话没看懂,应该是:重建信号为正弦波,f为频率?)。低通滤波器会去除频率高于滤波器定义的某个频率的所有频率。直观的说,低通滤波器去除了信号的尖锐部分,对信号进行了blur。理想的低通滤波器是sinc滤波器,也就是图5.18的底部。

T05

傅里叶分析(参考文献[1447])解释了为什么sinc滤波器是最理想的低通滤波器。因为,在频域空间,低通滤波器是一个box滤波器,当其与信号相乘时,会出去滤波器宽度以上的所有频率。然后将box滤波器从频域转为空间域,就得到了正弦函数。同时,乘法运算被转换成了卷积函数,这个属于虽然我们没有描述,但是在本节中将一直使用。

如图所示,使用sinc滤波器重建信号可获得更平滑的结果。如图5.21所示,采样信号引入信号中的高频分量(突变),低通滤波器的任务是去除这些分量。实际上,sinc滤波器过滤掉了所有频率高于采样率/2的正弦波。如公式5.22所示,当采样频率为1.0(采样信号的最大频率必须小于1/2)的时候,sinc函数是最完美的重构滤波器。一般来说,假设采样频率为fs,即相邻样本之间的间隔为1/fs。对于这种情况,完美的重构滤波器为sinc(fsx),它消除了所有频率高于fs/2的频率。这个在重采样中很有用(详见下一节)。然而,由于sinc滤波器的宽度是无限的,而且在某些领域是负的,所以在实际应用中很少有用。(Patrick:这里挺复杂的)

T05

上图为使用sinc滤波器去重建信号,sinc滤波器是最理想的低通滤波器。

一方面是质量比较低的box和tent滤波器,一方面是不实用的sinc滤波器,我们需要在两者之间取一个中间值。最广泛使用的滤波器(参考文献[1214、1289、1413、1793])介于这两者之间。所有的这些滤波器都余sinc函数类似,但是又都有所限制。最接近sinc函数的滤波器在部分区域会出现负值,然而负值通常是不需要的,所以通常使用一些没有负值产生的滤波器(通常被称为高斯滤波器,因为它们要么来自高斯曲线,要么类似高斯曲线)。第12.1节更详细的讨论了滤波器的函数和使用。

使用了任何滤波器后,都会得到一个连续的信号,然而在CG领域,不能直接显示连续信号,但可以使用它们将连续信号重新采样到其它尺寸,即放大或者缩小信息。接下来讨论这个问题

Resampling重采样

重采样是用于缩放采样信号。假设原始采样点位于整数坐标,即采样点之间有单位间隔。假如重采样后,希望新的采样点间隔为a。当a>1为缩小(降采样),a<1为放大(升采样)

两者中比较简单的是升采样,所以我们先聊升采样。假如采样信号如前一节所示被重构,得到一个连续的重构信号,那么现在要做的就是以期待的间隔对重构信号进行重新采样。这个过程如图5.22所示

T05

左侧为采样信号和重构信号,右侧为以两倍的频率重采样重构信号,也就是放大。

但是这个技术并不适用于降采样,否则将无法避免走样,所以需要使用sinc的滤波器从采样信号中创建连续信号(参考文献[1447、1661])。这样就可以按照所需要的间隔进行重新采样了。如图5.23所示。这样,增加低通滤波的宽度,将重采样的频率降低到原来的一半,除去了更多高频信息。这种方式,类似于先模糊(去除高频信息),然后以较低分辨率重新采样图像。

T05

左侧为采样信号和重构信号,右侧为宽度为原来两倍的滤波器,这样的话,就是降采样

下面,将以采样和滤波理论为框架,讨论实时渲染中反走样的各种算法。

5.4.2 屏幕空间反走样

当没有很好的采样和滤波的时候,三角形的边缘就会出现明显的走样现象。阴影边界、高光或者其他颜色快速变化的地方都可能会出现类似的问题。本节的算法有助于提高这些情况的渲染质量。它们都是针对屏幕进行处理,也就是仅对渲染管线的输出进行处理。没有一种AA算法是完美的,每种技术在质量、捕捉尖锐细节、移动物件、内存消耗、GPU消耗等方面都各有不同的优化和劣势。

在图5.13中的黑色三角形示例中,其中一个问题是采样率低。如果在每个像素的网格单元中心处取一个样本,只能知道该中心点是否会被三角形覆盖。而如果在网格单元中通过更多的采样点进行采样,并以某种方式混合这些样本,就可以计算出更好的像素颜色,如图5.24所示。

T05

如上图所示,通过一个位于像素中心的采样点,采样一个红色三角形,由于三角形无法覆盖样本,所以即使大部分像素被红色覆盖,但依然认为是白色。而在右边,针对每个像素使用四个样本,可以看到其中两个样本被红色三角形覆盖,所以像素颜色会被认为是粉红色。


虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2019 年 7 月 22 日发表,原文链接(http://geekfaner.com/shineengine/Translation5_RealTime_Rendering_4th_Edition1.html)