赞
踩
本系列文章由@浅墨_毛星云 出品,转载请注明出处。
文章链接: http://blog.csdn.net/poem_qianmo/article/details/49719247
作者:毛星云(浅墨) 微博:http://weibo.com/u/1723155442
本文工程使用的Unity3D版本: 5.2.1
概要:本文讲解了Unity中着色器编译多样化的思路,并对Standard Shader中正向基础渲染通道的源码进行了分析,以及对屏幕油画特效进行了实现。
众所周知,Unity官方文档对Shader进阶内容的讲解是非常匮乏的。本文中对Stardard Shader源码的一些分析,全是浅墨自己通过对Shader源码的理解,以及Google之后理解与分析而来。如有解释不妥当之处,还请各位及时指出。
依然是附上一组本文配套工程的运行截图之后,便开始我们的正文。本次的选用了新的场景,正如下图中所展示的。
城镇入口(with 屏幕油画特效):
城镇入口(原始图):
图依然是贴这两张。文章末尾有更多的运行截图,并提供了源工程的下载。先放出可运行的exe下载,如下:
提示:在此游戏场景中按F键可以开关屏幕特效。
着色器编译多样化算是Unity5中Shder书写的新特性,标准着色器之所以能独当一面,正是得益于这种特性,在这里先对此特性进行一个简单的说明与讲解。
此部分参考自Unity5.2.1版官方文档(http://docs.unity3d.com/Manual/SL-MultipleProgramVariants.html),经翻译&理解后而成。如有解释不妥当之处,还请各位及时指出。
Unity5中使用了一种被称为着色器编译多样化(Multiple shader program variants)的新技术,常被称为“megashaders”或“uber shaders”,并通过为每种情况提供不同的预处理指令来让着色器代码多次被编译来实现。
在Unity中,这可以通过#pragmamulti_compile或者#pragma shader_feature指令来在着色器代码段中实现。这种做法对表面着色器也可行。
在运行时,相应的着色器变体是从材质的关键词中取得的(Material.EnableKeyword和 DisableKeyword),或者全局着色器关键词(Shader.EnableKeyword和 DisableKeyword)。
若我们定义如下指令:
也就表示定义了两个变体:FANCY_STUFF_OFF和FANCY_STUFF_ON。在运行时,其中的一个将被激活,根据材质或者全局着色器关键词(#ifdef FANCY_STUFF_OFF之类的宏命令也可以)来确定激活哪个。若两个关键词都没有启用,那么将默认使用前一个选项,也就是关闭(OFF)的选项FANCY_STUFF_OFF。
需要注意,也可以存在超过两个关键字的multi_compile编译选项,比如,如下代码将产生4种着色器的变体:
当#pragma multi_compile中存在所有名字都是下划线的一个指定段时,就表示需在没有预处理宏的情况下产生一个空的着色器变种。这种做法在着色器编写中比较常见,因为这样可以在不影响使用的情况下,避免使用两个关键词,这样就节省了一个变量个数的占用(下面会提到,Unity中关键词个数是有129个的数量限制的)。例如,下面的指令将产生两个着色器变体;第一个没有定义,第二个定义为FOO_ON:
这样就省去了一个本来需要定义出来的 FOO_OFF(FOO_OFF没有定义,自然也不能使用),节省了一个关键词个数的占用。
若Shader中有如上定义,则可以使用#ifdef来进行判断:
根据上面已经定义过的FOO_ON,此#ifdef判断的结果为真,代码段1部分的代码就会被执行到。反之,若#pragma multi_compile __FOO_ON一句代码没有交代出来,那么代码段1部分的代码就不会被执行。
这就是着色器编译多样化的实现方式,其实理解起来很容易,对吧。
1.2 shader_feature和multi_compile之间的区别
#pragma shader_feature 和#pragma multi_compile非常相似,唯一的区别在于采用了#pragmashader_feature语义的shader,在遇到不被使用的变体的时候,就不会将其编译到游戏中。所以,shader_feature中使得所有的设置到材质中的关键词都是有效的,而multi_compile指令将从全局代码里设置关键词。
另外,shader_feature还有一个仅仅含有一个关键字的快捷表达方式,例如:
此为#pragma shader_feature _ FANCY_STUFF的一个简写形式,其扩展出了两个着色器变体,第一种变体自然为不定此FANCY_STUFF变量(那么若在稍后的Shader代码中进行#ifdef FANCY_STUFF的判断,则结果为假),第二种变体为定义此FANCY_STUFF变量(此情况下#ifdef FANCY_STUFF的判断结果为真)。
1.3 多个multi_compile连用会造成指数型增长
可以提供多个multi_compile流水线,然后着色器的结果可以被编译为几个流水线的排列组合,比如:
第一行中有3种选项,第二行中有两种选项,那么进行排列组合,总共就会有六种选项(A+D, B+D, C+D, A+E, B+E, C+E)。
容易想到,一般每以个multi_compile流水线,都控制着着色器中某一单一的特性。请注意,着色器总量的增长速度是非常快的。
比如,10条包含两个特性的multi_compil指令,会得到2的10次方,也就是1024种不同的着色器变体。
1.4 关于Unity中的关键词限制Keyword limit
当使用着色变量时,我们应该记住,Unity中将关键词的数量限制在了128个之内(着色变量算作关键字),且其中有一些已经被Unity内置使用了,因此,我们真正可以自定义使用关键词的数量以及是小于128个的。同时,关键词是在单个Unity项目中全局使用并计数的,所以我们要千万小心,在同一项目中存在的但没用到Shader也要考虑在内,千万不要合起来在数量上超出Unity的关键词数量限制了。
1.5 Unity内置的快捷multi_compile指令
如下有Unity内置的几个着色器变体的快捷多编译指令,他们大多是应对Unity中不同的光线,阴影和光照贴图类型。详情见rendering pipeline 。
大多数内置的快捷指令导致了很多着色的变体。若我们熟悉他们且知道有些并非所需,可以使用#pragmaskip_variants语句跳过其中一些的编译。例如:
OK,通过上面经过翻译&理解过后的官方文档材料,应该对Unity中的着色器编译多样化有了一个理解。说白了,着色器变体的定义和使用与宏定义很类似。
上面交代了这么多,看不懂没关系,我们提炼一下,看懂这段提炼,关于着色器变体的意义与使用方式,也就懂了大半了。
若我们在着色器中定义了这一句:
这句代码理解起来,也就是_THIS_IS_A_SAMPLE被我们定义过了,它是存在的,以后我们如果判断#ifdef _THIS_IS_A_SAMPLE,那就是真了。我们可以在这个判断的#ifdef…… #endif块里面实现自己需要的实现代码X,这段实现代码X,只会在你用#pragma multi_compile 或#pragmashader_feature定义了_THIS_IS_A_SAMPLE这个“宏”的时候会被执行,否则,它就不会被执行到。
实现代码X的执行与不执行,全靠你对变体的定义与否。这就是着色器编译多样化的实现方式,一个着色器+多个CG头文件的小团队(如标准着色器),可以独当一面,一个打一群,可以取代一大堆独立实现的Shader的原因所在。
这一节主要用来解析Standard Shader中正向基础渲染通道的源码。
先上Standard Shader正向渲染基础通道(Shader Model 3.0版)的Shader源代码:
OK,一起来稍微分析一下上述代码。基本上是逐行注释,所以找几个容易疑惑的点来提一下。
第一处,着色器编译多样化部分,代码如下:
上文刚讲过着色器编译多样化的一些理解,理解起来就是这样,这边定义了很多的“宏”、 _NORMALMAP、_ALPHATEST_ON、_ALPHABLEND_ON、_EMISSION、_METALLICGLOSSMAP、_DETAIL_MULX2、_PARALLAXMAP,在顶点和片段着色器实现部分,可以用#ifdef _EMISSION类似的宏命令来对不同情况下的实现进行区别对待。
第二处,着色器编译多样化快捷指令部分,上文的讲解部分也有分别提到,这里代码注释已经很详细,如下:
第三处,顶点着色函数和片段着色函数声明部分,代码如下:
这里比较关键,指明了这个pass中顶点着色函数和片段着色函数分别是名为vertForwardBase和fragForwardBase的函数。而这两个函数定义于何处?看包含头文件是什么即可。一起来看一下第四处。
第四处,CG头文件包含部分,代码如下:
很简单的一句话,但却像一切编程语言中头文件的包含一样,非常关键,不能缺少。vertForwardBase和 fragForwardBase的函数全都定义于此“UnityStandardCore.cginc”头文件中。
OK,我们转到“UnityStandardCore.cginc”头文件,继续分析下去。先从vertForwardBase函数开始。
vertForwardBase函数也已详细注释好,代码如下:
基本步骤已经在代码注释中用序号列出,以下将对其中的主要知识点进行讲解。首先看一下函数的输出参数——VertexInput。
此结构体定义于UnityStandardInput.cginc头文件中,是顶点着色函数vertForwardBase的输入参数,相关代码如下所示:
此结构体比较通用,不仅仅是用于正向基础渲染通道,毕竟是定义在UnityStandardInput.cginc头文件中的。
各个变量的含义,注释中已经写到了,好像没有什么值得多说的,再来看下顶点输出结构体。
顾名思义,VertexOutputForwardBase结构体就是正向基础渲染通道特有的输出结构体,定义于UnityStandardCore.cginc头文件中,注释后的代码如下:
从这里开始,做一个规定,为了方便对照和理解,以下贴出代码中也会贴出原始的英文注释——先翻译为中文,以 || 结束,在 || 后附上原始的英文。
就像这样:
//最终的二次多项式 || Final quadraticpolynomial
OK,我们继续,vertForwardBase函数中有很多知识点值得拿出来讲一讲的。
UNITY_INITIALIZE_OUTPUT(type,name) –此宏用于将给定类型的名称变量初始化为零。在使用旧版标准所写的Shader时,经常会报错“Try adding UNITY_INITIALIZE_OUTPUT(Input,o); like this in your vertfunction.”之类的错误,加上这句就不会报错了。
_Object2World,Unity的内置矩阵,世界坐标系到对象坐标系的变换矩阵,简称“世界-对象矩阵”。
UNITY_MATRIX_MVP为当前的模型矩阵x视图矩阵x投影矩阵,简称“模型-视图-投影矩阵”。其常用于在顶点着色函数中,通过将它和顶点位置相乘,从而可以把顶点位置从模型空间转换到裁剪空间(clip space)中。也就是通过此矩阵,将三维空间中的坐标投影到了二维窗口中。
TexCoords函数用于获取纹理坐标,定义UnityStandardInput.cginc头文件中,相关代码如下:
函数实现代码中的_MainTex、_UVSec、_DetailAlbedoMap都是此头文件定义的全局的变量。
其中还涉及到了一个TRANSFORM_TEX宏,在这边也提一下,它定义于UnityCG.cginc头文件中,相关代码如下:
此函数位于unitystandardcore.cginc头文件中,原型和注释如下:
其中,SHADER_TARGET宏代表的值为和着色器的目标编译模型(shader model)相关的一个数值。
例如,当着色器编译成Shader Model 3.0时,SHADER_TARGET 便为30。我们可以在shader代码中由此来进行条件判断。相关代码如下:
UnityObjectToWorldNormal是Unity内置的函数,可以将法线从模型空间变换到世界空间中,定义于UnityCG.cginc头文件中,相关代码如下:
而其中的normalize( )函数太常见不过了,是来自CG语言中的函数,作用是归一化向量。
UnityObjectToWorldDir函数用于方向值从物体空间切换到世界空间,也定义于UnityCG.cginc头文件中,相关代码如下:
可以看到,就是返回一个世界-对象矩阵乘以方向值归一化后的结果,比较好理解。
CreateTangentToWorldPerVertex函数用于在世界空间中为每个顶点创建切线,定义于UnityStandardUtils.cginc头文件中,相关代码如下:
其中的unity_WorldTransformParams是UnityShaderVariables.cginc头文件中定义的一个uniform float4型的变量,其w分量用于标定奇数负比例变换(odd-negativescale transforms),通常取值为1.0或者-1.0。
此宏用于进行阴影在各种空间中的转换,定义于AutoLight.cginc中。在不同的情况下,此宏代表的意义并不相同。下面简单进行下展开分析。
对应于屏幕空间中的阴影,也就是#if defined (SHADOWS_SCREEN),其相关代码如下:
也就是说,这种情况下的TRANSFER_SHADOW(a)宏,代表了一句代码,这句代码就是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));
此句代码的含义是:将世界-阴影坐标乘以世界-模型坐标和物体顶点坐标的积,也就是先将物体坐标转换成世界坐标,再将世界坐标转换成阴影坐标,并将结果存放于a._ShadowCoord中。
而对于聚光灯的阴影,也就是#if defined (SHADOWS_DEPTH)&& defined (SPOT)
有如下定义:
可以发现,这种情况下的TRANSFER_SHADOW(a)宏代表的语句也是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));
同上,用途就是先将物体坐标转换成世界坐标,再将世界坐标转换成阴影坐标,并将结果存放于a._ShadowCoord中。
而对于点光源的阴影,也就是#if defined (SHADOWS_CUBE),有如下定义:
也就是说,这种情况下的TRANSFER_SHADOW(a)宏代表语句a._ShadowCoord = mul(_Object2World, v.vertex).xyz -_LightPositionRange.xyz;
想了解此代码的含义,先要知道_LightPositionRange变量的含义。
这个变量是UnityShaderVariables.cginc头文件中定义的一个全局变量:
从英文注释可以发现,此参数的x,y,z分量表示世界空间下光源的坐标,而w为世界空间下范围的倒数。
那么此句代码的含义,也就是先将物体-世界矩阵乘以物体顶点坐标,得到物体的世界空间坐标,然后取坐标的xyz分量,与光源的坐标相减,并将结果赋给a._ShadowCoord。
而对于关闭阴影的情况,也就是#if !defined (SHADOWS_SCREEN)&& !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE),有如下定义:
这种情况下的TRANSFER_SHADOW(a)宏代表的是空白,并没有什么用。
定义于UnityStandardCore.cginc头文件中。详细注释后的代码如下:
其中有一些小的点,这边提出来讲一下。
unity_LightmapST变量类型为float4型,定义于UnityShaderVariables.cginc头文件中,存放着光照贴图操作的参数的值:
此宏定义于UnityCG.cginc中,相关代码如下:
可以发现,这个宏,其实就是将LIGHTMAP_OFF(关闭光照贴图)宏和DYNAMICLIGHTMAP_OFF(关闭动态光照贴图)宏的定义进行了封装。
UNITY_SAMPLE_FULL_SH_PER_PIXEL宏定义于UnityStandardConfig.cginc头文件中。其实也就是一个标识符,用0标示UNITY_SAMPLE_FULL_SH_PER_PIXEL宏是否已经定义。按字面上理解,启用此宏表示我们将采样计算每像素球面调和光照,而不是默认的逐顶点计算球面调和光照并且线性插值到每像素中。其实现代码如下,非常简单:
4)ShadeSH9函数
ShadeSH9就是大家常说的球面调和函数,定义于UnityCG.cginc头文件中,相关代码如下:
ShadeSH3Order函数,我将其翻译为三序球面调和函数。定义于UnityCG.cginc头文件中,相关代码如下:
Shade4PointLights为Unity为我们准备好的逐顶点光照处理函数,定义于unityCG.cginc头文件中,相关代码如下:
TANGENT_SPACE_ROTATION宏定义于UnityCG.cginc中,作用是声明一个由切线空间的基组成的3x3矩阵,相关代码如下:
也就是说,使用TANGENT_SPACE_ROTATION宏也就表示定义了上述代码所示的float3 类型的binormal和float3x3类型的rotation两个变量。且其中的rotation为3x3的矩阵,由切线空间的基组成。可以使用它把物体空间转换到切线空间中。
UNITY_OPTIMIZE_TEXCUBELOD宏的定义非常简单,就是用0标识是否开启此功能,如下所示:
reflect函数是CG语言的内置函数。
reflect(I, N) 根据入射光方向向量I,和顶点法向量N,计算反射光方向向量。其中I 和N必须被归一化,需要特别注意的是,这个I 是指向顶点的;且此函数只对三元向量有效。
UNITY_TRANSFER_FOG宏相关代码定义于UnityCG.Cginc头文件中,用于的相关代码如下所示:
可以发现,关于此宏的定义,主要集中在如下几句:
而其中宏定义依赖的UNITY_CALC_FOG_FACTOR宏,定义于这段代码的一开头,也根据不同的场合,计算方法分为了几个版本。
OK,顶点着色器分析完篇幅都这么多了,这一节就到这里。
之前的文章中提出,Unity中的屏幕特效通常分为两部分来实现:
下面依旧是从这两个方面对本次的特效进行实现。
依旧老规矩,先上注释好的Shader代码。
需要注意,本次油画效果的思路来自于Shadertoy中的一个油画效果的实现:https://www.shadertoy.com/view/MsXSRN#。
此Shadertoy页面贴出的基于GLSL的Shader代码的void mainImage( out vec4 fragColor,in vec2 fragCoord )函数对应于Unity 中Shader的片段着色器。本次Shader中片段着色函数中的实现方法基本由Shadertoy中的这个OilPaint shader优化和精简而来,具体原理应该估计要翻国外的paper来写,会花费不少的时间,精力有限,在这边就暂且不细展开了。暂时只需知道这边就是在片段着色器用类似滤波的操作计算出了不同的颜色值并输出即可。
另外需要注意一点,此Shader的_Radius值越大,此Shader就越耗时,因为_Radius决定了双层循环的次数,而且是指数级的决定关系。_Radius值约小,循环的次数就会越小,从而有更快的运行效率。
C#脚本文件的代码几乎可以从之前的几个特效中重用,只用稍微改一点细节就可以。下面也是贴出详细注释的实现此特效的C#脚本:
而根据脚本中参数的设定,就有分辨率和半径两个参数可以自定义条件,如下图:
下面一起看一下运行效果的对比。
还是那句话,贴几张场景的效果图和使用了屏幕特效后的效果图。在试玩场景时,除了类似CS/CF的FPS游戏控制系统以外,还可以使用键盘上的按键【F】,开启或者屏幕特效。
城镇一隅(with 屏幕油画特效):
城镇一隅(原始图):
城镇路口(with 屏幕油画特效):
城镇路口(原始图):
城镇一隅之二(with 屏幕油画特效):
城镇一隅之二(原始图):
木质城墙和手推车(with 屏幕油画特效):
木质城墙和手推车(原始图):
路边(with 屏幕油画特效):
路边(原始图):
图就贴这些,更多画面大家可以从文章开头下载的本文配套的exe场景,进行试玩,或者在本文附录中贴出的下载链接中下载本文配套的所有游戏资源的unitypackage。
至此,这篇博文已经1万1千多字。感谢大家的捧场。下周浅墨有些事情,所以停更一次,我们下下周,再会。
【百度云】包含博文示例场景所有资源与源码的unitypackage下载
本系列文章由@浅墨_毛星云 出品,转载请注明出处。
文章链接: http://blog.csdn.net/poem_qianmo/article/details/49719247
作者:毛星云(浅墨) 微博:http://weibo.com/u/1723155442
本文工程使用的Unity3D版本: 5.2.1
概要:本文讲解了Unity中着色器编译多样化的思路,并对Standard Shader中正向基础渲染通道的源码进行了分析,以及对屏幕油画特效进行了实现。
众所周知,Unity官方文档对Shader进阶内容的讲解是非常匮乏的。本文中对Stardard Shader源码的一些分析,全是浅墨自己通过对Shader源码的理解,以及Google之后理解与分析而来。如有解释不妥当之处,还请各位及时指出。
依然是附上一组本文配套工程的运行截图之后,便开始我们的正文。本次的选用了新的场景,正如下图中所展示的。
城镇入口(with 屏幕油画特效):
城镇入口(原始图):
图依然是贴这两张。文章末尾有更多的运行截图,并提供了源工程的下载。先放出可运行的exe下载,如下:
提示:在此游戏场景中按F键可以开关屏幕特效。
着色器编译多样化算是Unity5中Shder书写的新特性,标准着色器之所以能独当一面,正是得益于这种特性,在这里先对此特性进行一个简单的说明与讲解。
此部分参考自Unity5.2.1版官方文档(http://docs.unity3d.com/Manual/SL-MultipleProgramVariants.html),经翻译&理解后而成。如有解释不妥当之处,还请各位及时指出。
Unity5中使用了一种被称为着色器编译多样化(Multiple shader program variants)的新技术,常被称为“megashaders”或“uber shaders”,并通过为每种情况提供不同的预处理指令来让着色器代码多次被编译来实现。
在Unity中,这可以通过#pragmamulti_compile或者#pragma shader_feature指令来在着色器代码段中实现。这种做法对表面着色器也可行。
在运行时,相应的着色器变体是从材质的关键词中取得的(Material.EnableKeyword和 DisableKeyword),或者全局着色器关键词(Shader.EnableKeyword和 DisableKeyword)。
若我们定义如下指令:
也就表示定义了两个变体:FANCY_STUFF_OFF和FANCY_STUFF_ON。在运行时,其中的一个将被激活,根据材质或者全局着色器关键词(#ifdef FANCY_STUFF_OFF之类的宏命令也可以)来确定激活哪个。若两个关键词都没有启用,那么将默认使用前一个选项,也就是关闭(OFF)的选项FANCY_STUFF_OFF。
需要注意,也可以存在超过两个关键字的multi_compile编译选项,比如,如下代码将产生4种着色器的变体:
当#pragma multi_compile中存在所有名字都是下划线的一个指定段时,就表示需在没有预处理宏的情况下产生一个空的着色器变种。这种做法在着色器编写中比较常见,因为这样可以在不影响使用的情况下,避免使用两个关键词,这样就节省了一个变量个数的占用(下面会提到,Unity中关键词个数是有129个的数量限制的)。例如,下面的指令将产生两个着色器变体;第一个没有定义,第二个定义为FOO_ON:
这样就省去了一个本来需要定义出来的 FOO_OFF(FOO_OFF没有定义,自然也不能使用),节省了一个关键词个数的占用。
若Shader中有如上定义,则可以使用#ifdef来进行判断:
根据上面已经定义过的FOO_ON,此#ifdef判断的结果为真,代码段1部分的代码就会被执行到。反之,若#pragma multi_compile __FOO_ON一句代码没有交代出来,那么代码段1部分的代码就不会被执行。
这就是着色器编译多样化的实现方式,其实理解起来很容易,对吧。
1.2 shader_feature和multi_compile之间的区别
#pragma shader_feature 和#pragma multi_compile非常相似,唯一的区别在于采用了#pragmashader_feature语义的shader,在遇到不被使用的变体的时候,就不会将其编译到游戏中。所以,shader_feature中使得所有的设置到材质中的关键词都是有效的,而multi_compile指令将从全局代码里设置关键词。
另外,shader_feature还有一个仅仅含有一个关键字的快捷表达方式,例如:
此为#pragma shader_feature _ FANCY_STUFF的一个简写形式,其扩展出了两个着色器变体,第一种变体自然为不定此FANCY_STUFF变量(那么若在稍后的Shader代码中进行#ifdef FANCY_STUFF的判断,则结果为假),第二种变体为定义此FANCY_STUFF变量(此情况下#ifdef FANCY_STUFF的判断结果为真)。
1.3 多个multi_compile连用会造成指数型增长
可以提供多个multi_compile流水线,然后着色器的结果可以被编译为几个流水线的排列组合,比如:
第一行中有3种选项,第二行中有两种选项,那么进行排列组合,总共就会有六种选项(A+D, B+D, C+D, A+E, B+E, C+E)。
容易想到,一般每以个multi_compile流水线,都控制着着色器中某一单一的特性。请注意,着色器总量的增长速度是非常快的。
比如,10条包含两个特性的multi_compil指令,会得到2的10次方,也就是1024种不同的着色器变体。
1.4 关于Unity中的关键词限制Keyword limit
当使用着色变量时,我们应该记住,Unity中将关键词的数量限制在了128个之内(着色变量算作关键字),且其中有一些已经被Unity内置使用了,因此,我们真正可以自定义使用关键词的数量以及是小于128个的。同时,关键词是在单个Unity项目中全局使用并计数的,所以我们要千万小心,在同一项目中存在的但没用到Shader也要考虑在内,千万不要合起来在数量上超出Unity的关键词数量限制了。
1.5 Unity内置的快捷multi_compile指令
如下有Unity内置的几个着色器变体的快捷多编译指令,他们大多是应对Unity中不同的光线,阴影和光照贴图类型。详情见rendering pipeline 。
大多数内置的快捷指令导致了很多着色的变体。若我们熟悉他们且知道有些并非所需,可以使用#pragmaskip_variants语句跳过其中一些的编译。例如:
OK,通过上面经过翻译&理解过后的官方文档材料,应该对Unity中的着色器编译多样化有了一个理解。说白了,着色器变体的定义和使用与宏定义很类似。
上面交代了这么多,看不懂没关系,我们提炼一下,看懂这段提炼,关于着色器变体的意义与使用方式,也就懂了大半了。
若我们在着色器中定义了这一句:
这句代码理解起来,也就是_THIS_IS_A_SAMPLE被我们定义过了,它是存在的,以后我们如果判断#ifdef _THIS_IS_A_SAMPLE,那就是真了。我们可以在这个判断的#ifdef…… #endif块里面实现自己需要的实现代码X,这段实现代码X,只会在你用#pragma multi_compile 或#pragmashader_feature定义了_THIS_IS_A_SAMPLE这个“宏”的时候会被执行,否则,它就不会被执行到。
实现代码X的执行与不执行,全靠你对变体的定义与否。这就是着色器编译多样化的实现方式,一个着色器+多个CG头文件的小团队(如标准着色器),可以独当一面,一个打一群,可以取代一大堆独立实现的Shader的原因所在。
这一节主要用来解析Standard Shader中正向基础渲染通道的源码。
先上Standard Shader正向渲染基础通道(Shader Model 3.0版)的Shader源代码:
OK,一起来稍微分析一下上述代码。基本上是逐行注释,所以找几个容易疑惑的点来提一下。
第一处,着色器编译多样化部分,代码如下:
上文刚讲过着色器编译多样化的一些理解,理解起来就是这样,这边定义了很多的“宏”、 _NORMALMAP、_ALPHATEST_ON、_ALPHABLEND_ON、_EMISSION、_METALLICGLOSSMAP、_DETAIL_MULX2、_PARALLAXMAP,在顶点和片段着色器实现部分,可以用#ifdef _EMISSION类似的宏命令来对不同情况下的实现进行区别对待。
第二处,着色器编译多样化快捷指令部分,上文的讲解部分也有分别提到,这里代码注释已经很详细,如下:
第三处,顶点着色函数和片段着色函数声明部分,代码如下:
这里比较关键,指明了这个pass中顶点着色函数和片段着色函数分别是名为vertForwardBase和fragForwardBase的函数。而这两个函数定义于何处?看包含头文件是什么即可。一起来看一下第四处。
第四处,CG头文件包含部分,代码如下:
很简单的一句话,但却像一切编程语言中头文件的包含一样,非常关键,不能缺少。vertForwardBase和 fragForwardBase的函数全都定义于此“UnityStandardCore.cginc”头文件中。
OK,我们转到“UnityStandardCore.cginc”头文件,继续分析下去。先从vertForwardBase函数开始。
vertForwardBase函数也已详细注释好,代码如下:
基本步骤已经在代码注释中用序号列出,以下将对其中的主要知识点进行讲解。首先看一下函数的输出参数——VertexInput。
此结构体定义于UnityStandardInput.cginc头文件中,是顶点着色函数vertForwardBase的输入参数,相关代码如下所示:
此结构体比较通用,不仅仅是用于正向基础渲染通道,毕竟是定义在UnityStandardInput.cginc头文件中的。
各个变量的含义,注释中已经写到了,好像没有什么值得多说的,再来看下顶点输出结构体。
顾名思义,VertexOutputForwardBase结构体就是正向基础渲染通道特有的输出结构体,定义于UnityStandardCore.cginc头文件中,注释后的代码如下:
从这里开始,做一个规定,为了方便对照和理解,以下贴出代码中也会贴出原始的英文注释——先翻译为中文,以 || 结束,在 || 后附上原始的英文。
就像这样:
//最终的二次多项式 || Final quadraticpolynomial
OK,我们继续,vertForwardBase函数中有很多知识点值得拿出来讲一讲的。
UNITY_INITIALIZE_OUTPUT(type,name) –此宏用于将给定类型的名称变量初始化为零。在使用旧版标准所写的Shader时,经常会报错“Try adding UNITY_INITIALIZE_OUTPUT(Input,o); like this in your vertfunction.”之类的错误,加上这句就不会报错了。
_Object2World,Unity的内置矩阵,世界坐标系到对象坐标系的变换矩阵,简称“世界-对象矩阵”。
UNITY_MATRIX_MVP为当前的模型矩阵x视图矩阵x投影矩阵,简称“模型-视图-投影矩阵”。其常用于在顶点着色函数中,通过将它和顶点位置相乘,从而可以把顶点位置从模型空间转换到裁剪空间(clip space)中。也就是通过此矩阵,将三维空间中的坐标投影到了二维窗口中。
TexCoords函数用于获取纹理坐标,定义UnityStandardInput.cginc头文件中,相关代码如下:
函数实现代码中的_MainTex、_UVSec、_DetailAlbedoMap都是此头文件定义的全局的变量。
其中还涉及到了一个TRANSFORM_TEX宏,在这边也提一下,它定义于UnityCG.cginc头文件中,相关代码如下:
此函数位于unitystandardcore.cginc头文件中,原型和注释如下:
其中,SHADER_TARGET宏代表的值为和着色器的目标编译模型(shader model)相关的一个数值。
例如,当着色器编译成Shader Model 3.0时,SHADER_TARGET 便为30。我们可以在shader代码中由此来进行条件判断。相关代码如下:
UnityObjectToWorldNormal是Unity内置的函数,可以将法线从模型空间变换到世界空间中,定义于UnityCG.cginc头文件中,相关代码如下:
而其中的normalize( )函数太常见不过了,是来自CG语言中的函数,作用是归一化向量。
UnityObjectToWorldDir函数用于方向值从物体空间切换到世界空间,也定义于UnityCG.cginc头文件中,相关代码如下:
可以看到,就是返回一个世界-对象矩阵乘以方向值归一化后的结果,比较好理解。
CreateTangentToWorldPerVertex函数用于在世界空间中为每个顶点创建切线,定义于UnityStandardUtils.cginc头文件中,相关代码如下:
其中的unity_WorldTransformParams是UnityShaderVariables.cginc头文件中定义的一个uniform float4型的变量,其w分量用于标定奇数负比例变换(odd-negativescale transforms),通常取值为1.0或者-1.0。
此宏用于进行阴影在各种空间中的转换,定义于AutoLight.cginc中。在不同的情况下,此宏代表的意义并不相同。下面简单进行下展开分析。
对应于屏幕空间中的阴影,也就是#if defined (SHADOWS_SCREEN),其相关代码如下:
也就是说,这种情况下的TRANSFER_SHADOW(a)宏,代表了一句代码,这句代码就是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));
此句代码的含义是:将世界-阴影坐标乘以世界-模型坐标和物体顶点坐标的积,也就是先将物体坐标转换成世界坐标,再将世界坐标转换成阴影坐标,并将结果存放于a._ShadowCoord中。
而对于聚光灯的阴影,也就是#if defined (SHADOWS_DEPTH)&& defined (SPOT)
有如下定义:
可以发现,这种情况下的TRANSFER_SHADOW(a)宏代表的语句也是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));
同上,用途就是先将物体坐标转换成世界坐标,再将世界坐标转换成阴影坐标,并将结果存放于a._ShadowCoord中。
而对于点光源的阴影,也就是#if defined (SHADOWS_CUBE),有如下定义:
也就是说,这种情况下的TRANSFER_SHADOW(a)宏代表语句a._ShadowCoord = mul(_Object2World, v.vertex).xyz -_LightPositionRange.xyz;
想了解此代码的含义,先要知道_LightPositionRange变量的含义。
这个变量是UnityShaderVariables.cginc头文件中定义的一个全局变量:
从英文注释可以发现,此参数的x,y,z分量表示世界空间下光源的坐标,而w为世界空间下范围的倒数。
那么此句代码的含义,也就是先将物体-世界矩阵乘以物体顶点坐标,得到物体的世界空间坐标,然后取坐标的xyz分量,与光源的坐标相减,并将结果赋给a._ShadowCoord。
而对于关闭阴影的情况,也就是#if !defined (SHADOWS_SCREEN)&& !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE),有如下定义:
这种情况下的TRANSFER_SHADOW(a)宏代表的是空白,并没有什么用。
定义于UnityStandardCore.cginc头文件中。详细注释后的代码如下:
其中有一些小的点,这边提出来讲一下。
unity_LightmapST变量类型为float4型,定义于UnityShaderVariables.cginc头文件中,存放着光照贴图操作的参数的值:
此宏定义于UnityCG.cginc中,相关代码如下:
可以发现,这个宏,其实就是将LIGHTMAP_OFF(关闭光照贴图)宏和DYNAMICLIGHTMAP_OFF(关闭动态光照贴图)宏的定义进行了封装。
UNITY_SAMPLE_FULL_SH_PER_PIXEL宏定义于UnityStandardConfig.cginc头文件中。其实也就是一个标识符,用0标示UNITY_SAMPLE_FULL_SH_PER_PIXEL宏是否已经定义。按字面上理解,启用此宏表示我们将采样计算每像素球面调和光照,而不是默认的逐顶点计算球面调和光照并且线性插值到每像素中。其实现代码如下,非常简单:
4)ShadeSH9函数
ShadeSH9就是大家常说的球面调和函数,定义于UnityCG.cginc头文件中,相关代码如下:
ShadeSH3Order函数,我将其翻译为三序球面调和函数。定义于UnityCG.cginc头文件中,相关代码如下:
Shade4PointLights为Unity为我们准备好的逐顶点光照处理函数,定义于unityCG.cginc头文件中,相关代码如下:
TANGENT_SPACE_ROTATION宏定义于UnityCG.cginc中,作用是声明一个由切线空间的基组成的3x3矩阵,相关代码如下:
也就是说,使用TANGENT_SPACE_ROTATION宏也就表示定义了上述代码所示的float3 类型的binormal和float3x3类型的rotation两个变量。且其中的rotation为3x3的矩阵,由切线空间的基组成。可以使用它把物体空间转换到切线空间中。
UNITY_OPTIMIZE_TEXCUBELOD宏的定义非常简单,就是用0标识是否开启此功能,如下所示:
reflect函数是CG语言的内置函数。
reflect(I, N) 根据入射光方向向量I,和顶点法向量N,计算反射光方向向量。其中I 和N必须被归一化,需要特别注意的是,这个I 是指向顶点的;且此函数只对三元向量有效。
UNITY_TRANSFER_FOG宏相关代码定义于UnityCG.Cginc头文件中,用于的相关代码如下所示:
可以发现,关于此宏的定义,主要集中在如下几句:
而其中宏定义依赖的UNITY_CALC_FOG_FACTOR宏,定义于这段代码的一开头,也根据不同的场合,计算方法分为了几个版本。
OK,顶点着色器分析完篇幅都这么多了,这一节就到这里。
之前的文章中提出,Unity中的屏幕特效通常分为两部分来实现:
下面依旧是从这两个方面对本次的特效进行实现。
依旧老规矩,先上注释好的Shader代码。
需要注意,本次油画效果的思路来自于Shadertoy中的一个油画效果的实现:https://www.shadertoy.com/view/MsXSRN#。
此Shadertoy页面贴出的基于GLSL的Shader代码的void mainImage( out vec4 fragColor,in vec2 fragCoord )函数对应于Unity 中Shader的片段着色器。本次Shader中片段着色函数中的实现方法基本由Shadertoy中的这个OilPaint shader优化和精简而来,具体原理应该估计要翻国外的paper来写,会花费不少的时间,精力有限,在这边就暂且不细展开了。暂时只需知道这边就是在片段着色器用类似滤波的操作计算出了不同的颜色值并输出即可。
另外需要注意一点,此Shader的_Radius值越大,此Shader就越耗时,因为_Radius决定了双层循环的次数,而且是指数级的决定关系。_Radius值约小,循环的次数就会越小,从而有更快的运行效率。
C#脚本文件的代码几乎可以从之前的几个特效中重用,只用稍微改一点细节就可以。下面也是贴出详细注释的实现此特效的C#脚本:
而根据脚本中参数的设定,就有分辨率和半径两个参数可以自定义条件,如下图:
下面一起看一下运行效果的对比。
还是那句话,贴几张场景的效果图和使用了屏幕特效后的效果图。在试玩场景时,除了类似CS/CF的FPS游戏控制系统以外,还可以使用键盘上的按键【F】,开启或者屏幕特效。
城镇一隅(with 屏幕油画特效):
城镇一隅(原始图):
城镇路口(with 屏幕油画特效):
城镇路口(原始图):
城镇一隅之二(with 屏幕油画特效):
城镇一隅之二(原始图):
木质城墙和手推车(with 屏幕油画特效):
木质城墙和手推车(原始图):
路边(with 屏幕油画特效):
路边(原始图):
图就贴这些,更多画面大家可以从文章开头下载的本文配套的exe场景,进行试玩,或者在本文附录中贴出的下载链接中下载本文配套的所有游戏资源的unitypackage。
至此,这篇博文已经1万1千多字。感谢大家的捧场。下周浅墨有些事情,所以停更一次,我们下下周,再会。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。