图形引擎实战:变体剔除经验分享
既URP之后的近年shader中定义的keyword数量爆炸增长,编译后变体数量更是指数级增长;变体剔除成为打包过程中必不可少的一步,相较于之前buildin管线下,urp中的变体剔除也有不少变化,这里分享一下个人在处理变体剔除过程中遇到的一些问题。
Unity中的变体剔除:
这里使用的是unity 2021.3.19版本,URP版本为12.1.7版本;
首先在分析变体问题之前,我们可以先看一下shader界面的编译,查看到底有哪些变体组合被编译了出来,然后再决定要剔除的变体;
shader界面选择编译平台,这里会显示当前生成了多少变体组合,点击show后打开文件进一步可以看到具体有哪些组合;
每个pass的具体组合都一一列出了,第一行的“Keywords stripped away when not used”指的是shader_feature定义的keyword会根据当前是否有出现过在编辑器中剔除,如果发现组合中没有材质中使用的keyword,实际运行一次带有相关材质的场景就会发现变体组合数量增加了;第二行则是由multi_compile定义的keyword;但是这里的数量其实也并不是将所有的keyword都组合一遍,也受系统的剔除影响,例如graphics设置下的剔除设置未勾选Realtime Non-Directional时,所有带有DYNAMICLIGHTMAP_ON的变体组合都会被剔除;
另外,在定义中通常会使用multi_compile_fragment的类似定义,使keyword仅在fragment中生效;在进行剔除时,自定义的剔除规则都需要通过继承接口 OnPrecessShader来实现;
public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
可以通过snippet.shaderType来判断当前处理的是vertex部分还是fragment部分;例如想要剔除未开启软阴影的变体组合:
multi_compile_fragment _ _SHADOWS_SOFT
就需要在函数中判断仅在fragment阶段剔除不含_SHADOWS_SOFT的变体组合,但是这里unity有一个问题,在打包openGL平台时,会发现所有处理的shaderType都是vertex,而metal、vulkan等都会出现vertex和fragment能正常判断;
这是因为编译出的shader文件中用于区分vertex和fragment的定义不同,通过shader界面的Complie and show code来查看编译出的文件:
上图中glcore下通过 #ifdef 来定义Fragment,matal中定义vertex和fragment是一样的。
URP工程中影响剔除的流程:
1.Graphics中的剔除设置
这里会影响Lightmap、Fog、Instacing相关的变体剔除,Lightmap设置中未被勾选的都将会被剔除;
2.FilterAttribute
unity的官方说明https://docs.unity3d.com/cn/2022.2/ScriptReference/ShaderKeywordFilter.FilterAttribute.html
URP的脚本UniversalRenderPipelineAsset中有相关的使用,例如
[ShaderKeywordFilter.ApplyRulesIfGraphicsAPI(GraphicsDeviceType.OpenGLES2)]
[ShaderKeywordFilter.RemoveIf(true, keywordNames: ShaderKeywordStrings.LightLayers)]
在openGLES2平台编译时会剔除所有包含_LIGH_LAYER的变体组合;
这里分享一个可能出现的问题,一些shader在编写的时候基于老的URP版本,在移植新的URP工程时没有调整过keyword的定义;例如URP12.1.7 下关于主光阴影的定义:
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
而老一些版本的URP关于主光阴影的定义是:
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
定义的差别很大,如果不注意可能会导致剔除出问题;
3.URP的globalsetting
相关的处理脚本名称为 ShaderPreprocessor;
基本逻辑为收集当前的RenderPipelineAsset以及volume中的性能设置项,然后剔除所有没有使用过的特性对应的keyword;
4.编译时未使用的shader_feature剔除
这里就是为什么不需要运行时关闭或者开启的keyword要使用shader_feature来定义的原因了,在实际打包时未在场景中使用过的或者未被收集到的shader_feature就会被直接剔除不会打进包里;所以如果要把shader打进bundle那么一定要保证变体收集正确,收集到所有材质使用的变体组合;
5.自定义的剔除规则
通过c#脚本实现OnPrecessShader接口,自定义剔除规则,同样可以设置执行顺序,决定URP的剔除顺序;
剔除方案的两种思路:
1.黑名单剔除
顾名思义就是将所有没有使用到的特性所需要的keyword剔除,最直接的例如:
DEBUG相关、额外灯光阴影、系统雾、INSTANCE相关等;
这个也是URP的自带剔除的思路,将所有没有用到的feature相关的keyword剔除;
一般对于项目来说把总的shader内存占用控制到50M以下就可以接受,30M以下比较优秀;
2.仅保留收集到的变体组合
这种做法就需要保证收集到的变体是完全的,包括所有需要动态开关的变体;且不同平台使用的变体组合会有差异包括高低配不同配置下;
剔除时将所有不包含在变体收集文件中的变体组合全部剔除;那么难点就在于如何将所有使用的变体组合全部收集到,仅靠unity跑场景收集很难收集全;与unity支持的人员交流提供了一个思路,在引擎底层编译shader的位置添加一个记录,然后将测试设备上的记录回传到服务器,通过大量的测试流程来逐渐完善变体收集;
另外,由于某些URP的问题也可能导致实际收集到的变体组合中包含错误的keyword;例如在测试中发现,通过framedebugger 看到的变体组合总是包含_ADDITIONAL_LIGHT_SHADOWS,但是render asset中是没有开启addtional light shadow选项的;查看代码后发现,即使没有开启这个选项,URP管线中也总是会执行AdditionalLightShadowCasterPass,且判断没有额外灯光需要渲染阴影时也总是会开启这个keyword;
但是在打包时,因为没有开启这个feature(额外光阴影),URP自带的剔除代码会直接将所有包含_ADDITIONAL_LIGHT_SHADOWS keyword的变体组合剔除;这就有可能导致收集到的变体组合被剔除;
编写时注意的问题:
1.shader变体定义不要引入未使用的定义
例如:multi_compile_fog是关于系统雾的定义,如果未使用系统雾效果,那么完全没必要定义;
multi_compile_shadowcaster是shadowcaster pass中用于区分点光源或者平行光阴影的keyword,如果工程没开启过点光源阴影那么也完全没必要定义;
2.keyword定义要放入对应的pass中,如果在subshader中定义那么所有的pass都会包含大量变体组合;
实际上变体数量爆炸的仅有主要的光照pass,其他depth only和shadow caster的变体数量都很少,但如果keyword定义区间错误数量就会直接翻倍;
3.使用的系统keyword定义一定要和URP一致
例如上面提到的不同版本URP关于keyword定义会有差别,直接移植shader需要注意区别
欢迎加入我们!
感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com
#在找工作求抱抱##搜狐畅游##游戏引擎##游戏#