专栏首页循迹漫聊虚幻引擎UE热更新:Shader更新策略

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 code library机制的内容。在材质资源中有一个属性bShareCode标记当前的Shader是存在于Shader Library中还是Inline:

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):

bool FShaderCodeLibrary::IsEnabled()
{
	return FShaderCodeLibraryImpl::Impl != nullptr;
}

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

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

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

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

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运行时):

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

[ShaderCodeLibrary]
bEnableInliningWorkaround_Windows=True
+MaterialToInline=/Game/StarterContent/Materials/M_Brick_Clay_Beveled

在执行Cook时有ShouldInlineShaderCode的检测:

实现强制inline。

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:

在代码中也做了封装:

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的方式导致包体增大的问题,是一种结合了两者优点的方案。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • UE热更新:Create Shader Patch

    之前的热更新系列文章中介绍了UE热更新的流程和打包细节,其实有一些热更补丁优化的工程实践我觉得也可以详细介绍。

    查利鹏
  • UE热更新:Questions & Answers

    HotPatcher项目开源这一年多以来,经过了不少的更新和优化,也被越来越多的开发者选择作为自己项目的热更新方案,期间有不少人陆陆续续询问UE4热更新相关遇到...

    查利鹏
  • UE热更新:拆分基础包

    在之前的几篇文章中,分别介绍了UE热更新的实现机制,以及热更的自动化流程,近期打算继续写几篇文章介绍下UE里热更新中资源包管理的流程和规则。

    查利鹏
  • 缓存更新策略

    问题:项目中,Redis用了缓存热点数据,持久化数据在MySQL DB中;那么Redis缓存数据什么时候更新呢?

    awk
  • UE热更新:Config的重载与应用

    在UE引擎中有大量的配置使用ini来进行设置与控制。对于项目而言,了解其中哪些是能够更新的,能够对制定项目的更新内容规则有帮助。并且,UE很多功能都是通过CVa...

    查利鹏
  • App更新策略课程-检查更新实现

    用户1130025
  • App更新策略课程-实现进度更新

    用户1130025
  • UE热更新:基于UnLua的Lua编程指南

    UE使用的是C++这种编译型语言,在编译之后就成了二进制,只有通过玩家重新安装才能打到更新游戏的目的。但是对于游戏业务而言,对于需求调整和bug修复时间要求非常...

    查利鹏
  • UE热更新:资产管理与审计工具

    在前面的文章中,介绍了基础包的拆分规则和实现,在基础的打包规则稳定之后,日常开发中的关注重点就转向侧重于项目的资产管理和包体资源审计、分析项目中的资产大小和冗余...

    查利鹏
  • UE热更新:基于HotPatcher的自动化流程

    HotPatcher是我之前开源的一个UE4热更新版本管理和资源打包工具,可以方便地进行版本间的差异分析和pak打包。之前的文章为了直观地介绍都是基于手动地在编...

    查利鹏
  • 热更新

    什么是热更新? 不停机更新,实时更新。HotUpdateHotFix Unity中需要APP重启 真正热更新不重启就做更新

    祝你万事顺利
  • App更新策略课程-定义后台更新服务

    用户1130025
  • UE性能分析:内存优化

    在开发游戏时,程序性能是需要着重考虑的问题,因为要尽可能覆盖最多的用户群体,就要考虑那些中低端设备的运行效果,兼容非常多配置差异的硬件,在这种情况下,怎么样分析...

    查利鹏
  • UE热更新:资源的二进制补丁方案

    先前介绍了一系列UE中热更新工程实践的文章,能够实现基于原始工程的资源的版本比对与差异更新。但默认情况下资源的更新是基于文件的更新,某个资源产生了变动,就要将该...

    查利鹏
  • UE资源热更打包工具HotPatcher

    HotPatcher是一个用于管理热更版本和资源打包工具,用于追踪工程版本的原始资源变动来打出Patch。支持一键Cook多平台,一键打包多平台Patch,编辑...

    查利鹏
  • App更新策略课程介绍

    用户1130025
  • 【Concent杂谈】精确更新策略

    一晃就到2020年了,时间过得真的是飞快,伴随着q群一些热心小伙伴的反馈和我个人实际的业务落地场景,Concent已进入一个非常稳定的运行阶段了,在此开年之际,...

    腾讯新闻前端团队
  • App更新策略课程-实现通知栏进度更新

    用户1130025
  • Unity3d热更新(一):更新思路

    py3study

扫码关注云+社区

领取腾讯云代金券