立即注册
登录
搜索
前端开发
后端开发
虚幻引擎
U3D引擎
体感研发
数据库
论坛
BBS
本版
帖子
用户
麒麟软控
»
论坛
›
麒麟软控
›
虚幻引擎
›
虚幻引擎 自定义VertexFactory(三)Draw Indirect ...
返回列表
发新帖
虚幻引擎 自定义VertexFactory(三)Draw Indirect
品牌投资方
品牌投资方
当前离线
积分
4
1
主题
2
帖子
4
积分
新手上路
新手上路, 积分 4, 距离下一级还需 46 积分
新手上路, 积分 4, 距离下一级还需 46 积分
积分
4
发消息
发表于 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绘制管线。
上一篇:
教你使用MetaHuman制作虚拟数字人
下一篇:
「上海俊柯」产业内卷的时代‘虚幻引擎’能否出圈?
回复
举报
使用道具
分享
故事仍在南海
故事仍在南海
当前离线
积分
12
3
主题
6
帖子
12
积分
新手上路
新手上路, 积分 12, 距离下一级还需 38 积分
新手上路, 积分 12, 距离下一级还需 38 积分
积分
12
发消息
发表于 2023-2-10 15:55:37
|
显示全部楼层
感谢分享,最近也在看这方面的资料。楼主能在github上传一份代码吗?
回复
举报
使用道具
我能相信谁
我能相信谁
当前离线
积分
10
3
主题
4
帖子
10
积分
新手上路
新手上路, 积分 10, 距离下一级还需 40 积分
新手上路, 积分 10, 距离下一级还需 40 积分
积分
10
发消息
发表于 2023-2-10 15:56:17
|
显示全部楼层
暂时没有上传的计划,大部分代码都是来自虚幻niagara system。
回复
举报
使用道具
返回列表
发新帖
高级模式
B
Color
Image
Link
Quote
Code
Smilies
您需要登录后才可以回帖
登录
|
立即注册
浏览过的版块
体感研发
前端开发
后端开发
数据库
U3D引擎
快速回复
返回顶部
返回列表