[Unreal Engine] Vertex Stream配置机制解析

[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_ComputeSF_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.ushLandscapeVertexFactory.ush

这些步骤的接口都可以在我们的ACustomVertexFactory.ush中实现。具体要怎么实现就看自己的需求了,反正实现就好了。