(此文档官方提供中文版,而我这里再翻译一遍的原因:一是原版翻译有些用语我不习惯,可能有些意思没能准确传达给我;二是我想自己再翻译一遍可以让我看得更仔细。)
注意:虽然这个文档来源于5.2版本,但内容好像很多是过时的,比如DrawingPolicy所以现在只能学习个思路,代码细节很多都不能参考了,很可惜。
新手入门
虚幻引擎中渲染方面的代码非常多,所以要想很快地在较高层级来观察渲染过程,是较为困难的。阅读代码时,比较好的入手之处是FDeferredShadingSceneRenderer::Render,这里是渲染线程上渲染新的一帧的地方。此外,执行GPU分析命令并查看Draw事件也很有帮助。然后,您可以在 Visual Studio 中对Draw事件的名称进行文件中搜索,找出对应的 C++ 实现。
- 参阅 Shader Development 了解Shader的开发。
- 参阅 Coordinate Space Terminology 了解UE中使用的坐标空间的术语。
做渲染开发时一些有用的控制台命令(使用?作为参数且当前状态不带参数时可显示帮助)
控制台命令 | 作用 |
| 显示此帧总体的时长 ,以及游戏线程的时长、渲染线程的时长、GPU的时长。这几个中最长的就是瓶颈。不过,GPU(译者记录:这里中文版说的是“CPU”,哪个错了?)时长包含空闲时间,所以只有在“其为最长且没有其他与之相同”时才是瓶颈。 |
Ctrl+Shift+.或 | 重新编译上次保存 .usf 文件后发生变化的着色器。加载后这也是自动发生的。 |
| 测量渲染当前视图的 GPU 时间。可在弹出的 UI 或引擎日志中查看结果。 |
| 可视化显示多种 RenderTarget 的内容,并且可保存为 bmp 文件。 |
| 打开或关闭特定的showflag。使用 |
| 暂停游戏,但继续渲染。任何simulation都将停止。 |
| 变更游戏速度。此命令有助于在进行分析时减缓时间而不跳过simulation。例如 |
| 用于测试分屏。 |
| 设为 |
| 设置当前游戏视图的显示分辨率。在编辑器中不起作用。 |
做渲染开发时一些有用的命令行参数: (译者这里不熟悉,所以不翻译)
模块
渲染器代码存在于自己的 Renderer模块 中,这意味着它将编译为一个单独 dll 文件(使用non-monolithic编译方式时)。这可以使迭代更快,因为在渲染代码变更时无需重新链接整个应用程序。
Renderer模块依赖于引擎模块(Engine模块),因为其拥有许多向引擎的回调。然而当引擎需要调用渲染器中的某些代码时,这会通过某个接口来完成,通常为 IRendererModule 或 FSceneInterface。
场景内容的表示
在UE中,渲染器所见的场景由 FScene 中存储的Primitive组件和多种其他结构的列表所定义。一个Primitive的八叉树将被维护,用于加速空间查询。
场景内容的主要的类
UE中拥有一个和游戏线程并行运行的渲染线程。大多数跨越游戏线程与渲染线程的类都会分为两个部分——依据对相应数据拥有所有权的线程。
主要的类有:
类 | 描述 |
UWorld | 包含多个可互相交互的 Actor 和组件的世界。关卡可以流送进入和退出世界。程序中可以同时有多个世界处于激活状态。 |
ULevel | 一同加载/卸载并保存在同一地图文件中的 Actor 和组件合集。 |
USceneComponent | 需要添加到 FScene 中的对象,如光源、mesh、雾、等等。 |
UPrimitiveComponent | 可渲染或进行物理交互的对象。也作为可视性剔除与渲染属性设置(如是否投射阴影等)的单位。与所有 UObjects 一样,游戏线程拥有所有变量和数据,渲染线程不应直接对其进行访问。 |
ULightComponent | 代表光源。渲染器负责计算和添加其对场景的影响。 |
FScene | UWorld 的渲染器版本。对象只有在被添加到 FScene(注册component时会调用)后才算存在于渲染器中。渲染线程拥有 FScene 的所有数据,游戏线程无法直接对其进行修改。 |
FPrimitiveSceneProxy | UPrimitiveComponent 的渲染器版本,为渲染线程映射 UPrimitiveComponent 的数据。这个类定义在引擎模块中,用于划分为子类以支持不同类型的Primitive(骨骼、刚体、BSP 等)。它要实现一些非常重要的函数,如 GetViewRelevance、DrawDynamicElements 等。 |
FPrimitiveSceneInfo | 内部渲染器状态(针对于 FRendererModule 的实现),对应一个 UPrimitiveComponent 和 FPrimitiveSceneProxy。存在于渲染器模块中,因此引擎看不到它。 |
FSceneView | 引擎表示的 FScene 中的一个视图(View)。一个Scene可以调用不同的 FSceneRenderer::Render 来渲染不同的视图(编辑器的多视图),或者在一个 FSceneRenderer::Render 中渲染多个视图(分屏游戏)。每一帧都会有一个新的视图被构造。 |
FViewInfo | 渲染器内部表示的视图,存在于渲染器模块中。 |
FSceneViewState | ViewState 存储某个视图的在多个帧中需要的渲染器内部信息。在游戏中,每个 ULocalPlayer 只有一个ViewState。 |
FSceneRenderer | 为每个帧创建的类,用于封装跨帧的临时对象。 |
下面列出了这些类被定义在哪个模块中。如果你在尝试解决链接器问题,这些信息非常重要。
引擎模块 | 渲染器模块 |
UWorld | FScene |
UPrimitiveComponent / FPrimitiveSceneProxy | FPrimitiveSceneInfo |
FSceneView | FViewInfo |
ULocalPlayer | FSceneViewState |
ULightComponent / FLightSceneProxy | FLightSceneInfo |
下面列出了这些类的数据被哪个线程所维护。请务必了解您正在编写的代码的数据属于哪个线程,避免出现 Race Condition。
游戏线程 | 渲染线程 |
UWorld | FScene |
UPrimitiveComponent | FPrimitiveSceneProxy / FPrimitiveSceneInfo |
- | FSceneView / FViewInfo |
ULocalPlayer | FSceneViewState |
ULightComponent | FLightSceneProxy / FLightSceneInfo |
材质的类
类 | 描述 |
FMaterial | 用于渲染的材质的接口。可用于访问材质属性(如混合模式)。包含一个 Shader Map,渲染器将使用这个Map来得到特定的Shader。 |
FMaterialResource | 针对于 UMaterial 的 FMaterial 接口实现。 |
FMaterialRenderProxy | 材质在渲染线程上的表示。可用于访问 FMaterial 接口和各个标量、向量和纹理参数。 |
UMaterialInterface | 这是个抽象类,是游戏线程上的材质功能的接口。用于得到渲染用的 FMaterialRenderProxy 和作为数据源的 UMaterial。 |
UMaterial | 材质的数据源。通过节点网络进行编辑制作。计算出用于着色、设置混合模式、等等的材质属性。 |
UMaterialInstance | 这是个抽象类,表示 UMaterial 的实例。实例们使用同一套 UMaterial 节点网络,但提供不同的参数(标量、向量、纹理、静态开关)。每个实例都有一个父项 UMaterialInterface。因此,材质实例的父项可能是 UMaterial,但也可能是另一个 UMaterialInstance。所以这会形成一个链,但链的最终端是 UMaterial。 |
UMaterialInstanceConstant | 只能在编辑器中修改的 UMaterialInstance。可以提供标量、向量、纹理和静态开关参数。 |
UMaterialInstanceDynamic | 可以在运行时修改的 UMaterialInstance。可提供标量、向量和纹理参数。无法提供静态开关参数,且无法成为另一 UMaterialInstance 的父项。 |
Primitive Component
Primitive组件是 “确定可视性和相关性” 的基本单位。举个例子,“occlusion” 和 “视锥剔除” 都是以Primitive为单位进行的。因此在设计系统时,考虑组件的大小十分重要。每个组件都有一个边界,用于多种操作如:剔除、阴影投射、确定光照影响、等等。
组件只有在注册之后才会对场景(以及渲染器)可见。如果游戏线程代码更改了组件属性,那么必须调用组件上的 MarkRenderStateDirty(),才能将更改传递给渲染线程。
FPrimitiveSceneProxy 和 FPrimitiveSceneInfo
FPrimitiveSceneProxy 是 UPrimitiveComponent 的渲染线程版本,根据Component的类型划分子类。它定义在引擎模块中,并在渲染时有函数调用。FPrimitiveSceneInfo 是PrimitiveComponent的状态,定义在渲染器模块内部,对外不可见。
重要的 FPrimitiveSceneProxy 的函数
函数 | 描述 |
GetViewRelevance | 在帧的开始从 InitViews 调用,填充一个 FPrimitiveViewRelevance 结构并返回。 |
DrawDynamicElements | (假如Proxy表示自己拥有动态的相关性)任何此Proxy相关的pass中都会调用此函数来绘制此Proxy。 |
DrawStaticElements | (假如Proxy表示自己拥有静态的相关性)当Primitive在游戏线程中被添加时,将会调用Proxy的此函数来提交StaticMesh元素。 |
场景渲染顺序
渲染器按照 “期望将数据合并到RenderTarget上的顺序” 处理场景。例如,“仅深度” 的Pass会比 BasePass先渲染,这样就可以得到 Heirarchical Z (HiZ) 数据,从而降低BasePass中的着色消耗。此顺序是由Pass函数在 C++ 中调用的顺序静态决定的。
相关性
FPrimitiveViewRelevance 是说明了哪些 Pass 与 Primitive 相关。Primitive 可能有多个元素,且元素们有不同的相关性,因此 FPrimitiveViewRelevance 相当于所有元素的相关性的逻辑 OR (译者注:元素中只要有任意一个是相关的,那么就是真,只有都不相关是才是假)。这表示一个 Primitive 可以同时具有不透明和透明的相关性,有动态和静态的相关性,这并不互斥。
FPrimitiveViewRelevance 还会表明 Primitive 是否需要使用动态 (bDynamicRelevance) 和/或静态 (bStaticRelevance) 渲染路径。
Drawing Policy
Drawing Policy 包括了 “通过特定的着色器” 来渲染mesh的逻辑。它使用 FVertexFactory 接口来抽象出mesh的类型,并使用 FMaterial 接口来抽象材质的数据。
在最底层,一个 Drawing Policy 会负责持有一组 “mesh材质着色器” 以及一个 “顶点工厂(vertex factory)”,它会将顶点工厂的 buffer 与 RHI(渲染硬件接口) 绑定,将mesh材质着色器与 RHI 绑定,设置适当的着色器参数,然后执行 RHI 的 DrawCall。
Drawing Policy 的函数
函数 | 描述 |
构造函数 | 根据给定的顶点工厂和材质ShaderMap,找到合适的Shader,并存储这些引用。 |
CreateBoundShaderState | 为Drawing Policy 创建 RHI 绑定的 shader state。 |
Matches/Compare | 提供一个函数可以让静态绘制列表(static draw lists)中的 Drawing Policy 可以相互比较。Matches函数 必须比较 DrawShared 依赖的所有因素。 |
DrawShared | 设置 DrawingPolicy之间Matches函数返回True 的 RHI state。例如,most drawing policies sort on material and vertex factory, so shader parameters depending only on the material can be set, and the vertex buffers specific to the vertex factory can be bound。state 应尽可能在此处设置,而非 SetMeshRenderState,因为 DrawShared 在静态渲染路径中调用更少。 |
SetMeshRenderState | 设置 特定于此mesh的(或是任何DrawShared没设置的) RHI state。这比 DrawShared 调用的次数多得多,因此此处性能非常关键。 |
DrawMesh | 实际发出 RHI 的 DrawCall。 |
渲染路径(Rendering paths)
UE 拥有动态渲染路径(能够提供更多的控制,但遍历较慢)和静态渲染路径(缓存尽可能接近 RHI 级别)。两者差异基本是上层的,因为在最底层它们都使用Drawing Policy。应确保各个渲染Pass(Drawing Policy)在需要时能够同时处理两个渲染路径。
动态渲染路径
动态渲染路径使用 TDynamicPrimitiveDrawer 并对每个要渲染的PrimitiveSceneProxy调用 DrawDynamicElements。
FViewInfo::VisibleDynamicPrimitives会跟踪出需要使用动态路径来渲染的Primitive列表。每个渲染 Pass 都需要遍历此列表,并调用各个Primitive上的 DrawDynamicElements。随后,Proxy的 DrawDynamicElements 按照需要的数目组合出多个 FMeshElements,并将其随 DrawRichMesh 或 TDynamicPrimitiveDrawer::DrawMesh 提交。这样最终会创建一个新的临时 Drawing Policy,调用 CreateBoundShaderState、DrawShared、SetMeshRenderState 、最终是 DrawMesh。
动态渲染路径能够提供很高的灵活性,因为每个Proxy都在 DrawDynamicElements 中有一个回调函数,这样它就可在其中执行该组件特别的逻辑。它的插入消耗极小,但遍历消耗很大,因为不存在 state 排序,且不使用缓存。
静态渲染路径
静态渲染路径通过 “静态绘制列表(static draw lists)” 实现。mesh在加入到场景时会插入到静态绘制列表中。在插入过程中,将调用 Proxy 上的 DrawStaticElements 来收集 FStaticMeshElements。然后随 CreateBoundShaderState 的结果,创建并存储一个DrawingPolicy。新的DrawingPolicy将根据其 Compare 和 Matches 函数排序,并插入到静态绘制列表中的适当位置(参见 TStaticMeshDrawList::AddMesh)。在 InitViews 中,一个包含静态绘制列表中的可见性数据的 位列表(bit array) 会初始化并传递到 TStaticMeshDrawList::DrawVisible 中,也就是实际对列表进行绘制的地方)。DrawShared 对所有相互匹配的DrawingPolicy只会调用一次,而 SetMeshRenderState 和 DrawMesh 会对每个 FStaticMeshElement(参见 TStaticMeshDrawList::DrawElement)调用。
静态渲染路径会将许多工作移动到 “加入场景时”,这会大大加快 “渲染时” 的场景遍历。对于静态mesh,在渲染线程上使用静态绘制列表的渲染会快 3 倍,从而允许场景中出现更多的静态网格体。由于静态绘制列表会在加入场景时缓存数据,因此它们仅能缓存与视图无关的状态。那些很少重新加入场景,但经常需要渲染的Primitive非常适合静态绘制列表。
静态渲染路径可能会出现 bug,因为它对于每个 state bucket 只调用一次 DrawShared。这些 bug 可能会很难调查,因为它们受影响于场景中mesh的渲染顺序和加入顺序。特别的视图模式(如仅光照、无光照等)会强制所有Primitive使用动态路径,因此如果在强制使用动态渲染路径时 bug 消失,则其很可能是由于某DrawingPolicy的 DrawShared 和/或 Matches 函数的错误实现而出现的。
上层的渲染顺序
下面将描述从 FDeferredShadingSceneRenderer::Render 开始渲染一帧的流程:
操作 | 描述 |
GSceneRenderTargets.Allocate | 如果需要,重新分配全局场景的RenderTarget,使其对当前视图足够大。 |
InitViews | 通过多种剔除方法为视图初始化Primitive的可见性,设立此帧可见的动态阴影、 intersects shadow frustums with the world if necessary (for whole scene shadows or preshadows)。 |
PrePass / Depth only pass | RenderPrePass / FDepthDrawingPolicy。渲染遮挡物,对景深buffer仅输出景深。该Pass可以在多种模式下工作:禁用、仅遮蔽,或完全景深,具体取决于激活的功能需要什么。该Pass通常的用途是初始化 Hierarchical Z 以降低 BasePass 的着色消耗,因为BasePass的像素着色器消耗非常大。 |
Base pass | RenderBasePass / TBasePassDrawingPolicy。渲染不透明和 masked 的材质,向 GBuffer 输出材质属性。光照图贡献和天空光照也会在此计算并加入场景颜色。 |
Issue Occlusion Queries / BeginOcclusionTests | 触发将用于下一帧 InitViews 的延迟遮蔽查询。这会通过渲染所查询物体的包围盒(有时还会将相邻的包围盒组合在一起以减少绘制调用)来完成。 |
Lighting | 各个光照将渲染出ShadowMap ,光照贡献会累加到场景颜色。它混合使用了标准的延迟光照,以及Tiled延迟着色。光照也会在透明光照体积中累加。 |
Fog | 雾和大气在延迟Pass中对不透明表面进行逐像素的计算。 |
Translucency | 半透明物体会累加到屏外的RenderTarget,在那里会应用逐顶点的雾,因而可以合并到场景中。半透明物体的光照在一个单独Pass中计算最终结果,以正确blend。 |
Post Processing | 多种后期处理效果均通过 GBuffers 来应用。这里也将半透明物体合并到场景中。 |
以上是相当简单概略的介绍。如需了解详情,请阅读相关代码,或者GPU剖析日志。
渲染硬件接口 (RHI)
RHI 是平台专用图形 API 之上的一个层简单的封装。UE 中的 RHI 抽象层尽可能得低,这样大多数功能都能以“与平台无关”得代码写成,从而能够在支持所需FeatureLevel的任何平台上运行。
ERHIFeatureLevel 将对功能进行划分,以降低复杂度。如果平台无法支持某个 FeatureLevel 所需的全部功能,则其必须降低层级,直至找到一个能全部支持功能的层级。
FeatureLevel | 描述 |
SM5 | 大体上对应于 D3D11 Shader Model 5,但由于 OpenGL 4.3 限制,纹理仅可以同时使用 16 个。支持曲面细分、计算着色器和CubeMap数组。支持延迟渲染。 |
SM4 | 对应 D3D11 的 Shader Model 4,这与 SM5 基本相同,但没有曲面细分、计算着色器和CubeMap数组。支持延迟渲染。不支持 Eye Adaptation(因为其使用计算着色器)。 |
ES3_1 | 对应OpenGL ES3.1、Vulkan和Metal支持的功能。 |
渲染state分组
渲染state会根据其影响的管线部分而分组。例如,RHISetDepthState 可设置所有与景深缓冲相关的state。
渲染state默认值
由于渲染state数量太多了,因此在每次绘制之前对它们全部设置一遍是不现实的。所以 UE 具有隐性设置的一组 state,它们被认为是设置为了默认值(因此,它们在变更后必须恢复为默认值),和另一组少得多的需要显性设置的state。没有隐性默认值的state有:
- RHISetRenderTargets
- RHISetBoundShaderState
- RHISetDepthState
- RHISetBlendState
- RHISetRasterizerState
- 任何 RHISetBoundShaderState 设置的着色器的依赖项
其他所有state均视为已设置为其默认值(即关联的 TStaticState 的定义,如默认的 stencil state 由 RHISetStencilState(TStaticStencilState<>::GetRHI()) 设置。