虚幻引擎 自定义VertexFactory(三)Draw Indirect

1

主题

3

帖子

6

积分

新手上路

Rank: 1

积分
6
发表于 2023-2-10 15:55:05 | 显示全部楼层
引言

之前的文章中,我介绍了如何修改Vertex Factory来自定义自己的顶点渲染流程。这篇文章中我将介绍如何实现Instancing,以及将资源的准备转到GPU端实现DrawIndirect流程。如果你还没阅读过前面的文章,请先快速浏览一下,我的代码都是从之前顺下来的,如果不看可能会有不好理解的地方。代码基于引擎版本4.26.2,默认读者知道Instancing渲染模式,对于DrawIndirect会稍微带过一下。
一、Instancing

为了简单起见,我们还是基于动态光照。实现Instancing我们需要两个数据,origin和transform。
Shader端

跟TexCoord一样,我们使用Manual Vertex Fetch。所以Shader端我们一样从uniform buffer,也就是CustomVF里取值。为了兼容性,我们在shader里定义一个USE_MYINSTANCING来控制是否编译Instancing代码。这个编译环境的修改是放在C++代码里实现的,shader中只需要使用即可。
如果你之前没有在FVertexFactoryInput中声明 InstanceId,那么请补上。 同样在 FPositionAndNormalOnlyVertexFactoryInput 和 FPositionOnlyVertexFactoryInput 中,也需要补充。



声明InstanceId

在FVertexFactoryIntermediates 这个中间量中,我们需要添加上InstanceOrigin和Transform,为了之后计算世界位置使用。


在中间找一个地方,我们定义一些必要的获取和计算Transform的函数。


接下来我们就开始修改之前的代码,让它能兼容Instancing的情况。
首先是计算Tangent。


然后是GetVertexFactoryIntermediates,因为我们添加了对应的成员,需要给这些变量赋值。


对应三种Input的 VertexFactoryGetWorldPosition。






到这里Instancing的shader代码已经修改完毕了。可以看到由于我们按照虚幻引擎官方的LocalVertexFactory来写,很方便就可以拓展新的功能。
C++端

在C++端,我们只需要绑定对应的两个新的SRV即可。首先我们先定义USE_MYINSTANCING。我们先做两个准备工作,首先在CustomVertexFactory中的ModifyCompilationEnvironment中来控制


然后我们检查CalcBounds这个函数,我自己写的时候Bound只有原来的Cube那么大,现在需要扩大到一个固定值,不然会被Cull掉。


声明UniformBuffer中两个新的SRV。



在Component里添加一个InstanceCount用来控制Instance的数量,同样在SceneProxy中也创建一个InstanceCount。





SceneProxy中也需要一个InstanceCount

在SceneProxy中定义两个SRV的Ref。


在CreateRenderThreadResources中,我们来创建InstanceTransform。


在GetDynamicMeshElements中,我们将InstanceCount传给BatchElement中的NumInstances。


这里我们就可以运行了。打开引擎观察到我们已经可以在DynamicLighting的情况下正确创建Instanced Mesh了。


调用的Draw Command也变成了Draw Indexed Instanced。


二、DrawIndirect

介绍

DrawIndirect,和传统的CPU发送指令,GPU接受绘制指令不同——例如我们之前实现的,每个MeshBatchElement都需要我们显式传值,而是通过一个Buffer来传递参数。



DrawCall参数的显式传值

这样我们就可以使用ComputeShader来修改这个Buffer里的参数值,从而利用GPU的高效并行优势,来实现诸如GPU Culling这样的优化。
修改DrawIndirect的核心,就是将我们的DrawCommand变成一个DrawIndirectCommand,以及传递DrawIndirectArgs这个Buffer。
我们先来处理DrawIndirectArgs的生成。
ComputeShaderManager

我们新建ComputeShaderManager的.h 和.cpp文件,这个类将用于Dispatch 生成IndirectArgs buffer的compute shader。


首先我们定义一个DrawIndirectArgsGenCS的compute shader。


需要注意最后三个参数,DrawIndirectArgsParm是用来传递IndirectArgs的UAV;NumIndicesPerInstanceParm是每个Instance的Index数量;InstanceCountParm就是Instance数量。对于虚幻而言,DrawIndirectArgs的结构如下。


0是Index数量,1是Instance的数量,2是Index的起始位置,3是顶点的,4是Instance开始的位置。这些Location都是指在Buffer中的位置。对于这次的教程,BaseVertexLocation和StartInstanceLocation都是0,所以我就直接没传值,你也可以暴露出来。
我们在Shaders文件夹下新建一个ComputeShaders并将这个路径添加到Shader Directory里。


创建一个DrawIndirectArgsGenCS.usf文件。ArgsGenCS = Arguments Generation Compute Shader。


因为现在功能很少,所以shader代码也很短,只需要将参数填充到buffer中对应的位置即可。


注意StartindewxLocation,目前也是从0开始,所以同样也没有传值。
在.cpp文件中,实现一下刚才的ArgsGenCS。


因为我们现在只需要一个线程来赋值,所以我们填充1,对应刚才ArgsGenCS中的numthreads。


接下来实现一些必要的函数。


回到.h文件中,我们定义一个FArgsGenInfo结构体,方便传递和更新参数。


最后我们定义我们的Manager类。


注意里面的bFlag是防止重复创建UAV用的,采取其他方式也可以。不然会导致每次资源都会被释放,从而无法正确在屏幕上绘制。
.cpp文件中,我们先定义这些函数。


AddDrawIndirect是用于添加DrawIndirect 任务用的。现在我们只有一个任务,今后可能会有多个任务同时需要处理,我们先实现一个任务的版本。


我们先初始化我们的GenInfo。


现在我们用CPU代替了原本用于创建这些的 shader,所以我们先使用这种方式来处理当Instance数量变化的情况,当我们在Component中修改了Instance Count,那么这里也会对应的重新初始化一个DrawIndirectBuffer。


最后我们将参数填充到ResourceArray中并生成RWBuffer。


TransitBufferToIndirectArgs是将RWBuffer的UAV转成IndirectArgs,这是一个好习惯,如果不转换可能会有未知的错误,甚至无法正常运行。


IssueDrawIndirectTask是最终Dispatch compute shader的函数。同样的,Transititon以及Unbind Buffer在使用前和使用后。


Manager实现完,回到Component,我们定义一个布尔值来控制是否使用DrawIndirect流程。


在SceneProxy中,定义一个同样的bool。


定义一个ComputeShaderManager的指针。


在构造函数中初始化新的变量。


在GetDynamicMeshElements中,在每个View的循环里,我们需要添加DrawIndirect任务,然后Dispatch CS,最后将Buffer的状态Transit到IndirectArgs。其实这个过程放在任意一个可以在场景中tick的函数均可,放在这里可以保证在BatchElement提交渲染命令之前,DrawIndirectArgs一定是有资源的。之后我们会再次回到这个话题。


最后需要让MeshBatcher知道我们的draw command是一个draw indirect command,所以我们要将NumPrimitives置0,当这个值为0,且IndirectArgsBuffer有值的时候,就会issue一个draw indirect command。


在刚才我们修改instance count的地方,我们加一个判断,当使用DrawIndirect的时候,将所需参数设置正确。


在引擎中我们开启DrawIndirect之后,可以看到在RenderDoc截帧的时候已经可以看到我们的draw indirect command了。




三、GPU提交buffer资源

GPU-driven

先来看一下我们大概的GPU-driven的数据是怎么传输的。这部分是根据虚幻官方的Niagara插件代码研究得来的。我们只展开一个mesh的情况,多个mesh的情况较为复杂。
因为准备Instance数据的数据来源是上一个compute shader的buffer,例如GPU culling阶段。这个阶段CPU无法获取culling的结果,或者说这样readback回来不合理。那么这个时候CPU在一开始分配资源的时候就需要给一个足够大的值,buffer才不会不够用。如果这次写回之后发现Buffer不够了,下一次可以Resize Buffer,把Buffer容量增加。
例如要渲染一片草地,我可以开一个1000个instance数据的buffer,最后我们需要提交的DrawIndirect里面的InstanceCount则是由之前Culling等一系列其他compute shader决定的。假设最后我们需要渲染100株小草,那么剩下的空间虽然看起来很浪费,但是对于现在GPU越来越大的显存而言,我们更希望一次性将资源全部上传到GPU上,从而减少CPU和GPU之间通信消耗的时间,例如减少DrawCall。
那么根据上面所述的需求,我们需要:
1.一个用于运算Instance数据的Shader。
2.用于传递Instance相关数据的Buffer。
3.Instance Buffer。
实现

接下来我们将刚才实现的CPU端准备instance buffer和instance的数量的这个流程,转成GPU流程。首先我们可以先将之前的InstanceCountParm改成InstanceArgsParm,因为之后可能还需要传递更多的Instance相关参数,所以我们使用一个UAV来在1.生成instance count的compute shader中,2.生成indirect draw args的compute shader中传递值。



修改参数类型



构造函数



SetParameters修改定义



unbind buffer

然后我们把ArgsGenCS中的代码的地方修改一下。


接下来需要写一个生成Instance Buffer的compute shader。
我们现在.h文件里添加一个新的定义。


然后我们将需要的函数补全,和ArgsGenCS一样。我们需要定义一个用于Instance Buffer生成的线程数量,我们定为256,以后会根据最大InstanceCount数量来计算。



初始化与参数绑定

因为上游没有Instance Count,我们先生成一个假的Instance Count来控制一下生成Instance 数据,用于测试功能。之后这个参数是需要删除的。



设置资源



解绑参数

新建一个InstanceBufferGenCS.usf,里面做的事情就是通过TaskIndex来给一些坐标。


然后我们在Manager中,补充一些Buffer成员和初始化Buffer的函数。



Buffer成员



Buffer SRV Getter


注意修改IssueDrawIndirectTask里面的SetParameter。原先参数是一个Int,这里需要改成我们新创建的UAV。


回到SceneProxy,我们需要在构造函数中初始化Buffer资源。



FCustomPrimitiveSceneProxy::FCustomPrimitiveSceneProxy

在CreateRenderThreadResources中,我们需要在两种模式下设置我们的Instance Buffer。



FCustomPrimitiveSceneProxy::CreateRenderThreadResources

最后我们在之前IssueDrawIndirectTask的地方,进行我们InstanceBuffer数据生成的Task,就可以完成整个渲染的步骤了。



FCustomPrimitiveSceneProxy::GetDynamicMeshElements

打开引擎,观察到如下结果。

GPU Instancing 虚幻4
https://www.zhihu.com/video/1603717631005192193
打开RenderDoc,可以观察到我们的DrawIndirect cmd,以及正确的资源绑定。


四、结语

这篇文章的主要Take away是,实现使用GPU来准备渲染资源,同时使用Compute shader来提交Draw Indirect 渲染指令。
扩展思路,我们可以做出如下效果。使用一张高度图作为坐标输入,然后对每个像素的高度采样,在上面生成一个Instance。这个修改比较简单,就不展开讲解了。


接下来我们就需要实现GPU Culling的流程,继续实现真正的GPU driven instance绘制管线。
回复

举报 使用道具

3

主题

6

帖子

12

积分

新手上路

Rank: 1

积分
12
发表于 2023-2-10 15:55:37 | 显示全部楼层
感谢分享,最近也在看这方面的资料。楼主能在github上传一份代码吗?
回复

举报 使用道具

3

主题

5

帖子

12

积分

新手上路

Rank: 1

积分
12
发表于 2023-2-10 15:56:17 | 显示全部楼层
暂时没有上传的计划,大部分代码都是来自虚幻niagara system。
回复

举报 使用道具

您需要登录后才可以回帖 登录 | 立即注册
快速回复 返回顶部 返回列表