UnityShader中着色器编译多样化

      很久没写东西了, 这些笔记是参考网上资料和自己得理解来写得,最近项目做完了才有一些空余时间,接着研究Unity着色器多样化,对于刚开始接触着色器得人,还是比较困难。自己趟两年水,我才敢说自己刚刚入门。今天主题是关于Unity着色器编译多样化。

       从Unity5系列开始采用被称为着色器编译多样化得新技术,被称为uber Shaders或者megashaders,并且通过每种情况提供不同得预处理指令来让着色器代码多次被编译来实现。

        在Unity中,这可以通过#pragma multi_compile 或者#pragma shader_feature指令来在着色器代码中实现,这种做法对表面着色器也可行。

如果我们定义:

                          #pragma multi_compile FANCY_STUFF_OFFFANCY_STUFF_ON运行时,

        其中一个将被激活,根据材质或者全局着色器关键词(#ifdef FANCY_STUFF_OFF之类的宏命令也可以)来确定激活哪一个,若两个关键词都没有启用,那么将默认使用前一个选项,也就是关闭(OFF)的选项FANCY_STUFF_OFF。

当然也可存在超过两个关键字的multi_compile编译选项,比如,如下代码将产生四种着色器的变体。

                  #pragma multi_compile SIMPLE_SHADINGBETTER_SHADING GOOD_SHADING BEST_SHADING

为嘛编辑的时候有的连写,有的不会连写。

        当#pragma multi_compile中存在所有名字都是下划线的一个指定段时,就表示须在没有预处理的情况下产生一个空的着色器变种。这种做法在着色器编写中比较常见。因为这样可以在不影响使用的情况下,避免使用两个关键词,这样就节省一个变量个数的占用。

例如,下面的指令将产生两个着色器变体,第一个没有意义,第二个定义为FOO_ON

       #pragma multi_compile __ FOO_ON

这样就省掉一个本来需要定义出来的FOO_OFF(FOO_OFF没有定义,自然也不能被使用),这样就节省了一个关键词个数的占用。

定义的意义:

如果Shader中有如上定义,则可以使用#ifdef来进行判断

#ifdef FOO_ON

//代码段1

#endif

根据上面已经定义过的FOO_ON,此时#ifdef判断的结果为真,代码段1部分的代码就会被执行到。反之,若#pragma multi_compile __ FOO_ON一句代码没交代出来,那么代码段1部分的代码就不会被执行。

shader_feature和multi_compile之间的区别

#pragma shader_feature和#pragma multi_compile非常相似,唯一区别就在于采用了#pragma shader_feature语义的shader,在遇到不被使用的变体的时候,就不会将其编译到游戏中。所以,shader_feature中使得所有的设置到材质中的关键词都是有效的,而

multi_compile指令将从全局代码里设置关键词。

当shader_feature还有一个仅仅含有一个关键字的快捷表达方式

例如:#pragma shader_feature FANCY_STUFF

此为#pragma shader_feature _FANCY_STUFF的一个简写形式,其扩展出两个着色器变体,第一个变体自然为不定义此FANCY_STUFF变量,第二个变体为定义此FANCY_STUFF变量

多个multi-compile连用会造成指数级增长

可以提供多个multi_compile流水线,然后着色器结果可以编译为几个流水线的排列组合,比如:

#pragma multi_compile A B C

#pragma multi_compile D E

第一行中有三种选项,第二行中有两种选项,那么进行排列组合,总共就会有六种选项

(A + D,B+D,A+E,B+E,C+E)

容易想到,一般每个multi_compile流水线,都控制着色器中某一单一的特性,请注意着色器总量的增长速度十分快。

比如:10条包含两个特性的multi_compile指令,会得到2的10次方,也就是1024种不同着色器变体。

关于Unity中的关键词限制Keyword limit

当使用着色器变量时,我们应该记住,Unity中将关键词的数量限制在128个以内(着色器变量算作关键字),且其中有一些被Unity内置使用了,因此我们真正可以自定义使用关键词的数量是小于128个的。同时,关键词是在单个Unity项目中全局使用被计算的,所以我们要千万小心,在同一项目中存在的没用到的Shader也要考虑在内。

 

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). 

 

使用某些指令跳过某些变得的编译

大多数内置的快捷指令导致了很多着色器的变体,若我们熟悉他们并且知道有一些并非所需要,可以使用#pragma skip_variants语句跳过其中一些的编译。

#pragma multi_compile_fwdadd

#pragma skip_variants POINT POINT_COOKIE

关于上面的解释看不懂没关系,下面这段提炼,关于着色器变体的意义与使用方式,也就懂大半了。

若我们在着色器中定义了一句:

#pragma shader_feature_THIS_IS_A_SAMPLE

这句代码理解起来,也就是_THIS-IS_A_SAMPLE被我们定义过了,它是存在的。以后我们如果判断#ifdef _THIS_IS_A_SAMPLE,那就是真的。我们可以在这个判断#ifdef 。。。。。。#endif块里面实现自己需要实现的X,只会在你用#pragma multi_compile或#pragma shader_feature定义了 _THIS_IS_A_SAMPLE这个宏的时候才会被执行,否则就执行不到。

 

代码执行不执行,全靠你对变体定义与否。这就是着色器编辑多样化的实现方式。

一个着色器+多个CG头文件的小团队(标准着色器),可以独挡一面。一个打一群。可以取代一大堆独立实现的Shader的原因所在。

 

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头文件中,相关代码如下:

float4 TexCoords(VertexInput v)

{

float4 texcoord;

texcoord.xy = TRANSFORM_TEX(v.uv0, _MainTex); // Always source from uv0

texcoord.zw = TRANSFORM_TEX(((_UVSec == 0) ? v.uv0 : v.uv1), _DetailAlbedoMap);

return texcoord;

}

 

函数实现代码中的_MainTex、_UVSec、_DetailAlbedoMap都是此头文件定义的全局的变量。其中还涉及到了一个TRANSFORM_TEX宏,在这边也提一下,它定义于UnityCG.cginc头文件中,相关代码如下:

// 按比例和偏移进行二维UV坐标的变换

  1. #define TRANSFORM_TEX(tex,name) (tex.xy *name##_ST.xy + name##_ST.zw)

 

NormalizePerVertexNormal函数,此函数位于unitystandardcore.cginc头文件中。

//--------------------------【函数NormalizePerVertexNormal】-----------------------------

// 用途:归一化每顶点法线

// 说明:若满足特定条件,便归一化每顶点法线并返回,否则,直接返回原始值

// 输入:half3类型的法线坐标

// 输出:若满足判断条件,返回half3类型的、经过归一化后的法线坐标,否则返回输入的值

//-----------------------------------------------------------------------------------------------

half3 NormalizePerVertexNormal (half3 n)

{

//满足着色目标模型的版本小于Shader Model 3.0,或者定义了UNITY_STANDARD_SIMPLE宏,返回归一化后的值

#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

//实现代码A

#else

//实现代码B

#endif

 

UnityObjectToWorldNormal是Unity内置的函数,可以将法线从模型空间变换到世界空间中,定义于UnityCG.cginc头文件中

 

UnityObjectToWorldDir函数用于方向值从物体空间切换到世界空间,也定义于UnityCG.cginc头文件中

 

CreateTangentToWorldPerVertex函数用于在世界空间中为每个顶点创建切线,定义于UnityStandardUtils.cginc头文件中,相关代码如下:

half3x3 CreateTangentToWorldPerVertex(half3 normal, half3 tangent, half tangentSign)

{

//对于奇数负比例变换,我们需要将符号反向||For odd-negative scale transforms we need to flip the sign

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。

 

TRANSFER_SHADOW(a)宏,此宏用于进行阴影在各种空间中的转换,定义于AutoLight.cginc中。在不同的情况下,此宏代表的意义并不相同。

 

VertexGIForward函数:顶点正向全局光照函数

inline half4 VertexGIForward(VertexInput v, float3 posWorld, half3 normalWorld){ }

 

unity_LightmapST变量类型为float4型,定义于UnityShaderVariables.cginc头文件中,存放着光照贴图操作的参数的值

 

UNITY_SHOULD_SAMPLE_SH宏,此宏定义于UnityCG.cginc中,

//包含间接漫反射的动态&静态光照贴图,所以忽略掉球面调和光照 || Dynamic & Static lightmaps contain indirect diffuse ligthing, thus ignore SH

#define UNITY_SHOULD_SAMPLE_SH ( defined (LIGHTMAP_OFF) && defined(DYNAMICLIGHTMAP_OFF) )

注意:define和defined的区别

define:define用来定义一个常量,常量范围是全局范围,不用管作用域就可以在脚本的任何地方访问,一个常量一旦被定义,就不能再改变或取消定义。

defined:defined用来检测常量有没有被定义,若常量存在,则返回true,否则返回false。

可以发现,这个宏,其实就是将LIGHTMAP_OFF(关闭光照贴图)宏和DYNAMICLIGHTMAP_OFF(关闭动态光照贴图)宏的定义进行了封装。

 

UNITY_SAMPLE_FULL_SH_PER_PIXEL宏,此宏定义于UnityStandardConfig.cginc头文件中。其实也就是一个标识符,用0标示UNITY_SAMPLE_FULL_SH_PER_PIXEL宏是否已经定义。按字面上理解,启用此宏表示我们将采样计算每像素球面调和光照,而不是默认的逐顶点计算球面调和光照并且线性插值到每像素中。其实现代码如下,非常简单:

  1. #ifndef UNITY_SAMPLE_FULL_SH_PER_PIXEL
  2. #define UNITY_SAMPLE_FULL_SH_PER_PIXEL 0
  3. #endif

 

ShadeSH9(half4 normal)函数就是大家常说的球面调和函数,定义于UnityCG.cginc头文件中

 

ShadeSH3Order(half normal)函数,我将其翻译为三序球面调和函数。定义于UnityCG.cginc头文件中

 

Shade4PointLights函数为Unity为我们准备好的逐顶点光照处理函数,定义于unityCG.cginc头文件中。正向基础渲染通道中使用,根据4个不同的点光源计算出漫反射光照参数的rgb值。

 

TANGENT_SPACE_ROTATION宏定义于UnityCG.cginc中,作用是声明一个由切线空间的基组成的3x3矩阵。也就是说,使用TANGENT_SPACE_ROTATION宏也就表示定义了上述代码所示的float3 类型的binormal和float3x3类型的rotation两个变量。且其中的rotation为3x3的矩阵,由切线空间的基组成。可以使用它把物体空间转换到切线空间中。

 

UNITY_OPTIMIZE_TEXCUBELOD宏的定义非常简单,就是用0标识是否开启此功能,如下所示:

  1. #ifndef UNITY_OPTIMIZE_TEXCUBELOD
  2. #define UNITY_OPTIMIZE_TEXCUBELOD 0
  3. #endif

你可能感兴趣的:(Shader)