Loading... # [Unreal Engine] Vertex Stream配置机制解析 顶点流(Vertex Stream)配置通常是配置一个光栅化硬件管线的第一步。这一步对应硬件中的输入装配阶段(Input Assembler,IA),通常IA是一个由固定硬件实现的阶段,它基于设置的顶点流格式来解析顶点属性,并将其送入下一阶段顶点着色器中。 *好像之前还没见过讲这块的文章,所以这篇文章来研究下UE是怎么实现一个通用的顶点流配置和绑定对应顶点着色器的。* ## 顶点流格式配置 在UE中,这一步嵌套在Mesh渲染中,由一个`FMeshBatch`中指定的`FVectoryFactory`对象配置。顶点流配置通常在如下代码中配置: ``` // .h class FACustomVertexFactory : public FVertexFactory { DECLARE_VERTEX_FACTORY_TYPE_API(FVoxelMeshVertexFactory, VOXELMESH_API) struct FDataType { FVertexStreamComponent PositionStream; FVertexStreamComponent NormalStream; }; void InitRHI(FRHICommandListBase& RHICmdList) override { FVertexDeclarationElementList Elements; Elements.Add(AccessStreamComponent(Data.PositionStream, 0)); Elements.Add(AccessStreamComponent(Data.NormalStream, 1)); AddPrimitiveIdStreamElement(EVertexInputStreamType::Default, Elements, /* AttributeIndex = */ 1, /* AttributeIndex_Mobile = */0xFF); InitDeclaration(Elements); } FDataType Data; }; ``` 这就添加了两个顶点流配置。 其中,因为`FVertexStreamComponent`的构造函数长这样: ``` FVertexStreamComponent(const FVertexBuffer* InVertexBuffer, uint32 InOffset, uint32 InStride, EVertexElementType InType, EVertexStreamUsage Usage = EVertexStreamUsage::Default); FVertexStreamComponent(const FVertexBuffer* InVertexBuffer, uint32 InStreamOffset, uint32 InOffset, uint32 InStride, EVertexElementType InType, EVertexStreamUsage Usage = EVertexStreamUsage::Default); ``` 一眼就看出是运行时去配置的。一般设置这个的时机可能是在子类继承的`FSceneProxy::CreateRenderThreadResources(FRHICommandListBase& RHICmdList)`函数中。反正在`VertexBuffer`创建和上传完成后在**渲染线程**的任意时机设置一下就好了。 最后自定义`VertexFactory`中,记得覆盖这三个静态函数。这三个函数会在`IMPLEMENT_VERTEX_FACTORY_TYPE`宏中被静态注册使用,所以直接隐藏基类的函数就完事了。解释写注释里了: ``` // .h class FACustomVertexFactory : public FVertexFactory { /// 根据当前的FVertexFactoryShaderPermutationParameters状态,判断是否应该编译到这个派生状态。 /// /// 搞个这个函数无非就是为了减少Shader变体。至于这些变体哪来的,下面将绑定顶点着色器Shader时会讲到。 static VOXELMESH_API bool ShouldCompilePermutation(const FVertexFactoryShaderPermutationParameters& Parameters); /// 配置Shader编译环境。可以添加(修改)虚拟路径映射,也可以修改宏定义等等。 static VOXELMESH_API void ModifyCompilationEnvironment(const FVertexFactoryShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment); /// 跟预缓存PSO的机制有关,好像不在这里配置也无所谓,只是不会被缓存。 /// /// 没被缓存到的顶点流配置会在第一次被使用时创建。 static VOXELMESH_API void GetPSOPrecacheVertexFetchElements(EVertexInputStreamType VertexInputStreamType, FVertexDeclarationElementList& Elements); }; ``` 众所周知,UE的`DECLARE_`宏和`IMPLEMENT_`宏是互相配套的,那么我们要如何用这`IMPLEMENT_`宏来静态注册我们的`VertexFactory`呢? ``` // .cpp IMPLEMENT_VERTEX_FACTORY_TYPE(FACustomVertexFactory, "/ACustomShaderPath/ACustomVertexFactory.ush", EVertexFactoryFlags::UsedWithMaterials | EVertexFactoryFlags::SupportsStaticLighting | EVertexFactoryFlags::SupportsDynamicLighting | EVertexFactoryFlags::SupportsCachingMeshDrawCommands | EVertexFactoryFlags::SupportsPSOPrecaching ); ``` 这样就好。注意这里的Shdare文件后缀名是`.ush`。这里与UE为了复用代码搞出来的神奇操作有关,稍后就会讲到。 最后,我们需要声明我们自定义着色器的参数,就类似加一个`GlobalShader`要设置的参数。当然要记得继承一个基类`FVertexFactoryShaderParameters`: ``` // .h class FACustomVertexFactoryVertexShaderParameters : public FVertexFactoryShaderParameters { DECLARE_TYPE_LAYOUT(FACustomVertexFactoryVertexShaderParameters, NonVirtual); public: void GetElementShaderBindings( const class FSceneInterface* Scene, const FSceneView* InView, const class FMeshMaterialShader* Shader, const EVertexInputStreamType InputStreamType, ERHIFeatureLevel::Type FeatureLevel, const FVertexFactory* VertexFactory, const FMeshBatchElement& BatchElement, class FMeshDrawSingleShaderBindings& ShaderBindings, FVertexInputStreamArray& VertexStreams ) const; }; class FACustomVertexFactoryPixelShaderParameters : public FVertexFactoryShaderParameters { DECLARE_TYPE_LAYOUT(FACustomVertexFactoryPixelShaderParameters, NonVirtual); public: void GetElementShaderBindings( const class FSceneInterface* Scene, const FSceneView* InView, const class FMeshMaterialShader* Shader, const EVertexInputStreamType InputStreamType, ERHIFeatureLevel::Type FeatureLevel, const FVertexFactory* VertexFactory, const FMeshBatchElement& BatchElement, class FMeshDrawSingleShaderBindings& ShaderBindings, FVertexInputStreamArray& VertexStreams ) const; }; ``` 配合`IMPLEMENT_`宏: ``` // .cpp IMPLEMENT_TYPE_LAYOUT(FACustomVertexFactoryVertexShaderParameters); IMPLEMENT_TYPE_LAYOUT(FACustomVertexFactoryPixelShaderParameters); IMPLEMENT_VERTEX_FACTORY_PARAMETER_TYPE(FACustomVertexFactory, SF_Vertex, FACustomVertexFactoryVertexShaderParameters); IMPLEMENT_VERTEX_FACTORY_PARAMETER_TYPE(FACustomVertexFactory, SF_Pixel, FACustomVertexFactoryPixelShaderParameters); ``` *如果需要支持光追的话,还要加上`SF_Compute`和`SF_RayHitGroup`的。* **配置顶点流到这里就完事了,但是UE搞的神奇机制是什么呢?** ## 奇妙の自定义Shader方式 UE为了方便各种引擎渲染Pass去复用相同的顶点着色器,固定了部分顶点着色器流程。但是为了保留可编程性,通过类似渲染管线中留出的各个Shader阶段的方式,**预设了一些例程函数**,由与`FVertexFactory`绑定的`.ush`文件实现。 在用于*静态注册*的类型`FVertexFactoryType`中,我们可以找到这么一个函数: ``` // VertexFactory.h class FVertexFactoryType { /** * Calls the function ptr for the shader type on the given environment * @param Environment - shader compile environment to modify */ void ModifyCompilationEnvironment(const FVertexFactoryShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) const { // Set up the mapping from VertexFactory.usf to the vertex factory type's source code. FString VertexFactoryIncludeString = FString::Printf( TEXT("#include \"%s\""), GetShaderFilename() ); OutEnvironment.IncludeVirtualPathToContentsMap.Add(TEXT("/Engine/Generated/VertexFactory.ush"), VertexFactoryIncludeString); OutEnvironment.SetDefine(TEXT("HAS_PRIMITIVE_UNIFORM_BUFFER"), 1); (*ModifyCompilationEnvironmentRef)(Parameters, OutEnvironment); } } ``` 这里把`/Engine/Generated/VertexFactory.ush`指向了,我们在`IMPLEMENT_VERTEX_FACTORY_TYPE`中指定的Shader路径(其实就是用来构造这个类了)。全文搜索一下`/Engine/Generated/VertexFactory.ush`这个字符串,可以发现很多Pass的Shader都`include`了。当然最终的替换是在`ShaderPreprocessor`中做的,有兴趣的可以去看看,同样搜索这个路径就能找到了。 **了解了这个机制,最后就是我们可以自己实现什么函数给那些Pass用呢?** ## 实现顶点着色器 **先把我们的**`ACustomVertexFactory.ush`文件留空,去骗,去偷袭一波。原(U)神(E)启动!这时会开始编Shader,问题不大,包出错的。等它出错我们看看报错,这里去掉重复的了: ``` BasePassPixelShader.usf:470:2: error: unknown type name 'FVertexFactoryInterpolantsVSToPS' ShadowDepthVertexShader.usf:176:2: error: unknown type name 'FVertexFactoryInput' ShadowDepthVertexShader.usf:193:2: error: unknown type name 'FVertexFactoryIntermediates' ``` 骗到了,搜下`FVertexFactoryInput`先。这样就可以知道`FVertexFactoryInput`对应的是我们上面声明的顶点属性配置。像这样: ``` struct FVertexFactoryInput { float3 Position : ATTRIBUTE0; uint Normal : ATTRIBUTE1; }; ``` 我这里的`Normal`是打包后的数据,所以只是个`uint`。用同样的方法,可以知道 * `FVertexFactoryInterpolantsVSToPS`:对应需要从顶点着色器插值到片段着色器的数据。 * `FVertexFactoryIntermediates`:用于缓存一些可能会被多次用到的数据。 * `FVertexFactoryInput`:流入的顶点属性声明。 其中`FVertexFactoryIntermediates`的概念现在可能比较模糊,接下来会讲明白。 先随便从一个Pass入手,`BasePass`一看就很老实,让我们去欺负它吧。 打开`BasePassVertexShader.usf`,其中`#include "/Engine/Generated/VertexFactory.ush"`放在了`BasePassVertexCommon.ush`中,稍微有一点点绕。 可以发现,`Main`函数开头有一行: ``` FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input); ``` 通过`GetVertexFactoryIntermediates`函数,计算了一些中间数据,用于下面的步骤: ``` float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates); float3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates); FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPosition.xyz, TangentToLocal); float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPosition); Output.FactoryInterpolants = VertexFactoryGetInterpolants(Input, VFIntermediates, VertexParameters); ``` 可以发现,需要由`VertexFactory`实现的函数,也都是用`VertexFactory`作为前缀命名的,实现这些步骤的函数就好了。要抄的话,可以参考`LocalVertexFactory.ush`和`LandscapeVertexFactory.ush`。 这些步骤的接口都可以在我们的`ACustomVertexFactory.ush`中实现。具体要怎么实现就看自己的需求了,反正实现就好了。 最后修改:2024 年 11 月 18 日 © 禁止转载 打赏 赞赏作者 赞 1 如果觉得我的文章对你有用,请随意赞赏