版权声明:本文为CSDN博主「Papals」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
什么是 RDG
渲染依赖性图表(Rendering Dependency Graph) 也被称为 RDG 或渲染图表,是一个基于图表的调度系统,用于执行渲染管线的整帧优化。它利用 DirectX12 这样的现代 API ,使用 自动异步计算调度 以及更高效的 内存管理 和 屏障管理 来提高性能。
RDG 的概念如下:在 GPU 上并非立即执行通道,而是延迟到整个帧已记录到依赖性图表数据结构之中后再执行。当完成了对所有通道的收集之后,则按照依赖性的排序顺序对图表进行编译和执行。这主要是因为 DirectX 12、Vulkan 和 Metal2 之类的现代图形 API 选择将低级 GPU 管理的负担转移至应用程序本身。这提供了一个机会,可以利用渲染管线的高级情境来驱动调度,从而提高性能并且简化渲染堆栈。
我们今天的 RDG 有什么好处,就要看之前的方式有多菜。来看一下传统的下图分别是 07 年和 17 年的渲染系统的复杂程度
可以参考: Frostbite Rendering Architecture
为什么需要 RDG
这里当然不是非常的具体,不过我们都能看出这里非常的复杂。
当然,我们可以进行一些简单的简化,如下图
战地四的 pass 数(Feature)
面临的挑战:
- 显示的代码驱动的即时渲染模式
- 显示的资源管理
- 手动的 ESRAM 管理
- 每一个不同的游戏团队有着不同的实现方式
- 不同的渲染系统有紧密的耦合性
- 对于管线定制的扩展性差
- 游戏团队必须对这些定制 fork/diverge
- 代码数飞速增长,将产生很多重复性的代码
所以,我们需要将整个的渲染结构进行一个更高层次的抽象,让其拥有更高的扩展性,模块化,更好的可视化和 debug ,资源的自动管理,解耦模块并使其可自由组合。
这就是新的结构:
RDG 设计理念
我们将抛弃即时的渲染模式;将渲染代码分离到 pass 中,大致分为三个阶段
- Setup phase
- Compile phase
- Execute phase
Setup phase
我们将定义这个 pass 是一个 render pass, 还是 compute pass 我们将定义每一个 resource ,对于输入和输出对于 renderpass ,我们要定义资源的使用其是 read,write,还是 create。当然对于长期贮存的资源我们是要 imported 进 RDG 中,例如 History buffer for
TAA,Backbuffer
Compile phase
- 剔除没有使用的资源和 pass
- 计算资源的生命周期
- 根据使用情况分配实际的 GPU 资源
Execute phase
- 对于每个 pass 执行回调函数
- 这个阶段就是跟我们的之前的渲染流程一样的执行 draw,dispatch
Unreal RDG 实例
由于 Unreal 4.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 ,另一种是能够全局访问的 uniform buffer ,这两种是使用不同的宏来进行实现的。
1.1 局部参数
1.1.1 如何设置 shader Parameter
如果我们想要为自己的 shader 设置一些参数, 也就是我们是想生成一份自己的 uniformbuffer,如下图左边所示。在 C++ 中,右面就是他的对等结构。拥有这些参数能够保证我们的 shader 能够进行访问非自身的数据进行渲染处理。
当然,我们不能直接写成上图中右图的数据结构来直接使用。在 unreal
中,如果我们想要进行一个 shaderParameter 的设置,需要通过内部给的宏结构来进行配置,如下图所示,我们上面的结构可以写为:
宏 SHADER_PARAMETER_STRUCT 将内部填写的所有数据,生成编译期的反射数据.
我们可以简单的看一下他到底会生成的是一个什么样的反射数据 FShaderParametersMetadata
其中比较重要的是他的 FMember 部分,在这个部分里包含着我们刚刚在宏内部写的那些数据。总而言之,这里的 shader Parameter 的设置和之前的 unreal
渲染管线中的设置方式并没有什么不同,完全一样。
1.1.2 对齐原则
当然在设置中我们可能还需要注意一些简单的问题。由于 unreal 采用 16 字节自动对齐的原则,所以在编写代码时,我们实际上对任何成员的顺序是需要注意的。例如下图中的顺序调整。
宏系统的另一个特性是着色器数据的 自动对齐 。Unreal 引擎使用与平台无关的数据对齐规则来实现着色器的可移植性。
主要规则是,每个成员都是按照其大小的下一个幂进行对齐的,但前提是大于 4 个字节。例如:
指针是 8 字节对齐的(即使在 32 位平台上也是如此);
float、uint32、int32是 4 字节对齐的;FVector2D,FIntPoint是 8 字节对齐的;FVector和FVector4是 16 字节对齐的。
每个成员的自动对齐将不可避免地创建填充,如上面的注释所示。
1.1.3 不手动合并
1.1.4 和 shader 绑定
当我们申请完我们的 shaderParameter 后,我们需要通知那些 shader 是使用这个 shaderParameter 的。如何做到这一步呢。我们在 shader 的声明宏的下部增加一个 SHADER_USE_PARAMETER_STRUCT
或者我们可以使用内联的函数定义
1.1.5 数据设置和提交
在注释中我们能看到
Notes: Long therm, this macro will be no longer be needed.
1.2 uniform buffer
申请 uniform buffer 形式上没有什么不同
在写完后,我们需要再调用一个实现的宏,后面的字符串是在最终的 HLSL 中的真实姓名
在 unreal 的体系下,其 Common.ush 会引用代码生成的
这个生成的 .ush 就是我们在代码中设置的 uniformbuffer ,所以我们可以在任何地方进行访问。
并且最后生成的代码是以 "uniformbuffer名字.成员变量名" 的形式存在的
1.2.1 Parameter struct 的引用
我们就可以在 Parameter struct 中引用我们的 uniformbuffer
创建和设置 RenderGraph
接下来我们将要学习如何创建 RenderGraph 。最简单的,使用FRDGBuilder 进行创建,然后及逆行执行,这是我们的头尾需要运行的。
1.1 建立 texture
之前的的申请 texture 的基本流程比较复杂和固定,如今可以实时的在 RDG 中进行
1.2 为贴图建立 SRV, UAV
1.3 建立 RDG 的参数和 pass
之前我们讲解了普通的 shader 参数的创建,在这里,我们对 RDG 及逆行 Pass 参数的创建。
然后,我们将一个 pass 添加到渲染管线
并且我们将输出一些简单的调试 Event
上面这些代码我们大概经历三个阶段
- 分配参数
- 建立参数
- 将参数链接到 pass
在我们的最后,会经历一个 lambda 的函数,
1.4 建立和绑定 ColorRender Target,Depth Stencil Target
编写使用光栅管道的 pass 时,请将 RENDER_TARGET_BINDING_SLOTS() 宏添加到过程参数结构中。这将公开渲染目标和深度模板的输入,这些输入将由过程拾取。这些数据将被材质球忽略。
RT 作为绑定数组公开。每个绑定都接受 RDG 纹理和加载/存储操作。
同样,深度模板作为单独的绑定。它还分别接受 RDG 纹理以及深度和模板的加载/存储操作。此外,还可以指定纹理是读还是读写。
也可以使用 UAV 访问
RDG 当前使用 IpooleRenderTarget 接口来控制纹理的分配。有时需要将现有资源导入到 RDG 中(特别是在 RDG 转换过程中)。Builder 公开 RealStices 外部纹理,它返回由现有渲染目标支持的 RDG 纹理实例。
还可以从 FRDGTexture 中提取 rt 指针。这允许您跨 RDG 调用保留资源的内容。但是,提取会延迟到图形执行完毕;这是因为在执行期间可能会根据图形中资源的生存期来分配资源。因此,API公开了 QueueTextureExtraction,它允许您提供一个指针,该指针将在 graph 执行时填充。
1.5 建立 buffer
1.6 使用 SRV 读取 buffer
如果一个 buffer 使用了 SHADER_PARAMETER_RDG_BUFFER_SRV(). 着色器只能通过 SRV 读取。
1.7 间接 draw / dispatch buffer
间接的 draw / dispatch buffer 比较特殊独特,因为它们不是由着色器直接使用的。相反,将它们声明为 pass 参数上的 RDG 缓冲区,然后直接在 pass 中使用 RHI 间接绘制缓冲区。
Pass debug
RDG 自动向可视化纹理工具公开 FRDGTexture。任何写入操作都将被记录。只需在控制台中直接输入给 CreateTexture() 的调试名称,它就会出现。如果有多个 pass 使用相同的调试名称修改或重新定义新纹理,则捕获过程将捕获所有这些过程,但仅显示最后一个捕获实例。
等等
Screen Pass Framwork
什么是 Screen Pass 呢,如果一个 pass,其输入仅有图片,输出也仅是图片的 pass,我们称之为 ScreenPass。在这里其实大家会觉得后处理好像也是这样的名称,但我们这里仍然将之称为 ScreenPass。比如 SSS 是光照组成的一部分,而不是后处理,并且这样的称谓非常的直观。
对于一个 Screen Pass 而言,它需要一些 Shader parameters:贴图,viewport 等 pixel /compute shader
我们对于一个 Screen Pass 来说,我们是需要一个简单的 screen triangle 或者对于 VR 来说我们需要的是 HMD hidden area mesh .由于一个 Viewports 非常的重要,及逆行设置
我们要简单的理解 Texture Viewports 是和输入输出没有什么关系
我们要建立一个 Texture Viewport parameters
当然你是可以更改这些东西的,然后将其作为你的一个参数
我们在 HLSL 中进行定义,在 ScreenPass.ush
SCREEN_PASS_TEXTURE_VIEWPORT
为了使使用像素着色器更容易,该框架包含一个函数,用于将像素着色器过程直接添加到 RenderGraph。它抽象了一些细节,比如是使用 HMD 网格还是使用全屏三角形。提供输入和输出纹理视口,实现将自动处理 RHI 视口设置和 UV 坐标生成。如果需要更多的控制,则存在此函数的其他低级变体;例如,如果您需要手动设置您的 pass 并提交到命令列表。
最后,该框架提供了一种简单的变换类型,用于将一个视口空间中的 UV 坐标映射到另一个视口空间。要实例化,只需传递资源和目标的 Viewports 。在着色器代码中,将缩放/偏移因子应用于源 UV 坐标。在着色器中,可以使用 SCREEN_PASS_TEXTURE_VIEWPORT_TRANSFORM 宏快速定义变换。