Contents

UE4RenderingDependencyGraph

什么是RDG

渲染依赖性图表(Rendering Dependency Graph)

也被称为RDG或渲染图表,是一个基于图表的调度系统,用于执行渲染管线的整帧优化。它利用DirectX12这样的现代API,使用自动异步计算调度以及更高效的内存管理和屏障管理来提高性能。

RDG的概念如下:在GPU上并非立即执行通道,而是延迟到整个帧已记录到依赖性图表数据结构之中后再执行。当完成了对所有通道的收集之后,则按照依赖性的排序顺序对图表进行编译和执行。这主要是因为DirectX 12、Vulkan和Metal2之类的现代图形API选择将低级GPU管理的负担转移至应用程序本身。这提供了一个机会,可以利用渲染管线的高级情境来驱动调度,从而提高性能并且简化渲染堆栈。

我们今天的RDG有什么好处,就要看之前的方式有多菜。来看一下传统的下图分别是07年和17年的渲染系统的复杂程度

可以参考:

http://www.frostbite.com/2007/04/frostbite-rendering-architecture-and-real-time-procedural-shading-texturing-techniques

为什么需要RDG

https://img-blog.csdnimg.cn/20200217114206862.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70 这里当然不是非常的具体,不过我们都能看出这里非常的复杂。

当然,我们可以进行一些简单的简化,如下图

https://img-blog.csdnimg.cn/20200217114218328.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

战地四的pass数(Feature)

https://img-blog.csdnimg.cn/20200217114222924.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

面临的挑战:

  1. 显示的代码驱动的即时渲染模式
  2. 显示的资源管理
  3. 手动的ESRAM管理
  4. 每一个不同的游戏团队有着不同的实现方式
  5. 不同的渲染系统有紧密的耦合性
  6. 对于管线定制的扩展性差
  7. 游戏团队必须对这些定制fork/diverge
  8. 代码数飞速增长,将产生很多重复性的代码

所以,我们需要将整个的渲染结构进行一个更高层次的抽象,让其拥有更高的扩展性,模块化,更好的可视化和debug,资源的自动管理,解耦模块并使其可自由组合。

这就是新的结构:

https://img-blog.csdnimg.cn/20200217114257166.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

RDG设计理念

我们将抛弃即时的渲染模式;将渲染代码分离到pass中,大致分为三个阶段

1.Setup phase 2.Compile phase 3.Execute phase

Setup phase

我们将定义这个pass是一个render pass, 还是compute pass我们将定义每一个resource,对于输入和输出对于renderpass ,我们要定义资源的使用其是read,write,还是create。当然对于长期贮存的资源我们是要imported进RDG中,例如History buffer for TAA,Backbuffer

Compile phase

  1. 剔除没有使用的资源和pass

  2. 计算资源的生命周期

  3. 根据使用情况分配实际的GPU资源

Execute phase

  1. 对于每个pass执行回调函数

  2. 这个阶段就是跟我们的之前的渲染流程一样的执行draw,dispatch

Unreal RDG 实例

由于unreal4.23后,RDG才真正的加入,目前的资料还是比较的稀少,所以我们需要学习如何使用,最好的方式就是仿造引擎中一些已经使用到的地方。

FTAAShaderParameters,FSSDSignalTextures,FDOFGatherInputTextures,FDeferredLightUniformStruct

Screen pass pixel shader: ●SSS ●Motion Blur

Stenciltest: ●SSR Stencil Setup ●TAA Pixel Shader

Unreal 中的Shader 参数设置

对于RDG的理解需要一些简单的前置知识。也就是对shader进行参数。 我们的参数设置大体上而言分两种,第一种是只有某个shader能访问的局部shader parmeters,另一种是能够全局访问的uniformbuffer,这两种是使用不同的宏来进行实现的。

1.1 局部参数

1.1.1如何设置shader Parameter

如果我们想要为自己的shader设置一些参数,也就是我们是想生成一份自己的uniformbuffer,如下图左边所示。在C++中,右面就是他的对等结构。拥有这些参数能够保证我们的shader能够进行访问非自身的数据进行渲染处理。 https://img-blog.csdnimg.cn/20200217114604602.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

当然,我们不能直接写成上图中右图的数据结构来直接使用。在unreal 中,如果我们想要进行一个shaderParameter的设置,需要通过内部给的宏结构来进行配置,如下图所示,我们上面的结构可以写为: https://img-blog.csdnimg.cn/20200217114613749.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

宏 SHADER_PARAMETER_STRUCT将内部填写的所有数据,生成编译期的反射数据. https://img-blog.csdnimg.cn/20200217114628726.png

我们可以简单的看一下他到底会生成的是一个什么样的反射数据FShaderParametersMetadata https://img-blog.csdnimg.cn/20200217114631805.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

其中比较重要的是他的FMeber部分,在这个部分里包含着我们刚刚在宏内部写的那些数据。总而言之,这里的shader Parameter的设置和之前的unreal 渲染管线中的设置方式并没有什么不同,完全一样。

1.1.2 对齐原则

当然在设置中我们可能还需要注意一些简单的问题。由于unreal 采用16字节自动对齐的原则,所以在编写代码时,我们实际上对任何成员的顺序是需要注意的。例如下图中的顺序调整。

宏系统的另一个特性是着色器数据的自动对齐。Unreal引擎使用与平台无关的数据对齐规则来实现着色器的可移植性。

主要规则是,每个成员都是按照其大小的下一个幂进行对齐的,但前提是大于4个字节。例如:

指针是8字节对齐的(即使在32位平台上也是如此);

  1. 浮点、uint32、int32是四字节对齐的;

  2. FVector2D,FIntPoint是8字节对齐的;

  3. FVector和FVector 4是16字节对齐的。

每个成员的自动对齐将不可避免地创建填充,如上面的注释所示。

https://img-blog.csdnimg.cn/20200217114659878.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

1.1.3 不手动合并

https://img-blog.csdnimg.cn/20200217114706531.png https://img-blog.csdnimg.cn/20200217114711169.png

1.1.4 和shader绑定

当我们申请完我们的shaderParameter后,我们需要通知那些shader是使用着个shaderParameter的。如何做到这一步呢。我们在shader的声明宏的下部增加一个SHADER_USE_PARAMETER_STRUCT https://img-blog.csdnimg.cn/20200217114725537.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

或者我们可以使用内联的函数定义

https://img-blog.csdnimg.cn/20200217114729751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

1.1.5 数据设置和提交

https://img-blog.csdnimg.cn/20200217114737781.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70 note:在注释中我们能看到

Notes: Long therm, this macro will be no longer be needed.

1.2 uniform buffer

申请uniform buffer 形式上没有什么不同

https://img-blog.csdnimg.cn/20200217114746980.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

在写完后,我们需要再调用一个实现的宏,后面的字符串是在最终的hlsl中的真实姓名

https://img-blog.csdnimg.cn/20200217114754754.png

在unreal 的体系下,其Common.ush会引用代码生成的

https://img-blog.csdnimg.cn/20200217114802904.png

这个生成的.ush就是我们在代码中设置的uniformbuffer,所以我们可以在任何地方进行访问。

并且最后生成的代码是以"uniformbuffer名字.成员变量名"的形式存在的

https://img-blog.csdnimg.cn/20200217114807820.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

1.2.1Parameter struct的引用

我们就可以在Parameter struct中引用我们的uniformbuffer

https://img-blog.csdnimg.cn/20200217114815841.png

https://img-blog.csdnimg.cn/20200217114818426.png

创建和设置RenderGraph

接下来我们将要学习如何创建RenderGraph。最简单的,使用FRDGBuilder进行创建,然后及逆行执行,这是我们的头尾需要运行的。

https://img-blog.csdnimg.cn/20200217114826537.png

建立texture

之前的的申请texture的基本流程比较复杂和固定,如今可以实时的在RDG中进行

https://img-blog.csdnimg.cn/20200217114842685.png

https://img-blog.csdnimg.cn/20200217114846878.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

1.2为贴图建立SRV,UAV

https://img-blog.csdnimg.cn/20200217115934485.png

https://img-blog.csdnimg.cn/20200217115939276.png

1.3 建立RDG的参数和pass

之前我们讲解了普通的shader参数的创建,在这里,我们对RDG及逆行Pass参数的创建。

https://img-blog.csdnimg.cn/20200217115943197.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

然后,我们将一个pass添加到渲染管线

https://img-blog.csdnimg.cn/20200217115947594.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

并且我们将输出一些简单的调试Event

https://img-blog.csdnimg.cn/20200217115952427.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

上面这些代码我们大概经历三个阶段

  1. 分配参数
  2. 建立参数
  3. 将参数链接到pass

在我们的最后,会经历一个lambda的函数,

1.4 建立和绑定 ColorRender Target,Depth Stencil Target https://img-blog.csdnimg.cn/2020021712000790.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

编写使用光栅管道的pass时,请将RENDER_TARGET_BINDING_SLOTS()宏添加到过程参数结构中。这将公开渲染目标和深度模板的输入,这些输入将由过程拾取。这些数据将被材质球忽略。

https://img-blog.csdnimg.cn/20200217120011844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

RT作为绑定数组公开。每个绑定都接受RDG纹理和加载/存储操作。

https://img-blog.csdnimg.cn/20200217120016522.png

同样,深度模板作为单独的绑定。它还分别接受RDG纹理以及深度和模板的加载/存储操作。此外,还可以指定纹理是读还是读写。

https://img-blog.csdnimg.cn/20200217120020225.png

也可以使用UAV访问

https://img-blog.csdnimg.cn/20200217120023739.png

RDG当前使用IpooleRenderTarget接口来控制纹理的分配。有时需要将现有资源导入到RDG中(特别是在RDG转换过程中)。Builder公开RealStices外部纹理,它返回由现有渲染目标支持的RDG纹理实例。

https://img-blog.csdnimg.cn/20200217120027475.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

还可以从FRDGTexture中提取rt指针。这允许您跨rdg调用保留资源的内容。但是,提取会延迟到图形执行完毕;这是因为在执行期间可能会根据图形中资源的生存期来分配资源。因此,API公开了QueueTextureExtraction,它允许您提供一个指针,该指针将在graph执行时填充。

1.5 建立buffer

https://img-blog.csdnimg.cn/20200217120037615.png

https://img-blog.csdnimg.cn/20200217120040781.png

1.6 使用SRV入去buffer

如果一个buffer使用了SHADER_PARAMETER_RDG_BUFFER_SRV().着色器只能通过SRV读取。

https://img-blog.csdnimg.cn/20200217120047646.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

1.7 间接draw / dispatch buffer

间接的draw / dispatch buffer比较特殊独特,因为它们不是由着色器直接使用的。相反,将它们声明为pass参数上的RDG缓冲区,然后直接在pass中使用RHI间接绘制缓冲区。 https://img-blog.csdnimg.cn/20200217120055358.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

Pass debug

RDG自动向可视化纹理工具公开FRDGTexture。任何写入操作都将被记录。只需在控制台中直接输入给CreateTexture()的调试名称,它就会出现。如果有多个pass使用相同的调试名称修改或重新定义新纹理,则捕获过程将捕获所有这些过程,但仅显示最后一个捕获实例。

https://img-blog.csdnimg.cn/20200217120113338.png

https://img-blog.csdnimg.cn/20200217120116603.png

等等

4.Screen Pass Framwork

什么是Screen Pas呢,如果一个pass,其输入仅有图片,输出也仅是图片的pass,我们称之为ScreenPass。在这里其实大家会觉得后处理好像也是这样的名称,但我们这里仍然将之称为ScreenPass。比如SSS是光照组成的一部分,而不是后处理,并且这样的称谓非常的直观。

对于一个Screen Pass而言,它需要一些Shader parameters:贴图,viewport等Pixel /compute shader

我们对于一个Screen Pass来说,我们是需要一个简单的screen triangle或者对于VR来说我们需要的是HMD hiddenarea mesh .由于一个Viewports非常的重要,及逆行设置 https://img-blog.csdnimg.cn/20200217120139625.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

我们要简单的理解 Texture Viewports是和输入输出没有什么关系

https://img-blog.csdnimg.cn/20200217120143147.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

我们要建立一个Texture Viewport parameters

https://img-blog.csdnimg.cn/20200217120148853.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

当然你是可以更改这些东西的,然后将其作为你的一个参数

https://img-blog.csdnimg.cn/20200217120157991.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70 我们在hlsl中进行定义,在ScreenPass.ush

https://img-blog.csdnimg.cn/20200217120203277.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

为了使使用像素着色器更容易,该框架包含一个函数,用于将像素着色器过程直接添加到rendergraph。它抽象了一些细节,比如是使用HMD网格还是使用全屏三角形。提供输入和输出纹理视口,实现将自动处理RHI视口设置和UV坐标生成。如果需要更多的控制,则存在此函数的其他低级变体;例如,如果您需要手动设置您的pass并提交到命令列表。

https://img-blog.csdnimg.cn/20200217120208784.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70

最后,该框架提供了一种简单的变换类型,用于将一个视口空间中的UV坐标映射到另一个视口空间。要实例化,只需传递资源和目标的Viewports。在着色器代码中,将缩放/偏移因子应用于源UV坐标。在着色器中,可以使用SCREEN_PASS_TEXTURE_VIEWPORT_TRANSFORM宏快速定义变换。

https://img-blog.csdnimg.cn/20200217120219678.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMDMwODQz,size_16,color_FFFFFF,t_70