问答 Shader 变体使用策略

侑虎科技 · 2020年06月03日 · 1677 次阅读

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

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问答社区提供了回答


加载

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

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


我这有 2 个 AssetBundle, 一个名叫 aa, 一个名叫 bb。aa 会有这个问题,而 bb 没有。

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

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

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

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

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

该回答由 UWA 提供


内存

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问答社区提供了回答


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问答社区提供了回答


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问答社区提供了回答


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

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册