渲染

Real-time Rendering 第五章 渲染基础(上)

Spread the love

当你在渲染三维物体的图像的时候,模型不只是拥有本身的形状,他们也应该包含所希望的视觉表现。有应用程序本身决定,是基于真实感还是更加分割华,在下图可以看到。

这一章会讨论这些渲染的方面,对于真实感以及风格化渲染都非常有用。第15章专门讨论了风格化渲染,是本书的标志性部分,第九章到十四章则是专注于基于物理表现的用于真实感渲染。

5.1 渲染模型

在决定渲染物体的表现的时候第一步就是确定渲染模型,用于描述物体表面的颜色。这个会被非常多的因素影响,例如表面方向,视觉方向以及光照。

举一个例子,我们会使用一个Gooch shading model的变体。

Gooch, Amy, Bruce Gooch, Peter Shirley, and Elaine Cohen, “A Non-Photorealistic Lighting Model for Automatic Technical Illustration,” in SIGGRAPH ’98: Proceedings of the 25th Annual Conference on Computer Graphics and Interactive Techniques, ACM, pp. 447–452, July 1998. Cited on p. 103

这是一种非真实感的渲染模型,在15章有所提及。这种渲染模型设计用于增强技术图形的可识别度。

Gooch shading的基本思想是将表面发现与光源位置进行比较。如果法线朝向光照,那么将使用更温暖的色调,否则使用冷色调。在这些角度之间直接基于用户提供的颜色进行插值。在这个例子里面我们添加了一个风格化的高光效果,来给模型一个有光泽的表现。下图是渲染后的结果。

渲染模型经常有控制表现的变量。设置这些值往往是下一步调整模型的操作。我们的例子只有一种属性,表面色,就像上图所示。

就像大部分的模型,这个例子由表面方向以及视角与光照方向相关。对于渲染而言这些值往往是标准化之后的,例如下图中的说明一样。

现在我们已经定义了我们光照模型中所有的输入。我们能够看到下面的光照模型数学定义。

在这个方程式中,我们有以下的间接计算。

很多数学公式在其他的光照模型中也能找到。Clamp操作,一般是Clamp到0或者0到1的操作在shading中非常常见。我们使用符号,用于Clamp0到1,用于光照混合因子s。点乘操作出现了三次,每次都是由两个单位向量想成。这是非常常用的用法。两个向量的点乘是他们的长度以及角度cos的结果。所以两个单位长度的向量是cos的简化,在涉及到两个向量角度的时候非常有用。这些cos的组合在计算两个角度关系的时候非常方便而精确。比如光照模型中用到的光照方向与表面法向量。

另外一个常用的shading操作是对两个颜色进行插值。操作一般使用的方式,ca与cb随着t的变化从0到1进行插值。这种操作在这个渲染模型中出现了两次,第一次是裸粉色以及暖色的插值,第二次是高光插值。线性插值也是非常常用的操作,内置在了shader函数里面。一般是lerp或者mix,每种shader语言中都会有。

r=2(n*l)n-l计算了反射光线,光线l被n法线反射。没有之前两个操作这么常用,但是也是足够在大部分的shader语言中内置了reflect函数。

通过结合这么多不同的方法以及渲染参数,渲染模型可以产生大量风格化以及真实感的表现。

5.2 光源

我们的光照模型收到光源的影响非常简单。方向起到了非常大的作用。当然真实世界的光照相当复杂。真实世界有非常多的光源,并且有自己的尺寸、形状以及颜色和强度。间接光源就更加复杂了。我们在第9章会看到机遇物理的真实感光照模型需要将所有这些因素都考虑进去。

相反,风格化渲染模型可以将光照用于许多不同的方式,取决于应用程序的可视化需要。一些高度风格化的模型根本没有光照的概念,或者只是提供了简单的方向性。

下一步渲染模型的光照复杂度主要集中在用数字化的方式表现出光源的明暗效果。使用这种模型渲染出来的表面将会在光的不同明暗表现下表现出不同的效果。光照的距离、阴影(在第七章讨论)是否表面朝向光源(光照方向与法线角度大于等于90°)或者其他因素结合起来。

接下来一部进行数字化表现是光照强度的连续性。这个能够使用普通的插值来搞定,设定一个边界来设定强度,也许是0到1,或者是没有边界的以其他方式来影响强度。一种普通的方式是使用一个参数来表示光照对lit以及unlit部分的影响,光照强度klight线性缩放lit部分。

很容易扩展到RGB颜色版本。

以及多光源版本

非光照部分funlit(n,v)与不被光照影响的部分表现一致。有很多种表现形式,决定于希望的可视化类型以及应用程序的需要。举个例子,funlit()=(0, 0, 0)将会造成完全不被光照影响的情况下变成纯黑。换句话来说,费光照部分可以通过一些风格化表现的形式类似于Gooch model远离光照的冷色进行表现。这一部分的光照模型表现出一些不是直接受到光源影响的光照效果。类似于天光或者来自其他物体的间接光。这些类型的光照可以在第10章以及第11章看到。

我们在更早的时候提到了光照如果与表面角度大于90°就不会收到影响,背向的表面是不会收到影响。这个可以看做一种更常见的光照方向与表面的影响,以及对渲染的影响。尽管基于物理的原理,这种关系可以从简单的几何规则衍生出来,对于许多非基于物理、以及风格化的渲染模型也非常有用。

光照在表面的效果可以看做是一组射线,射线几种表面的密度与用于渲染表面的光照强度是一样的。在下图

显示了光照表面的横截面。光照射线射到表面的间隔与光照方向与表面的角度cos值成反比。所以可以看出l与n的cos值与光照强度有直接关系,我们可以看清这是两个单位向量的点乘。这里我们就可以推导出为什么光照方向相反的方向用起来更加方便,因为否则的话我们用之前还要取反一下。

更具体地来说光照强度与点乘是整数的时候是有正向关系的。负值则是光照方向的背面,完全不受光照影响。所以在光照渲染点乘之前我们需要将点乘结果进行clamp到0.使用x+ 意味着将负值clamp到0.可以得到。

光照模型支持多光源使用等式5.5的方式,或者等式5.6则是基于物理光照需要的。对于风格化模型也是有益的,因为帮助我们保证光照的一致性,特别是表面远离灯光或者在阴影之中。然而一些光照模型并不是很适合这个公式,这样的光照模型则会使用等式5.5.

最简单的用于flit()的函数是直接用一个颜色。

可以输出以下结果。

lit部分的光照模型使用的是兰伯特光照模型,Johann Heinrich Lambert在1760年就提出了!

Lambert, J. H., Photometria, 1760. English translation by D. L. DiLaura, Illuminating Engineering Society of North America, 2001. Cited on p. 109, 389, 390, 469

这个光照模型用于理想表面的光照模型,表面是完全无光泽的。我们给与了一些简化的兰伯特模型的说明,在第九章会被提到。兰伯特模型可以用于简单渲染,在很多的渲染模型中都是核心部分代码。

我们可以在等式5.3到5.6中可以看到,光照模型通过两种方式与渲染模型交互:光照方向l以及光照颜色clight 目前有许多类型的光照模型,在不同的场景使用不同的方式使用这两个参数。

我们接下去会讨论若干类型的光源类型,有一个共性:给与一个表面位置,每个光源在一个方向照亮表面。换句话说,这个光源表面看做一个无限小的点。这在真实世界光照中是不严谨的,但是大部分的光源距离相对于他们的光源表面大小来说没太大联系。在7.1.2节以及10.1节中我们会讨论一个范围的光照方向的照明方式,也就是区域光。

5.2.1 方向光(Directional Lights)

方向光是最简单的光源。l以及clight是常量,光照色也许会被阴影衰退,方向光没有位置。当然实际上的光源都是由特定的空间位置的,方向光是抽象的,对于大范围的场景光而言的有用的。例如一个20英尺外的泛光灯照明了一个小的桌面立体模型可以看做一个方向光。另一个例子就是整个场景被太阳照亮了,除非场景处于太阳系的一个星球。

方向光的概念可以扩展改变任意clight的值,而方向l则是一个常量。这经常用于特定的整块场景,用于性能或者创意的原因。举个例子,整个区域可以用两个定义的嵌套的盒装体积,clight在外部是(0,0,0)而内部则是一个常量值,相当于在盒内是一个常量,并且在两个区域之间进行线性的插值。

5.2.2 精确光源(Punctual Lights)

精确光源不是说时间相关,而是一个拥有位置的灯光,不像方向光。这样的灯光也没有尺寸,没有形状和大小,不像真实世界的光源。我们使用了Punctuak来自于拉丁语punctus,含义是点,意味着任意光源来自于单个明确的点。我们使用点光源(point light)来表示一种光照,例如向周围所有方向发光的灯光。所以点光源和投射光源和精确光源是完全不同的类型。灯光方向决定于当前渲染的表面点于灯光的位置。

这个等式其实就是向量标准化:将向量除以其长度。这个是另一个常用的渲染操作,就像之前提到的其他操作一样,这个操作也是内置在shader语言里面的,然而,有时候中间计算过程也是需要的,比如想要明确表示标准化,使用更多基础的操作。要计算精确光源的方向还需要以下操作。

因为点乘的两个向量等于两个单位的长度以及两个向量直接角度的cos值相乘,0°是1.0。一个向量与自身点乘就是他长度的平方。所以我们要知道其长度只需要与自身点乘然后开方就能得到相应的结果了。

中间结果就是r精确光源与渲染点的距离。因为使用了标准化的灯光方向r也需要计算对灯光颜色的衰退,基于灯光距离。这个在后面的章节也会提到。

点光源/泛光灯(Point/Omni Light)

精确光源在所有方向发射的话就会成为点光源或者泛光灯。对于点光源,变化就是函数中的距离r,原始仅有的随着距离衰弱光照强度的光源。下图显示了光强度是如何衰减的。

对于渲染的表面,射线从电光到表面的距离的照明面积是成正比的。不像之前横切面图5.4一样的表现,这个照明面积随着距离增加而成平方增大。所以距离与强度成1/r2的比例关系。这就可以让我们指定clight的强度。以下是关系等式。

这个等式经常用于表示inverse-square light attenuation反比乘法灯光衰退,然而这种写法目前还有一些问题,在技术实现上。首先,如果距离趋向于0的话clight的强度会接近于无线,当r到达0的时候我们会得到一个除零错误。为了解决这个,一个常用的修改就是加上一个小常量来避免。

Karis, Brian, “Real Shading in Unreal Engine 4,” SIGGRAPH Physically Based Shading in Theory and Practice course, July 2013. Cited on p. 111, 113, 116, 325, 336, 340, 342, 352, 355, 383, 385, 388, 421, 423

具体的这个值由应用程序自身来决定,比如对于虚幻引擎而言使用1cm。

Karis, Brian, “Real Shading in Unreal Engine 4,” SIGGRAPH Physically Based Shading in Theory and Practice course, July 2013. Cited on p. 111, 113, 116, 325, 336, 340, 342, 352, 355, 383, 385, 388, 421, 423

另外,在CryEngine以及寒霜引擎则是将rclamp到一个最小值。

Schulz, Nicolas, CRYENGINE Manual, Crytek GmbH, 2016. Cited on p. 111, 113, 631

Lagarde, S´ebastian, and Charles de Rousiers, “Moving Frostbite to Physically Based Rendering,” SIGGRAPH Physically Based Shading in Theory and Practice course, Aug. 2014. Cited on p. 111, 113, 115, 116, 312, 325, 336, 340, 341, 354, 371, 422, 426, 435, 503, 890

不像之前方式使用的常量,rmin是有物理解释的:物理物体发射光照的半径小于最小的r会造成渲染表面与物理光源穿插,是不可能的。

相反的第二个问题在于大距离的时候,这个问题不是关于视觉的而是关于性能的。light虽然永远下降,但是永远不会到达0,在有限的距离内(第20章)。有太多方式可以修改反比平方方程的。但是最好是尽可能减少对其的修改。为了避免硬裁切,一种方式是将平方反比等式乘以一个windowing function。这样的方程在虚幻以及寒霜引擎中都有用到。

Karis, Brian, “Tiled Light Culling,” Graphic Rants blog, Apr. 9, 2012. Cited on p. 113, 882

Karis, Brian, “Real Shading in Unreal Engine 4,” SIGGRAPH Physically Based Shading in Theory and Practice course, July 2013. Cited on p. 111, 113, 116, 325, 336, 340, 342, 352, 355, 383, 385, 388, 421, 423

Lagarde, S´ebastian, and Charles de Rousiers, “Moving Frostbite to Physically Based Rendering,” SIGGRAPH Physically Based Shading in Theory and Practice course, Aug. 2014. Cited on p. 111, 113, 115, 116, 312, 325, 336, 340, 341, 354, 371, 422, 426, 435, 503, 890

上面+2的表示clamp到这个值,如果负数,则在平方前clamp到0.下图表示了一个例子,等式5.14中的一个结果。

应用程序将会对需要选择的函数进行选择,举个例子,在rmax到达0 的方式在空间频率很低的时候非常重要。CryEngine没有使用光照贴图或者顶点Texture,所以它使用了更简单的方式,在0.8rmax到rmax的时候使用了线性衰减。

对于一些应用程序,符合反比平方的曲线不是有限的,所以其他的方式也会被使用。我们可以将5.11到5.14的等式一般化:

fdist(r)是一些距离函数。一些函数被称作distance falloff function。在一些情况下,使用非反比乘方的方程式因为性能的考量。比如在游戏《正当防卫2》中需要的灯光需要更加节约的算法,在平滑的同时避免了逐顶点的不自然效果。

在在其他的情况下,使用衰减函数是处于创作考虑。比如在虚幻中,可以使用真实以及风格化效果,有两种灯光衰减,就像等式5.12中描述的一个指数衰减方法可以创建非常多的衰减曲线效果。

Unreal Engine 4 Documentation, Epic Games, 2017. Cited on p. 114, 126, 128, 129, 262, 287, 364, 611, 644, 920, 923, 932, 934, 939

古墓丽影的开发者使用了样条曲线编辑器来编辑衰减效果,允许更自由的编辑变化。

Lacroix, Jason, “Casting a New Light on a Familiar Face: Light-Based Rendering in Tomb Raider,” Game Developers Conference, Mar. 2013. Cited on p. 114, 116

投影光(spotlights)

不像点光源,来自真实世界的光源光照在不同的方向都有不同的变化。这个变化可以由一个衰减函数描述,结合了方向的衰减函数就会变成以下等式:

不同的fdir(l)的选择可以产生出不同的光照效果,一个重要的类型是投影光,在一个圆锥体重投射光线。一个投影光的光照衰减函数围绕着投影光方向是旋转对称的。因此可以表示出一个角度与光照方向相反向量相关的方程。光照方向要相反是因为我们定义了l是点朝向灯光的,这里的话我们则需要点远离灯光的。

大部分的投影光方程使用了θs的cos,最基本的角度计算。投影光一般有一个umbra angle ,θu,将灯光保卫,所有大于这个角度的fdir(l)=0。这个角度能够用于剔除大于衰减距离的方向。一般也会有一个角度θp,小于这个角度的则是最大强度,而在两者之间的则是使用插值,如下图所示。

不同的方向衰减函数用于透射光,但是他们大体上看起来一样。举个例子,虚幻使用的fdir(l)与three.js使用的进行比较。

Lagarde, S´ebastian, and Charles de Rousiers, “Moving Frostbite to Physically Based Rendering,” SIGGRAPH Physically Based Shading in Theory and Practice course, Aug. 2014. Cited on p. 111, 113, 115, 116, 312, 325, 336, 340, 341, 354, 371, 422, 426, 435, 503, 890

Cabello, Ricardo, et al., Three.js source code, Release r89, Dec. 2017. Cited on p. 41, 50, 115, 189, 201, 407, 485, 552, 628

soomthstep函数是一个三次多项式经常用于光滑插值,在大部分的shading语言里面都有所内置。

下图显示了一些我们已经讨论的光照类型。

其他精确光源

有许多方式来描述不同的精确光源的颜色clight变化。

fdir(l)函数不仅仅限制于投影光的衰减函数,也可以表示许多的方向类型。包括复杂的制表类型来表现现实中测量的一些光源。Illuminating Engineering Society 定义了一种测量格式。IES的数据大量在游戏《Killzone:Shadow Fall》中有用到,虚幻以及寒霜引擎中也有用到。

Karis, Brian, “Real Shading in Unreal Engine 4,” SIGGRAPH Physically Based Shading in Theory and Practice course, July 2013. Cited on p. 111, 113, 116, 325, 336, 340, 342, 352, 355, 383, 385, 388, 421, 423

Lagarde, S´ebastian, and Charles de Rousiers, “Moving Frostbite to Physically Based Rendering,” SIGGRAPH Physically Based Shading in Theory and Practice course, Aug. 2014. Cited on p. 111, 113, 115, 116, 312, 325, 336, 340, 341, 354, 371, 422, 426, 435, 503, 890

Lagarde对此格式解析问题做了一个很好的总结。

Lagarde, S´ebastian, “IES Light Format: Specification and Reader,” S´ebastian Lagarde blog, Nov. 5, 2014. Cited on p. 116, 435

游戏古墓丽影中有一种精确光源使用了独立的衰减方式通过x、y、z的世界坐标,在古墓丽影中还使用了基于时间的光照强度,来制造闪烁的效果。

在6.9节我们会讨论如何通过贴图来控制灯光强度和颜色。

5.2.3 其他灯光类型

方向光与精确光源基本上描述了如何通过光照方向进行计算。不同类型的灯光可以通过其他的方式来进行定义。举个例子,古墓丽影用了胶囊体的灯光使用于条状光源,而并非使用一个点光源。

Lacroix, Jason, “Casting a New Light on a Familiar Face: Light-Based Rendering in Tomb Raider,” Game Developers Conference, Mar. 2013. Cited on p. 114, 116

对于每一个像素点,最接近于条形光的点用作灯光方向l。

使用l以及clight用于计算渲染方程,任意方程可以用于计算这些值。

目前讨论的这些灯光都是抽象的。在现实当中,光源有尺寸以及形状,他们通过不同的方向进行照明。在渲染时这样的灯光被称作区域光,他们在实时应用中的应用也越来越广泛。区域广渲染技术分成两种类型:模拟阴影边缘虚化(7.1.2节)以及模拟实际上一个区域的灯光渲染(10.1节)第二种类型的灯光是最光滑以及镜面感表面,灯光的形状以及大小可以在反射中看到。方向光以及精确光并不是弃用了,只不过它们不再像过去那么常用。近似的区域光实现起来非常简单,所以有更广泛的用途,增强的GPU性能也允许更多的精确的技术进行使用。

5.3 实现渲染模型

更实际的角度来看,shding以及光照公式还是需要用代码去实现。这一章节我们就会花时间去设计以及写这样的实现。我们也会看一看一个简单的实现例子。

5.3.1 计算的频率

当设计一个shading实现,计算必须根据其调用频率来分开。首先看一看需要的计算结果是不是在一个draw call里面都是一个常量。在这方面,计算可以通过应用程序来实现,例如在CPU,GPU Compute也可以用于特别大消耗的计算。结果可以通过图形API的uniform输入。

即使在这种情况下,也是存在更宽泛的计算评率,也就是一开始的时候需要算一次。最简单的例子就像是常量表达式,这个可以基于极少修改的参数,类似于硬件配置或者安装选项。例如shading计算也许在shader编译的时候就可以完成,甚至不需要设置一个uniform输入。当然,计算需要离线完成,在安装时或者程序载入时。

另一种情况是计算结果会在程序运行的过程当中变化,但是每一帧运行太慢了,不需要每一帧执行。举个例子,灯光因子决定于游戏世界一天当中的时间。如果计算开销非常大的话,每隔几帧进行更新是一个划算的做法。

另外一些计算需要每一帧计算,例如视角以及透视矩阵。或者每个模型一次,例如更新模型的灯光参数,一般由位置决定。或者每一个drawcall。更新模型上面的材质参数。通过更新频率来为uniform参数分组对于应用程序效率而言是非常有效的。快成帮助GPU最小化更新常量。

McDonald, J., “Don’t Throw It All Away: Efficient Buffer Management,” Game Developers Conference, Mar. 2012. Cited on p. 117

如果一个计算结果在每个drawcall中都会发生变化,则不能够通过uniform输入。这种计算会放在一个可编程阶段进行计算,如果有需要的话可以通过varying输入到其他阶段。理论上,shading计算可以在任意可编程阶段执行,任意一个阶段都有不同的执行频率。

  • 顶点Shader——每个细分前顶点

  • Hull Shader 每一个表面patch

  • Domain Shader——每一个细分后顶点

  • 几何Shader——每个图元

  • 像素Shader——每个像素

在实际中,大部分的shading计算都在每个像素中执行。在通过像素shader实现的同事,compute shader的使用也大幅增加了。在20章会提到很多例子。在其他的阶段中主要使用几何操作,类似于变化以及变形。要了解这种情况,我们会比较逐像素和逐顶点计算结果的区别。在更老的描述中,这些区别有时候会被描述成,Gouraud shading [578] 以及 Phong shading [1414]。

Gouraud, H., “Continuous Shading of Curved Surfaces,” IEEE Transactions on Computers, vol. C-20, pp. 623–629, June 1971. Cited on p. 118

Phong, Bui Tuong, “Illumination for Computer Generated Pictures,” Communications of the ACM, vol. 18, no. 6, pp. 311–317, June 1975. Cited on p. 118, 340, 416

虽然这些术语现在已经不怎么使用了。它们使用的都是等式5.1,但是表现出不同的效果。完整的渲染模型会在后续给出,我们会通过一个例子来进行实现。

上图显示了逐像素和逐顶点的渲染在不同的顶点密度上的表现。对于龙模型来说,是非常复杂的Mesh,两者的差别很小。但是在茶壶, 顶点着色器已经表现出来一些可见的高光的渲染错误,而在两个三角面片上的渲染则是完全不正确的。这是因为这些错误的部分,特别是高光,有着非常非线性的值变化。这让顶点shader非常难以正确表现。

原则上其实可以只把高光部分放到像素shader中,而剩余部分放在顶点shader上。这个不会造成画面错误,理论上也会节省开销。但是在实际使用中这种混合的方式实现经常是不可取的。线性变化的部分往往是最少消耗的部分,而分离的shading计算使用这种方式会造成足够的overhead,例如将计算结果拷贝追加到下一阶段,弊大于利。

就像我们之前提到的一样,在大部分顶点shader的实现中,都是非着色的操作,例如几何变化以及变形的操作。几何表面的属性,转换到适合的坐标系,由顶点着色器输出,由每一个三角形差值并且输出到像素shader作为可变的shader输入。这些类型一般包含了表面的位置,表面发现以及可选的切线向量如果需要用于法线映射。

需要注意的是顶点shader总是产生单位长度的表面发现,插值可能会改变它们的长度,可以看到下图左侧,因为这原因发现可能需要在像素着色器中重新标准化。然而顶点shader产生的发现长度依旧是有问题的。如果发现长度变化很大,例如顶点混合的边缘效果,将会使得插值错位。也可以在下图右边看到。由于这两个效果,向量经常在插值前以及插值后都要表转化,在顶点以及像素shader中都要这么做。

不像表面发现,指向特定位置的向量,例如视角向量以及精确光源的光照向量一般不会插值。插值表面位置用于在像素shader中计算这些值。不是使用标准化,而是在任何情况下都需要在像素shader中进行计算这个shader。这些向量都是通过向量减法得到的。如果因为一些原因需要对这些向量进行插值,不要对他们进行标准化,这会造成错误的结果,就如下图所示的。

之前我们提到了顶点shader将几何编码转换到了合适的坐标系。相机以及灯光位置通过uniform变量传给了像素shader,一般通过应用程序传入相同的坐标系。像素shader做的最小的一部分工作将所有的渲染模型向量带到相同的空间坐标系。但是什么坐标系是合适的坐标系?也是包含了世界坐标系,本地坐标系,相机坐标系或者更少用的,当前渲染模型的坐标系。这个一般由渲染系统来做决定,基于系统性,例如性能,灵活性以及简单性。举个例子,如果渲染的场景希望包含大量的灯光,世界空间或许会避免转换灯光位置。摄像机空间或许会更好,更好优化视角向量的操作以及更高的精度。(16.6节)

大部分的shader实现,包括我们将要讨论的例子,根据上述描述的,会有一定的要求。举个例子,一些应用程序选择有每一面的表面渲染用于风格化。这种风格被称作flat shading。下图有两个例子展示。

原则上,flat shading可以通过几何shader实现,但是现在一般使用顶点shader实现。这是假设每一个片元的属性以及其第一个顶点禁用了顶点插值。禁用插值会造成第一个顶点的值会用于整个图元。

5.3.2 实现例子

我们现在将展示一个渲染模型的实现。就像之前所提及的,这个渲染模型我将会用到类似于Gooch model的扩展(等式5.1),但是通过多光源进行修改。描述为:

以下是中间计算

以下公式来适应多光源情形,为了方便这里再写一下。

lit以及unlit在这里表现为:

冷色的非光照表现使得结果看起来更像原始等式。

大部分的渲染程序,材质的可变属性将会储存在顶点数据,或者贴图中(第6章)。然而为了保持这个例子的简单性,我们将颜色作为常量。

这个实现将使用shader的动态分支特性进行所有灯光的循环。这个方式能够在这样简单的场景中使用,但是对于大型或者复杂的场景则不能这么用。多光源渲染技术会在20章进行提及。当然为了更简洁我们将只会支持一种光源类型:点光源。虽然这个实现很简单,但是有两个最佳实践被提及。

渲染模型无法单独实现,而需要通过一个庞大的渲染框架实现。这个例子通过一个简单的WebGL2应用程序实现。通过修改Tarek Sherif的WebGL2例子“Phone-Shaded cube”而来。但是同样的原则会应用于更多复杂的框架之中。

Sherif, Tarek, “WebGL 2 Examples,” GitHub repository, Mar. 17, 2017. Cited on p. 122, 125

我们会讨论一些GLSL的代码以及JavaScript WebGL的程序调用。这样做的用意并非教授WebGL的API而是展示一些实现原则。我们会通过这些实现从内而外,从像素shader然后顶点shader最后到程序侧的API调用。

在shader代码之前,shader必须定义shader的输入以及输出。在3.3节讨论的,使用GLSL术语,shader输入分为两类。一种是uniform输入,也就是应用程序每个drawcall都相同的输入。而第二种则是varying输入,则会在shader调用之间变化,我们会看看像素shader的varying输入,在GLSL中输入被标志位in同样输出也是。

上面的像素shaer有一个单独的输出,也就是最终的shading颜色。像素着色器的输入必须与定点着色器的输出匹配,三角形插值之后被传送到像素着色器。这个像素着色器有两个varying输入:表面位置以及表面法线,在应用程序的世界空间坐标系中。uniform输入的数量非常大,为了简单我们之后展示两个,两个都是关于光源的。

因为有点光源,所以每个都需要定义一个位置以及一个颜色。这里定义vec4而不是vec3用于遵守GLSLstd140的数据结构标准。虽然在这种情况下std140会浪费一些空间,但是保证了CPU与GPU上一致的数据结构,也就是为什么用这种方式。Light结构体数组定义在了一个uniform block中,GLSL的特性将一组uniform变量绑定到一个buffer对象以获取更快的数据传输。数组长度取决于应用程序允许的最大灯光数量。我们可以看到,程序会将MAXLIGHTS字符串在编译之前替换成确定的值。常量整型uLightCount实际上是drawcall中激活的灯光数量。

下面我们看一下shader代码。

我们有一个被main函数调用的lit函数。这是对等式5.20以及5.21简单的实现。非lit部分以及暖光颜色度作为uniform值传入。因为这些值在drawcall过程中都是常量,应用程序可以不去计算这些值从而节约性能。

像素shader使用了若干GLSL内置函数。reflect函数,用于反射一个向量在这个例子中是光照向量,通过第二个向量定义的平面反射,在这个例子中是法线。因为我们希望灯光向量以及反射向量原理表面,所以我们传入之前需要取反。clamp函数有三个输入值,定义了一个裁剪范围以及一个裁剪的值。一个裁剪的特殊例子是从0到1,对应于HLSL的saturate函数,会更快,在大部分的GPU上都更高效。这也就是为啥我们在这里使用,即使我们只需要clamp到0,我们还是向上裁剪了1.函数miax也有三个输入,输出的是插值。在HLSL这个函数是lerp,对应“linear interpolation”最终,标准化来让一个向量大小变为1.

现在我们看看顶点shader。我们不会展示任何的uniform定义,因为我们已经在像素shader里面看过了一些例子,不过varying输入以及输出还是需要展示的。

需要注意的是,之前体积的vertex shader的输出需要对应pixel shader的输入。输入包含了数据的具体分布。vectex shader的代码如下所示。

有一个通常的操作,也就是将表面位置以及法线转换到世界空间坐标并且传入pixel shader。最终表面位置转换到裁剪空间并传入gl_Position,一个特殊的系统定义变量,用于栅格化。gl_Position是vectex shader一个必须的输出。

需要注意的是在vectex shader中的法线向量并没有标准化,他们不需要标准化,因为他们拥有一个长度为1的原始Mesh数据以及这个程序并没有执行任何关于顶点混合或者非uniform变化的操作,因为这些操作可能改变向量长度,这些影响向量长度的变化在之前的图中有展示过。

这个应用程序使用WebGL APi用于渲染以及shader组装。每一个可编程阶段是分别独立组装的,他们都组装到一个program object。这里是像素shader的组装代码。

注意 fragment shader,这个术语用于WebGL(以及OpenGL和其他基于之上的)之前本书也提过,为了统一,这里都使用pixel shader。这里的MAXLIGHTS被适当的数字值替代。多数的渲染框架使用相似的预编译手段。

有更多的代码来设置uniform以及初始化顶点数据,清理、绘制以及更多,你可以在这里查看程序。我们的目标是给与shader在一个编程环境如何跑起来的感觉,所以我们以这个作为结尾。

Sherif, Tarek, “WebGL 2 Examples,” GitHub repository, Mar. 17, 2017. Cited on p. 122, 125

5.3.3 材质系统

渲染框架很少使用单个shader实现。一般一个系统会用到很多材质,渲染模型以及shader。

在先前的章节中讨论的,一个shader是一个GPU可编程shader阶段的程序。这是一个底层API资源而不能直接给艺术家使用。相反,材质则是一个面向艺术家的可视化借口封装。材质有时候也是不可见的,例如碰撞属性,不过我们在这里不会进行讨论。

材质虽然是shader实现的,但是不是普通的一对一关系。在不同的渲染解决方案里,相同的材质或许使用不同的shader。一个shdare可以通过不同的材质分享。最通常的情况是参数化材质。最简单的格式,材质通过两个类型材质实体实现参数化:材质模板以及材质实例。每个材质模板描述了材质的类型以及可以设置的参数,例如颜色、贴图以及其他。每个材质实例则是针对参数指定了特定的值。一些渲染框架,例如Unreal允许一个更复杂以及分层的结构,材质模板在不同的等级衍生出其他模板。

Unreal Engine 4 Documentation, Epic Games, 2017. Cited on p. 114, 126, 128, 129, 262, 287, 364, 611, 644, 920, 923, 932, 934, 939

参数可以可能是在运行时指定的,通过一个uniform输入到shader程序,或者编译时,通过在shader编译前进行代入。一个普通的编译时类型是用于切换材质激活的特性。这个可以让艺术家通过勾选框选择自己想要的特性,降低shader的消耗,或者关闭微不足道的效果。

材质参数可能会和渲染模型一一对应,但是也不一定是这样。一个材质或许将渲染模型中的某个参数固定了,例如表面色。shader modle参数或许通过一系列计算的结果会作为材质的输入,例如插值后的顶点以及表面方向,甚至时间会作为一个计算因素算入。基于表面位置以及方向的材质参数经常用于地形材质。例如,高度以及表面法线用于控制雪的效果,混合白色的表面色以及水平高度。基于时间的shading经常用于动画材质,例如闪烁标记。

材质系统一个最重要的任务是将不同的shader函数分成不同的元素,然后控制他们如何组合,现在有很多方案来应用这样的组合方式:

  • 通过几何处理组合表面shader,例如刚体变换,顶点混合,形变,曲面细分,instancing,裁切。这些功能非常独立。表面shader取决于材质,几何处理基于mesh,所以很容易将他们与材质系统分离与组合。

  • 通过组合类似于像素裁切以及混合的操作组合表面shader。这个和移动GPU非常相关,这些GPU一般混合都在像素着色器处理。经常按照需求来选择这些不相关的操作用于表面着色。

  • 利用渲染模型计算本身来组合用于计算渲染模型的参数。这让我们可以编写渲染模型一次,而通过组合不同的方法来计算光照模型参数。

  • 将独立可选的材质特性进行组合,逻辑选择以及剩余的shader,这让我们可以将每一个特性分开来写。

  • 通过光源计算组合渲染模型以及参数计算,计算某个渲染点上每个灯光的颜色以及方向。一些类似于延迟渲染的技术,改变了这种组合的结构。在支持多种这样技术的渲染框架,增加了层的复杂性。

如果图形API可以提供模块化的shader代码形式的话会非常便利。但是令人沮丧的是,不像CPU代码,GPU代码不允许预编译连接代码片段。每一个shader阶段的程序单独编译。每个shader阶段的分离性确实也有提供局限性的模块性,也就是第一条:表面渲染与几何处理进行组合。但是这个并不完美,因为每个shader还会做其他的操作,其他类型的组合依旧需要解决。在这些限制之下,材质系统的组合只能在源码范围。这个涉及到了字符串操作,类似于连接与替换,经常通过C风格的处理方式,例如#include,#if以及#define。

早期的渲染系统只有很少的shader变体经常需要手写。这有一些好处,举个例子每个变体可以充分通过整体shader程序进行优化。然而,这个随着变体数量的增加而变成不可完成的任务。当所有不同部分以及可选项算进来的话,变体数量非常庞大。这也就是为啥模块化以及可组合性如此重要。

设计一个系统的第一个需要解决的问题是使用动态分支还是条件编译。在旧硬件上,动态分支经常是不可能或者非常慢的,所以实时分支不是一个选项。变体需要在编译时全部解决,包括所有可能的组合以及不同的灯光类型。

McTaggart, Gary, “Half-Life 2/Valve Source Shading,” Game Developers Conference, Mar.

  1. Cited on p. 127, 394, 402, 478, 488, 499

相反,现在GPU处理动态分支非常快,特别是在一个drawcall中的所有像素都走同一个分支的时候。今天大部分的函数性变量,例如灯光数量,是在运行时处理的。然而添加了大量的函数性变种产生了不同的消耗:增加的寄存器数量以及GPU占用率的减少,因此性能会下降。18.4.5节可以看到更多细节。所以编译时变体依旧是有价值的,它避免包含了从未运行的复杂的逻辑。

作为一个例子,我们想象一个应用程序支持三种灯光。两个灯光很简单,点光源以及方向光。第三种类型是一个普通投影光,支持制表光照模式以及其他复杂的特性,需要大量的shader代码进行实现。然而一般投影灯的用量少于5%。在过去一个分离的shader变体,用于计算不同的灯光组合是必要的,来避免动态分支。现在已经不需要这么做了,我们还是可以通过通过两个变体来做这件事情,一个用于投影灯是否大于等于1的情况,第二个则是投影灯刚好数量为0 的情况。为了简化代码,第二个变体可以有更低的寄存器占有率以及更好的性能。

现在材质系统同时拥有实施以及预编译的shader变体。即使不是将所有复杂度交给编译时,由于shader复杂度以及变体数量的增加,大部分的shader变体还是需要通过编译的方式来做。举个例子,在《Destiny: The Taken King》一些游戏区域查稿9000个shader变体用于一帧的渲染。

Tatarchuk, Natalya, and Chris Tchou, “Destiny Shader Pipeline,” Game Developers Conference, Feb.–Mar. 2017. Cited on p. 128, 129, 815

这个数量甚至可以变得更大。Unity渲染系统有着接近一千亿种可能的变体。只有那些实际使用的变体才会进行编译,但是shader编译系统需要重新设计来满足巨大的变体可能性。

Pranckeviˇcius, Aras, “Every Possible Scalability Limit Will Be Reached,” Aras’ blog, Feb. 5,

  1. Cited on p. 128

材质系统设计师使用不同的策略来达到目标。虽然有时候是互斥系统架构,但是多数情况下是几种策略同时起作用。策略有以下几种:

  • 代码重用——实现的函数通过分享文件,使用#include预处理,来获取他们想要的shader函数

  • 减法——一个shader经常作为一个ubershader或者supershader,集合成一个大的功能集合,结合编译时条件预处理以及动态分支去除不适用的部分,并且切换互斥的部分。

    McGuire, Morgan, “The SuperShader,” in Wolfgang Engel, ed., ShaderX4, Charles River Media, pp. 485–498, 2005. Cited on p. 128

    Trapp, Matthias, and J¨urgen D¨ollner, “Automated Combination of Real-Time Shader Programs,” in Eurographics 2007—Short Papers, Eurographics Association, pp. 53–56, Sept.

    1. Cited on p. 128

  • 增加——许多函数定义成一个包含输入以及输出连接的节点。类似于重用策略,但是更加结构化,节点组合可以通过text

    Delva, Michael, Julien Hamaide, and Ramses Ladlani, “Semantic Based Shader Generation Using Shader Shaker,” in Wolfgang Engel, ed., GPU Pro6, CRC Press, pp. 505–520, 2015. Cited on p. 128

    或者可视化图形编辑器。后者更容易被非工程师接受,例如技术美术,编辑新的材质模板。

    Tatarchuk, Natalya, and Chris Tchou, “Destiny Shader Pipeline,” Game Developers Conference, Feb.–Mar. 2017. Cited on p. 128, 129, 815

    Unreal Engine 4 Documentation, Epic Games, 2017. Cited on p. 114, 126, 128, 129, 262, 287, 364, 611, 644, 920, 923, 932, 934, 939

    一般只有一部分的shader可以提供可视化编辑。举个例子,在Unreal引擎的图形编辑器,只能影响渲染模型输入的计算

    Unreal Engine 4 Documentation, Epic Games, 2017. Cited on p. 114, 126, 128, 129, 262, 287, 364, 611, 644, 920, 923, 932, 934, 939

    如下图所示

  • 基于模板——定义接口,对于接口规范进行实现。这个更加类似于addtive模式用于更大块的功能。一般的例子是分离渲染模型的计算以及渲染模型本身的计算。Unreal有不同的 “matrial domain”

    Unreal Engine 4 Documentation, Epic Games, 2017. Cited on p. 114, 126, 128, 129, 262, 287, 364, 611, 644, 920, 923, 932, 934, 939

    包含了Surface domain用于计算光源颜色的调节。一个相似的Surface shader,unity也有提供

    Pranckeviˇcius, Aras, “Shader Compilation in Unity 4.5,” Aras’ blog, May 5, 2014. Cited on p. 129

    延迟渲染技术有类似的结构,其中G-buffer作为其接口。

更多的例子,《WebGL Insights》讨论了如何在shader管线里面进行大量的引擎控制。

Cozzi, Patrick, ed., WebGL Insights, CRC Press, 2015. Cited on p. 129, 1048

包括计算,有许多其他重要的设计思考在材质系统中,例如需要支持多平台最小程度的代码拷贝。这个包括为了性能的变体以及不同平台的能力,shadeing语言以及API。使用一个专门的处理器来处理自定义的shading语言方言。这允许通过自动翻译器编写平台无关的材质,翻译到不同平台的shader语言实现。Unreal以及Unity都有类似的系统。

材质系统也需要保证好的性能。包括专门的变体计算,有一些通用优化可以在材质系统使用。迪士尼的材质系统以及Unreal Engine自动检测一些Drawcall常量(类似冷暖色计算),并且将其移到shader之外。另一个例子是迪士尼的作用于系统,区分常量并且在不同的频率下进行更新,每一组常量都在合适的时机更新,减少了API的overhead。

正如我们所见,实现shading等式需要关注什么部分需要简化,哪些计算的频率是怎么样的,以及如何让用户修改并且控制外观。剩余的小节是关于抗锯齿、透明以及图片如何组合以及修改并且显示的细节。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.