[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
中实现。具体要怎么实现就看自己的需求了,反正实现就好了。