简单易懂的深度剖析研究unity表面着色器(Surface Shader)

先介绍一下表面着色器:

表面着色器实际上就是在顶点片元着色器上添加了一层抽象,顶点片元着色器是硬件能理解的渲染方式,而开发者应该使用一种更容易理解的方式。很多时候使用表面着色器,我们只需要告诉shader:“使用这些纹理去填充颜色。使用这个法线纹理去填充表面法线,使用兰伯特光照模型。其他的就不要来烦我了!”我们不需要考虑是使用前向渲染还是延迟渲染路径,场景中有多少点光源,他们的类型是什么,怎么处理这些光源,每个pass需要处理多少个光源灯问题。我们可以轻松实现常见的光照模型,甚至不需要和任何光照变量打交道,unity就帮我们处理好了每个光源的光照结果。一个表面着色器最重要的部分是两个结构体以及它的编译指令。其中两个结构体是表面着色器中不同函数之间信息传递的桥梁,而编译指令是我们和unity沟通的重要手段。

一、编译指令

编译指令是我们和unity沟通的重要方式,通过它可以告诉unity:“用这个表面函数设置表面属性,用这个光照函数模拟光照,我不要阴影和环境光,不要雾效!”只需要一句代码就可以完成这么多事情。

编译指令最重要的作用是指明该表面着色器使用的表面函数和光照函数。并设置一些可选参数,表面着色器的CG块中第一句代码往往就是他的编译指令。格式如下:

#pragma surface surfaceFunction lightModel [optionalparams]

其中#pragma surface 用于指明该编译指令是用于定义表面着色器的,在他后面需要指明使用的表面函数(surfaceFunction)和光照模型[lightModel]。同时,还可以使用一些可选参数来控制表面着色器的一些行为。

1.1 表面函数

表面着色器的优点在于抽象出了"表面"这一概念。与之前遇到的顶点/片元抽象层不同,一个对象的表面属性定义了它的反射率、光滑度、透明度等值。而编译指令中的surfaceFunction就用于定义这些表面属性。surfaceFunction通常就是名为surf的函数(函数名可以是任意的),它的函数格式的固定的:

void surf (Input IN,inout SurfaceOutput o)

void surf (Input IN,inout SurfaceOutputStandard o)

void surf (Input IN,inout SurfaceOutputStandardSpecular o)

其中后两个是unity 5中由于引入了基于物理的渲染而新添加的两种结构体。SurfaceOutput、SurfaceOutputStandard、SurfaceOutputStandardSpecular都是Unity内置的结构体,他们需要配合不同的光照模型使用。

在表面函数中,会使用输入结构体InputIN来设置各种表面的属性,并把这些属性存储在输出结构体SurfaceOutput、SurfaceOutputStandard、SurfaceOutputStandardSpecular中,在传递给光照函数计算光照结果。

1.2 光照函数

除了表面函数,我们还需要指定另一个非常重要的函数———光照函数。光照函数会使用表面函数中设置的各种表面属性,来应用某些光照模型,进而模拟物体表面的光照效果。Unity内置了基于物理的光照模型函数Standard和StandardSpecular(在UnityPBSLighting.cginc文件中被定义),以及简单的非基于物理的光照模型函数Lambert和BlinnPhong(在Lighting.cginc文件中被定义)。当然我们可以定义自己的光照函数。

1.3 其他可选参数

在编译指令的最后,我们可以设置一些可选参数,这些可选参数包含了很多非常有用的指令类型,例如:开启/设置透明度混合/透明度测试,指明自定义的顶点和颜色修改函数,控制生成的代码等。下面选取了一些比较重要的和常用的参数进行更深入的说明。

(1)自定义的修改函数。
除了表面函数和光照模型外,表面着色器还可以支持其他两种自定义的函数:顶点修改函数(vertex:VertexFunction)和最后的颜色修改函数(finalcolor:ColorFunction)。顶点修改函数允许我们自定义一些顶点属性,例如,把顶点颜色传递给表面函数,或者修改顶点位置,实现某些顶点动画等。最后的颜色修改函数则可以在颜色绘制到屏幕前,最后一次修改颜色值,例如实现自定义雾效等。

(2)阴影。

我们可以通过一些指令来控制和阴影相关的代码,例如,addshadow参数会为表面着色器生成一个阴影投射的pass,从而将物体正确地渲染到深度和阴影纹理中。但对于一些进行了顶点动画,透明度测试的物体,我们就需要对阴影投射进行特殊处理,为他们产生正确的阴影(比如树的叶子,会动的)。fullforwardshadows参数则可以在前向渲染路径中支持所有光源类型的阴影。默认情况下,unity只支持最重要的平行光的阴影效果。如果我们需要让点光源或聚光灯在前向渲染中也可以有阴影,就可以添加这个参数。相反的,如果不想对使用这个shader的物体进行任何阴影计算,就可以使用noshadow参数来禁用阴影。

(3)透明度混合和透明度测试。

我们可以通过alpha和alphatest指令来控制透明度混合和透明度测试。例如:alphatest:VariableName指令会使用名为VariableName的变量来剔除不满足条件的片源。此时,我们可能还需要使用上面提到的addshadow参数来生成正常的阴影投射的Pass。

(4)光照。

一些指令可以控制光照对物体的影响,例如,noambient参数会告诉Unity不要应用任何环境光照或光照探针(light probe)。novertexlights参数告诉unity不要应用任何逐顶点光照。noforwardadd会去掉所有前向渲染中的额外的pass。也就是说,这个shader只会支持一个逐像素的平行光。这个参数通常会用于移动平台版本的表面着色器中。还有一些用于控制光照烘焙,雾效模拟的参数,如nolightmap,nofog等。

(5)控制代码的生成。一些指令还可以控制表面着色器自动生成的代码,默认情况下,unity会为一个表面着色器生成相应的前向渲染路径、延迟渲染路径使用的Pass,这会导致生成的文件比较大。如果我们确定该表面,就可以exclude_path:deferred、exclude_path:forward和exclude_path:prepass来告诉unity不需要为某些渲染路径生成代码。

表面着色器支持的编译指令参数很多,为我们编写表面着色器提供了很大的方便。之前在顶点/片元着色器中需要耗费大量代码来完成的工作。在表面着色器只需要一个参数就可以了。当然表面着色器自身也有限制,优缺点后面讨论。

二、两个结构体

表面着色器支持最多自定义4种关键的函数:表面函数(用于设置各种表面性质,如反射率,法线等),光照函数(定义表面使用的光照模型),顶点修改函数(修改或传递顶点属性),最后的颜色修改函数(对最后的颜色进行修改)。那么。这些函数之间的信息传递是怎么实现的呢?例如:我们想把顶点颜色传递给表面函数。添加到表面反射率的计算中,这就是两个结构体的工作。

一个表面着色器需要使用两个结果体:表面函数的输入结构体Input,以及存储了表面属性的结构体SurfaceOutput(Unity5新引入了两种结构体SurfaceOutputStandard和SurfaceOutputStandardSpecular).

2.1 数据来源:Input结构体

Input结构体包含了许多表面属性的数据来源,因此他会作为表面函数的输入结构体(如果自定义了顶点修改函数,他还是顶点修改函数的输出结构体)。Input支持很多内置的变量名,通过这些变量名,我们告诉unity需要使用的数据信息。

需要注意的是,我们并不需要自己计算上述的各个变量,而只需要在Input结构体中按上述名称严格声明这些变量即可,Unity会在背后为我们准备好这些数据。而我们只需要在表面函数中直接使用他们即可。一个例外情况是,我们自定义了顶点修改函数,并需要向表面函数中传递一些自定义的数据。例如,为了自定义雾效,我们可能需要在顶点函数修改中根据顶点在视角空间下的位置信息计算雾效混合系数,这样我们就可以在Input结构体中定义一个名为half fog的变量,把计算结果存储在变量后进行输出。

2.2 表面属性:SurfaceOutput结构体

有了Input结构体来提供所需要的数据后,我们就可以据此计算各种表面属性,因此,另一个结构体就是用于存储这些表面属性的结构体。即SurfaceOutput、SurfaceOutputStandard和SurfaceOutputStandardSpecular,它会作为表面函数的输出,随后会作为光照函数的输入来进行各种光照计算。相比于Input结构体的自由性,这个结构体里面的变量是提前就声明好的,不可以增加也不会减少(如果没有对某些变量赋值,就会使用默认值)。SurfaceOutput的声明可以在Lighting.cginc文件中找到:

struct SurfaceOutput{

fixed3 Albedo; //对光源的反射率。通常由纹理采样和颜色属性的乘积计算而得。

fixed3 Normal; //表面法线的方向。

fixed3 Emission;//自发光。unity通常会在片元着色器最后输出前,使用c,rgb+=o.Emission;类似的语句进行颜色叠加

half Specular; //高光反射强度计算中指数部分的系数,影响高光反射的计算。例如float spec=pow(nh,s.Specular*128.0)*s.Gloss

fixed Gloss;  //高光反射中的强度系数,一般在包含高光反射的光照模型中使用

fixed Alpha;  //透明通道。如果开启透明度的话,会使用该值进行颜色混合
};

在一个表面着色器中,只需要选择上述三者中其一就行,这取决于使用了什么光照模型。一种比较简单的Lambert和BlinnPhong。另一种基于物理的光照模型,包括Standard和StandardSpecular,这种模型更加符合物体规律,但计算也会复杂很多。SurfaceOutputStandard结构体默认用于金属工作流程,对应了Standard光照函数,而SurfaceOutputStandardSpecular结构体用于高光工作流程。对应了StandardSpecular光照函数。

三、unity背后做了什么

untiy实际会在背后为表面着色器生成真正的顶点/片元着色器(包含了很多pass,这些pass有些是针对不同的渲染路径)。例如默认情况下Unity会为前向渲染路径生成LightMode为ForwardBase和ForwardAdd的Pass,为unity5之前的延迟渲染路径生成LightMode为PrePassBase和PrePassFinal的Pass,为unity5之后的延迟渲染路径生成LightMode为Deferred的Pass。还有一些pass是用于产生额外的信息,例如,为了给光照映射和动态全局光照提取表面信息,unity会生成一个LightMode为Meta的pass。有些表面着色器由于修改了顶点位置,因此我们可以利用addshadow编译指令生成它相应的LightMode为ShadowCaster的阴影投射pass。这些pass的生成的生成都是基于我们在表面着色器中的编译指令和自定义函数,都是有规律可循的。

unity有一个功能,可以定义表面着色器生成的代码一探究竟:在每个编译完成的表面着色器面板上。都有一个"Show generated code"按钮,点击就可以看到这个表面着色器生成的所有顶点片元着色器。

生成的代码不在细讲了。

四、Surface shader的缺点

表面着色器只是Unity在顶点片元着色器上面提供的一种封装,是一种更高层的抽象,但任何在表面着色器上完成的事情,我们都可以在顶点片元着色器上面重现。但不幸的是,这句话反过来并不成立。如果我们想获得便利,就需要牺牲自由度为代价,表面着色器虽然可以快速实现各种光照效果,但我们失去了对各种优化和各种特效实现的控制。因此使用表面着色器往往对性能造成一定的影响,而内置的shader,例如diffuse、Bumped Speculiar等都是用表面着色器编写的,尽管提供了相应的移动平台的版本,但这些版本只是去掉了额外的逐像素Pass、不计算全局光照和其他一些光照计算上的优化。但想要进行更多深层的优化,表面着色器就不要满足我们的需求了。

除了性能比较差以外,表面着色器还无法完成一些自定义的渲染效果,表面着色器的这些缺点让很多人更愿意使用自由的顶点/片元着色器来实现各种效果,尽管处理光照时这可能难度更大些。

最后给一些建议:

如果你需要和各种光源打交道,尤其是想要使用unity中的全局光照的话,你可能更喜欢使用表面着色器,但要时刻小心它的性能。

如果你需要处理的光源数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。

最重要的是:如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。

版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/100118784

最新文章