虚幻引擎 自定义VertexFactory(二)

1

主题

3

帖子

4

积分

新手上路

Rank: 1

积分
4
发表于 2022-12-3 19:22:57 | 显示全部楼层
引言

在上一篇文章中,我介绍了如何自定义一个最简单的VertexFactory。在这篇文章中,我们将拓展这个VertexFactory来实现阴影、法线、光照、材质的绘制。文章代码基于4.26.2。
一、阴影

我们先来看阴影的绘制。在虚幻引擎中搜索ShadowV(ertexShader),我们发现只有三个usf文件。


我们需要找的是ShadowDepthVertexShader。我们先来看ShadowDepthVertexShader的Main函数的参数。
void PositionOnlyMain(
        in FPositionAndNormalOnlyVertexFactoryInput Input,
        out FShadowDepthVSToPS OutParameters,
        out float4 OutPosition : SV_POSITION
        )
{
        ResolvedView = ResolveView();
        float4 WorldPos = VertexFactoryGetWorldPosition(Input);
        float3 WorldNormal = VertexFactoryGetWorldNormal(Input);
        float ShadowDepth;
        SetShadowDepthOutputs(
                ShadowDepthPass_ProjectionMatrix ,
                ShadowDepthPass_ViewMatrix ,
                WorldPos,
                WorldNormal,
                OutPosition,
                ShadowDepth
        );
                OutParameters.Dummy = 0;
}
那么这个里面我们发现需要一个FPositionAndNormalOnlyVertexFactoryInput。同样之前定义的函数VertexFactoryGetWorldPosition和VertexFactoryGetWorldNormal也需要一个这个struct做参数的版本。同样在算SceneDepth的时候,在PositionOnlyDepthVertexShader中,我们也需要一个FPositionOnlyVertexFactoryInput。
void Main(
        FPositionOnlyVertexFactoryInput Input,
        out float4 OutPosition : SV_POSITION
        )
{
        ResolvedView = ResolveView();
        float4 WorldPos = VertexFactoryGetWorldPosition(Input);
        {
                OutPosition =  mul(WorldPos, ResolvedView.TranslatedWorldToClip)
        }
}
在虚幻引擎中,除去默认的顶点定义,还有两种特殊的顶点定义:PositionOnly和PositionAndNormalOnly。这两种都是给刚才我们看到的渲染Depth的Vertex Shader使用的。



VertexFactory.cpp

我们需要在Shader端实现这几个结构体和对应的函数,并在C++端开启渲染阴影的开关即可。
Shader端实现

在之前写好的Shader文件中,我们添加需要的结构体。和上次写法一样,只是在FPositionAndNormalOnlyVertexFactoryInput中我们需要加一个Normal Attribute。和LocalVertexFactory一样,我们将这个Normal放在Attribute2。


我们同时也需要重载VertexFactoryGetWorldPosition。


重载VertexFactoryGetWorldNormal。


这样我们在Shader文件端的修改就写完了。但是我们现在直接运行还是看不到阴影,是因为C++端没有走阴影pass。
C++实现

在C++端,我们还需要设置一些值让阴影pass生效。首先,我们需要将ViewRelevance改成阴影Relevant。


接着,我们需要将GetDynamicMeshElements的MeshBatchElement改成可以投射阴影。


具体含义可以看MeshBatch.h里对应每一个项的解释。其中我们看到CastShadow表示是否可以在Shadow RenderPass里使用。上面的注释也解释了这些flag的作用,例如Depth-Only pass可以无视UV的差异,将顶点根据Position的信息合并到一起,因为深度Only只关心位置。那么Proxy通过这些Flags就可以快速地只提交需要的信息。



MeshBatch.h

Implement顶点工厂的宏,我们也需要支持PositionOnly。最后一个boolean是bSupportPositionOnly,我们在我们的cpp文件将最后一个变量设置成true即可。


这样我们就添加了阴影pass的渲染。打开虚幻引擎查看效果,可以看到我们的Cube已经投射了阴影。


二、法线

为了下面光照的效果,我们先添加法线信息。法线信息我们跟Position一样,通过顶点buffer输入进来。这里我们参考虚幻引擎StaticMesh的顶点法线Buffer来处理。



LocalVertexFactory.ush

以图中所示为例,我们可以看到TangentX和TangentZ是虚幻从FVertexFactoryInput里传过来的。那么在StaticMesh的VertexBuffer里这些数据是怎么存放的呢?我们看上面VertexFetch_PackedTangentsBuffer中关于Instance的代码,就会发现这个Tangent数据是从同一个Buffer中Offset出来的。我们再截帧验证一下我们的想法。渲染一个Cube并截帧,我们可以看到Vertex Shader阶段使用的Buffer为VertexFetch_PackedTangentsBuffer,根据代码,偶数Index为X,奇数Index为Z。




那我们这里根据LocalVertexFactory的实现把这些函数抄过来,基本就可以了。
Shader端实现

首先我们的Input要添加两个Attribute,TangentX和TangentZ,分别放在Attribute1和Attribute2即可。


然后我们在FVertexFactoryInterpolantsVSToPS中,添加两个作为输出的float4,来作为PixelShader的输出。


在FVertexFactoryIntermediates中我们添加上几个需要在VertexFactory中进行传递的矩阵,和一个符号位。


添加一个CalcTangentToLocal用于计算TangentToLocal矩阵。计算过程如下,其中我们跟虚幻引擎一致,最后重新计算一下TangentX,用来消除误差。


添加如下功能函数,计算TangentToWorld。


GetVertexFactoryIntermediates中我们调用刚才的函数,给对应的Intermediate参数赋值。这里我们顺道把Color设置成白色,为了之后更好的和官方的结果作对比。


GetMaterialPixelParameters中,同样我们给Result中对应的参数赋值。其中AssembleTangentToWorld这个函数是虚幻引擎MaterialTemplate中定义好的函数。


GetMaterialVertexParameters中也需要给Result赋值。


给FVertexFactoryInterpolantsVSToPS中的Tangent相关参数赋值。


最后将这两个函数修改成对应的参数输出即可,之前我们使用的是一些临时数据。


C++端

我们在C++端添加对应的数据来让shader中计算生效。回到我们的FCustomVertexFactory类,加一个Public成员变量TangentVertexBuffer。


然后我们在UpdateStaticMesh的时候,设置一下这个buffer的值。


在顶点工厂的InitRHI中,我们Bind一下Buffer。注意我们刚才观察从截帧中观察TangentVertexBuffer的结构,TangentX是从0开始,8为stride,即跳8个值是下一个值的开头。同理TangentZ是从4开始,也是8为stride进行读取。


然后我们在顶点定义里添加上新的TangentX和TangentZ。


我们设置之后好Mesh之后截帧,拿一个官方的Cube做对比,可以看到Buffer的信息在计算出来之后结果是一模一样的,证明我们的Normal计算正确了。



上:我们计算的,下:虚幻官方的

如果我们想在场景里看到计算生效,我们就需要在Component里添加一个lit的材质。我是在Component初始化的时候直接赋值一个DefaultMaterial。


现在我们在场景里打开法线Buffer观察,我们可以看到法线已经正确计算。


三、光照

这里我为了简单,就只实现了动态光照,而动态光照不需要单独计算LightMap和ShadowMap坐标,所以Shader端不需要改动。
C++端

在虚幻引擎中,Mesh和场景灯光交互的类型是通过一个枚举来定义的。



SceneManagement.h

而描述灯光和场景交互的信息则是一个LightInteraction类里的静态函数来完成的。



SceneManagement.h

我们先在FCustomPrimitiveSceneProxy里实现一个嵌套的FCustomPrimitiveLCI,这个类继承自FLightCacheInterface。


为了让光照生效,我们要实现GetLightRelevance这个函数。这个其实和GetViewRelevance是一个意思,即获取和每种光照有关系的数据。


我们在FCustomPrimitiveSceneProxy中再加一个Private成员变量用于访问里面的函数和变量。


在cpp文件中,GetInteraction中我们这样实现。


再定义一下GetLightRelevance,从代码中我们可以看出,每个flag对应的都是一种光照的计算方式。


GetDynamicMeshElements中,我们将刚才创建的ComponentLightInfo赋值到MeshBatch.LCI上。


最后我们在构造函数里,将这个component初始化一下。


我们注意到刚才在.h文件中有一个FMeshMapBuildData类。


我们观察之后发现,这个类里存储的就是一些用于构建LightMap和ShadowMap的一些数据。


那么我们需要在UCustomPrimitiveComponent类中也创建一个对应的FMeshMapBuildData。回到我们的Component类中,我们新创建如下几个成员。


我们再override一下PostInitProperties。我们需要在这个函数里来创建我们的Guid,否则这个Guid会在序列化阶段被覆盖。




我们定义一下GetMeshMapBuildData,这段代码我都是从Landscape.cpp中抄过来的,里面的内容也是很明了,就是针对不同情况来创建我们的BuildData供光照函数使用。(题外话,这也证明学习引擎最好的途径就是源代码,多尝试,多抄,就能理解。)


在引擎中,我们拖拽一盏点光源,注意我们需要将光源的类型改成Movable。


可以看到我们的cube和光源正确地交互了。


四、材质

目前我们还是用的default material,现在我们来修改代码,让我们的cube可以使用我们指定的材质。我们还需要添加一套uv坐标,从而可以让贴图生效。
Manual Vertex Fetch

这里我们使用Manual Vertex Fetch的方式,来获取uv坐标。第一是因为虚幻引擎也是通过这种方式来获取相关信息,我们之后支持Instancing渲染模式;第二是因为这种方式相对原来的通过硬件Input获取数据更自由。
C++端

这部分我们先来看C++端的实现,先定义一下我们的UniformBuffer的结构,我们需要1.一个用于读取Buffer的一些Index信息,2.一个Buffer用于我们存储TexCoord。


在我们的VertexFactory类里添加一个UniformBuffer。


添加Get,Set函数。


最后我们创建一个继承自FVertexFactoryShaderParameters的类。这个类是顶点工厂用来进行参数绑定的接口,可以看到我们在GetElementShaderBindings这个函数里面,我们将UniformBuffer进行一个Shader的参数绑定。


在.cpp文件中,我们在开头先通过宏来Implement一下我们新添加的两个类。第一个是UniformBuffer的结构。后面的名称"CustomVF",是用于在Shader里面访问该UniformBuffer的名称。


然后我们Implement一下VertexFactoryShaderParameter类。


在CreateRenderThreadResources的时候,先创建一个FCustomVertexFactoryParameters对象,然后将对象中对应的buffer进行初始化。我们可以看到Parmeters里面有四个参数,我们这次主要使用NumTexCoords。然后我们将LOD[0]的TexCoordsSRV绑到UniformBuffers对应的TexCoordBuffer上。而当StaticMesh为空,即无法读取这些数据的时候,我们需要给定一个GNullColorVertexBuffer。最后我们调用SetParameters来赋值即可。


最后我们定义一下SetParameters。至此C++端的定义就完成了,接下来我们在Shader端将对应的数据交给PixelShader就可以了。


Shader端

我们先在开头定义一下几个变量,用于访问VertexFetch_Parameter中的每一项。


这里的NUM_TEX_COORD_INTERPOLATORS是在MaterialTemplate.ush里定义的。在虚幻引擎中,当我们使用材质编辑器进行材质Shader的编辑之后,编译出来的hlsl文件,就是基于MaterialTemplate再加上编译器编译出来的。
FVertexFactoryInterpolantsVSToPS里我们定义TexCoords。


当我们的材质编辑器里有INTERPOLATORS的时候,我们就定义获取材质UV坐标的函数。


GetMaterialPixelParameters函数中,我们也需要获取TexCoord给FMaterialPixelParameters。


GetMaterialVertexParameters也需要做相应的改动,从而可以给Result的UV坐标赋值。


最后我们修改VertexFactoryGetInterpolantsVSToPS函数,给Intepolants赋值。


这样我们就可以在虚幻引擎中获取到我们Mesh上的UV坐标了。我们再修改Component,让我们可以通过面板修改渲染用的材质。


GetUsedMaterials里我们将这个Material返回即可。


这样我们就可以使用我们自己的材质了。在虚幻引擎中创建一个带材质的物体,例如我这里使用Houdini插件里的一个cube来做实验。


可以看到我们的材质和UV坐标都可以成功读取了。


这里我创建了一个材质输出uv到颜色,验证了uv坐标也是正确的。
五、结语

至此我们已经成功将一个StaticMesh通过我们自己的VertexFactory渲染到屏幕上了,接下来我们将继续拓展这个VertexFactory,进行Instancing渲染,最后我们将使用DrawIndirect来进行GPU Instancing的绘制流程。
回复

举报 使用道具

1

主题

2

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2025-2-27 14:51:37 | 显示全部楼层
纯粹路过,没任何兴趣,仅仅是看在老用户份上回复一下
回复

举报 使用道具

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