资源Cook构建
默认情况下,在 UE 中打包项目时,会拉起 BuildCookRun 来执行 Compile/Cook/Pak/Stage 等一系列流程。
在 UE 中,只有参与 Cook 的资源才会被打包
可以实现自定义的 Cook 过程,将 Cook 任务分配至不同的进程乃至机器实现并行化,加速 UE 的构建过程。
留意一下 Shader Library 和 AssetRegistry 是否存在 排序问题
- 初步验证 Shader Library 、 AssetRegistry 存在多次构建不同的情况
UE 参与 Cook 的资源,从逻辑上大概可以分为这个类别:
项目设置中的关键必要资源
- StartupMap
- GameMode
- GameInstance
- DefaultTouchInterface
默认打包的几个 UI 目录
哪几个? 为什么需要? 可以裁剪吗?
引擎启动时通过代码加载的资源
eg. UClass的CDO
1
static ConstructorHelpers::FObjectFinder<UMaterialInterface> NormalMaterialRef(TEXT("/Game/Assets/UI/spineplugin/UI_SpineUnlitNormalMaterial"));
这些资源在运行时有什么用?
可以裁剪吗? => 从合理性上,不建议裁剪,可能引发 UClass的逻辑错误引发异常
项目设置中配置的 Cook 资源(包括
Directory to Alway Cook
、PrimaryAssetLabel
等标记的要进行 Cook 的资源)通过执行
FGameDelegates::Get().GetCookModificationDelegate()
传递给 CookOnTheFlyServer 的资源。在一定条件下若没有指定资源,则分析项目、插件目录下的资源
分析规则是什么?会导致多打包吗?怎么裁剪?
本地化资源(UE 支持给不同的 Culture 使用不同的资源,不过不常用)
CookCommandlet
引擎中提供了 UCookCommandlet,来实现资源的 Cook,在打包流程中,它由 UAT 拉起。默认的 Cook 命令如下:
1
2
3
4
5
6
7
8
9
10
11
12
D:/UnrealEngine/Engine/Engine/Binaries/Win64/UE4Editor-Cmd.exe
D:/UnrealProjects/Blank425/Blank425.uproject
-run=Cook
-TargetPlatform=WindowsNoEditor
-fileopenlog
-unversioned
-abslog=D:/UnrealEngine/Engine/Engine/Programs/AutomationTool/Saved/Cook-2021.12.07-15.47.10.txt
-stdout
-CrashForUAT
-unattended
-NoLogTimes
-UTF8Output
它会执行到 UCookCommandlet
的 main 中:
Source\Editor\UnrealEd\Private\Commandlets\CookCommandlet.cpp
1
2
3
4
5
6
7
8
9
10
/* UCommandlet interface
*****************************************************************************/
int32 UCookCommandlet::Main(const FString& CmdLineParams)
{
COOK_STAT(double CookStartTime = FPlatformTime::Seconds());
Params = CmdLineParams;
ParseCommandLine(*Params, Tokens, Switches);
// ...
}
它在经过一些参数检测后会将执行流程传递到 CookByTheBook
,创建出 CookOnTheFlyServer
,并调用 StartCookByTheBook
。
引擎打包时的资源都会在 CookOnTheFlyServer 中被 Cook,并生成 Shader 和 AssetRegistry,可以说 CookOnTheFlyServer 就是 UE 打包过程中将在编辑器中通用格式的 uasset,序列化为平台格式的过程。
Cook步骤
UE 打包时加载资源的思路是:
先找到本地 uasset 文件,将路径转换为 PackageName,进行加载,在加载时会把依赖的资源也加载了,然后将其一起 Cook。
这是cook一个资源的分析流程还是一堆资源的? 重复的依赖项是怎么处理的?
这个加载的次序有保证吗?是否会导致两次构建的次序不一致
- 源码关键词 EInstigator
StartupPackages
在 CookOnTheFlyServer.cpp
的 UCookOnTheFlyServer::Initialize
中,将已经加载到内存中的资源添加到了 CookByTheBookOptions->StartupPackages
中:
这里是不是就会涉及到 垃圾资源了? 或者说 由于某些意外的代码导致 内存中包含了不需要的资源?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// void UCookOnTheFlyServer::Initialize( ECookMode::Type DesiredCookMode, ECookInitializationFlags InCookFlags, const FString &InOutputDirectoryOverride )
if (IsCookByTheBookMode())
{
CookByTheBookOptions = new FCookByTheBookOptions();
for (TObjectIterator<UPackage> It; It; ++It)
{
if ((*It) != GetTransientPackage())
{
CookByTheBookOptions->StartupPackages.Add(It->GetFName());
UE_LOG(LogCook, Verbose, TEXT("Cooker startup package %s"), *It->GetName());
}
}
bHybridIterativeDebug = FParse::Param(FCommandLine::Get(), TEXT("hybriditerativedebug"));
}
CookOnTheFlyServer 在后续的流程会将它们添加到 Cook 列表中,并会处理重定向器。
AllMaps
没有通过命令行指定任何地图的情况下会给 MapIniSections
添加 AllMaps
:
它是 DefaultEditor.ini
中的 Section
:
1
2
3
4
[AllMaps]
+Map=/Game/Maps/Login
+Map=/Game/Maps/LightSpeed
+Map=/Game/Maps/VFXTest
启动之前会全局编译一遍 GlobalShader:
\Engine\Source\Editor\UnrealEd\Private\CookOnTheFlyServer.cpp
什么是GlobalShader?全局编译的目的是什么? 能不能用按需编译?可以引入缓存机制吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void UCookOnTheFlyServer::SaveGlobalShaderMapFiles(const TArrayView<const ITargetPlatform* const>& Platforms)
{
// we don't support this behavior
check( !IsCookingDLC() );
for (int32 Index = 0; Index < Platforms.Num(); Index++)
{
TArray<FString> Files;
TArray<uint8> GlobalShaderMap;
// make sure global shaders are up to date!
FShaderRecompileData RecompileData(Platforms[Index]->PlatformName(), SP_NumPlatforms, ODSCRecompileCommand::Changed, &Files, nullptr, &GlobalShaderMap);
check( IsInGameThread() );
FString OutputDir = GetSandboxDirectory(RecompileData.PlatformName);
UE_LOG(LogCook, Display, TEXT("Checking global shaders for platform %s"), *RecompileData.PlatformName);
RecompileShadersForRemote(RecompileData, OutputDir);
}
}
通过 GRedirectCollector 获取资源:
UE5 的流程有所改动,需要跟一下断点
UI
默认情况下,UE 会将 Engine/Config/BaseEditor.ini#L271 中 ContentDirectories
下的目录添加到 Cook 列表中:
引擎中的默认配置如下,也可以修改 DefaultEditor.ini
添加其他的目录:
BaseEditor.ini
1 2 3 4 5 6 [UI] ; Directories specifying assets needed by Slate UI, assets in these directories are always cooked even if not referenced +ContentDirectories=/Game/UI +ContentDirectories=/Game/Widget +ContentDirectories=/Game/Widgets +ContentDirectories=/Engine/MobileResources
这些目录下的资源会被打包:CookOnTheFlyServer.cpp#L6217
CollectFilesToCook
1 2 3 4 5 6 7 { TArray<FString> UIContentPaths; if (GConfig->GetArray(TEXT("UI"), TEXT("ContentDirectories"), UIContentPaths, GEditorIni) > 0) { UE_LOG(LogCook, Warning, TEXT("The [UI]ContentDirectories is deprecated. You may use DirectoriesToAlwaysCook in your project settings instead.")); } }
Directory to Alway cook
Project Setgings
-Directory to Alway cook
DirectoriesToAlwaysCook
Maps
- AlwaysCookMaps CookOnTheFlyServer.cpp#L5223
- MapToCook CookOnTheFlyServer.cpp#L5253
DefaultGame.ini
1 2 [/Script/UnrealEd.ProjectPackagingSettings] +MapsToCook=(FilePath="/Game/HandheldAR/Maps/HandheldARBlankMap")
Never Cook CookOnTheFlyServer.cpp#L5317
AllMaps CookOnTheFlyServer.cpp#L5266
Cultures
这里代表的并不是多语言,而是针对不同的 Culture,可以支持使用不同的资源。Asset Localization
Cultures 资源的获取代码:CookOnTheFlyServer.cpp#L6714
如以下目录下的资源,并会递归子目录:
1 /Game/L10N/en/
DefaultTouchInterface
DefaultTouchInterface 是引擎中配置的虚拟摇杆类,它可能不会被其他的资源依赖,但也需要被打包,所以在 Cook 时会单独获取它:
1
2
3
4
5
6
7
8
9
10
FConfigFile InputIni;
FString InterfaceFile;
FConfigCacheIni::LoadLocalIniFile(InputIni, TEXT("Input"), true);
if (InputIni.GetString(TEXT("/Script/Engine.InputSettings"), TEXT("DefaultTouchInterface"), InterfaceFile))
{
if (InterfaceFile != TEXT("None") && InterfaceFile != TEXT(""))
{
SoftObjectPaths.Emplace(InterfaceFile);
}
}
GetCookModificationDelegate
绑定代理,可以传递给 CookCommandlet,要进行 Cook 的文件:
1
2
3
// allow the game to fill out the asset registry, as well as get a list of objects to always cook
TArray<FString> FilesInPathStrings;
FGameDelegates::Get().GetCookModificationDelegate().ExecuteIfBound(FilesInPathStrings);
注意,传递过来的需要是 **uasset 文件的绝对路径**,并不是
/Game/xxx
等资源路径
AssetManager
通过 UAssetManager::Get().ModifyCook
函数,来访问 PrimaryAssetTypeInfo(在项目设置中的配置、PrimaryAssetLabelId 等)。
如果命令行没有显式指定任何资源、以及从 FGameDelegates::Get().GetCookModificationDelegate()
、UAssetManager::Get().Modify
都没有获取到任何资源,则添加插件、项目所有的资源:
执行条件为:
DefaultEditor.ini
中的AlwaysCookMaps
为空,AllMaps
为空- 项目设置中
List of maps to include in a packaged build
为空 - 项目设置中
DirectoriesToAlwaysCook
为空 FGameDelegates::Get().GetCookModificationDelegate()
获取到的资源为空UAssetManager::Get().ModifyCook
获取到的资源为空
则会通过 NormailizePackageNames
获取 /Engine
、/Game
以及所有启用插件的 umap、uasset:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// If no packages were explicitly added by command line or game callback, add all maps
if (FilesInPath.Num() == InitialPackages.Num() || bCookAll)
{
TArray<FString> Tokens;
Tokens.Empty(2);
Tokens.Add(FString("*") + FPackageName::GetAssetPackageExtension());
Tokens.Add(FString("*") + FPackageName::GetMapPackageExtension());
uint8 PackageFilter = NORMALIZE_DefaultFlags | NORMALIZE_ExcludeEnginePackages | NORMALIZE_ExcludeLocalizedPackages;
if (bMapsOnly)
{
PackageFilter |= NORMALIZE_ExcludeContentPackages;
}
if (bNoDev)
{
PackageFilter |= NORMALIZE_ExcludeDeveloperPackages;
}
// assume the first token is the map wildcard/pathname
TArray<FString> Unused;
for (int32 TokenIndex = 0; TokenIndex < Tokens.Num(); TokenIndex++)
{
TArray<FString> TokenFiles;
if (!NormalizePackageNames(Unused, TokenFiles, Tokens[TokenIndex], PackageFilter))
{
UE_LOG(LogCook, Display, TEXT("No packages found for parameter %i: '%s'"), TokenIndex, *Tokens[TokenIndex]);
continue;
}
for (int32 TokenFileIndex = 0; TokenFileIndex < TokenFiles.Num(); ++TokenFileIndex)
{
AddFileToCook(FilesInPath, TokenFiles[TokenFileIndex]);
}
}
}
但默认添加了过滤器,会排除引擎目录的资源(/Engine
)以及本地化目录下的资源(/*/L10N/
)。
其实就是会添加项目、插件、除了 L10N
目录下的所有 uasset 和 umap。
收集打包的内容主要在 UCookOnTheFlyServer::CollectFilesToCook
这个函数中:CookOnTheFlyServer.cpp#L5200 中。
- 每个平台设置的地图、GameMode、GameInstance:CookOnTheFlyServer.cpp#L5450
- InputIni 中的
DefaultTouchInterface
:CookOnTheFlyServer.cpp#L5499
DefaultInput.ini
1 2 [/Script/Engine.InputSettings] DefaultTouchInterface=/Engine/MobileResources/HUD/LeftVirtualJoystickOnly.LeftVirtualJoystickOnly
Shader Cook流程
入口:UCookCommandlet::CookByTheBook
UCookOnTheFlyServer::StartCookByTheBook
初始化,构建任务队列
InitShaderCodeLibrary
FShaderCodeLibrary::InitForCooking
FShaderCodeLibraryImpl::FShaderCodeLibraryImpl
初始化ShaderCodeLibrary,这是一个静态实例
OpenShaderCodeLibrary(LibraryName)
FShaderCodeLibrary::OpenLibrary(LibraryName)
FShaderCodeLibraryImp::OpenLibrary(LibraryName)
对每个目标平台,调用 FEditorShaderCodeArchive::OpenLiabrary(LibraryName) 准备后续保存Shader
构建任务队列CookRequests
主循环
UCookOnTheFlyServer::TickCookOnTheSide
不断取出CookRequests 队列的材质,触发编译;
这里因为是异步,那么就会存在次序的问题
UMaterial::BeginCacheForCookedPlatformData
(见下文”材质编译”小节)
FMaterial::BeginCompileShaderMap
- 把Shader编译任务加入到GShaderCompilingManager的队列中,异步编译(见下文”编译触发”小节)
- 这个过程中会检查DDC,如果存在,直接从DDC中获取shader code
- 异步编译,使用HLSLCC调用各个平台对应的后端进行处理(见下文”编译进行”小节)
FShaderCompilingManager::ProcessAsyncResults
检查GShaderCompilingManager异步编译的结果(见下文”编译完成”小节)
FShaderCompilingManager::ProcessCompiledShaderMaps
把完成编译的Shader加入DDC
如果发现材质Cook完成,会通过SaveCookedPackages调用UMaterial::Serialize (见下文”材质序列化”小节)
FShaderResource::SerializeShaderCode
把完成编译的Shader加入ShaderCodeLibrary
所有Cook完成后,调用UCookOnTheFlyServer::SaveShaderCodeLibrary (见下文”ShaderLibrary序列化流程”小节)
FShaderCodeLibrary::SaveShaderCodeLibrary
序列化ShaderCodeLibrary
FShaderCodeLibrary::PackageNativeShaderLibrary
把ShaderCodeLibrary编译到native格式,仅iOS
FShaderCodeLibraryImp::PackageNativeShaderLibrary
FEditorShaderCodeArchive::PackageNativeShaderLibrary
IShaderFormatArchive::Finalize
最终序列化到Package中
材质Cook流程
分两个阶段:
- 编译阶段:CookServer的TickCookOnTheSide会不断触发材质Cook,材质Cook又会触发Shader编译。
- 序列化阶段:也发生在TickCookOnTheSide函数内,这时会取出Cook完成的材质进入序列化。
这两个阶段都会发生ShaderMap的序列化,第一次序列化到DDC,第二次序列化到材质Package。
在DDC存在的情况下,会跳过编译。
需要注意,第一次序列化时,Shader Code进入DDC,第二次序列化时,Shader Code进入ShaderCodeLibrary。
材质和Shader的结构
- UMaterial和UMaterialInstance是材质资源,在Game Thread加载和使用
- FMaterial是渲染线程的材质资源,在Render Thread使用
- FMaterialResource是FMaterial的包装,提供序列化、初始化等功能
- 一个UMaterial可以包含多个FMaterialResource,使用Quality Level和Feature Level两级索引来管理
- 一个FMaterial对应一个FMaterialShaderMap
- FMaterialShaderMap使用Vertex Factory Type和Shader Type两级索引来管理Shader
- 一个Shader对应一个FShaderResource,而FShaderResource是FShader的包装
- FShaderResource负责引用和初始化RHI资源,提供给渲染命令使用
材质编译
入口:UMaterial::BeginCacheForCookedPlatformData
- CacheResourceShadersForCooking
- 遍历需要的QualityLevel,分配FMaterialResource
- CacheShadersForResources
- 遍历FMaterialResource,检查是否需要Cook(首先根据FeatureLevel,其次分析材质使用的QualitySwitchNode,其次判断材质是否有QualityOverride)
- FMaterialResource::CacheShaders
- FMaterial::CacheShaders
- FMaterialShaderMap::LoadFromDerivedDataCache
- 如果找到了,从DDC里面反序列化
- 如果DDC里不存在,则调用FMaterial::BeginCompileShaderMap启动编译(见下文”编译触发”小节)
- FMaterialShaderMap::LoadFromDerivedDataCache
- 编译完成后,等待CookOnTheFlyServer调用ProcessAsyncResults
- FShaderCompilingManager::ProcessCompiledShaderMaps(见下文”编译完成”小节)
- 把Cache好的一系列FMaterialResource存到CachedMaterialResourcesForCooking
- FShaderCompilingManager::ProcessCompiledShaderMaps(见下文”编译完成”小节)
- FMaterial::CacheShaders
- FMaterialResource::CacheShaders
- 遍历FMaterialResource,检查是否需要Cook(首先根据FeatureLevel,其次分析材质使用的QualitySwitchNode,其次判断材质是否有QualityOverride)
材质序列化
入口:UMaterial::Serialize
- SerializeInlineShaderMaps
- 遍历CachedMaterialResourcesForCooking 中的每一个FMaterialResource
- FMaterialShaderMap::Serialize
- 遍历 SortedMeshShaderMaps(每个对应一种VF)
- FMeshMaterialShaderMap::SerializeInline
- 遍历所有shader
- SerializeShaderForSaving
- FShaderResource::SerializeShaderCode
- FShaderCodeLibrary::AddShaderCode 把shader code保存到shader code library
- FShaderCodeLibrary找到对应的EditorShaderCodeArchive,加入其中
- FShaderResource::SerializeShaderCode
- SerializeShaderForSaving
- 遍历所有shader
- FMeshMaterialShaderMap::SerializeInline
- 遍历 SortedMeshShaderMaps(每个对应一种VF)
- FMaterialShaderMap::Serialize
- 遍历CachedMaterialResourcesForCooking 中的每一个FMaterialResource
Shader编译流程
除了GlobalShader,大部分Shader编译都是异步的。
首先会由编辑器或者CookCommandlet触发Shader编译,编译任务由GShaderCompilingManager负责分配给workers,并不断将完成编译的ShaderMap序列化到DDC。
Shader的编译通过下面的流程把材质转换为目标平台可以识别的代码:
- 翻译材质蓝图为HLSL
- 设置材质参数(宏定义)
- 设置VertexFactory参数(宏定义)
- 设置Shader参数(宏定义)
- 把HLSL编译成目标平台使用的Shader格式
编译触发
- FMaterial::BeginCompileShaderMap
- 构建一个新的FMaterialShaderMap
- FHLSLMaterialTranslator::Translate 把材质翻译成HLSL
- FHLSLMaterialTranslator::GetMaterialEnvironment 获取材质特性相关的设置 -> Input.SharedEnvironment
- FMaterialShaderMap::Compile 把HLSL编译成平台相关的shader
- FMaterial::SetupMaterialEnvironment 获取材质渲染相关的设置 -> Input.SharedEnvironment
- 遍历所有VertexFactory,取得VF对应的MeshShaderMap
- FMeshMaterialShaderMap::BeginCompile
- 遍历所有ShaderType,使用ShouldCacheMeshShader 判断ShaderType是否需要编译
- FMeshMaterialShaderType::BeginCompileShader
- 构建一个NewJob
- FVertexFactoryType::ModifyCompilationEnvironment 获取VF相关的设置 -> Input.Environment
- FMeshMaterialShaderType::SetupCompileEnvironment 获取Shader相关的宏 -> Input.Environment
- GlobalBeginCompileShader
- 构建NewJob->Input
- 继续构建Input->Environment
- Add(NewJob)
- 遍历Mesh无关的Shaders,流程同上
- 遍历Pipeline Shaders,流程同上
- GShaderCompilingManager->AddJobs(NewJobs) 添加到全局编译队列中,等待进程异步编译
- FMeshMaterialShaderType::BeginCompileShader
- 遍历所有ShaderType,使用ShouldCacheMeshShader 判断ShaderType是否需要编译
- FMeshMaterialShaderMap::BeginCompile
编译进行
以多进程编译OpenGL Shader为例
- ShaderCompileWorker::ProcessCompilationJob
- FShaderFormatGLSL::CompileShader
- FOpenGLFrontend::CompileShader
- SetupPerVersionCompilationEnvironment
- 设置一系列编译器相关的宏
- PreprocessShader 预处理:头文件替换,删除注释
- FHlslCrossCompilerContext::Init 初始化编译器
- FHlslCrossCompilerContext::Run 调用HLSLCC进行编译
- RunFrontend 词法分析,语法分析(生成AST),语义分析(生成HIR)
- RunBackend生成Main函数,代码优化
- 这里的几个关键函数由FOpenGLBackend实现
- FOpenGLBackend::GenerateCode 生成GLSL代码
- BuildShaderOutput
- 根据输出代码,生成FShaderCompilerOutput,其中包含参数map、采样数、最终代码、Hash值、编译错误等信息
- 生成FOpenGLCodeHeader,其中包含了Shader类型、名称、参数绑定、UniformBuffer映射等运行时依赖的信息
- 序列化FOpenGLCodeHeader到最终输出的ShaderCode中。运行时会先反序列化Header,随后才读取并编译shader实体
- FOpenGLFrontend::CompileShader
- FShaderFormatGLSL::CompileShader
以Native方式编译iOS的Shader时,主要区别在于,编译的产物是Metal平台的硬件无关中间表达(IR),而非文本,其文件格式是一种Native的二进制(MetalLib)。由于MetalLib无法被引擎读取,引擎又需要在渲染提交时获取Shader的反射信息,因此UE将这个Header统一保存到额外的MetalMap文件中。
编译完成
入口FShaderCompilingManager::FinishCompilation 或 FShaderCompilingManager::ProcessAsyncResults
- FShaderCompilingManager::ProcessCompiledShaderMaps
- 遍历FShaderMapFinalizeResults,找到其对应的FMaterialShaderMap和FMaterial
- FMaterialShaderMap::ProcessCompilationResults 处理整个ShaderMap的编译结果
- ProcessCompilationResultsForSingleJob
- 找到对应VF的FMeshMaterialShaderMap
- FMeshMaterialShaderType::FinishCompileShader 处理单个Shader的编译结果
- FShaderResource::FindOrCreateShaderResource
- FShaderResource::FShaderResource
- FShaderResource::CompressCode
- Code = FShaderCompilerOutput.Code 二进制Shader Code最终进入ShaderResource
- 将新生成的Shader加入FMeshMaterialShaderMap
- FShaderResource::CompressCode
- InitOrderedMeshShaderMaps 把FMeshMaterialShaderMap重新排序
- SaveToDerivedDataCache 把编译结果序列化到DDC
- FMaterialShaderMap::Serialize 这一步和上文“材质序列化”小节一样,区别在于这次是存到内存而非Package
- 把序列化的结果存到DDC(包括shader code)
- FShaderResource::FShaderResource
- FShaderResource::FindOrCreateShaderResource
- ProcessCompilationResultsForSingleJob
- FMaterialShaderMap::ProcessCompilationResults 处理整个ShaderMap的编译结果
- 遍历FShaderMapFinalizeResults,找到其对应的FMaterialShaderMap和FMaterial
ShaderLibrary序列化流程
几个关键的类:
- FEditorShaderCodeArchive:Cook时使用,负责保存Shader信息
- FShaderCodeArchive:运行时使用,负责读取Shader信息
- IShaderFormatArchive:运行和Cook时使用,平台相关的类,负责具体的序列化和反序列工作(仅Native模式)
上述类之间的关系:
- Cook时,Shader通过FShaderCodeLibrary,进入FEditorShaderCodeArchive
- Cook结束后,FEditorShaderCodeArchive将所有Shader序列化到Library文件中。
- 如果是iOS,会再通过IShaderFormatArchive,把Shader编译成metallib
- 运行时,FShaderCodeLibrary读取多个FShaderCodeArchive,从中加载shader
总体流程
入口FShaderCodeLibraryImp::SaveShaderCode
- FEditorShaderCodeArchive::Finalize
- 保存ShaderHash->ShaderEntry的索引(以TMap的形式)
- 保存所有ShaderCode
编译Native Library(仅Native模式)
- 首先,在Shader编译阶段,会调用XCode把Shader编译成二进制的IR
- 创建IShaderFormatArchive
- 遍历FEditorShaderCodeArchive中所有Shader
- Strip
- 将Shader加入IShaderFormatArchive
- 把Shader中编译好的二进制IR保存到中间文件夹,并且生成ShaderID
- 把ShaderID加入Shader列表
- 把ShaderHash加入ShaderMap
- Finalize
- 调用XCode把中间文件夹里的所有二进制IR链接起来
SkipEditorContent
在 CookOnTheFlyServer 中会忽略 /Engine/Editor*
与 /Editor/VREditor*
中的资源:
1
2
3
4
5
6
7
8
// don't save Editor resources from the Engine if the target doesn't have editoronly data
if (IsCookFlagSet(ECookInitializationFlags::SkipEditorContent) &&
(PackagePathName.StartsWith(TEXT("/Engine/Editor")) || PackagePathName.StartsWith(TEXT("/Engine/VREditor"))) &&
!Target->HasEditorOnlyData())
{
Result = ESavePackageResult::ContainsEditorOnlyData;
bCookPackage = false;
}
Cook 时的依赖加载
虽然上面列出了引擎打包时将会包含的资源,但是,它们还不是全部,因为它们都还是单个的资源或者单个的目录,并没有包含资源的依赖关系。
所以,在 UE 进行 Cook 时还会进行实质上的依赖分析。
考虑以下两个问题:
- 如果一个地图中放了一个 C++ 实现的 Actor,而在它的构造函数代码中有加载某个资源,怎么将其打包呢? 从资源依赖的角度上,是没有依赖关系的
1
2
3
4
5
6
7
AMyActor::AMyActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
UTexture2D* Texture2D = LoadObject<UTexture2D>(nullptr, TEXT("/Game/TextureResources/T_ImportTexture.T_ImportTexture"));
}
- 没有在 AssetRegistry 引用关系中的资源依赖,如何打包?如
AnimSequence
的BoneCompressionSettings
和CruveCompressionSettings
配置,并不在依赖关系中,从 AsssetRegistry 获取动画序列的依赖时找不到它们。
但是它们在 uasset 的 ImportTable 中有记录:
从 Cooked 的 uasset 中也可以看到:
为什么不能从 Asset 的 ImportTable 里直接扫描依赖呢?
这是因为访问 ImportTable 需要真正加载资源,当资源量非常大时,这是非常耗费硬件资源和时间的,而从 AssetRegistry 访问依赖关系则无需将资源加载到内存里,所以速度非常快。
UE 默认的实现机制:
- 引擎启动时会创建 CDO,会执行类的构造函数 »
EInstigator::StartupPackage
- UE 的资源在加载时会将它依赖的资源也给加载了
UE 在 Cook 时实现了一个方案,监听所有创建的 UObject,将其添加至 Cook 列表中,确保在 C++ 的构造函数中加载的资源和通过 ImportTable 加载的依赖资源也能被 Cook。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct FPackageTracker : public FUObjectArray::FUObjectCreateListener, public FUObjectArray::FUObjectDeleteListener
{
FPackageTracker::FPackageTracker(FPackageDatas& InPackageDatas)
:PackageDatas(InPackageDatas)
{
for (TObjectIterator<UPackage> It; It; ++It)
{
UPackage* Package = *It;
if (Package->GetOuter() == nullptr)
{
LoadedPackages.Add(Package);
}
}
NewPackages.Reserve(LoadedPackages.Num());
for (UPackage* Package : LoadedPackages)
{
NewPackages.Add(Package, FInstigator(EInstigator::StartupPackage));
}
GUObjectArray.AddUObjectDeleteListener(this);
GUObjectArray.AddUObjectCreateListener(this);
}
FPackageTracker::~FPackageTracker()
{
GUObjectArray.RemoveUObjectDeleteListener(this);
GUObjectArray.RemoveUObjectCreateListener(this);
}
virtual void NotifyUObjectCreated(const class UObjectBase* Object, int32 Index) override;
virtual void NotifyUObjectDeleted(const class UObjectBase* Object, int32 Index) override;
}
所以,基于相同的思路,我们只需要通过 AssetRegistry
获取资源的依赖关系,存储一个粗略的资源列表,然后在 Cook 时监听 UObject 的创建,若不在扫描的资源列表中,就将其添加至 Cook 队列,从而实现完整的资源打包过程。