前言
很多时候我们可能需要在运行时编辑网格,或者干脆直接程序生成,大多数时候可能用UProceduralMeshComponent就搞定了,但是目前版本(5.1)UProceduralMeshComponent还处于实验阶段,虽说足够易用但有时候还是会有其他问题,这时候就需要深入底层,去研究一下从我们建立的indexbuffer,vertexbuffer,是如何扔给管线去渲染的,如何更新他们更加优雅,以及如果想实现自己的Vertex shader来配合管线等等,好了开始吧
现成的轮子们
首先来总结下UE里现成的轮子都有哪些,以及他们都是怎么做的
FDynamicMeshBuilder
首先是FDynamicMeshBuilder,其实如果研究mesh最简单的不是UProceduralMeshComponent而是FDynamicMeshBuilder,寥寥两三百行就把如何最简单的配置图元方法展示了出来,提供的方法也简明扼要,在类声明上面的注释里也有说明

通过全局搜索也会发现该类在引擎中大部分都是用来绘制debug信息用的

接下来我们直接跳到Draw函数,很明显它就是那个把信息丢给管线的函数,至于各个参数的含义在此不表,注释里写的蛮清楚了

点进去,会发现Draw函数首先将一些渲染资源(vertexbuffer,indexbuffer,VertexFactory和PrimitiveUniformBuffer)调用RegisterDynamicResource扔给了PDI,这里的PDI其实就是一个我们需要绘制的视口接口类

前两种buffer很好理解,index直接继承自FIndexBuffer,vertex由顶点位置颜色切向UV组成,而buffer类名里有Pooled,很明显跟普通的buffer相比添加了池化的功能,池化实现也很简单,从一个FDynamicMeshBufferAllocator类的池中分配资源

而VertexFactory在这里的类型继承自FLocalVertexFactory和FDynamicPrimitiveResource,FDynamicPrimitiveResource只是一个接口,而FLocalVertexFactory是用的最多的基类,里面配置了一些VS和顶点布局之类的东西,比如上面组成顶点信息的几个值都可以在这里找到,除此之外还有那个PrimitiveUniformBuffer也在FLocalVertexFactory里有所说明,所以除非你需要自定义VS否则大多时候直接用FLocalVertexFactory就可以,更细节的内容这个有更好的文章讲过了在这里贴个链接就不做重复工作了
Creating a Custom Mesh Component in UE4(Part 2)
至于RegisterDynamicResource函数,点进去会发现他注册并初始化了对应的RenderResource,具体的函数就是FRenderResource::InitResource(),这个函数在下面要说的其他mesh类中也会被调用,无论形式如何,所有渲染资源的初始化都会最终走到这里

初始化完渲染资源再往下就是配置图元了,把上面注册的渲染资源设置上,渲染选项配置正确,最后PDI->DrawMesh,清空类下面的指针,因为已经交由PDI托管了.

UProceduralMeshComponent
接下来是大名鼎鼎的UProceduralMeshComponent(PMC),比上述的FDynamicMeshBuilder稍复杂一点点,也是我们用的最多的程序生成mesh类,同时也是一个Component,这样还可以方便与现有的gameplaye框架结合组装actor
首先我先简明扼要的描述一下UE的多线程渲染是怎么回事,结合PMC自顶向下的描述如何组织数据结构与数据同步,头次接触的朋友可能会一头雾水(比如我),结尾我会给出写的贼棒的大佬们的参考文章
UE中将渲染线程从游戏线程中分离了出来(其实还有个RHI线程,不过这里不谈),两条线程并行,且拥有隔离开来的数据数据防止竞争,对应一下就是
| GameThread | RenderThread |
|---|---|
| UProceduralMeshComponent | FProceduralMeshSceneProxy |
| FProcMeshSection | FProcMeshProxySection |
类似的命名规则在源码中比比皆是,基本可以认为AnythingProxy就是游戏线程Anything在渲染线程的替身(砸瓦鲁多!),分别存储了在各自线程中各自需要的数据,比如上表中FProcMeshSection就比FProcMeshProxySection多放了一个包围盒,包围盒显然不是渲染线程该关心的数据,而FProcMeshProxySection则相较于FProcMeshSection多了Material和VF等数据,而且就算是相同的buff数据,存储方式也是不同的


接下来再说生命周期,游戏线程里的数据自然是我们创建组件的的时候创建,而渲染线程的资源则是引擎帮我们自动创建好的,通过覆写CreateSceneProxy()函数来创建不同的代理(第一行的宏是测性能用的,这里我们并不关心)
通过调用MarkRenderStateDirty()来重新生成代理类,剩下的就不需要我们操心了
另外其实很多时候不需要调用MarkRenderStateDirty(),通过查找在UActorComponent中的注释,我们会发现还有其他的标记方法:
在其他源码中,如UCableComponent,我们会发现标记为MarkRenderDynamicDataDirty(),这样可以不用整个更新,改为这个标记后调用的就是CreateRenderState_Concurrent了,这样在这个函数中只需要把新的数据copy过去,可以省下部分操作,其余的MarkRenderTransformDirty和MarkRenderInstancesDirty同理
让我们回到FProceduralMeshSceneProxy的构造函数中,第一句注释就会看到先把所有的section从游戏线程copy过来,顶点数据copy的时候甚至还用的是上面FDynamicMeshBuilder中相同的顶点类型的同时甚至还有一个专门用于转换的函数……..后续会发现之所以用它是因为FStaticMeshVertexBuffers提供了基于这个类型初始化的接口InitFromDynamicVertex,而这个函数其实也不复杂,不愿绕圈圈直接用FProcMeshVertex其实也行…..
再接下来就是渲染资源初始化了,其实调用的还是FRenderResource::InitResource(),注意这个函数可以在游戏线程执行,而且这个代理类的构造函数是在游戏线程执行的,不过一样还是抛给渲染线程做的
再往下就是设置材质可见性什么的了
那么如何把数据抛给管线呢?
我们可以看到在GetDynamicMeshElements()这个函数中有曾相识的操作,配置buffer和vf,然后渲染类型,最后AddMesh(),跟上面的FDynamicMeshBuilder如出一辙.
那我想要更新这些数据来做到运行时编辑呢,或者新增一个Section?
我们可以从PMC中看到UpdateMeshSection和CreateMeshSection两类函数,CreateMeshSection函数还是在操作PMC中的Section,并在最后一行看到了我们前面曾经看到过的MarkRenderStateDirty(),也就是重新创建一个代理类来满足新增的Section,从而实现新增.
而更新Section则不同,首先还是更新PMC中的数据,然后打一个更新包,更新包中包含了需要的数据,把更新包压入渲染线程
而渲染线程更新的逻辑则是先把代理类中的数据更新好,然后会出现一大长串的copy,用于向RHI提交我们的tertex四君子,最后RHI再把数据塞进显存
显然这个过程其实是有很多问题的,比如这里数组长度其实是不可增的,如果需要增加的话肯定会溢出,所以只能整个删掉重来,或者当只需要在一个大大的mesh里更新一点点数据数据也是整个copy
而且万一buff很大且多,以DX11为例,在RHILockBuffer的时候会flush,所以我在测试的时候大批量更新会顿卡,DX12同理(感谢群里大佬答疑)
所以如果需求更加特别导致PMC不能满足需求,UE还给我们提供了一个更加离谱的东西,那就是接下来要说的了
UDynamicMeshComponent
其实这个组件的使用在已经有非常专业的文章来讲解了,翻译的版本也在知乎上,UE5里只是改了个名字
那么在这里前人说过的内容就不多说了,主要还是来看一下上述问题在这里是如何解决的
在对应的FDynamicMeshSceneProxy中,所有的buffer,vf什么的都放进了FMeshRenderBufferSet这个对象里
然后点进去会发现 它给予了用户更多可操控的手段,比如仅更新被更改过的buffer
也可以当buffer不够大的时候使用UpdateRHI()释放调RHI资源重新申请,经测试居然可以规避掉上述顿卡的问题,不过之前曾经怀疑频繁申请释放会不会造成显存碎片等问题,后经资料查询+源码对比会发现,在DX环境下UE会使用BuddySystem来分配upload buffer和default buffer,可以在Engine\Source\Runtime\D3D12RHI\Private\D3D12Allocation.h下看到,所以应该不会有很大的问题
至于更新大范围内的小部分数据还有UOctreeDynamicMeshComponent可供选择,不过需要自行维护一个八叉树,该类没有提供更好的方法,如何使用就只能自己扣源码了,Google上这个类的信息甚至连一页都搜不出来,不过源码在对整体有把握之后其实难度已经不高了,剩下的就是发挥工匠精神的时候
自定义UPrimitiveComponent
经过上面的探索,会发现其实自定义一个PrimitiveComponent也不是很难,综合一下抄一抄很容易就能跑起来,不过具体值不值得还是要再商酌一下的,这里贴一个自制的很简单的类MinecraftVoxelComponent,很多东西没处理凑活着看吧,祝生活愉快
参考
原文发表于知乎。