unity shader之——着色器的组织和复用

一、cginc文件

1.1 unity的UnityCG.cginc文件

我们经常使用#include 指令包含UnityCG.cginc。这个文件中包含了unity预定义的大量结构和函数,通过#include指令可以复用这些结构和函数,而不必每次都重新定义它们。

1.2 定义自己的cginc文件

我们可以定义自己的cginc文件,然后用#include指令包含该文件,实现着色器代码的复用。虽然包含了整个cginc文件,但unity只会在实际代码中包含被用到的cginc文件中的那部分代码。

二、通过UsePass来复用通道

2.1 定义自己要复用的通道

我们可以为定义的Pass声明名字,以便外部引用,名字要大写,因为在unity引擎内部,所有通道名都是大写的。

2.2 复用这些通道

复用格式如下:

UsePass "Tut/Organize/UsePass/MyPasses/RED"

三、定义着色器的关键字

3.1 使用关键字改变着色器的行为

我们可以自定义着色器关键字,用它们组织着色器的框架,让他们可以在不同的平台上实现预期效果。Unity4.x版本中,整个工程中着色器关键字最多不能超过64个(包括unity自己内置关键字),在unity5.0版本中,关键字数量会增加到128个。一般检测方法可以用 #if defined(XX)、 #ifdef XX、 #ifndef XX,然后#endif。

3.2 自定义着色器关键字

Shader "Tut/Organize/KeyWords/KeyWord_1" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		pass{
		CGPROGRAM
		#define MY_Condition_1    //定义了一个关键字
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"
		struct vertOut{
			float4 pos:SV_POSITION;
		};
		vertOut vert(appdata_base v)
		{
			vertOut o;
			o.pos=UnityObjectToClipPos(v.vertex);
			return o;
		}
		float4 frag(vertOut i):COLOR
		{
			float4 c=float4(0,0,0,0);
			#if defined(MY_Condition_1)       //检测此关键字是否被定义,并执行不同的运算
			c=float4(1,0,0,0);
			#endif
			return c;
		}
		ENDCG
		}//end pass
	} 
}

但是这样做仍然是静态的,只会在着色器编译时起作用,如果想在运行时通过关键字改变着色器的行为呢?

四、ShaderVariant shader变体介绍

4.1 ShaderVariant

举个例子,对于一个支持法线贴图的Shader来说,用户肯定希望无论是否为材质提供法线贴图它的Shader都能正确的进行渲染处理。一般有两种方法来保证这种需求:

1.在底层shader(GLSL,HLSL等)定义一个由外部传进来的变量(如int),有没有提供法线贴图由外部来判断并给这个shader传参,若是有则传0,否则传1,在Shader用if对这个变量进行判断,然后在两个分支中进行对应的处理。

2.对底层shader封装,如Unity的ShaderLab就是这种,然后在上层为用户提供定义宏的功能,并决定宏在被定义和未被定义下如何处理。最终编译时,根据上层的宏定义,根据不同的组合编译出多套底层shader.

上述两种方法,各有利弊,对于前者由于引入了条件判断,会影响最终shader在GPU上的执行效率。而后者则会导致生成的shader源码(或二进制文件)变大。Unity中内置的Shader往往采取的是后者,所以这里只讨论这种情况。

Unity的Shader中通过multi_compile和shader_feature来定义宏(keyword)。最终编译的时候也是根据这些宏来编译成多种组合形式的Shader源码,其中每一种组合就是这个Uniy Shader的一个Variant。unity内置宏可以用#pragma skip_variants XX来屏蔽,这样对减少shaderlab内存大小应该也有帮助。

4.2 Material与ShaderVariant的关系

一个Material同一时刻只能对应它所使用的Shader的一个variant。进行切换的要使用Material.EnableKeyword()和Material.DisableKeyword()来开关对应的宏,然后Unity会根据你设定的组合来匹配响应的shader variant进行渲染。如果你是在编辑器非运行模式下进行的修改那么这些keyword的设置会被保存到材质的.mat文件中,尝试用NotePad++打开.mat文件,你应该会看到类似于下面的一段内容(需要在编辑器设置里把AssetSerializationMode设置为Force Text):

%YAML 1.1
 
%TAG !u! tag:unity3d.com,2011:
 
--- !u!21 &2100000
 
Material:
 
  serializedVersion: 6
 
  m_ObjectHideFlags: 0
 
  m_PrefabParentObject: {fileID: 0}
 
  m_PrefabInternal: {fileID: 0}
 
  m_Name: New Material
 
  m_Shader: {fileID: 4800000, guid: 3e0be7fac8c0b7c4599935fa92c842a4, type: 3}
 
  m_ShaderKeywords: _B
 
  m_LightmapFlags: 1
 
  m_CustomRenderQueue: -1
 
  …

其中的m_ShaderKeywords就保存了这个材质球使用了哪些宏(keyword).

如果你手头有built-in Shader的源码可以打开里面的StandardShaderGUI.cs看一下Unity自己事怎么处理对于StandardShader的keyword设置的。

另外Shader.EnableKeyword,和Shader.DisableKeyword是对Shader进行全局宏设置的,这里不提了。

五、使用multi_Compile编译着色器的多个版本

5.1 使用multi_compile实现多次编译

unity为我们提供了multi_compile选项,让unity针对不同的定义条件或关键字编译多次。然后在运行时,在脚本中开启或关闭关键字,从而使用着色器不同条件下的代码版本。下面是这种着色器的一个例子:

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
 
Shader "Tut/Organize/KeyWords/Multi_Compile" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		pass{
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#pragma multi_compile MY_multi_1 MY_multi_2
		#include "UnityCG.cginc"
		struct vertOut{
			float4 pos:SV_POSITION;
		};
		vertOut vert(appdata_base v)
		{
			vertOut o;
			o.pos=UnityObjectToClipPos(v.vertex);
			return o;
		}
		float4 frag(vertOut i):COLOR
		{
			float4 c=float4(0,0,0,0);
			#ifdef MY_multi_1
			c=float4(0,1,0,0);
			#endif
			#ifdef MY_multi_2
			c=float4(0,0,1,0);
			#endif
			return c;
		}
		ENDCG
		}//end pass
		
	} 
	CustomEditor "ExamEditor_2"
}

5.2 在脚本中选择着色器的版本

当使用multi_compile编译出着色器的多个版本之后,就可以在脚本中通过着色器的类函数来启用或者关闭全局全局的关键字,从而实现着色器的版本选择。使用multi_compile似乎可以动态选择着色器版本,但是该方法是以着色器的冗余副本为代价的。比如,有两个multi_compile A B C、multi_compile D E,会实际产生(A+D,A+E,B+D,B+E,C+D,C+E)6个着色器的版本,因此使用multi_compile来实现动态着色器选择是有代价的,慎用。

控制脚本例子如下:

 if (flip1)
            {
                Shader.EnableKeyword("MY_multi_1");
                Shader.DisableKeyword("MY_multi_2");
            }
            else
            {
                Shader.EnableKeyword("MY_multi_2");
                Shader.DisableKeyword("MY_multi_1");
            }

六、使用shader_feature编译着色器的多个版本

6.1multi_compile和shader_feature的区别

multi_compile是一直都有的,shader_feature是后来的unity版本中加入的关键字。

举例介绍一下multi_compile和shader_feature:

1. 如果你在shader中添加了

#pragma multi_compile _A _B
#pragma multi_compile _C _D

那么无论这些宏是否真的被用到,你的shader都会被Unity编译成四个variant,分别包含了_A _C,_A _D, _B _C,_B _D四种keyword组合的代码

2. 如果是

#pragma shader_feature _A _B
#pragma shader_feature _C _D

那么你的shader只会保留生成被用到的keyword组合的variant,至于如何判定哪些组合被用到了,等后面提到Assetbundle时候再说。

6.2ShaderVariant与Assetbundle的关系

我当时发现打成AB包之后shader_feature所定义的宏没有被正确包含进去。

上面说了multi_compile定义的keyword是一定能正确的生成对应的多种组合的shaderVariant,但shader_feature不尽然,Unity引入shader_feature就是为了避免multi_compile那种完整编译所导致组合爆炸,很多根本不会被使用的shader_variant也会被生成。Unity在处理shader_feature时会判断相应的keyword组合是否被使用。需要区分一下几种情况:

1. 如果shader没有与使用它的材质打在一个AB中,那么shader_feature的所有宏相关的代码都不会被包含进AB包中(有一种例外,就是当shader_feature _A这种形式的时候是可以的),这个shader最终被程序从AB包中拿出来使用也会是错误的(粉红色).

2. 把shader和使用它的材质放到一个AB包中,但是材质中没有保存任何的keyword信息(你在编辑器中也是这种情况),shader_feature会默认的把第一个keyword也就是上面的_A和_C(即每个shader_feature的第一个)作为你的选择。而不会把_A _D,_B _C,_B _D这三种组合的代码编译到AB包中。

3. 把shader和使用它的材质放到一个AB包中,并且材质保存了keyword信息(_A _C)为例,那么这个AB包就只包含_A _C的shaderVariant.

可以看到shader_feature所定义的keyword产生的ShaderVariant并不是全部被打包到AB中,特别是你想在游戏运行时动态的通过EnableKeyWorld函数来进行动态修改材质使用的shaderVariant,如果一开始就没有把对于variant放进AB包,自然也就找不到。

6.3 ShaderVariantCollection

要正确的让各种variant正确的在游戏运行时正确处理,

最直接暴力的两种方法:

1. 把Shader放到在ProjectSetting->Graphics->Always Include Shaders列表里,Unity就会编译所有的组合变种。

2. 把Shader放到Resources文件夹下,也会正确处理,我猜也应该是全部keyword组合都编译。

但是这两种情况最大的问题就是组合爆炸的问题,如果keyword比较少还好,要是多了那真是不得了,比如你把standardShader放进去,由于它有大量的keyword,全部变种都生成的话大概有几百兆。另外一个问题就是这种办法没法热更新。自然不如放到AB包里的好控制。

放到AB包就又涉及到shader_feature的处理,为了在运行时动态切换材质的shadervariant,可以在工程里新建一堆材质,然后把每个材质设置成一种想要的keyword组合,把他们和shader放到一起打到一个AB中去,这样虽然能让shadervariant正确生成,但是这些Material是完全多余的。

为了解决这种问题,Unity5.0以后引入了ShaderVariantCollection(下面简称SVC),这里不讲用法,只说问题,这个SVC文件可以让我指定某个shader要编译都要编译带有哪些keyword的变种。并且在ProjectSetting->Graphics界面新加了一个Preloaded Shaders列表,可以让你把SVC文件放进去,编译时指定的Shader就会按照SVC中的设置进行正确的variant生成,而不会像Always Include Shaders列表中的那样全部变种都生成。

但是它在AB中的表现可就不尽如人意了,要让它起作用,就必须把它和对应的shader放在一个AB中,而且除了5.6以外版本,我试了几个都不能正确使用,不是一个variant都没生成,就是只生成一个shadervariant(和放一个没有设置keyword的材质效果一样).你可以自己用UnityStudio打开查看一下生成的AB内容。

应该正确的理解Unity提供multi_compile和shader_feature以及ShaderVariantCollection的意图,根据自己的情况来选择合理的解决方案。

七、使用自定义的材质编辑器

除了在脚本中选择着色器版本外,还可以通过扩展MaterialEditor实现静态的选择。首先编写脚本ExamEditor_2,在这个类中重写OnInspectorGUI方法,代码如下:

public class ExamEditor_2 : MaterialEditor 
{
 
	public override void OnInspectorGUI ()
	{
		base.OnInspectorGUI ();
 
		if (!isVisible)
				return;
 
		Material targetMat = target as Material;
		string [] keyWords = targetMat.shaderKeywords;
		bool switon = keyWords.Contains ("MY_multi_1");
 
		EditorGUI.BeginChangeCheck ();//GUI变动开始
		switon = EditorGUILayout.Toggle ("MY_multi_1",switon);
		if(EditorGUI.EndChangeCheck())//GUI变动结束
		{
			var keys=new List<string>{switon?"MY_multi_1":"MY_multi_2"};
			targetMat.shaderKeywords=keys.ToArray();
			EditorUtility.SetDirty(targetMat);
		}
	}
}

然后在shader里添加:

CustomEditor "ExamEditor_2"

完成上述程序就可以在材质编辑器使用复选框来切换选项了,如下图:


7.1 MaterialEditor

unity提供了丰富的类和接口,使我们能扩展Unity的编辑器功能。对于材质编辑,也提供了相关的类。MaterialEditor就是这样一个 编辑器类,可以像扩展普通编辑器类一样集成、重写MaterialEditor中的方法。但是要放到Editor文件夹下才能生效。

下面编写一个类,获取当前材质编辑器中材质的一个range类型的浮点数、一个整数,并将其值展示出来:

public class ExamEditor_1 : MaterialEditor {
	
	public override void OnInspectorGUI ()
	{
		base.OnInspectorGUI ();
		
		if (!isVisible)
			return;
		
		Material targetMat = target as Material;//我们正在编辑的材质
		Shader shader = targetMat.shader;
		//第二个材质属性
		string label1 = ShaderUtil.GetPropertyDescription(shader, 1);
		string propertyName1 = ShaderUtil.GetPropertyName(shader, 1);
		float val1 = targetMat.GetFloat (propertyName1);
		//第三个材质属性
		string label2 = ShaderUtil.GetPropertyDescription(shader, 2);
		string propertyName2 = ShaderUtil.GetPropertyName(shader, 2);
		int val2 = targetMat.GetInt (propertyName2);
 
		//第1个浮点值的展示
		EditorGUILayout.LabelField(label1+"/"+propertyName1," "+val1);
		//第2个整型的展示
		EditorGUILayout.LabelField(label2+"/"+propertyName2," "+val2);
 
 
		EditorGUI.BeginChangeCheck ();//GUI变动开始
 
		if(EditorGUI.EndChangeCheck())//GUI变动结束
		{
			EditorUtility.SetDirty(targetMat);
		}
	}

下面是自定义材质在编辑器的显示方式:


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

最新文章