[MenuItem("Shader/Collect Variants")] |
staticvoidCollectShaderVariants() |
{ |
varcollection = newShaderVariantCollection(); |
varfolders = newstring[]{"Assets/Shaders"}; |
varshaders = AssetDatabase.FindAssets("t:Shader", folders); |
foreach (var guid in shaders) |
{ |
varpath = AssetDatabase.GUIDToAssetPath(guid); |
varshader = AssetDatabase.LoadAssetAtPath<Shader>(path); |
varvariant = newShaderVariantCollection.ShaderVariant(shader, PassType.ForwardAdd, "DIRECTIONAL", "SHADOWS_OFF"); |
collection.Add(variant); |
} |
AssetDatabase.CreateAsset(collection, "Assets/AutoGenerated.shadervariants"); |
} |
ShaderVariants(下文用shader变种描述)是unity中用来合并组织shader的一个方式之一,在shader中的使用类似宏定义。最近项目使用shader变种的时候发现了一些坑,所以做了如下实验和总结。其中前两节是基础部分,看官方文档也可以了解,只是通过实验来加强理解。第三节shader变种的打包是重点描述的,比较重要。
一、生成shader变种机制
为了做实验,制作shader如下:
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | Shader "Custom/Color" { SubShader { Pass { Cull Off ZWrite Off Lighting Off
CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc"
#pragma shader_feature RED GREEN BLUE //#pragma shader_feature GREEN //#pragma shader_feature BLUE //#pragma multi_compile RED GREEN BLUE //#pragma multi_compile __ GREEN
struct v2f { fixed4 pos : SV_POSITION; };
v2f vert (appdata_base v) { v2f o; o.pos = mul (UNITY_MATRIX_MVP, v.vertex); return o; }
half4 frag(v2f i) : COLOR { fixed4 c = fixed4(0,0,0,1); #ifdef RED c += fixed4(1,0,0,1); #endif #ifdef GREEN c += fixed4(0,1,0,1); #endif #ifdef BLUE c += fixed4(0,0,1,1); #endif return c; } ENDCG } }
CustomEditor "ColorsGUI" } |
shader的变种数量可以通过shader面板上面查看到,点击Show按钮可以看到都有哪些变种
做如下实验对比shader_feature:
单一行命令
#pragma shader_feature RED
两个变种: __, RED
#pragma shader_feature RED GREEN
两个变种:RED,GREEN
#pragma shader_feature RED GREEN BLUE
三个变种:RED,GREEN,BLUE
#pragma multi_compile RED
一个变种:RED
#pragma multi_compile RED GREEN
两个变种:RED,GREEN
#pragma multi_compile RED GREEN BLUE
三个变种:RED,GREEN,BLUE
分析:shader_feature 和multi_compile 在Keyword 数量大于1时,生成变种的机制是一样的,都是一个keyword一个变种;当keyword只有1个时,shader_feature 会增加一个none变种。再来做个实验:
#pragma shader_feature __ RED
两个变种: none, RED
可见,当shader_feature 的keyword数量是1时不论是否有__符号,都会增加一个空keyword(__),除了这个在生成变种的机制上和multi_compile都是一致的。
多行命令
#pragma multi_compile __ RED
#pragma multi_compile __ GREEN
四个变种:__,RED,GREEN,RED GREEN
分析:多行命令就是单行命令的乘法组合,shader_feature和multi_compile除了单一keyword时是否补__之外,在多行命令中也是一致的。
二、匹配shader变种机制
为了实验shader变种的匹配,做一个方便定义keyword的shader界面,代码如下:
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public class ColorsGUI: ShaderGUI {
private static bool bRed = false; private static bool bGreen = false; private static bool bBlue = false;
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) { // render the default gui base.OnGUI(materialEditor, properties);
Material targetMat = materialEditor.target as Material;
bRed = Array.IndexOf(targetMat.shaderKeywords, "RED") != -1; bGreen = Array.IndexOf(targetMat.shaderKeywords, "GREEN") != -1; bBlue = Array.IndexOf(targetMat.shaderKeywords, "BLUE") != -1;
EditorGUI.BeginChangeCheck();
bRed = EditorGUILayout.Toggle("红", bRed); bGreen = EditorGUILayout.Toggle("绿", bGreen); bBlue = EditorGUILayout.Toggle("蓝", bBlue);
if (EditorGUI.EndChangeCheck()) { if (bRed) targetMat.EnableKeyword("RED"); else targetMat.DisableKeyword("RED"); if (bGreen) targetMat.EnableKeyword("GREEN"); else targetMat.DisableKeyword("GREEN"); if (bBlue) targetMat.EnableKeyword("BLUE"); else targetMat.DisableKeyword("BLUE"); } } } |
shader界面如下:
这样,勾选一个颜色,就会enable一个keyword,通过查看结果颜色就能知道匹配到了哪个shader变种,实验如下:
#pragma multi_compile RED GREEN(两个变种:RED, GREEN)
材质keyword为RED : 显示红色(匹配RED)
材质keyword为GREEN : 显示绿色(匹配GREEN)
材质keyword为__ : 显示红色(匹配RED)
材质keyword为RED GREEN: 显示红色(匹配RED)
分析:当keyword存在正好匹配的变种时直接匹配、当keyword不存在匹配变种时取第一个变种
三、shader变种打包
打包的代码如下:
[C#] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | [MenuItem("Assets/Build AssetBundles")] static void BuildAllAssetBundles() { List<AssetBundleBuild> maps = new List<AssetBundleBuild>(); maps.Clear(); //资源打包
string[] files = { "Assets/ShaderVariants/Resources/red.prefab", };
AssetBundleBuild build = new AssetBundleBuild(); build.assetBundleName = "ShaderVariantsPrefab"; build.assetNames = files; maps.Add(build);
string[] file2s = { "Assets/ShaderVariants/Resources/Shader/Colors.shader", };
AssetBundleBuild build2 = new AssetBundleBuild(); build2.assetBundleName = "ShaderVariantsShader"; build2.assetNames = file2s; maps.Add(build2);
BuildAssetBundleOptions options = BuildAssetBundleOptions.DeterministicAssetBundle; BuildPipeline.BuildAssetBundles("Assets/StreamingAssets", maps.ToArray(), options, BuildTarget.StandaloneWindows);
AssetDatabase.Refresh(); } |
读包并实例化的代码如下:
[C#] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class BundleLoader : MonoBehaviour { void Start () { load(); }
public void load() { StartCoroutine(LoadMainGameObject("file://" + Application.dataPath + "/StreamingAssets/" + "ShaderVariantsShader")); StartCoroutine(LoadMainGameObject("file://" + Application.dataPath + "/StreamingAssets/" + "ShaderVariantsPrefab")); }
private IEnumerator LoadMainGameObject(string path) { WWW bundle = new WWW(path);
yield return bundle;
if (bundle.url.Contains("ShaderVariantsShader")) { //依赖shader包 bundle.assetBundle.LoadAllAssets(); } else { UnityEngine.Object obj = bundle.assetBundle.LoadAsset("assets/ShaderVariants/resources/red.prefab"); Instantiate(obj); bundle.assetBundle.Unload(false); } } } |
为了对比shader_feature 和multi_compile 以及shader依赖和非依赖打包,做如下实验:
#pragma shader_feature RED GREEN BLUE
将选中RED关键字的prefab打包,加载bundle和其中的prefab,显示了红色,此时改变此材质的keyword为GREEN或者BLUE,没有效果
#pragma multi_compile RED GREEN BLUE
将选中RED关键字的prefab打包,加载bundle和其中的prefab,显示了红色,此时改变此材质的keyword为GREEN或者BLUE,可以显示绿色和蓝色
分析:shader_feature声明变种时,打包只会打包被资源引用的keyword变种,multi_compile声明变种时,打包会把所有变种都打进去
#pragma shader_feature RED GREEN BLUE
将选中RED关键字的prefab和shader依赖打包,加载bundle和其中的prefab,显示了异常粉红,任何变种都没有生效
分析:shader_feature标记的shader单独依赖打包时,任何变种都不会打进去,分析原因估计是unity认为单包中shader没有被引用过
总结:unity5中新出的shader_feature可以只将引用过的shader变种打进包里面,听起来很有用,可是大部分项目中为了节省冗余shader的内存,shader都是作为依赖包单独成一包的,此时没有任何shader变种被打进包中;更何况即使shader没有依赖打包,如果计划代码中动态修改shader的变种而不是记录在材质里面,此时也不能用shader_feature。基本上我们的项目中shader_feature可以废弃了。。。
以上代码地址:https://github.com/liuxq/UnityForBlog
自己的开源游戏demo:https://github.com/liuxq/StriveGame
============================Shader 热更新========================
1. shader可以热更新。
2. 使用multi_compile生成Shader Variant时,材质可以直接热更新。
3. 使用shader_feature生成Shader Variant时,可以使用ShaderVariantCollection来记录所有使用到的变种,实现材质热更新。(目前仍有bug,必须将shader、SVC和所有材质放在一个AB中)
4. 不要用Shader.Find找自己包里的shader,使用AssetBundle.LoadAsset<Shader>()
5. shadercache随材质存储,材质可以热更新。
针对以上问题我做了一系列测试,记录如下:
测试一:
准备一个shader ,一个材质,一个cube做的prefab,各自打成一个AB。
在一个空场景中用脚本按如下顺序加载:
shaderAB->materialAB->prefabAB->prefab->GameObject。显示正常。
(事实上只要保证prefabAB->prefab->GameObject的顺序,materialAB和shaderAB在同一函数的任意位置都可以。Unity应该是延迟处理了资源的引用关系)
修改shader,打成新的ab,改名或者另存为备用。
发布以后,在文件夹中找到对应的shaderAB,使用新的shaderAB替换。
重新启动,效果已经更新。
结论一: shader可以热更新!
测试平台:android和pc standalone
代码稍作修改可以在运行时实现热更新。
测试二:
准备3个shader,引用同一个头文件。shader和cginc全部进入一个ab里。
运行时先加载shaderAB,然后用一个按钮切换shader
结果如下表:
1] 在Standalone或者移动平台上会有shader丢失;在Editor模式下会使用旧的shader,仔细分析后猜测是在Editor模式下,shader.Find的查找顺序如下:已加载的 AssetBundle->Shader源文件。而在发布平台上,由于没有散的shader源文件,所以直接丢失。
[2] AssetBundle.LoadAsset的路径要使用Manifest中记载的路径,如下形式:
Assets/Shaders/Src/shaderTest2.shader
结论二:可以在运行时手动替换上AB中的shader,但必须使用AssetBundle.LoadAsset!
·可以使用cginc头文件!
·可以使用文件夹管理Shader!
·最好完全不使用Shader.Find,除非你100%确定这个shader不会热更新。
关于Shader.Find,个人猜测如下:
Unity内部使用一个字典或者HashSet来支持Shader.Find,这里暂且叫它ShaderMap。ShaderMap的键是ShaderLab语法中的名字;值是Shader文件的GUID。
ShaderMap生成于Build项目时,保存了来自三个地方的shader cache引用关系:
1. Resources中的shader或Resources其中其他资源引用到的shader
2. 任意场景中引用到的shader
3. StreamingAssets中Asset Bundle内的Shader
运行时使用ShaderFind,只能找到这些Shader,如果对应GUID的shader不存在,查找就会失败,即使热更新后加入了新的Asset Bundle中含有同名Shader(即ShaderLab语法同名)。
4. 目前没有办法在发布以后动态更新ShaderMap。
测试三:
准备两个同样的shader,设定好#ifdef FEATURE,其一使用multi_compile,其二使用shader_feature
准备四个材质,分别对应
·multi_compile FEATURE on
·multi_compile FEATURE off
·shader_feature FEATURE on
·shader_feature FEATURE off
所有shader打成一个ab, 所有material打成一个ab
在运行时切换4个材质。结果如下:
结论三:
·使用shader_feature的uber shader无法热更新!(结论已更新)
·若将shader存储于自定义AB时,仅按照所有shader_feature都没有定义的方式来编译。并且不会汇报这个编译过程中的任何错误!(如:在shader中定义了shader_feature A B;并且依赖于A、B二者任一必须定义的话,编译就会出错。)
·Unity并不会在发布平台上编译缺失的变种。(直接拿个现有的变种凑数?)
测试四
放弃热更新shader,检查在使用shader_feature的时候,材质能否热更新。即能否在热更新时生成缺失的变种。
准备一个uber shader。再来3个材质,各使用不同的变种,并分别打成m1,m2,m3三个包。发布时仅选择m1发布,然后在运行时热更新,使用m2,m3替换m1,显示效果达到预期。
这时候注意到m1,m2,m3体积分别为11,9,11KB,应该不只是存有shader引用和相关参数。因此再将m1,m2,m3打为一个ab,体积为11kb。
结论四:
·在shader进入mainAssets的前提下,材质可以热更新。
·shader cache随material ab存储,多个引用了同样shader(变种)的材质会重复存储cache。
更新测试五
使用ShaderVariantCollection,记录所有用到的variant。
将SVC和shader打入一个ShaderAB。
将材质打成MaterialAB
运行时加载ShaderAB,取SVC,WarmUp,再加载MaterialAB。结果丢失部分variant
更换分包方式,SVC、shader和Material打成一个包。一切正常。
结论五:
·使用ShaderVariantCollection可以做到带变种的材质更新。
·目前版本(5.6.0和5.5.2)依然有bug,必须将SVC、shader和所有对应材质放在一起才能做到可热更新。