在之前的一些文章中,介绍了UE热更新丢失Shader使用默认材质的处理问题,以及Shader Patch的方案。
详情可见文章:
其实运行时丢失材质的根本原因是新添加或修改的资源所依赖的Shader没有被打包,运行时读取失败导致的错误。本篇文章来介绍一下Shader更新的策略和优缺点,以及对引擎内机制的分析,并提供一个结合Shader Patch与Inline Shader Code优点的优化方案,已经在HotPacther中实现。
UE在Cook时有两种Shader的序列化策略:
- Share Material Shader Code:会把项目所需的Shader单独的序列化为Shader Code Library文件(
ushaderbytecode
或metailib
),能够降低包体大小,避免Shader的冗余,读取shader时会从shaderbytecode中查找。 - Inline Shader Code:将Shader序列化至材质uasset,避免管理Shader Code Library,每个材质在Cook之后它本身就包含了所需的shader,整个包内具有冗余,会增加包体的大小。
在新建工程的默认项目配置中,Project Settings
-Project
-Packaging
-Shadre Material Shader Code
为关闭状态。
但是为了降低包体的大小,通常在打包时会开启,打出来的包就包含了ushaderbytecode:
但是,如果我们后续更新了材质,没有更新包内变动之后的shader,就会导致前文中介绍的材质丢失的问题。
所以材质丢失是有两个前提决定的:
- 开启了Share Material Shader Code
- 更新材质后没有更新ushaderbytecode库
根据这两个前提,可以制定Shader更新的策略:
- 每次更新之后就更新最新的Shade Code Library
- 对更新的材质实现inline Shader Code的cook
但是,这两个策略又引出了各自的问题:
Shader Code Library:
- 完整的Shader Code Library太过巨大
- Shader Patch需要管理额外的Metadata文件
- 每次Patch必须基于完整的shadebytecode
Inline Shader Code:
- UE默认Cook Commandlet没有提供Cook单个资源的方法
- Inline Shader Code会增大包体
Shader Patch
在UE里进行Shader Patch的流程,我之前已经写了一篇文章:UE热更新:Create Shader Patch。
这里讲一些上篇文章中没提到的创建Shader code library机制的内容。在材质资源中有一个属性bShareCode
标记当前的Shader是存在于Shader Library中还是Inline:
1 | bool FShaderMapBase::Serialize(FArchive& Ar, bool bInlineShaderResources, bool bLoadedByCookedMaterial, bool bInlineShaderCode) |
FShaderLibraryCooker::IsShaderLibraryEnabled
的实现(在UE4.26之前是FShaderCodeLibrary::IsEnabled
):
1 | bool FShaderCodeLibrary::IsEnabled() |
而FShaderCodeLibraryImpl::Impl
的创建在以下两个函数中:
1 | // for Cooking |
其实是分了两个阶段:Cook存储时和读取时,都需要获取到当前资源依赖的shader是存储在shader libraray中还是inline了。
默认情况下,这两个函数在启动编辑器时都不会被执行,因为毕竟理论上他们一个是在打包时一个是在非Editor运行时才被需要,他们分别被用在Cook时:
1 | void UCookOnTheFlyServer::InitShaderCodeLibrary(void) |
以及FEngineLoop::PreInitPreStartupScreen
中(Non-Editor运行时):
并且,对于开启了Share Shader Library的情况,UE也提供了一个机制(4.26+),可以强制制定某些资源的Shader是Inline的,使用一下方式配置:
1 | [ShaderCodeLibrary] |
在执行Cook时有ShouldInlineShaderCode
的检测:
实现强制inline。
Inline Shader Code
在上一节的介绍中,已经提到了在Cook时是否将Shader序列化到Shader Library的前提条件:FShaderCodeLibrary::IsEnabled
(4.25-)或FShaderLibraryCooker::IsShaderLibraryEnabled
(4.26+),想要实现Inline Shader Code的方法:
- UE的CookCommandlet,在项目设置中关闭
bShareMaterialShaderCode
- 自己实现Cook流程,不执行
FShaderLibraryCooker::InitForCooking
对于第一种方式,每次想要Cook Inline的时候,都需要手动去修改项目设置,较为不便,所以推荐第二个方式。但是UE没有提供Cook单个资源的接口,我在HotPacther中实现了Cook单个资源的方式,支持编辑器中选中资源、目录,以及在Patch中对资源进行Cook:
在代码中也做了封装:
1 | static bool CookPackage( |
使用这个接口就能实现在编辑器中的Cook,当Cook资源是材质时,会使用Inline Shader Code的方式序列化。
Better Way
前面已经提到了,使用Shader Patch和Inline Shader Code的方式各有各的优缺点:
- Shader Patch需要完整Cook,对两份完整的Shader code做Patch,项目巨大时执行效率不高,并且有额外的管理成本。
- Inline Shader Code:无需管理任何额外文件,缺点是无法共用shader,具有冗余。
所以,我为HotPacther提供一个结合两者优点的方案:
- Cook时依然开启Share Shader Code Library
- 仅把当前Patch中Cook资源时所有编译的Shader收集起来,生成单独的Shader Library
这样不用再手动执行Shader Patch,也不用担心Inline Shader Code的方式导致包体增大的问题,是一种结合了两者优点的方案。
得益于HotPatcher的流程机制,能够在插件的Cook阶段中把当前平台所有Cook资源的shader收集起来(每个打包的平台可能会具有多种shader格式,如Android的GLSL_ES
或Vulkan
)。
在所有的资源都Cook完毕之后,采集到了本次Cook所有Shader,将其生成Shader Code Library并直接打包至pak中。是一种无需进行新旧版本patch的增量shader方案,所以,也无需完整的Cook,更无需管理Shader code的版本。
所有的功能在HotPatcher中只需简单地开关即可实现:
若不开启bSharedShaderLibrary
,则当前Patch中的Cook的shader全部是inline shader code。反之Shader则是Shared的,降低Shader的冗余。
还有一些其他的选项:
bNativeShader
:与项目设置中SharedMaterialNativeLibraries
相同,开启之后IOS平台会编译成metallibShaderNameRule
:为当前Patch生产的Shader Library的命名,提供了三种模式:当前Patch的版本号、项目名、自定义ShaderLibMountPoint
则是将shader library打包至Pak中所存储的路径,在运行时加载Shader Library需要指定。
注意:打包时默认具有
Global
和项目名的Shader Library,注意不要有重名。
在插件中开启bSharedShaderLibrary
后,Windows和Android会生成ushaderbytecode,并打包至Pak中:
IOS如果开启bNativeShader,则会编译出metallib和metalmap:
由于IOS的Native Shader的加载机制,不能把metallib和metalmap打包到pak中,不能通过UFS,需要直接从磁盘加载。
为Patch打包出的Shader Library,需要在Mount之后调用加载才能使用,我在插件中同样有封装函数:
1 | bool UFlibPatchParserHelper::LoadShaderbytecode(const FString& LibraryName, const FString& LibraryDir); |
这里需要传递的参数,就是ShaderNameRule
中的规则,如果是版本号,就传递版本号的字符串,第二个参数就是Shader Library在Pak中的路径,传递ShaderLibMountPoint
值即可。
如果开启了Shared Shader Libraty,但是只进行了Mount,没有LoadShaderbytecode,同样会出现材质丢失的效果。
Cook Shader Format
HotPatcher在Cook阶段,会使用项目设置中配置的各个平台的Shader格式进行Cook,每个平台可以具有多种Shader Format,这里以最常用的Windows/Android/IOS三个平台为例。
Windows
Window打包时是获取GEngineIni
下的TargetRHIs
,默认是PCD3D_SM5
,也可以添加ES3.1
:
1 | [/Script/WindowsTargetPlatform.WindowsTargetSettings] |
引擎中Windows获取Shader Formats的相关代码:GenericWindowsTargetPlatform.h#L229
Android
Android是也是在引擎中配置的(UE在4.25及之后的版本上舍弃了ES2的支持):
引擎中Android获取Shader Formats的相关代码:AndroidTargetPlatform.cpp#L390
IOS
IOS则是Metal,但是否编译为Native,项目设置和HotPatcher中都有bNativeShader
的选项,并且也要根据是否在Mac上编译或者Win上具有Metal Compiler(4.26+)。
注意:引擎中访问Windows Metal Toolchin的路径为
C:\Program Files\Metal Developer Tools
,默认安装会在这个位置,若不在这个路径下,就需要在Project Settings-IOS-Build
下修改Override location of Metal Toolchain
。
在HotPatcher
中勾上bNativeShader
就会在支持的环境上编译出metallic/metalmap。
引擎中IOS平台获取Shader Formats的相关代码:IOSTargetPlatform.cpp#L581
Native Metal Shader
之前笔记中提到,苹果推出了Windows上的 Metal Shader Compiler,可以在Windows上编译出Metal Native Shader:Windows Metal Shader Compiler for IOS,但是UE在4.26之后才提供了引擎层面的支持,在4.25及之前的引擎版本中,metal只能通过Mac来打包,使用Windows远程构建就只能使用Text Shader,也就是ushaderbytecode,性能低于Native,在运行时有以下log:
1 | LogMetal: Display: Loaded a text shader (will be slower to load) |
引擎中的代码:Engine/Source/Runtime/Apple/MetalRHI/Private/Shaders/Types/Templates/MetalBaseShader.h
在支持Compile Native Metal的环境上(Mac/Win),HotPatcher也支持了对Patch中资源的Shader编译Native格式。
虽然项目设置中有打包时启用Native Shader的选项,但是这个开关并不会对打包后运行时的代码有任何影响。不管基础包是否开启了Native,在运行时三种shader的更新方式都可以:
- ushaderbytecode
- inline shader code
- Native(metallib/metalmap)
对于调用OpenLibrary
的加载,在Metal平台会优先加载Native的,若加载失败才会加载ushaderbytecode。
- 尝试加载metalmap,若加载失败,则
FMetalDynamicRHI::RHICreateShaderLibrary
返回nullptr (Apple\MetalRHI\Private\MetalShaders.cpp
) - 加载metalmap失败,则加载ushaderbytecode
1 | class FShaderLibraryInstance |
所以,不管基础包内的shader library是何种类型,这Shader的更新方式都可以用。
加载Native Metal Shader有一个需要注意的问题,
metallib
/metalmap
文件名必须全小写,不然会加载失败。通过HotPatcher打包的Native Shader Library已做处理。
我创建了一个测试环境,不管是Native/ushaderbytecode,都能够正常加载:
End
本文研究了UE中热更Shader几种不同方式的优缺点,并基于HotPacther实现了无需Patch的Shader增量更新,可以完美地替代Shader Patch和Inline Shader Code的方案。
根据该功能,可以实现某个地图或者某种模块依赖的shader全在独立的包中,这样能够让shader也解耦,完全可以按需下载而不用完整的存在基础包中。