UE热更新:Shader更新策略

在之前的一些文章中,介绍了UE热更新丢失Shader使用默认材质的处理问题,详情可见:UE热更新:Questions & Answers#热更的资源没有效果/材质丢失

其实运行时丢失材质的根本原因是新添加或修改的资源所依赖的Shader没有被打包,运行时读取失败导致的错误。本篇文章来介绍一下Shader更新的策略和优缺点,以及对引擎内机制的分析,并提供一个结合优点的优化思路,后续准备在HotPacther中实现。

UE在Cook时有两种Shader的序列化策略:

  1. Share Material Shader Code:会把项目所需的Shader单独的序列化为Shader Code Library文件(ushaderbytecodemetailib),能够降低包体大小,避免Shader的冗余,读取shader时会从shaderbytecode中查找。
  2. Inline Shader Code:将Shader序列化至材质uasset,避免管理Shader Code Library,每个材质在Cook之后它本身就包含了所需的shader,整个包内具有冗余,会增加包体的大小。

在新建工程的默认项目配置中,Project Settings-Project-Packaging-Shadre Material Shader Code为关闭状态。

但是为了降低包体的大小,通常在打包时会开启,打出来的包就包含了ushaderbytecode:

但是,如果我们后续更新了材质,没有更新包内变动之后的shader,就会导致前文中介绍的材质丢失的问题。
所以材质丢失是有两个前提决定的:

  1. 开启了Share Material Shader Code
  2. 更新材质后没有更新ushaderbytecode库

根据这两个前提,可以制定Shader更新的策略:

  1. 每次更新之后就更新最新的Shade Code Library
  2. 对更新的材质实现inline Shader Code的cook

但是,这两个策略又引出了各自的问题:

Shader Code Library:

  1. 完整的Shader Code Library太过巨大
  2. Shader Patch需要管理额外的Metadata文件
  3. 每次Patch必须基于完整的shadebytecode

Inline Shader Code:

  1. UE默认Cook Commandlet没有提供Cook单个资源的方法
  2. Inline Shader Code会增大包体

Shader Patch

在UE里进行Shader Patch的流程,我之前已经写了一篇文章:UE热更新:Create Shader Patch

Shader Patch

这里讲一些上篇文章中没提到的创建Shader code library机制的内容。在材质资源中有一个属性bShareCode标记当前的Shader是存在于Shader Library中还是Inline:

1
2
3
4
5
6
7
8
9
10
bool FShaderMapBase::Serialize(FArchive& Ar, bool bInlineShaderResources, bool bLoadedByCookedMaterial, bool bInlineShaderCode)
{
// ...
bool bShareCode = false;
#if WITH_EDITOR
bShareCode = !bInlineShaderCode && FShaderLibraryCooker::IsShaderLibraryEnabled() && Ar.IsCooking();
#endif // WITH_EDITOR
Ar << bShareCode;
// ...
}

FShaderLibraryCooker::IsShaderLibraryEnabled的实现(在UE4.26之前是FShaderCodeLibrary::IsEnabled):

ShaderCodeLibrary.cpp
1
2
3
4
bool FShaderCodeLibrary::IsEnabled()
{
return FShaderCodeLibraryImpl::Impl != nullptr;
}

FShaderCodeLibraryImpl::Impl的创建在以下两个函数中:

1
2
3
4
// for Cooking
void FShaderCodeLibrary::InitForCooking(bool bNativeFormat);
// for Runtime
void FShaderCodeLibrary::InitForRuntime(EShaderPlatform ShaderPlatform);

其实是分了两个阶段:Cook存储时和读取时,都需要获取到当前资源依赖的shader是存储在shader libraray中还是inline了。

默认情况下,这两个函数在启动编辑器时都不会被执行,因为毕竟理论上他们一个是在打包时一个是在非Editor运行时才被需要,他们分别被用在Cook时:

Editor/UnrealEd/Private/CookOnTheFlyServer.cpp
1
2
3
4
5
6
7
8
9
10
11
void UCookOnTheFlyServer::InitShaderCodeLibrary(void)
{
const UProjectPackagingSettings* const PackagingSettings = GetDefault<UProjectPackagingSettings>();
bool const bCacheShaderLibraries = IsUsingShaderCodeLibrary();
if (bCacheShaderLibraries && PackagingSettings->bShareMaterialShaderCode)
{
FShaderLibraryCooker::InitForCooking(PackagingSettings->bSharedMaterialNativeLibraries);
// ...
}
// ...
}

以及FEngineLoop::PreInitPreStartupScreen中(Non-Editor运行时):
FShaderCodeLibrary::InitForRuntime

并且,对于开启了Share Shader Library的情况,UE也提供了一个机制(4.26+),可以强制制定某些资源的Shader是Inline的,使用一下方式配置:

DefaultEngine.ini
1
2
3
[ShaderCodeLibrary]
bEnableInliningWorkaround_Windows=True
+MaterialToInline=/Game/StarterContent/Materials/M_Brick_Clay_Beveled

在执行Cook时有ShouldInlineShaderCode的检测:

实现强制inline。

Inline Shader Code

Inline Shader Code

在上一节的介绍中,已经提到了在Cook时是否将Shader序列化到Shader Library的前提条件:FShaderCodeLibrary::IsEnabled(4.25-)或FShaderLibraryCooker::IsShaderLibraryEnabled(4.26+),想要实现Inline Shader Code的方法:

  1. UE的CookCommandlet,在项目设置中关闭bShareMaterialShaderCode
  2. 自己实现Cook流程,不执行FShaderLibraryCooker::InitForCooking

对于第一种方式,每次想要Cook Inline的时候,都需要手动去修改项目设置,较为不便,所以推荐第二个方式。但是UE没有提供Cook单个资源的接口,我在HotPacther中实现了Cook单个资源的方式,支持编辑器中选中资源、目录,以及在Patch中对资源进行Cook:

在代码中也做了封装:

FlibHotPactherEditorHelper.h
1
2
3
4
5
6
7
static bool CookPackage(
const FAssetData& AssetData,
UPackage* Package,
const TArray<FString>& Platforms,
TFunction<void(const FString&)> PackageSavedCallback = [](const FString&){},
class TMap<FString,FSavePackageContext*> PlatformSavePackageContext = TMap<FString,FSavePackageContext*>{}
);

使用这个接口就能实现在编辑器中的Cook,当Cook资源是材质时,会使用Inline Shader Code的方式序列化。

更好的方式

前面已经提到了,使用Shader Patch和Inline Shader Code的方式各有各的优缺点。所以,我准备后续为HotPacther提供一个结合两者优点的方案:

  1. Cook时依然开启Share Shader Code Library
  2. 仅把当前Patch中Cook资源时所有编译的Shader收集起来,存储为单独的Shader Library

即只把Patch中编译的Shader也序列化为Shader Code Library,这样不用再手动执行Shader Patch,也不用担心Inline Shader Code的方式导致包体增大的问题,是一种结合了两者优点的方案。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:UE热更新:Shader更新策略
文章作者:查利鹏
发布时间:2021年09月13日 10时24分
本文字数:本文一共有2.5k字
原始链接:https://imzlp.com/posts/15810/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!