虚幻引擎 自定义VertexFactory(一)

5

主题

7

帖子

18

积分

新手上路

Rank: 1

积分
18
发表于 2022-12-1 18:34:59 | 显示全部楼层
引言

在之前自定义MeshComponent那篇文章中,我们可以看到虚幻引擎内置了很多VertexFactory。那么如果我们要实现如复杂的效果,自定义自己的数据结构等等目的,就需要自定义VertexFactory。这篇文章中我会讲解如何自定义一个Vertex Factory,需要你对顶点渲染和顶点工厂的概念有一定的了解。如果文章中出现错误,或者你觉得更好的写法,请不吝指出。文章代码基于4.26.2。在第一篇中,我们先来全面梳理虚幻引擎中的顶点工厂,有哪些必须的函数和结构体,以及他们的作用。
一、准备工作

准备工作就是新建一个空的插件,然后需要映射一个存放Vertex Shader的路径。流程参考我写的这篇文章。
二、顶点渲染流程

在虚幻引擎中,我们可以拿来了解顶点渲染流程的shader文件主要是这两个:一个是PointCloudVertexFactory,另一个就是BasePassVertexShader.usf。
我们先来关注BasePassVertexShader.usf,看名字我们就知道这个shader是base pass用来渲染顶点的入口。我们先来看Main函数的参数。


其中红框中的部分我们可以暂时忽略,剩余的就只有三项:Input, VertexID 和 Output。我们只需要关心Input,因为Output的结构虚幻已经定义好了。我们来看Input用在哪里了。这里因为代码中和我们关注的东西无关的宏太多,建议直接去RenderDoc里截帧,然后观察编译出来的Shader代码,具体怎么做戳这里(施工中)。我截了一个放在这里作为参考。
void Main(
        FVertexFactoryInput Input,
        out  FBasePassVSToPS  Output
        )
{
        uint EyeIndex = 0;
        ResolvedView = ResolveView();

        FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
        float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates);
        float4 WorldPosition = WorldPositionExcludingWPO;
        float4 ClipSpacePosition;
        float3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates);
        FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPosition.xyz, TangentToLocal);
        {
                WorldPosition.xyz += GetMaterialWorldPositionOffset(VertexParameters);
        }

        {
                float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPosition);

                ClipSpacePosition =  mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip) ;

                Output.Position =  ClipSpacePosition ;
        }

        Output.FactoryInterpolants =  VertexFactoryGetInterpolantsVSToPS (Input, VFIntermediates, VertexParameters);
        ;
}
其中,除去ResolveView(),mul和GetMaterialWorldPositionOffset,其他的函数和一些结构体都是我们需要实现的。只要我们复用了BasePassVertexShader做我们的Vertex Shader,那么我们就需要实现这些函数,还有对应的结构体。我们首先来看几个最基础的结构体的数据结构,如果没有定义会导致编译报错的那种级别。我们可以参考着PointCloudVertexFactory.ush,建议在旁边打开,一遍对应一边看。
VS中的结构体

FVertexFactoryInput


定义输入VS的数据布局,需要匹配C++端FVertexFactory里的数据类型。注意这里的顺序和InitRHI里面的顺序一致。之后我们自己写的时候会再提到这个点。这里就是Vertex Shader参数中的Input的结构。可以看到,在PointCloudVertexFactory中只有顶点ID和InstanceID。

FVertexFactoryInterpolantsVSToPS


从VS传递到PS的顶点工厂数据。对应到VS里,就是VertexFactoryGetInterpolantsVSToPS的输出。因为VS之后,如果没有其他特殊处理,那么这个输出的Output结构体就需要这里的值进行Pixel Shader的渲染。在PointCloudVertexFactory中,只有Color值和一个DummyTexCoord。

FVertexFactoryIntermediates


用于存储将在多个顶点工厂函数中使用的缓存中间数据。这个数据主要是计算的时候方便传值用。结构的定义是用户自己定义的。回到Vertex Shader里我们会发现很多函数的参数里都有它,所以这个是一个很重要的结构。
有了这三个,我们的Vertex Factory就可以正确地运转了。接下来我们就来看必需的函数定义。
VS中的函数

GetVertexFactoryIntermediates


这个函数是用来计算该顶点对应的Intermedate的。前面的大段计算都是为了最后赋值做准备,这里就需要用户自己写一些函数来进行每个属性的具体操作。

VertexFactoryGetWorldPosition


从顶点着色器调用来获得世界空间的顶点位置。

VertexFactoryGetTangentToLocal


计算世界空间下的切线空间到本地空间的变换矩阵。

GetMaterialVertexParameters


返回每个顶点上的材质参数,例如我们看到顶点色、世界坐标等等。这是给材质编辑器里使用的顶点数据赋值的函数。
GetMaterialPixelParameters


计算PixelShader的参数。这个函数没有在上面的Vertex Shader上出现,是因为这个是Pixel Shader阶段调用的。我们可以看到例如顶点色和UV坐标,都是在材质编辑器里可以使用的Material 参数。
VertexFactoryGetRasterizedWorldPosition


返回ClipSpace计算前的位置,大部分都是直接返回输入的WorldPosition,极少数Vertex Factory会在这里做微调。

VertexFactoryGetInterpolantsVSToPS


这个地方就是返回最终我们计算后,用于构建VS到PS插值的结构的地方。例如UV坐标,Tangent向量等,都需要从这里通过FVertexFactoryInterpolantsVSToPS结构体传出。
总结

我们可以看到,对于BasePassVertexShader来说,只要能实现这些必须的函数,那么我们就可以正确地渲染出图像。它本身是一个含有函数入口的usf。与之对应的,是一个个VertexFactory,是含有函数定义的ush。虚幻就是这样来实现它的模块化结构的,当需要有新的顶点操作的时候,只需要改VertexFactory的ush,通过宏来控制编译shader文件就可以了。所以我们可以打开LocalVertexFactory来做对比,就能发现,LocalVertexFactory里面看起来很大的一个文件,其实归根结底是因为里面分支太多,导致函数在不同的宏控制下,定义会有很大区别。
三、具体实现

我们现在知道Shader端具体需要什么数据了,那么我们先趁热打铁把shader写好,再去考虑C++端是怎么实现的。这篇文章中我们要实现的是一个只会画Primitive的顶点工厂,后面我们会逐步添加法线、阴影、材质以及Instancing,以及DrawIndirect的GPU Instancing。
1. 编写VertexFactory Shader

在新插件准备好的Shader路径下,我们新建一个VertexFactory.ush。这里名字可以随便起,以下我按照我自己写的来举例。这里的VertexFactory的函数均需要实现,可以说是最简单的VertexFactory了,去掉其中的任意一个函数都会报错。
我们先引用一个VertexFactoryCommon.ush文件。打开这个文件我们可以看到里面有很多虚幻引擎已经准备好的例如空间转换等常用数学函数。



VertexFactoryCommon.ush

我们在我们的顶点工厂文件开头include它。


接下来我们一步步实现之前说的结构体和函数即可。
先是FVertexFactoryInput.


定义输入VS的数据布局,需要匹配C++端FVertexFactory里的数据类型。注意这里的顺序和顶点工厂中InitRHI里面的InAttributeIndex一致。



这里和VertexFactoryInput里的Attribute index匹配。

剩下的SV_VertexID和SV_InstanceID则是hlsl的语法,这里InstanceID我们暂时用不到。具体请看这里。
然后是FVertexFactoryInterpolantsVSToPs,这里我是直接抄的PointCloudVertexFactory。这里暂时用不到,写什么都可以。


FVertexFactoryIntermediates的实现也很简单。


必要的结构体写完了,接下来就是函数的实现。
GetVertexFactoryIntermediates,根据我们刚才Intermediates的定义,我们将Intermediates的数据赋值即可。


Input就是我们最开始定义的FVertexFactoryInput。
GetMaterialPixelParameters我们直接按照FVertexFactoryInterpolantsVSToPS的格式赋值就行了。


目前我们不需要这些数据是正确的,之后我们再回到这里把它改成需要的样子。
GetMaterialVertexParameters是顶点的数据,可以看到VertexColor等在材质编辑器里常见的变量。


VertexFactoryGetWorldPosition用于从顶点着色器调用来获得世界空间的顶点位置。这个TransformLocalToTranslatedWorld是在VertexFactoryCommon里定义好的,直接调用即可。


VertexFactoryGetRasterizedWorldPosition直接返回输入的额WorldPosition即可,看最上面VertexShader的代码可以得知,这里只是将带有WPO的位置输出一遍,对于Mesh来说不需要做特殊处理。


VertexFactoryGetInterpolantsVSToPS组装Vertex Shader 到Pixel Shader中需要插值的变量。目前我们不使用其他数据,所以直接传一个Dummy值防止报错即可。可以看到Interpolants的值是从Intermediate中来的。


VertexFactoryGetPreviousWorldPosition,顾名思义,获取上一帧的世界位置。这里我们直接先放回WorldPosition就可以了。


最后两个函数也是很容易理解。第一个VertexFactoryGetTangentToLocal是返回TangentToLocal矩阵。这里我们先给个Identity就行。下一个是VertexFactoryGetWorldNormal,因为现在我们没有光照计算,所以用不到Normal,所以直接给一个向上的法线就行。


至此,一个最小的VertexFactory就做完了。那么我们接下来就在C++端去实现渲染资源的准备、绑定、提交绘制等功能。
2. C++端

对于不了解MeshComponent的,建议先去看我的另一篇文章,里面有介绍写一套自定义MeshComponent的方法。
我们先新建一个继承自UMeshComponent的组件类。主要因为它实现了一些接口,省去了我们的额外工作。


主要这个component需要有一个StaticMesh的变量和一个控制可视化的开关,没有开关的话崩溃不容易找到原因。还有一个Material,之后会用到。


接下来我们声明重写一些PrimitiveComponent的接口。


定义都比较简单,CreateSceneProxy需要注意类名是自己声明的SceneProxy的类名字。


GetUsedMaterials,我们先直接返回Material变量。


然后我们声明一下FCustomVertexFactory类。


最后我们添加两个成员变量。


定义InitRHI。这个函数是用来构建渲染资源的,就是定义一个顶点上有哪些数据的。现在最前面写上如下的语句


然后我们填充FVertexFactoryInput里的Position,也就是ATTRIBUTE0的数据。后面我们还会有TangentBasis,目前就只有Position。


在RenderDoc的InputLayout里,我们可以看到每个ATTRIBUTE具体的信息。其实在Input结构体中,名称并不重要,重要的是ATTRIBUTE X 这个值和InitRHI中对应。这里还有一个坑,就是Mesh数据的PositionVertexBuffer格式是Float3,而不是Float4。


剩下两个函数没啥好说的,直接写好就可以了。




接下来是FCustomPrimitiveSceneProxy,这次最大的类。


其实和其他SceneProxy没啥区别,我们照例一一实现这些函数。
首先我们来看GetViewRelevance,我们只需要将DynamicRelevance和RenderInMainPass打开就可以了。目前别的暂时还不需要。


创建渲染线程资源,直接将我们的VertexFactory初始化即可。


这个函数是我自己用来更新StaticMesh的顶点Buffer用的,你也可以通过外部来改变这个Buffer,例如创建一个Mananger。


在构造函数中,我们调用UpdateStaticMesh来更新一些VertexBuffer。因为面板上Component里的UPROPERTY Mesh更新会重新生成SceneProxy,所以我改掉了之前在Tick里更新的代码,直接在这里初始化即可,更正确。


这两个函数照抄即可。


接下来就是GetDynamicMeshElements了。
首先,当我们不可见的时候返回,这个值是从Component里我们声明的UPROPERTY里来的。当我们没有设置可渲染的Mesh的时候,也直接返回。


接下来是一些例行公事,主要是生成MaterialProxy和判断Wireframe


目前为了简单,我们只处理LOD0级。


然后我们将对应的数据一一赋值。
NumPrimitives,Prim数量。其中StaticMesh是通过如下方式:
--StaticMeshRender.cpp[941]--
NumPrimitives = Section.NumTriangles;
来获得Primitives的数量的。如果Primitives的数量为0,那么就代表要使用IndirectDrawArgs。


IndexBuffer,对于这个PointCloud的案例来讲,我们使用trilist的话,就是032,013的两个组合。
对Mesh数据而言同样的,这里已经创建好了,只需要:
BatchElement.IndexBuffer = MeshRenderData->LODResources[LODIndex].IndexBuffer; 即可。


MaxVertexIndex最后一个顶点的Index。就是BaseVertexIndex + VerticesCount - 1
例如在PointCloudSceneProxy中,我们可以看到这个MaxIndex就是PointCount * 4 - 1
这是因为一个Quad有4个顶点,所以是*4。主要是这个顶点数据不是由Mesh产生的。如果是Mesh的,那么直接通过
BatchElement.MaxVertexIndex = VertexBuffers.PositionVertexBuffer.GetNumVertices() - 1;
这样就可以直接获取这个数量。


MinVertexIndex最小的那个顶点的Index。


最后我们再讲上这些,就完成了。


在CPP文件的最上头,我们还需要IMPLEMENT一下我们用的顶点工厂,之后还有ShaderParameter。后面五个boolean的含义在代码下方。





每一个Boolean的具体含义

四、结果

打开虚幻引擎,新建一个Actor,挂上我们的CustomPrimitiveComponent,选好想要画的Mesh,打开IsVisible得到如下结果。


这样我们就完成了一个最基础的VertexFactory的shader到c++的实现。之后我们再在这个基础上进行拓展即可。
回复

举报 使用道具

3

主题

7

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2022-12-1 18:35:41 | 显示全部楼层
虽然看不懂,但是我大受震撼  [专业]
回复

举报 使用道具

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