《Unity3D高级编程之进阶主程》第七章,渲染管线与图形学(四) - 多重采样以及着色器编译原理

GPU上的多重采样(Multisampling)与反走样(Antialiasing)

多重采样(Multisampling)是一种对几何图元的边缘进行平滑处理的技术,也称为反走样技术之一。

===

OpenGL支持几种不同的反走样技术,比如线段反走样,多边形反走样,纹理图像压缩的质量以及导数精度设置。实时上这几个反走样技术都是以开关的形式在OpenGL中存在,算法大致是将原本单一的线条或像素块周围填充更多的像素块,具体的填充算法细节比我们想象的要复杂的多,而且不同OpenGL是现在的算法中也有细微的差异,我们在这里不进行深入讨论。

多重采样的工作方式是对每个像素的几何图元进行多次采样。在多次采样后,每个像素点不仅仅只是单个颜色(以及除了颜色外的深度值、模板值等信息),还记录了许多样本值。

这些样本值类似于将一个像素分拆成了更小型的像素,每个像素都存储着颜色、深度值、模板值等信息,当我们需要呈现最终图像的内容时,这个像素的所有样本值会被解析为最终像素的颜色。

在Unity3D中对这方面的反走样功能也提供了支持,可以通过Quality Settings中的AntiAliasing来设置,它将启用图形接口(OpenGL或DirectX)中的多边形的反走样算法,并且开启多重采样,根据多重采样信息对多边形边缘进行像素填充。

    AntiAliasing 可以设置3档采样质量分别是 2倍, 4倍 and 8倍的多重采样。

GPU上的反走样代价是消耗更多的GPU算力和显存,庆幸的它并不消耗CPU算力。

着色器编译过程与变体

我们在知道GPU渲染管线如何运作后,对着色器编译过程仍然需要知晓,我们还是以使用OpenGL为例来学习着色器在Unity3D中从编译到执行的全过程。

着色程序的编译过程与C语言等编译语言的编译过程非常类似,只是C语言在编译时是以离线的方式进行,而着色器程序的编译则是当引擎需要时,通过引擎调用图形接口(OpenGL或DirectX)的方式来进行编译,整个应用程序只需要编译一次后面可以重复利用,这和我们通常所说的JIT(Just in time 即时编译)有点相似。

上面所说的这些都是图形引擎控制和执行的,也就是当需要某个着色器程序时Unity3D引擎通过判断是否存在已经编译好的着色器程序,来决定是否编译着色器代码或是重用已经编译好的着色器程序。

那么着色程序从编译到执行过程到底是怎样的呢?让我们来讲一讲。

首先当引擎得知渲染需要用到的Shader不曾被编译过时,就会调用图形接口 glCreateShader 为着色器创建一个新的着色器对象。

然后通过程序从Shader文件中获取Shader内容(字符串)并调用 glShaderSource 将源代码(字符串)关联到刚刚创建的着色器对象上。

这时着色器对象已经关联了源代码,我们可以通过调用图形接口的编译接口 glCompileShader 对这个着色器对象进行编译。

编译完成后,我们可以通过 glGetShaderInfoLog 来获得编译信息以及是否成功的结果。

到此仅仅是某个着色器对象编译完成,这个着色器对象可能是顶点着色器,也可能是片元着色器,或也许是细分着色器或几何着色器。

通常情况下,有好几个着色器需要编译,顶点着色器和片元着色器通常都成对出现,则会创建相应的着色器对象来分别编译它们的源代码。

有了着色器对象还不够,我们需要把这些着色器关联起来。

首先引擎会使用 glCreateProgram 需要创建一个空的着色器程序。

然后多次调用 glAttachShader 来一个个地绑定着色器对象。

当所有必要的着色器对象关联到着色器程序之后,就可以链接对象来生成可执行程序了,调用 glLinkProgram 将所有关联的着色器对象生成一个完整的着色器程序。

当然,着色器对象也可能存在某些问题,因此在链接过程中依然可能失败,引擎通过 glGetProgramiv 来查询链接操作的结果,以及通过 glGetProgramInfoLog 接口来获取程序链接的日志信息,由此我们就可以判断错误原因。

成功完成了着色器程序的链接后,引擎就可以通过调用 glUseProgram 来运行着色器程序。

我们平常在Unity3D中用到的Shader中的Pass,每个Pass中都有着色器需要编译,因此每次在绘制不同的Pass时都会对Pass中的顶点着色器和片元着色器进行编译。也就是说,Unity引擎会为每个Pass标签生成一个着色器程序,生成这些着色器程序后,执行顺序仍然按照Pass的先后次序来。

除了着色器的编译流程外,Unity3D中的Shader Varant(变体)在实际开发中也是比较严重的问题

那么什么是“变体”呢。其实它是由Unity3D自身的宏编译指令引起的,它为不同情况而编译生成不同的着色器程序。从引擎端的说法来看,Unity3D把不同的编译版本拆分成了不同的着色器源代码,在运行时再通过图形接口将这些着色器源代码编译成不同的着色器程序。

为什么要使用宏编译指令导致生成这么多的着色器程序呢?因为要简化Shader,让同一个Shader在不同材质球上的应用不同的效果时更加便捷,修改和完善起来也更加高效。

假如编写很多同一个风格但不同效果的Shader,在使用和维护过程中会造成诸多的麻烦和不便,为了统一风格,为了提高效率,也为了能更好的打通各部门之间的沟通渠道,以及能让美术同学能更好的发挥对画面效果的调整,将同一个风格不同效果的Shader写在同一个Shader文件里是必不可少的,这样能更加容易的统一美术风格和制作流程,目的就是为了让风格更加统一,沟通更加便捷,效率更加高。

那么我们来看看Unity3D是怎么通过编译指令来编写宏以及它是怎么生成着色器源代码的。

Unity3D的Shader中使用

#pragma multi_compile
#pragma shader_feature

两个指令来实现着色器程序的自定义宏,它既适用于顶点片元着色器也适用于表面着色器。

在运行时,Unity3D会根据材质(Material)的关键字(Material的对象方法EnableKeyword和DisableKeyword)或者全局着色器关键字(Shader的类方法EnableKeyword和DisableKeyword)

我们通过 multi_compile 指令编写例如:

#pragma multi_compile A_ON B_ON
这样会生成并编译两个Shader(变体),一个是AON的版本,一个是BON的版本。

运行的时候Unity3D会根据材质(Material)的关键字或者Shader全局关键字判断应该使用哪个Shader,如果两个关键字都为false,那么会使用第一个(A_ON)Shader变体。

我们也可以创建多个组合关键字例如:

#pragma multi_compile A B C
#pragma multi_compile D E

这样会使得Shader的变体成倍的增加,例如上述的预编译方式,会生成 3x2 = 6 个变体,分别是 A+D、 B+D、 C+D、 A+E、 B+E、 C+E 六种。

假如multi_compile组合多到10行,每行2个,就是2的10次方个Shader(变体)就是1024个,这样生成这1024个Shader(变体),在打包时会耗费很长在变体的生成上的打包时间。

除了 multicompile 之外,另外一个指令 shaderfeature 也可以设置预编译宏,与 multicompile 的区别是 shaderfeature 不会将没有被使用到的Shader(变体)打包进包内,因此 shaderfeature 更适合材质球的关键字指定预编译内容,因为Unity3D只生成和编译被使用的预编译情况,而 multicompile 更适合全局Shader指定关键字,因为它会把所有组合都编译一遍,无论有没有用到。

除了这两个自定义预编译指令,Unity3D 本身自带的一些内建的 multi_compile 的快捷写法也会导致Shader变体的产生:

    multi_compile_fwdbase 为前向渲染编译多个变体,不同的变体处理不同的光照贴图的计算,并且控制了主平行光的阴影的开关。

    multi_compile_fwdadd 为前向渲染额外的光照部分编译多个变体,不同的变体处理不同灯光类型,平行光,聚光灯,点光,以及他们附带的cookie纹理版本。

    multi_compile_fwdadd_fullshadows 和 multi_compile_fwdadd 一样,并且包含了灯光的实时阴影功能。

    multi_compile_fog 为处理不同的雾效类型(off/linear/exp/exp2)扩展了多个变体。
总的来说,无论是 multicompile 还是 shaderfeature 亦或内建预编译指令,都会造成 Shader(变体)数量的增多,使得打包时间增加,运行时编译次数增多。当Unity3D在运行时检测到需要渲染的材质球里是不曾被编译的Shader时,则会将与自己匹配的Shader变体拎出来编译一下生成一个着色器程序,因此为了应对变体在运行时的编译消耗,通常会在运行时提前将所有Shader变体编译一下,使得运行中不再有Shader编译的CPU消耗。

参考资料:

《OpenGL编程指南》

《Unit3D Documentation》 https://docs.unity3d.com/Manual/SL-MultipleProgramVariants.html

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第七章,渲染管线与图形学(四) - 多重采样以及着色器编译原理

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号