本篇目标
当使用者在UE4的编辑器内点下了Build按钮,一系列Build命令被触发,而“光照”则是其中重要的一环。我希望粗略的了解一下这背后的代码逻辑,随后我发现这个过程和 Swarm、Lightmass 这两个概念有关。具体来讲,这个过程涉及的代码分布在下面几个部分中:
UnrealEd模块
中相关的代码。主要是EditorBuildUtils.h/cpp
、Lightmass文件夹中的代码、StaticLightingSystem文件夹中的代码。SwarmInterface模块
。定义了与Swarm进行交互的接口。\Source\Programs\UnrealLightmass项目
。负责生成\Engine\Binaries\Win64\UnrealLightmass.exe
\Source\Programs\UnrealSwarm
文件夹包含的几个c#项目。负责生成\Engine\Binaries\DotNET\SwarmAgent.exe
与\Engine\Binaries\DotNET\SwarmCoordinator.exe
。以及依赖的一些dll。
我的目标是弄明白在一次Lightmass的Build流程中,这几个部分所扮演的角色。
关于Lightmass这套系统的一些原理,我从《Lightmass源码分析之 与Swarm交互 - 知乎》中以及前后相关的文章中学到了很多。
观察依赖关系
SwarmAgent.exe和SwarmCoordinator.exe
这几个部分中,Swarm的程序是相对独立的。在《官方文档:Unreal Swarm》中也可以看到,当在另一台机器上部署swarm来帮助联机渲染时,只需要将Swarm相关的exe与dll拷贝过去就可以了,并不需要UE4引擎本身。
在\Source\Programs\UnrealSwarm
下,主要有两个sln:SwarmAgent.sln
与SwarmCoordinator.sln
。
打开SwarmCoordinator.sln
后可以发现一个同样叫SwarmCoordinator
的项目,而这个项目的属性》应用程序》输出类型是Windows应用程序
,也就是会输出exe。而剩余的项目的输出类型都是类库
,也就是会输出dll。此外,可以看到SwarmCoordinator项目
依赖了SwarmCommonUtils项目
与SwarmCoordinatorInterface项目
。
同理,打开SwarmAgent.sln
可以发现:Agent项目
依赖了AgentInterface项目
、SwarmCommonUtils项目
、SwarmCoordinatorInterface项目
、UnrealControls项目
。
也就是说,Agent 和 Coordinator 都依赖了SwarmCommonUtils
以及各自的Interface。不过 Agent 还额外依赖了UnrealControls
,以及Coordinator的Interface。看来Agent调用了Coordinator的接口。
总结这几个程序集的依赖关系:
UnrealLightmass.exe
UnrealLightmass
是一个C++项目,所以用了UBT进行编译,因此也有配套的UnrealLightmass.Target.cs
这个目标描述文件,以及UnrealLightmass.Build.cs
这个模块描述文件。
在UnrealLightmass
这个项目的属性页可以看到它调用了UBT并使用UnrealLightmass.Target.cs
作为目标,最后输出UnrealLightmass.exe。
在UnrealLightmass.Build.cs
可以看到它依赖了一些诸如Core
、CoreUObject
这些UE4核心的模块,以及SwarmInterface
模块。
SwarmInterface模块
在SwarmInterface.Build.cs
中可以看到它只依赖了Core
、CoreUObject
模块。但它还指出了需要AgentInterface.dll
:
// Copy the AgentInterface DLL to the same output directory as the editor DLL.
RuntimeDependencies.Add("$(BinaryOutputDir)/AgentInterface.dll", "$(EngineDir)/Binaries/DotNET/AgentInterface.dll", StagedFileType.NonUFS);
// Also copy the PDB, if it exists
if(File.Exists(Path.Combine(EngineDirectory, "Binaries", "DotNET", "AgentInterface.pdb")))
{
RuntimeDependencies.Add("$(BinaryOutputDir)/AgentInterface.pdb", "$(EngineDir)/Binaries/DotNET/AgentInterface.pdb", StagedFileType.DebugNonUFS);
}
此外,关于这个模块我目前不太理解的是,它还包括一些C#代码:
那么这些C#代码该如何与其他C++部分交互呢?有待后续研究。
不过在其中的SwarmInterface.cs
中确实能看到它需要AgentInterface
。
UnrealEd模块
UnrealEd
模块包含了相当多的内容,烘焙光照只是其中一部分。
在UnrealEd.Build.cs
中可以看到,它依赖了SwarmInterface
模块。
而对于UnrealLightmass
,它需要include:
// Add include directory for Lightmass
PublicIncludePaths.Add("Programs/UnrealLightmass/Public");
搜索Lightmass
命名空间也确实发现了很多使用之处:
总结依赖关系图
他们之间的依赖关系可以表示成如下:
箭头的含义并不尽相同,为此我标注了每一个箭头。但相同的是:箭头尖端的代码对箭头尾端的代码是无知的。
观察StaticLightingSystem的各个阶段
当按下Build按钮后:
FEditorBuildUtils::EditorBuild
被调用
随后,UEditorEngine::BuildLighting
被调用来构建光照,而FStaticLightingSystem
负责来维护整个光照构建流程。
FStaticLightingSystem
有私有变量CurrentBuildStage
表示了当前所处的阶段:
/** Lighting comes in various stages (amortized, async, etc.), we track them here. */
enum LightingStage
{
NotRunning,
Startup,
AmortizedExport,
SwarmKickoff,
AsynchronousBuilding,
AutoApplyingImport,
WaitingForImport,
ImportRequested,
Import,
Finished
};
FStaticLightingSystem::LightingStage CurrentBuildStage;
下面,尝试粗略观察一下每个阶段所做的事情。
0. NotRunning
CurrentBuildStage
在构造函数中赋值为NotRunning
。
随后,当构建光照一开始就被赋值为Startup
。
1. Startup
这个阶段包含了很多准备工作,主要是在FStaticLightingSystem::BeginLightmassProcess
中完成的。
1.1 GatherStaticLightingInfo
上面的信息就是来自于:
1.2 CreateLightmassProcessor
BeginLightmassProcess
中创建了一个新的FLightmassProcessor
对象。
而在FLightmassProcessor
的构造函数中,FSwarmInterface
接口的OpenConnection
函数被调用。
FSwarmInterfaceImpl
是FSwarmInterface
的实现。而FSwarmInterfaceImpl::OpenConnection
具体执行的内容实际由C#代码所指定。具体来说:
C++有一个FSwarmInterface
:
而C#也有一个FSwarmInterface
:
不过C++的版本的OpenConnection
等函数并没有本体,只是企图从外部得到一个函数指针。而C#版的就有具体函数的行为。
在C#版的FSwarmInterface
的OpenConnection
函数中,EnsureAgentIsRunning
被调用,而它启动了SwarmAgent.exe
:
1.3 InitiateLightmassProcessor
其中FLightmassExporter::WriteToChannel
尝试将“光照”、“模型”、“地形”等数据从UE的格式导出为Lightmass的格式:
例如灯光:
void Copy( const ULightComponentBase* In, Lightmass::FLightData& Out )
{
FMemory::Memzero(Out);
Out.LightFlags = 0;
if (In->CastShadows)
{
Out.LightFlags |= Lightmass::GI_LIGHT_CASTSHADOWS;
}
if (In->HasStaticLighting())
{
Out.LightFlags |= Lightmass::GI_LIGHT_HASSTATICSHADOWING;
Out.LightFlags |= Lightmass::GI_LIGHT_HASSTATICLIGHTING;
}
else if (In->HasStaticShadowing())
{
Out.LightFlags |= Lightmass::GI_LIGHT_STORE_SEPARATE_SHADOW_FACTOR;
Out.LightFlags |= Lightmass::GI_LIGHT_HASSTATICSHADOWING;
}
if (In->CastStaticShadows)
{
Out.LightFlags |= Lightmass::GI_LIGHT_CASTSTATICSHADOWS;
}
Out.Color = In->LightColor;
// Set brightness here for light types that only derive from ULightComponentBase and not from ULightComponent
Out.Brightness = In->Intensity;
Out.Guid = In->LightGuid;
Out.IndirectLightingScale = In->IndirectLightingIntensity;
}
从中就可以看出来它得到一个U对象ULightComponentBase
的参数,并将他们赋值给了一个F对象FLightData
对应的参数。而FLightData
是在\Source\Programs\UnrealLightmass\Public\SceneExport.h
中所定义的。
在InitiateLightmassProcessor
最后,CurrentBuildStage
被赋值为AmortizedExport
随后,FEditorBuildUtils::EditorBuild
返回。
2. AmortizedExport
之后,FStaticLightingSystem::UpdateLightingBuild()
在主循环中被调用,不断处理接下来的阶段:
当LightmassProcessor->ExecuteAmortizedMaterialExport()
返回“完成时”,CurrentBuildStage
被赋值为SwarmKickoff
。
3. SwarmKickoff
当CurrentBuildStage==SwarmKickoff
时,KickoffSwarm()
立马被调用,其中的逻辑很简单:
bool bSuccessful = LightmassProcessor->BeginRun();
if (bSuccessful)
CurrentBuildStage = FStaticLightingSystem::AsynchronousBuilding;
在LightmassProcessor::BeginRun
中,UnrealLightmass.exe
以及其依赖的dll被指定:
这些信息被加入到了一个NSwarm::FJobSpecification
对象中,而它将作为调用FSwarmInterface::BeginJobSpecification
时的参数。
最后,当LightmassProcessor->BeginRun()
成功时,CurrentBuildStage
被赋值为AsynchronousBuilding
。
4. AsynchronousBuilding
此时,UE4编辑器内会不断更新目前的进度:
此处的代码是:
FText Text = FText::Format(LOCTEXT("LightBuildProgressMessage", "Building lighting{0}: {1}%"), FText::FromString(ScenarioString), FText::AsNumber(LightmassProcessor->GetAsyncPercentDone()));
FStaticLightingManager::Get()->SetNotificationText( Text );
最后,当LightmassProcessor->Update()
给出“完成”的结果时,CurrentBuildStage
会被赋值为AutoApplyingImport
(如果失败的话则直接被赋值为Finished
)
5. AutoApplyingImport
接下来:
在FStaticLightingManager::ProcessLightingData()
中,FStaticLightingSystem::FinishLightmassProcess()
被调用,它将CurrentBuildStage
赋值为了Import
。
当然,这只是暂时的,因为在FStaticLightingManager::ProcessLightingData()
执行后CurrentBuildStage
就变为了Finished
了。
6. Finished
当CurrentBuildStage
变为了Finished
之后,ActiveStaticLightingSystem
将变为NULL,因此,FStaticLightingSystem::UpdateLightingBuild()
再也不会被执行了:
至此Build流程结束。
总结
总的来说,目前的观察还是在一个粗粒度级别上的。对于很多细节还并不明白,例如:
SwarmInterface模块
中的C#代码如何与C++部分进行交互?SwarmAgent
与SwarmCoordinator
将如何交互?FLightmassExporter::WriteToChannel
中的数据将怎样导出给Lightmass?- Lightmass的结果又将如何返回?
这些都值得后续研究。