Shader变体使用策略

Shader变体使用策略

1)Shader变体使用策略
2)AssetBundle产生的SerializedFile卸载不干净
3)如何优化LWRP下产生的大量RenderTexture
4)场景的灯光保存在Prefab中,烘焙参数丢失
5)如何跳过Shader中某个Pass不执行

这是第180篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题,建议阅读时间10分钟,认真读完必有收获。

UWA 问答社区:answer.uwa4d.com
UWA QQ群2:793972859(原群已满员)


Shader

Q:最近在做项目的Shader变体收集,以及Shader打包编译优化相关的内容,我想在此把我实现的步骤尽可能地描述清楚,望大家帮忙指点一二。

整个模块分了三部分:

  • 工程Shader变体的收集;
  • 把Unity搜集到的变体拆分保存为独立文件,一个Shader对应一个变体集资源,利于打包和后续使用;
  • 使用Unity 2018的IPreprocessShaders接口,来优化Shader编译时间,这部分还比较新,我感觉自己使用的方法有点歪了。

在实现的过程中我遇到下面这些问题:

  • ShaderVariantCollection,Unity引入这个东西的实际用意,从官方文档解释来看,主要目的是为了提高预加载的速度,和打包以及编译关系不大,江湖上流传的变体打包方法似乎都没有提及;
  • 用了一些并没有开放的方法来获取变体信息,或许有更好的方法望告知;
  • 在变体集的创建过程中,缺失一些信息,我只是简单粗略地做了一些假设;
  • Unity搜集下来的Shader变体集,并没有区分UsePass、Fallback这类被引用进来的Shader(这些Shader没有出现在工程的任何材质上,故无法搜集),导致接下来的依赖打包会出现变体丢失的风险;
  • 新的IPreprocessShaders接口,Unity提出这个更多地是为了配合SRP。我对它的使用方法,是我拍脑袋想出来的,实际效果似乎也有,但没有特别明显。

思路的详细描述以及可用的部分代码在这里:
https://github.com/lujian101/ShaderVariantCollector

A:这块之前关注过一些讨论,也做过一点工作,不是很深入,简单聊一下,仅供参考。

关于题主的几个问题:

1、在设备上Shader的加载其实并不慢,通常慢在编译上,也就是Warmup做的事情。因为不同设备GPU以及驱动不一样,因此手游上的Shader是没办法在打包的时候编译的,这就需要到设备上进行编译。(说到这个,我其实也不是很清楚在打包过程中观察到的Shader编译是在干嘛。大约是把Unity自己的Shader格式转换成目标平台的Shader格式,比如:GLSL等,这也的确是一种编译。)ShaderVariantCollection要能够有针对性地提预热(提前编译)Shader,自然是要在打包的时候根据需求收集可能用到的变体,这就和打包有了关系。至于编译,就像前面所说,打包时需要转换,到设备上才真正编译Shader。

2、暂时我也不知道更开放的方式来获取变体信息,编辑器下用些反射方法属性什么的都正常,毕竟没有源码。

3、其实要做完整收集挺难的,除了你文章中列举的那些坑之外,高中低配置导致代码来动态更改的宏同样需要收集对应的变体,对于这部分,也只能做一些假设,如果一定要预热所有可能,还要把它们加入到变体列表中,这也可能导致你Warmup的内容其实并不是本次运行一定要的。

4、Fallback的Shader应该是可以正确收集的,至于对应的变体我还真没注意过。话说后来我们项目主要的Shader都不使用Fallback,或者Fallback到我们自己的一个默认简单Shader中,这样会好办一些,大不了这个Shader就AlwaysInclude都可以。

5、新的接口还真没用过,想来这个的确可以用来做一些事情。在优化Shader编译时间方面,大的多变体Shader在打包的时候的确有很长时间的编译,比如:Standard,但感觉Unity默认应该就会根据需要的进行处理,比如:Standard Shade,如果你放置到AlwaysInclude下,你一次打包要编译超久(1个小时甚至更多),不放置在AlwaysInclude下只自己打包,编译时间就不会很长,说明这里也是按需编译的。不过这只是我个人依据经验的猜想,未必正确,你可以找一个复杂Shader做下实验对比,看看能够优化到什么程度。

说到底,想要完美地收集所有可能用到的Shader其实未必是一件能够实现的事情,我们最初也想做到运行时没有CreateGPUProgram的消耗,但是最后发现还是有很多妥协,比如:高低配对应的Shader如果都做预热,就会有很多无效的消耗,而不做高低配的预热,玩家切换配置或者某些低配模式显示角色需要高配效果的时候就会有卡顿。

因此最终的效果还是要经过真正项目的验证,根据Profile统计的结果进行权衡和迭代,才能得到和这个项目匹配的最佳答案。

(由于篇幅限制,更多精彩回答请戳以下链接查看。)

感谢贾伟昊@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5da86670e84db43d6efbda72


加载

Q:很简单的一个问题,只是简单地按A键加载一个AssetBundle,按B键卸载一个AssetBundle。

但是Serialized Files为什么卸不干净?这是卸载前后对比:


我这有2个AssetBundle, 一个名叫aa, 一个名叫bb。aa会有这个问题,而bb没有。
(例子工程请戳原问答获取。)

A:确实是打包为应用程序之后,还是没有卸载掉。

使用UWA的资源检测与分析得到结果如图:

因为有依赖关系,我尝试了一下同时加载aa和bb,并同时卸载。保证引用完整没有缺失。发现它全部都卸载掉了。

综上,猜测也许是有依赖关系的包缺失导致的?

题主可以尝试:
1、换一个有依赖关系的包,看看会不会发生同样的结果。
2、会不会是Sprite打包时较为特殊,尝试不用包含Sprite的有依赖关系的包尝试。
3、我记得原来的版本是不管这种缺失的,可以同样的包,用比较老的版本再试一下。

该回答由UWA提供,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5d833859f6bfcf23aea6a907


内存

Q:使用了LWRP与Unity Postprocessing,内存中有大量的RenderTexture,这些占用内存达100多MB,如下图:

想知道如何优化这部分内存占用?有没有实在的方案或者方向?

A1:Bloom应该可以降下分辨率而且也可以减少采样次数(不需要上下采样5张),还有就是减少OnRenderImage的函数,这个可以减少TempBuffer或者ImageEffects的贴图。大头其实还是你的_Camera 使用的,这个如果能减少或者降分辨率,你的内存就下来了。
感谢李星@UWA问答社区提供了回答

A2:首先LWRP会默认blit出每帧的Color Buffer和Depth Buffer,用于扭曲、折射等效果,如果项目中没有这样的需要可以关闭,可以节省掉_CameraColorTexture和_CameraDepthTexture的内存,这个在摄像机上就有选项可以关。

其次是RenderTexture格式的问题,截图的RenderTexture格式为ARGB Half,如果没有在A通道存储内容的需求,可以把RenderTexture的格式换成R11G11B10,RenderTexture内存可以减半,同时仍然可以支持HDR。

最后是Bloom,Diffusion这个参数调小可以减少迭代次数,减少RenderTexture的数量。也可以改一下Bloom的代码,把第一次Bloom降采样的分辨率降为屏幕原分辨率的1/4,也可以减小Bloom RenderTexture的内存。

感谢王阳@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5d91b136a27d4e20a003aec1


Lightmap

Q:项目使用subtractive模式烘焙GI,烘焙好之后,把部分场景保存成Prefab供动态加载,Light也一起放到了Prefab里,发现的现象是运行时Mixed的Light对静态物体生效了。排查下来,只要把场景里的灯拖出来做成Prefab,灯光的bakingOutput属性就被重置掉了。

在场景里的Light属性(需要开Inspector的debug):

拖出来做成Prefab后的Light属性:

现在通过用脚本保存灯光的bakingOutput属性运行时再赋值过去来解决。但是没有从原理上理解原因。官方文档上也没有这样的描述。

请问各位有没有了解过为什么会这样?具体是哪种操作可能导致这种情况。
(引擎版本是Unity 2018.4.0f1)

A:不是很清楚具体原理,但是可以做这样的推测:

1、Baking Output信息应当是场景在烘焙的时候设置的,用于记录这个光源有没有进行过烘焙,以及一些烘焙参数,这样在渲染的时候决定哪些光源影响哪些物体才有依据;

2、烘焙信息是跟着场景走的,如果一个物体或者光源和场景没有关联了,那它身上存储的烘焙信息也就没有意义了;

3、因此,在一个物体或者光源从场景中变成一个Prefab的时候,那些和烘焙相关的信息被重置,是一个稳妥的做法,因为你做成一个Prefab,意味着它可能被使用于不同场景,那残留的之前的烘焙信息也就没有意义了。反过来说,如果保留,你在别的场景中apply了这些信息,那其它场景的效果就错了。无法通用的信息,保留在Prefab中,是危险的。

4、对于被场景物件也是一样的。烘焙之后的场景物件,拖拽成一个Prefab,然后你扔回场景里,烘焙信息依然是没有的,它们对于Lightmap的信息是不同的,如下图所示:

另外一点猜测是,场景存储一个光源是否被烘焙过,可能使用的是Local Identifier In File,你可以观察下光源如果变成Prefab,这个就变了(这是当然的,因为都不在一个File里了),但是你开关场景,Light的Instance ID会改变,但是这个Local Identifier In File是不会改变的。


猜测是否正确,还要看源码。

感谢贾伟昊@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5d9f2cb039b494208a46512a


Rendering

Q:我想要的是跳过某个Pass不执行,后面自己用CommandBuffer执行特定Pass,目前卡在了多个Pass都执行了,谢谢。

A:暂时还不知道特别好的方法,常规的做法就是编写两个Shader,然后在它们之间切换。避免冗余,把公共函数写到cginc文件里,或者使用UsePass直接指定Pass。

Unity 5.6之后有这么一个接口:Material.SetShaderPassEnabled,但是这个接口不像名字上说得那么美好,它只能通过LightMode来开关对应Pass。

参考:
https://forum.unity.com/threads/ignore-a-shader-pass-under-certain-conditions-variable-value.232817/

https://forum.unity.com/threads/5-6-how-to-use-material-setshaderpassenabled.466532/

或者写个宏控制Pass内部的代码开关,但DrawCall省不掉,对于性能最理想的应该还是切换Shader。如果用SRP,自己写渲染流程,也许可以直接控制Pass渲染,这块没实践过,不过想来也比较复杂,需要用指定Pass逐个渲染才能通过判断条件跳过某些Pass,而不能直接用一个Shader绘制了。这样看来,似乎还是切换Shader好些。

感谢贾伟昊@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5da4886a743afa3d3cc90558

封面图来源:UnityShaderLibrary


今天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题也许都只是冰山一角,我们早已在UWA问答网站上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入,也许你的方法恰能解别人的燃眉之急;而他山之“石”,也能攻你之“玉”。

官网:www.uwa4d.com
官方技术博客:blog.uwa4d.com
官方问答社区:answer.uwa4d.com
UWA学堂:edu.uwa4d.com
官方技术QQ群:793972859(原群已满员)