图形引擎实战:变体剔除经验分享

既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

#在找工作求抱抱##搜狐畅游##游戏引擎##游戏#
全部评论

相关推荐

04-12 13:42
江南大学 C++
点赞 评论 收藏
分享
05-09 13:22
门头沟学院 Java
点赞 评论 收藏
分享
评论
6
4
分享

创作者周榜

更多
牛客网
牛客企业服务