欢迎访问设·集合!

设·集合

您现在的位置是:首页 > 渲染软件 > UE

UE4渲染引擎模块简介(1)

设·集合小编 发布时间:2023-02-27 09:54:10 1219次最后更新:2024-03-08 10:50:08

UE4官方文档中《Graphics Programming Overview》开篇即说:UE4的渲染代码太多故难以从宏观上快速预览它的全貌(There is a lot of rendering code in Unreal Engine 4 (UE4) so it is hard to get a quick high level view of what is going on)。这一官方说辞从侧面说明了UE4渲染引擎的复杂性是很高的,这个说法多少有点推卸责任,也颇具劝退之意。但我们自己做为一个合格的程序员,在做任何技术选型的时候最基本的要求总该是:我选的方案在其内涵和外延上至少要能贴合或拔高项目对该功能块的需求,并且这个方案得是我能全程能Hold得住而不是挖深坑用以自埋的。在这一前提下对UE4的渲染引擎乃至UE4引擎本身做一个宏观的整体性的评估就必不可少了。

当然UE4渲染引擎的FeatureList非常棒且推进迅捷,所以在功能性和前瞻性方面往往大超项目预期,往往并不是评估的重点。许多公司之所以选UE4做为项目的引擎必选项,是因为老板看到基于UE的吃鸡大热,UE4所产出的其它产品和宣传视频也惊艳绝伦,于是乎脑袋一热双手一拍,技术人员就麻着胆子硬着头皮,战战兢兢开始玩弄UE(或者说被UE摁在地上摩擦了)。

本文的内容是从渲染引擎的宏观功能上罗列UE4的覆盖面和划分方式,尚不会涉及到具体每个功能模块的实现细节。本文在讨论渲染模块的时候还假设大家均具备这些图形引擎常识:渲染API的功能范畴、如何组织基础的渲染管线、夸平台图形引擎需要基础框架支持的最小集

先从顶层来看一次完整的渲染

给渲染器输入以原始的几何和材质数据,渲染器把几何和材质数据转换为渲染API所支持的数据、渲染状态、Shader及Shader参数并由这些数据组装为一个RenderPipeline,然后执行该RenderPipeline,得到渲染结果后交换到渲染的目标Context上去(如Windows下的一个窗口,Android下的一个View等)。一个3D渲染引擎的核心工作就是组织好这一宏观上的工作流,使其最大化利用目标平台的硬件资源(CPU,GPU,内存,硬盘或闪存等)和特性,使其使用最便利、性能最优,效果最佳。

UE4的渲染系统也不例外,所以我们的渲染功能的识别方式的基于以上基本过程和传统的3D引擎功能划分来做。UE4的模块(Module)和我们将要讨论的渲染功能模块不存在一一对应关系,可能UE4的一两个类即实现一个功能块,或一个UE4的模块(Module)除了包含数个渲染相关的功能。

UE4场景和场景管理(Scene 、SceneManager)

在UE4中不存在传统引擎中的严格一一对应的Scene和SceneManager,它的实现是散落在许多类中。

传统引擎中的Scene一般表达一个渲染用的世界。这个概念在UE4中有两个类和它对应:用于游戏线程中的UWorld类和用于渲染线程中的FScene类.UE4中的中UWorld和FScene有一一对应关系,UWorld用于游戏线程,用于用户的主动操作(如创建、删除世界中的物件等),而FScene则隐藏于渲染线程,由UWorld和世界中的对象被动操作。在游戏过程中,一般只存在一个UWorld实例(在过渡的时候可能有两个),但在编辑器形态下,一般会存在许多个UWorld对象——一般来说,一个UWorld对象表达一个单独的编辑器窗口。

UE4和其它支持大世界的引擎一样支持游戏场景中的物体动态加载和卸载。但它对于大世界的拆分方式是比较独特的——UE4的场景的划分模式不是基于物件级而是基于子关卡级来做。在UE4中,一个UWorld由一个一直存在的持久关卡(ULevel类)和多个动态加载卸载的子关卡组成。UE4中这种动态加载卸载的子关卡叫做流关卡(StreamingLevel ,ULevelStreaming类),且场景中的具体物件都是放置在关卡或流关卡中而不是直接位于UWorld中。

UE4中的流关卡的加、卸载策略实现是由UWorldComposition类来负责的。这是一个基于视点距离和流关卡卡包围盒的简单的加载策略实现。

用于渲染线程的FScene不具备复杂的场景管理功能,它有一些数组用于各类管理场景可渲染对象和灯光,它有两个Octree结构用于空间的快速查询——一个用于灯光,另一个用于其它的可渲染对象,它还有一个DrawList用于Cache各个渲染Pass的指令。


UE4场景中的物体(SceneObject)

当我们在UE编辑器中往场景里拖一个NPC,或放置一个灯光,一个后处理盒(PostProcess Volume)的时候,我们都是往该关卡中添加了一个AActor子类实例,UE4关卡和流关卡中每一个独立物件由一个AActor及其子类的对象实例来建模表达

AActor及其子类本身并不直接持有渲染所需的数据,AActor基于组合模式设计,可持有数个UActorComponent实例,具体的渲染相关的数据均在UActorComponent及其子类的实例中。

USceneComponent见名知义,它是UActorCompoent子类里可用于场景中的组件基类。USceneComponent有两个主要作用:它包含Transform数据,它可以支持Attachment。

UPrimitiveComponent是USceneComponent的子类,它是所有可渲染组件的基类。它包含一系列的几何数据,而做为附赠品,它同时也可以做为碰撞数据使用。

ULightComponentBase是USceneComponent的另一个子类,它是所有灯光组件的基类。

渲染相关的主要Component类结构层次

UE4要渲染API封装

UE4中的渲染API封装是个独立的模块(Module),他们把它命名为RHI(Render Hardware Interface)。RHI的接口定义上倾向于向最新的渲染API靠近(如DX12和Vulkan),它除了提供渲染API提供的主要接口转发外,还对CommandList,ShaderCache、StateCache和GpuProfiler做了基本的封装。

RHI的转发实现在RHICommandList.h文件里,可以看到其实现除了基本的条件判断,大都是直接 转调渲染API实现的RHI子模块里的渲染指令。

UE4具体的实现了以下RHI模块的封装

D3D11RHI ,基于D3D11 Feature的RHI封装

D3D12RHI,基于D3D12 Feature的RHI封装

MetalRHI,基于Metal 1和2的RHI封装

OpenGLGLDrv ,它同时实现了Windows,Linux,Android,Ios,Web等各个平台的Opengl,包含GL3,GL4.x和GLES2,GLES3和H5上的Feature.

EmptyRHI和NullDrv,这两个都是对RHI的空实现

UE4的材质系统

UE4对材质系统的封装可以理解为RenderPipiline输入的所有数据中除了几何体数据之外的所有其它数据。它包括渲染所需要选择的光照模型、光照自身的照射分布函数、材质模型及该材质模型所需要的输入参数,渲染状态数据,为各种顶点格式和渲染分支生成的Shader,以及一个提供给用户编辑态使用的节点图等等。

UE4中可用于渲染的材质分为两种:一种是材质模板(UMaterial),另一个是基于材质模板的材质实例(UMaterialInstance).这两货都是UMaterialInterface的子类。只有UMaterial材质模板带有可编辑的节点图并可拒此生成对应的Shader组合,而UMaterialInstance材质实例则只需要引用UMaterial对应的Shader.UMaterialInstance只能修改材质模板暴露出来的材质参数。

对渲染层来说,一般并不需要区分材质实例和材质模板本身。

FMaterialResourceFMaterail的子类,用于UMaterial的渲染,具体来说FMaterialResource负责为各个渲染API和材质所支持的各种质量等级生成对应的Shader组合。所以,每个UMaterial都会包含多个FMaterialResource。

FMaterialRenderProxyFMaterial用于渲染线程的代理,它可以透过FMaterail和UMaterialInterface访问到Shader、渲染状态,光照模型等所有用户设置好的材质参数。

UE4的材质中光照模型是不可定制的,所以在不魔改源码的前提下,你无法修改其光照模型,比如你想实现一个NPR的Ramp给光照分层时。

UE4中Shader生成

FShader是UE4中所有Shader的基类,它有两个主要的子类

FGlobalShader:全局Shader,会自动注册到全局ShaderCache中

FMaterialShader:用于材质(编辑器)的Shader,所有的后处理、UI、用于模型渲染的Shader都是它的子类。

UE4 Shader生成分两部分,第一部分是把材质编辑器中的节点图编译成HLSL代码,这一部分是通过FHLSLMaterialTranslator来完成的。

UE4 Shader生成的第二部分是把HLSL生成多平台的Shader代码,如Windows上的HLSL,Android上的GLSL,IOS上的MetalShader,简单的流程是这样:

如果是目标平台是HLSL相关的平台,则使用ShaderCompilerCommon模块编译出HLSL AST,再适配到不同的SM Feature上。

如果目标平台是非HLSL相关的平台,则先通过Hlslcc模块(在源码的ThirdParty中)把HLSL编译成基于Mesa自定义的GLSL ByteCode的AST,对该AST再使用GLSLOptimizer进行优化,并把对应的AST通过不同平台的Shader编译后端把Mesa GLSL ByteCode生成不同的Shader源码。

第二部分编译是通过启动ShaderCompilerWorker实用程序并行编译

ShaderCompilerWorker的只是简单封装了一下就转调了IShaderFormat的各个子类的CompileShader,而ShaderFormat则会调用对应的xxxxFrontend(FOpenGLFrontend)进行具体的Shader生成。

基本生成流程如下图

从上面的介绍可以看到UE4的Shader跨平台方案,和U3D一样,使用的字节码的方案,不过一个用的是HLSL BC,一个使用的是Mesa BC。Shader Cross Compile在有了Spir-v之后或者大家都往其迁移是更靠谱的方式——毕竟这是个有强大开源组织在维护、升级和推动的天然跨平台的字节码,而且其Optimizier也在持续维护,要比目前的UE使用的glsloptimizer可维护性会更好。

关于Shader编译的细节和优化、后续还会有文章详细介绍,而这篇文章的字数看起来也比较有点多了,所以就此打住,更多的关于UE4渲染模块的简介也在后面的文章里再续。

广告位

热心评论

评论列表