我们通过assetbundle加载材质球或shader时常遇到一个问题:在电脑上测试ok的shader,在手机上显示一片粉红。
出现这种情况的原因有很多,但是其中可能性最大的就是shader变体丢失导致的。
ab资源加载中的shader是如何被引用到的?
加载ab资源中的材质时,会主动索引并加载使用的shader。
“被包含在构建包体中”,指的这几种情况:
- shader文件在Resources文件夹下;
- shader文件被设置在Setting => Graphics => Always Included Shaders中;
- shader被构建包含的场景中使用;这几种情况下shader资源会被打进apk包中。
基于shader是否在构建包体中,有以下2种情况:
- 如果shader被包含在构建包体内,则可以直接加载使用。
- 如果shader未被包含在构建包体内,需要通过ab资源加载使用时,使用前shader需要首先加载shader所在的ab资源。 如果ab资源尚未加载,此时使用shader的材质就会出问题。
为什么ab中的shader变体会丢失?
multi_compile和shader_feature是着色器中定义宏的关键字,不同的关键字会生成不同的变体。
相比于multi_compile指令,使用 shader_feature 指令定义的着色器在构建时,没有在材质球中使用到的变体将不会包含在最终的构建中。
所以 shader_feature 用于材质中设置的关键字,而 multi_compile 更适合通过代码来全局设置的关键字。
在Assetbundle打包材质与其引用的shader时:
- 如果shader和材质在同一个ab包中,在打包时会自动检索到使用的keyword,并将其对应的变体编译打包入ab包中。
- 如果shader和使用到的材质不在同一个ab包中,则打包时无法检索到使用到了哪些keyword,所以材质使用的变体可能丢失。
这就是ab打包过程中的变体丢失的问题。
如何避免shader变体丢失?
第一种方法:将shader加入到ProjectSetting=>Graphics=>Always Included Shaders 中。
加在这里的shader不会剔除变体,所以适用于变体少且全部使用到的shader。
如果加入有很多变体的shader,会导致打包包体膨胀。
注意:shader_feature定义的关键字如果未使用,即使加入到了这里,变体也不会保留!
第二种方法:使用ShaderVirantCollection。
我们可以使用ShaderVirantCollection文件来定义使用到的变体,这样打ab包时会自动将定义的变体编译并打包,这样就避免了变体丢失。
注意:ShaderVirantCollection也必须入包(与shader放入同一个ab包),否则不生效。
可以通过shader control插件来搜集使用到的shader变体,从而避免人工查找容易漏掉了某些变体。
当然shader control可以仅可以查询到项目中材质球使用到的shader关键字,如果是通过代码加载/设置shader keyword,则无法通过shader control查询到。
assetbundle打包shader的最佳方案
下面是我当前项目中assetbundle打包shader的方案,暂时定义为我自己的最佳方案:
- 把assetbundle资源中引用到的shader都放到一个ab包中,在进入游戏时首先加载此ab包,这样可以保证后续所有使用的材质没有问题。
- 常用到且变体数量少的shader,可以将shader加入到Always Included Shaders 中,然后在打包ab资源时剔除这些shader。
- 需要assetbundle打包的shader,使用ShaderVirantCollection定义需要打包的变体。可以通过shader control插件来搜集材质球使用到的shader变体,结合人工查找代码中调用使用到的变体。
multip_compile 和shader_feature的区别
相同点:
- 声明Keyword,用来产生Shader的变体(Variant)
#pragma multi_compile A B
//OR #pragma shader_feature A B
- 这个Shader会被编译成两个变体:一是只包含A模块代码的变体A;二是只包含B模块代码的变体B;
- 指定的第一个关键字是默认生效的,即默认使用变体A;
- 在脚本里用Material.EnableKeyword或Shader.EnableKeyword来控制运行时具体使用变体A还是变体B;
- 它们声明的Keyword是全局的,可以对全局的包含该Keyword的不同Shader起作用;
- 全局最多只能声明256个(2020.3以后的全局keyword的数量限制是384)这样的Keyword;
- 请注意Keyword的数量和变体的数量之间的关系,并可能由此导致的性能开销,比如声明#pragma multi_compile A B和#pragma multi_compile D E 这样的两组Keyword会产生 2x2=4 个Shader变体,但若声明10组这样的keyword,则该Shader会产生1024个变体;
不同点:
如果使用shader_feature,build时没有用到的变体会被删除,不会打出来。也就是说,在build以后环境里,运行代码Material.EnableKeyword("B")可能不起作用,因为没有Material在使用变体B,所以变体B没有被build出来,运行时也找不到变体B。
如果想解决这个问题,可以采取以下办法中的其中一种:
- 使用multi_complie 代替 shader_feature,multi_complie 会把所有变体build出来;
- 把这个Shader加入“always included shaders”中 (Project Settings -> Graphic);
- 创造一个使用变体B的Material,强行说明变体B有用;
局部keword
全局的Keyword只能有256个,这或许会最终对我们造成限制,而且大部分Keyword并不需要进行全局声明。
因此,我们可以使用multi_complie_local来声明局部的、只在该Shader内部起作用的Keyword,用法相似:
#pragma multi_compile_local __ A
//OR #pragma shader_feature_local __ A
注意:
- local Keyword仍有数量限制,每个Shader最多可以包含64个local Keyword;
- 因为这种Keyword是局部的,Material.EnableKeyword仍是有效的,但对Shader.EnableKeyword或CommandBuffer.EnableShaderKeyword这种全局开关来说无法使用;
- 当你既声明了一个全局的Keyword A ,同时又声明了一个同名的、局部的Keyword A,那么优先认为Keyword A是局部的。
参考链接
Unity Manual - 着色器变体和关键字
Unity Blog - Stripping scriptable shader variants
【Unity游戏开发】马三的游戏性能优化自留地