文章链接: 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下载,如下:
【可运行的本文配套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)。
multi_compile的用法简析
若我们定义如下指令:
- #pragma multi_compile FANCY_STUFF_OFFFANCY_STUFF_ON
也就表示定义了两个变体:FANCY_STUFF_OFF和FANCY_STUFF_ON。在运行时,其中的一个将被激活,根据材质或者全局着色器关键词(#ifdef FANCY_STUFF_OFF之类的宏命令也可以)来确定激活哪个。若两个关键词都没有启用,那么将默认使用前一个选项,也就是关闭(OFF)的选项FANCY_STUFF_OFF。
需要注意,也可以存在超过两个关键字的multi_compile编译选项,比如,如下代码将产生4种着色器的变体:
- #pragma multi_compile SIMPLE_SHADINGBETTER_SHADING GOOD_SHADING BEST_SHADING
当#pragma multi_compile中存在所有名字都是下划线的一个指定段时,就表示需在没有预处理宏的情况下产生一个空的着色器变种。这种做法在着色器编写中比较常见,因为这样可以在不影响使用的情况下,避免使用两个关键词,这样就节省了一个变量个数的占用(下面会提到,Unity中关键词个数是有129个的数量限制的)。例如,下面的指令将产生两个着色器变体;第一个没有定义,第二个定义为FOO_ON:
- #pragma multi_compile __ FOO_ON
这样就省去了一个本来需要定义出来的 FOO_OFF(FOO_OFF没有定义,自然也不能使用),节省了一个关键词个数的占用。
若Shader中有如上定义,则可以使用#ifdef来进行判断:
根据上面已经定义过的FOO_ON,此#ifdef判断的结果为真,代码段1部分的代码就会被执行到。反之,若#pragma multi_compile __FOO_ON一句代码没有交代出来,那么代码段1部分的代码就不会被执行。
这就是着色器编译多样化的实现方式,其实理解起来很容易,对吧。
shader_feature和multi_compile之间的区别
#pragma shader_feature 和#pragma multi_compile非常相似,唯一的区别在于采用了#pragmashader_feature语义的shader,在遇到不被使用的变体的时候,就不会将其编译到游戏中。所以,shader_feature中使得所有的设置到材质中的关键词都是有效的,而multi_compile指令将从全局代码里设置关键词。
另外,shader_feature还有一个仅仅含有一个关键字的快捷表达方式,例如:
- #pragma shader_feature FANCY_STUFF
此为#pragma shader_feature _ FANCY_STUFF的一个简写形式,其扩展出了两个着色器变体,第一种变体自然为不定此FANCY_STUFF变量(那么若在稍后的Shader代码中进行#ifdef FANCY_STUFF的判断,则结果为假),第二种变体为定义此FANCY_STUFF变量(此情况下#ifdef FANCY_STUFF的判断结果为真)。
多个multi_compile连用会造成指数型增长
可以提供多个multi_compile流水线,然后着色器的结果可以被编译为几个流水线的排列组合,比如:
- #pragma multi_compile A B C
- #pragma multi_compile D E
第一行中有3种选项,第二行中有两种选项,那么进行排列组合,总共就会有六种选项(A+D, B+D, C+D, A+E, B+E, C+E)。
容易想到,一般每以个multi_compile流水线,都控制着着色器中某一单一的特性。请注意,着色器总量的增长速度是非常快的。
比如,10条包含两个特性的multi_compil指令,会得到2的10次方,也就是1024种不同的着色器变体。
关于Unity中的关键词限制Keyword limit
当使用着色变量时,我们应该记住,Unity中将关键词的数量限制在了128个之内(着色变量算作关键字),且其中有一些已经被Unity内置使用了,因此,我们真正可以自定义使用关键词的数量以及是小于128个的。同时,关键词是在单个Unity项目中全局使用并计数的,所以我们要千万小心,在同一项目中存在的但没用到Shader也要考虑在内,千万不要合起来在数量上超出Unity的关键词数量限制了。
Unity内置的快捷multi_compile指令
如下有Unity内置的几个着色器变体的快捷多编译指令,他们大多是应对Unity中不同的光线,阴影和光照贴图类型。详情见rendering pipeline 。
- multi_compile_fwdbase - 此指令表示,编译正向基础渲染通道(用于正向渲染中,应用环境光照、主方向光照和顶点/球面调和光照(Spherical Harmonic Lighting))所需的所有变体。这些变体用于处理不同的光照贴图类型、主要方向光源的阴影选项的开关与否。
- multi_compile_fwdadd - 此指令表示, 编译正向附加渲染通道(用于正向渲染中;以每个光照一个通道的方式应用附加的逐像素光照)所需的所有变体。这些变体用于处理光源的类型(方向光源、聚光灯或者点光源),且这些变种都包含纹理cookie。
- multi_compile_fwdadd_fullshadows – 此指令和上面的正向渲染附加通道基本一致,但同时为上述通道的处理赋予了光照实时阴影的能力。
- multi_compile_fog - 此指令表示,编译出几个不同的Shader变体来处理不同类型的雾效(关闭/线性/指数/二阶指数)(off/linear/exp/exp2).
使用指令跳过某些变体的编译
大多数内置的快捷指令导致了很多着色的变体。若我们熟悉他们且知道有些并非所需,可以使用#pragmaskip_variants语句跳过其中一些的编译。例如:
- #pragma multi_compile_fwdadd
-
- #pragma skip_variants POINT POINT_COOKIE
OK,通过上面经过翻译&理解过后的官方文档材料,应该对Unity中的着色器编译多样化有了一个理解。说白了,着色器变体的定义和使用与宏定义很类似。
对知识的提炼
上面交代了这么多,看不懂没关系,我们提炼一下,看懂这段提炼,关于着色器变体的意义与使用方式,也就懂了大半了。
若我们在着色器中定义了这一句:
- #pragma shader_feature _THIS_IS_A_SAMPLE
这句代码理解起来,也就是_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中正向基础渲染通道的源码。
先上Standard Shader正向渲染基础通道(Shader Model 3.0版)的Shader源代码:
-
-
-
- SubShader
- {
-
- Tags { "RenderType"="Opaque" "PerformanceChecks"="False" }
-
-
- LOD 300
-
-
-
-
- Pass
- {
-
- Name "FORWARD"
-
-
- Tags { "LightMode" = "ForwardBase" }
-
-
- Blend [_SrcBlend] [_DstBlend]
-
- ZWrite [_ZWrite]
-
-
- CGPROGRAM
-
-
- #pragma target 3.0
-
-
- #pragma exclude_renderers gles
-
-
- #pragma shader_feature _NORMALMAP
- #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
- #pragma shader_feature _EMISSION
- #pragma shader_feature _METALLICGLOSSMAP
- #pragma shader_feature ___ _DETAIL_MULX2
- #pragma shader_feature _PARALLAXMAP
-
-
-
-
- #pragma multi_compile_fwdbase
-
- #pragma multi_compile_fog
-
-
- #pragma vertex vertForwardBase
- #pragma fragment fragForwardBase
-
-
- #include "UnityStandardCore.cginc"
-
-
- ENDCG
- }
- ……
- }
OK,一起来稍微分析一下上述代码。基本上是逐行注释,所以找几个容易疑惑的点来提一下。
第一处,着色器编译多样化部分,代码如下:
-
- #pragma shader_feature _NORMALMAP
- #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
- #pragma shader_feature _EMISSION
- #pragma shader_feature _METALLICGLOSSMAP
- #pragma shader_feature ___ _DETAIL_MULX2
- #pragma shader_feature _PARALLAXMAP
上文刚讲过着色器编译多样化的一些理解,理解起来就是这样,这边定义了很多的“宏”、 _NORMALMAP、_ALPHATEST_ON、_ALPHABLEND_ON、_EMISSION、_METALLICGLOSSMAP、_DETAIL_MULX2、_PARALLAXMAP,在顶点和片段着色器实现部分,可以用#ifdef _EMISSION类似的宏命令来对不同情况下的实现进行区别对待。
第二处,着色器编译多样化快捷指令部分,上文的讲解部分也有分别提到,这里代码注释已经很详细,如下:
-
-
-
- #pragma multi_compile_fwdbase
-
-
- #pragma multi_compile_fog
第三处,顶点着色函数和片段着色函数声明部分,代码如下:
-
- #pragma vertex vertForwardBase
- #pragma fragment fragForwardBase
这里比较关键,指明了这个pass中顶点着色函数和片段着色函数分别是名为vertForwardBase和fragForwardBase的函数。而这两个函数定义于何处?看包含头文件是什么即可。一起来看一下第四处。
第四处,CG头文件包含部分,代码如下:
-
- #include"UnityStandardCore.cginc"
很简单的一句话,但却像一切编程语言中头文件的包含一样,非常关键,不能缺少。vertForwardBase和 fragForwardBase的函数全都定义于此“UnityStandardCore.cginc”头文件中。 OK,我们转到“UnityStandardCore.cginc”头文件,继续分析下去。先从vertForwardBase函数开始。
1、顶点着色函数——vertForwardBase
vertForwardBase函数也已详细注释好,代码如下:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- VertexOutputForwardBase vertForwardBase (VertexInput v)
- {
-
- VertexOutputForwardBase o;
-
- UNITY_INITIALIZE_OUTPUT(VertexOutputForwardBase, o);
-
-
- float4 posWorld = mul(_Object2World, v.vertex);
-
-
- #if UNITY_SPECCUBE_BOX_PROJECTION
- o.posWorld = posWorld.xyz;
- #endif
-
-
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- o.tex = TexCoords(v);
-
- o.eyeVec = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos);
-
-
- float3 normalWorld = UnityObjectToWorldNormal(v.normal);
-
-
-
- #ifdef _TANGENT_TO_WORLD
-
- float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
-
- float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w);
-
- o.tangentToWorldAndParallax[0].xyz = tangentToWorld[0];
- o.tangentToWorldAndParallax[1].xyz = tangentToWorld[1];
- o.tangentToWorldAndParallax[2].xyz = tangentToWorld[2];
-
- #else
- o.tangentToWorldAndParallax[0].xyz = 0;
- o.tangentToWorldAndParallax[1].xyz = 0;
- o.tangentToWorldAndParallax[2].xyz = normalWorld;
- #endif
-
-
- TRANSFER_SHADOW(o);
-
-
- o.ambientOrLightmapUV = VertexGIForward(v, posWorld, normalWorld);
-
-
- #ifdef _PARALLAXMAP
-
- TANGENT_SPACE_ROTATION;
-
- half3 viewDirForParallax = mul (rotation, ObjSpaceViewDir(v.vertex));
-
- o.tangentToWorldAndParallax[0].w = viewDirForParallax.x;
- o.tangentToWorldAndParallax[1].w = viewDirForParallax.y;
- o.tangentToWorldAndParallax[2].w = viewDirForParallax.z;
- #endif
-
-
- #if UNITY_OPTIMIZE_TEXCUBELOD
-
- o.reflUVW = reflect(o.eyeVec, normalWorld);
- #endif
-
-
- UNITY_TRANSFER_FOG(o,o.pos);
-
-
- return o;
- }
基本步骤已经在代码注释中用序号列出,以下将对其中的主要知识点进行讲解。首先看一下函数的输出参数——VertexInput。
2、顶点输入结构体——VertexInput
此结构体定义于UnityStandardInput.cginc头文件中,是顶点着色函数vertForwardBase的输入参数,相关代码如下所示:
-
- struct VertexInput
- {
- float4 vertex : POSITION;
- half3 normal : NORMAL;
- float2 uv0 : TEXCOORD0;
- float2 uv1 : TEXCOORD1;
-
- #if defined(DYNAMICLIGHTMAP_ON) || defined(UNITY_PASS_META)
- float2 uv2 : TEXCOORD2;
- #endif
- #ifdef _TANGENT_TO_WORLD
- half4 tangent : TANGENT;
- #endif
- };
此结构体比较通用,不仅仅是用于正向基础渲染通道,毕竟是定义在UnityStandardInput.cginc头文件中的。
各个变量的含义,注释中已经写到了,好像没有什么值得多说的,再来看下顶点输出结构体。
3、顶点输出结构体——VertexOutputForwardBase
顾名思义,VertexOutputForwardBase结构体就是正向基础渲染通道特有的输出结构体,定义于UnityStandardCore.cginc头文件中,注释后的代码如下:
-
- struct VertexOutputForwardBase
- {
- float4 pos : SV_POSITION;
- float4 tex : TEXCOORD0;
- half3 eyeVec : TEXCOORD1;
- half4 tangentToWorldAndParallax[3] : TEXCOORD2;
- half4 ambientOrLightmapUV : TEXCOORD5;
- SHADOW_COORDS(6)
- UNITY_FOG_COORDS(7)
-
-
- #if UNITY_SPECCUBE_BOX_PROJECTION
- float3 posWorld : TEXCOORD8;
- #endif
-
- #if UNITY_OPTIMIZE_TEXCUBELOD
- #if UNITY_SPECCUBE_BOX_PROJECTION
- half3 reflUVW : TEXCOORD9;
- #else
- half3 reflUVW : TEXCOORD8;
- #endif
- #endif
- };
从这里开始,做一个规定,为了方便对照和理解,以下贴出代码中也会贴出原始的英文注释——先翻译为中文,以 || 结束,在 || 后附上原始的英文。
就像这样:
//最终的二次多项式 || Final quadraticpolynomial
OK,我们继续,vertForwardBase函数中有很多知识点值得拿出来讲一讲的。
4、UNITY_INITIALIZE_OUTPUT宏
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)中。也就是通过此矩阵,将三维空间中的坐标投影到了二维窗口中。
7、TexCoords函数
TexCoords函数用于获取纹理坐标,定义UnityStandardInput.cginc头文件中,相关代码如下:
- float4 TexCoords(VertexInput v)
- {
- float4 texcoord;
- texcoord.xy = TRANSFORM_TEX(v.uv0, _MainTex);
- texcoord.zw = TRANSFORM_TEX(((_UVSec == 0) ? v.uv0 : v.uv1), _DetailAlbedoMap);
- return texcoord;
- }
函数实现代码中的_MainTex、_UVSec、_DetailAlbedoMap都是此头文件定义的全局的变量。
其中还涉及到了一个TRANSFORM_TEX宏,在这边也提一下,它定义于UnityCG.cginc头文件中,相关代码如下:
-
- #define TRANSFORM_TEX(tex,name) (tex.xy *name##_ST.xy + name##_ST.zw)
8、 NormalizePerVertexNormal函数
此函数位于unitystandardcore.cginc头文件中,原型和注释如下:
-
-
-
-
-
-
- half3 NormalizePerVertexNormal (half3 n)
- {
-
- #if (SHADER_TARGET < 30) || UNITY_STANDARD_SIMPLE
- return normalize(n);
-
- #else
- return n;
- #endif
- }
其中,SHADER_TARGET宏代表的值为和着色器的目标编译模型(shader model)相关的一个数值。
例如,当着色器编译成Shader Model 3.0时,SHADER_TARGET 便为30。我们可以在shader代码中由此来进行条件判断。相关代码如下:
- #if SHADER_TARGET < 30
-
- #else
-
- #endif
9、UnityObjectToWorldNormal函数
UnityObjectToWorldNormal是Unity内置的函数,可以将法线从模型空间变换到世界空间中,定义于UnityCG.cginc头文件中,相关代码如下:
-
- inline float3 UnityObjectToWorldNormal( in float3 norm )
- {
-
-
- return normalize(_World2Object[0].xyz * norm.x + _World2Object[1].xyz * norm.y + _World2Object[2].xyz * norm.z);
- }
而其中的normalize( )函数太常见不过了,是来自CG语言中的函数,作用是归一化向量。
10、UnityObjectToWorldDir函数
UnityObjectToWorldDir函数用于方向值从物体空间切换到世界空间,也定义于UnityCG.cginc头文件中,相关代码如下:
-
- inline float3 UnityObjectToWorldDir( in float3 dir )
- {
- return normalize(mul((float3x3)_Object2World, dir));
- }
可以看到,就是返回一个世界-对象矩阵乘以方向值归一化后的结果,比较好理解。
11、CreateTangentToWorldPerVertex函数
CreateTangentToWorldPerVertex函数用于在世界空间中为每个顶点创建切线,定义于UnityStandardUtils.cginc头文件中,相关代码如下:
- half3x3 CreateTangentToWorldPerVertex(half3 normal, half3 tangent, half tangentSign)
- {
-
- half sign = tangentSign * unity_WorldTransformParams.w;
- half3 binormal = cross(normal, tangent) * sign;
- return half3x3(tangent, binormal, normal);
- }
其中的unity_WorldTransformParams是UnityShaderVariables.cginc头文件中定义的一个uniform float4型的变量,其w分量用于标定奇数负比例变换(odd-negativescale transforms),通常取值为1.0或者-1.0。
12、TRANSFER_SHADOW(a)宏
此宏用于进行阴影在各种空间中的转换,定义于AutoLight.cginc中。在不同的情况下,此宏代表的意义并不相同。下面简单进行下展开分析。
对于屏幕空间中的阴影(Screen space shadows)
对应于屏幕空间中的阴影,也就是#if defined (SHADOWS_SCREEN),其相关代码如下:
- #if defined (SHADOWS_SCREEN)
- ……
- #if defined(UNITY_NO_SCREENSPACE_SHADOWS)
- #define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex ) );
-
- #else // not UNITY_NO_SCREENSPACE_SHADOWS
-
- #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
- ……
- #endif
也就是说,这种情况下的TRANSFER_SHADOW(a)宏,代表了一句代码,这句代码就是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));
此句代码的含义是:将世界-阴影坐标乘以世界-模型坐标和物体顶点坐标的积,也就是先将物体坐标转换成世界坐标,再将世界坐标转换成阴影坐标,并将结果存放于a._ShadowCoord中。
对于聚光灯阴影(Spot light shadows)
而对于聚光灯的阴影,也就是#if defined (SHADOWS_DEPTH)&& defined (SPOT)
有如下定义:
- #if defined (SHADOWS_DEPTH) && defined (SPOT)
- #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));
- ……
- #endif
可以发现,这种情况下的TRANSFER_SHADOW(a)宏代表的语句也是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));
同上,用途就是先将物体坐标转换成世界坐标,再将世界坐标转换成阴影坐标,并将结果存放于a._ShadowCoord中。
对于点光源阴影(Point light shadows)
而对于点光源的阴影,也就是#if defined (SHADOWS_CUBE),有如下定义:
- #if defined (SHADOWS_CUBE)
- #define TRANSFER_SHADOW(a) a._ShadowCoord = mul(_Object2World, v.vertex).xyz - _LightPositionRange.xyz;
- ……
- #endif
也就是说,这种情况下的TRANSFER_SHADOW(a)宏代表语句a._ShadowCoord = mul(_Object2World, v.vertex).xyz -_LightPositionRange.xyz;
想了解此代码的含义,先要知道_LightPositionRange变量的含义。
这个变量是UnityShaderVariables.cginc头文件中定义的一个全局变量:
- uniform float4 _LightPositionRange;
从英文注释可以发现,此参数的x,y,z分量表示世界空间下光源的坐标,而w为世界空间下范围的倒数。
那么此句代码的含义,也就是先将物体-世界矩阵乘以物体顶点坐标,得到物体的世界空间坐标,然后取坐标的xyz分量,与光源的坐标相减,并将结果赋给a._ShadowCoord。
对于关闭阴影(Shadows off)的情况
而对于关闭阴影的情况,也就是#if !defined (SHADOWS_SCREEN)&& !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE),有如下定义:
- #if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE)
- #define TRANSFER_SHADOW(a)
- ……
- #endif
这种情况下的TRANSFER_SHADOW(a)宏代表的是空白,并没有什么用。
13、 VertexGIForward函数
定义于UnityStandardCore.cginc头文件中。详细注释后的代码如下:
-
- inline half4 VertexGIForward(VertexInput v, float3 posWorld, half3 normalWorld)
- {
-
- half4 ambientOrLightmapUV = 0;
-
-
-
-
- #ifndef LIGHTMAP_OFF
- ambientOrLightmapUV.xy = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
- ambientOrLightmapUV.zw = 0;
-
-
-
- #elif UNITY_SHOULD_SAMPLE_SH
-
-
- #if UNITY_SAMPLE_FULL_SH_PER_PIXEL
- ambientOrLightmapUV.rgb = 0;
-
-
-
- #elif (SHADER_TARGET < 30) || UNITY_STANDARD_SIMPLE
- ambientOrLightmapUV.rgb = ShadeSH9(half4(normalWorld, 1.0));
-
-
- #else
-
- ambientOrLightmapUV.rgb = ShadeSH3Order(half4(normalWorld, 1.0));
- #endif
-
-
-
- #ifdef VERTEXLIGHT_ON
-
- ambientOrLightmapUV.rgb += Shade4PointLights (
- unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
- unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
- unity_4LightAtten0, posWorld, normalWorld);
- #endif
- #endif
-
-
- #ifdef DYNAMICLIGHTMAP_ON
- ambientOrLightmapUV.zw = v.uv2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
- #endif
-
- return ambientOrLightmapUV;
- }
其中有一些小的点,这边提出来讲一下。
unity_LightmapST变量
unity_LightmapST变量类型为float4型,定义于UnityShaderVariables.cginc头文件中,存放着光照贴图操作的参数的值:
UNITY_SHOULD_SAMPLE_SH宏
此宏定义于UnityCG.cginc中,相关代码如下:
-
- #define UNITY_SHOULD_SAMPLE_SH ( defined (LIGHTMAP_OFF) && defined(DYNAMICLIGHTMAP_OFF) )
可以发现,这个宏,其实就是将LIGHTMAP_OFF(关闭光照贴图)宏和DYNAMICLIGHTMAP_OFF(关闭动态光照贴图)宏的定义进行了封装。
UNITY_SAMPLE_FULL_SH_PER_PIXEL宏
UNITY_SAMPLE_FULL_SH_PER_PIXEL宏定义于UnityStandardConfig.cginc头文件中。其实也就是一个标识符,用0标示UNITY_SAMPLE_FULL_SH_PER_PIXEL宏是否已经定义。按字面上理解,启用此宏表示我们将采样计算每像素球面调和光照,而不是默认的逐顶点计算球面调和光照并且线性插值到每像素中。其实现代码如下,非常简单:
- #ifndef UNITY_SAMPLE_FULL_SH_PER_PIXEL
- #define UNITY_SAMPLE_FULL_SH_PER_PIXEL 0
- #endif
ShadeSH9函数
ShadeSH9就是大家常说的球面调和函数,定义于UnityCG.cginc头文件中,相关代码如下:
-
-
- half3 ShadeSH9 (half4 normal)
- {
- half3 x1, x2, x3;
-
-
- x1.r = dot(unity_SHAr,normal);
- x1.g = dot(unity_SHAg,normal);
- x1.b = dot(unity_SHAb,normal);
-
-
- half4 vB = normal.xyzz * normal.yzzx;
- x2.r = dot(unity_SHBr,vB);
- x2.g = dot(unity_SHBg,vB);
- x2.b = dot(unity_SHBb,vB);
-
-
- half vC = normal.x*normal.x - normal.y*normal.y;
- x3 = unity_SHC.rgb * vC;
- return x2 + x3 + x1;
- }
ShadeSH3Order函数 ShadeSH3Order函数,我将其翻译为三序球面调和函数。定义于UnityCG.cginc头文件中,相关代码如下:
-
-
- half3 ShadeSH3Order(half4 normal)
- {
- half3 x2, x3;
-
- half4 vB = normal.xyzz * normal.yzzx;
- x2.r = dot(unity_SHBr,vB);
- x2.g = dot(unity_SHBg,vB);
- x2.b = dot(unity_SHBb,vB);
-
-
- half vC = normal.x*normal.x - normal.y*normal.y;
- x3 = unity_SHC.rgb * vC;
-
- return x2 + x3;
- }
Shade4PointLights函数
Shade4PointLights为Unity为我们准备好的逐顶点光照处理函数,定义于unityCG.cginc头文件中,相关代码如下:
-
- float3 Shade4PointLights (
- float4 lightPosX, float4 lightPosY, float4 lightPosZ,
- float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
- float4 lightAttenSq,
- float3 pos, float3 normal)
- {
-
- float4 toLightX = lightPosX - pos.x;
- float4 toLightY = lightPosY - pos.y;
- float4 toLightZ = lightPosZ - pos.z;
-
- float4 lengthSq = 0;
- lengthSq += toLightX * toLightX;
- lengthSq += toLightY * toLightY;
- lengthSq += toLightZ * toLightZ;
-
- float4 ndotl = 0;
- ndotl += toLightX * normal.x;
- ndotl += toLightY * normal.y;
- ndotl += toLightZ * normal.z;
-
- float4 corr = rsqrt(lengthSq);
- ndotl = max (float4(0,0,0,0), ndotl * corr);
-
- float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
- float4 diff = ndotl * atten;
-
- float3 col = 0;
- col += lightColor0 * diff.x;
- col += lightColor1 * diff.y;
- col += lightColor2 * diff.z;
- col += lightColor3 * diff.w;
- return col;
- }
14、TANGENT_SPACE_ROTATION宏
TANGENT_SPACE_ROTATION宏定义于UnityCG.cginc中,作用是声明一个由切线空间的基组成的3x3矩阵,相关代码如下:
-
- #define TANGENT_SPACE_ROTATION
- float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
- float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
也就是说,使用TANGENT_SPACE_ROTATION宏也就表示定义了上述代码所示的float3 类型的binormal和float3x3类型的rotation两个变量。且其中的rotation为3x3的矩阵,由切线空间的基组成。可以使用它把物体空间转换到切线空间中。
15、UNITY_OPTIMIZE_TEXCUBELOD宏
UNITY_OPTIMIZE_TEXCUBELOD宏的定义非常简单,就是用0标识是否开启此功能,如下所示:
- #ifndef UNITY_OPTIMIZE_TEXCUBELOD
- #define UNITY_OPTIMIZE_TEXCUBELOD 0
- #endif
reflect函数是CG语言的内置函数。
reflect(I, N) 根据入射光方向向量I,和顶点法向量N,计算反射光方向向量。其中I 和N必须被归一化,需要特别注意的是,这个I 是指向顶点的;且此函数只对三元向量有效。
UNITY_TRANSFER_FOG宏相关代码定义于UnityCG.Cginc头文件中,用于的相关代码如下所示:
-
- #if defined(FOG_LINEAR)
-
- #define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w
- #elif defined(FOG_EXP)
-
- #define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor)
- #elif defined(FOG_EXP2)
-
- #define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor)
- #else
- #define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = 0.0
- #endif
-
-
- #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
-
-
- #define UNITY_FOG_COORDS(idx) float fogCoord : TEXCOORD##idx;
-
-
-
-
- #if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)
-
- #define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord = unityFogFactor
-
- #else
-
- #define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord = (outpos).z
- #endif
-
- #else
- #define UNITY_FOG_COORDS(idx)
- #define UNITY_TRANSFER_FOG(o,outpos)
- #endif
可以发现,关于此宏的定义,主要集中在如下几句:
- #if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)
-
- #define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord = unityFogFactor
-
- #else
-
- #define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord = (outpos).z
- #endif
而其中宏定义依赖的UNITY_CALC_FOG_FACTOR宏,定义于这段代码的一开头,也根据不同的场合,计算方法分为了几个版本。
OK,顶点着色器分析完篇幅都这么多了,这一节就到这里。
三、屏幕油画特效的实现
之前的文章中提出,Unity中的屏幕特效通常分为两部分来实现:
下面依旧是从这两个方面对本次的特效进行实现。
Shader实现部分
依旧老规矩,先上注释好的Shader代码。
-
-
- Shader "浅墨Shader编程/Volume10/OilPaintEffect"
- {
-
- Properties
- {
- _MainTex("Base (RGB)", 2D) = "white" {}
- _Distortion("_Distortion", Range(0.0, 1.0)) = 0.3
- _ScreenResolution("_ScreenResolution", Vector) = (0., 0., 0., 0.)
- _ResolutionValue("_ResolutionValue", Range(0.0, 5.0)) = 1.0
- _Radius("_Radius", Range(0.0, 5.0)) = 2.0
- }
-
-
- SubShader
- {
-
- Pass
- {
-
- ZTest Always
-
-
- CGPROGRAM
-
-
- #pragma target 3.0
-
-
- #pragma vertex vert
- #pragma fragment frag
-
-
- #include "UnityCG.cginc"
-
-
- uniform sampler2D _MainTex;
- uniform float _Distortion;
- uniform float4 _ScreenResolution;
- uniform float _ResolutionValue;
- uniform int _Radius;
-
-
- struct vertexInput
- {
- float4 vertex : POSITION;
- float4 color : COLOR;
- float2 texcoord : TEXCOORD0;
- };
-
-
- struct vertexOutput
- {
- half2 texcoord : TEXCOORD0;
- float4 vertex : SV_POSITION;
- fixed4 color : COLOR;
- };
-
-
-
-
-
-
- vertexOutput vert(vertexInput Input)
- {
-
- vertexOutput Output;
-
-
-
- Output.vertex = mul(UNITY_MATRIX_MVP, Input.vertex);
-
- Output.texcoord = Input.texcoord;
-
- Output.color = Input.color;
-
-
- return Output;
- }
-
-
-
-
-
- float4 frag(vertexOutput Input) : COLOR
- {
-
- float2 src_size = float2(_ResolutionValue / _ScreenResolution.x, _ResolutionValue / _ScreenResolution.y);
-
-
- float2 uv = Input.texcoord.xy;
-
-
- float n = float((_Radius + 1) * (_Radius + 1));;
-
-
- float3 m0 = 0.0; float3 m1 = 0.0;
- float3 s0 = 0.0; float3 s1 = 0.0;
- float3 c;
-
-
- for (int j = -_Radius; j <= 0; ++j)
- {
- for (int i = -_Radius; i <= 0; ++i)
- {
- c = tex2D(_MainTex, uv + float2(i, j) * src_size).rgb;
- m0 += c;
- s0 += c * c;
- }
- }
-
-
- for (int j = 0; j <= _Radius; ++j)
- {
- for (int i = 0; i <= _Radius; ++i)
- {
- c = tex2D(_MainTex, uv + float2(i, j) * src_size).rgb;
- m1 += c;
- s1 += c * c;
- }
- }
-
-
- float4 finalFragColor = 0.;
- float min_sigma2 = 1e+2;
-
-
- m0 /= n;
- s0 = abs(s0 / n - m0 * m0);
-
- float sigma2 = s0.r + s0.g + s0.b;
- if (sigma2 < min_sigma2)
- {
- min_sigma2 = sigma2;
- finalFragColor = float4(m0, 1.0);
- }
-
-
- m1 /= n;
- s1 = abs(s1 / n - m1 * m1);
-
- sigma2 = s1.r + s1.g + s1.b;
- if (sigma2 < min_sigma2)
- {
- min_sigma2 = sigma2;
- finalFragColor = float4(m1, 1.0);
- }
-
-
- return finalFragColor;
- }
-
- ENDCG
- }
-
- }
- }
需要注意,本次油画效果的思路来自于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#脚本文件的代码几乎可以从之前的几个特效中重用,只用稍微改一点细节就可以。下面也是贴出详细注释的实现此特效的C#脚本:
- using UnityEngine;
- using System.Collections;
-
-
- [ExecuteInEditMode]
-
- [AddComponentMenu("浅墨Shader编程/Volume10/ScreenOilPaintEffect")]
- public class ScreenOilPaintEffect : MonoBehaviour
- {
-
- #region Variables
-
-
- public Shader CurShader;
- private Material CurMaterial;
-
-
- [Range(0, 5),Tooltip("分辨率比例值")]
- public float ResolutionValue = 0.9f;
- [Range(1, 30),Tooltip("半径的值,决定了迭代的次数")]
- public int RadiusValue = 5;
-
-
- public static float ChangeValue;
- public static int ChangeValue2;
- #endregion
-
-
- #region MaterialGetAndSet
- Material material
- {
- get
- {
- if(CurMaterial == null)
- {
- CurMaterial = new Material(CurShader);
- CurMaterial.hideFlags = HideFlags.HideAndDontSave;
- }
- return CurMaterial;
- }
- }
- #endregion
-
-
-
-
- void Start ()
- {
-
- ChangeValue = ResolutionValue;
- ChangeValue2 = RadiusValue;
-
-
- CurShader = Shader.Find("浅墨Shader编程/Volume10/ScreenOilPaintEffect");
-
-
- if(!SystemInfo.supportsImageEffects)
- {
- enabled = false;
- return;
- }
- }
-
-
-
-
- void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture)
- {
-
- if(CurShader != null)
- {
-
- material.SetFloat("_ResolutionValue", ResolutionValue);
- material.SetInt("_Radius", RadiusValue);
- material.SetVector("_ScreenResolution", new Vector4(sourceTexture.width, sourceTexture.height, 0.0f, 0.0f));
-
-
- Graphics.Blit(sourceTexture, destTexture, material);
- }
-
-
- else
- {
-
- Graphics.Blit(sourceTexture, destTexture);
- }
-
-
- }
-
-
-
-
-
- void OnValidate()
- {
-
- ChangeValue = ResolutionValue;
- ChangeValue2 = RadiusValue;
- }
-
- void Update ()
- {
-
- if (Application.isPlaying)
- {
-
- ResolutionValue = ChangeValue;
- RadiusValue=ChangeValue2;
- }
-
- #if UNITY_EDITOR
- if (Application.isPlaying!=true)
- {
- CurShader = Shader.Find("浅墨Shader编程/Volume10/ScreenOilPaintEffect");
- }
- #endif
-
- }
-
-
-
-
- void OnDisable ()
- {
- if(CurMaterial)
- {
-
- DestroyImmediate(CurMaterial);
- }
-
- }
- }
而根据脚本中参数的设定,就有分辨率和半径两个参数可以自定义条件,如下图:

下面一起看一下运行效果的对比。
四、最终的效果展示
还是那句话,贴几张场景的效果图和使用了屏幕特效后的效果图。在试玩场景时,除了类似CS/CF的FPS游戏控制系统以外,还可以使用键盘上的按键【F】,开启或者屏幕特效。
城镇一隅(with 屏幕油画特效):

城镇一隅(原始图):

城镇路口(with 屏幕油画特效):

城镇路口(原始图):

城镇一隅之二(with 屏幕油画特效):

城镇一隅之二(原始图):

木质城墙和手推车(with 屏幕油画特效):

木质城墙和手推车(原始图):

路边(with 屏幕油画特效):

路边(原始图):

图就贴这些,更多画面大家可以从文章开头下载的本文配套的exe场景,进行试玩,或者在本文附录中贴出的下载链接中下载本文配套的所有游戏资源的unitypackage。
至此,这篇博文已经1万1千多字。感谢大家的捧场。下周浅墨有些事情,所以停更一次,我们下下周,再会。

附: 本博文相关下载链接清单
【百度云】博文示例场景exe下载
【百度云】包含博文示例场景所有资源与源码的unitypackage下载
【Github】屏幕油画特效实现源码
看了上面的文章 热爱游戏创作的你是不是已经开始热血沸腾了呢?是不是迫不及待的想加入游戏团队成为里面的一员呢?
福利来啦~赶快加入腾讯GAD交流群,人满封群!每天分享游戏开发内部干货、教学视频、福利活动、和有相同梦想的人在一起,更有腾讯游戏专家手把手教你做游戏!
腾讯GAD游戏程序交流群:484290331