VertexFactory 从顶点数据到 Shader (一) C++ VertexFactory

MrWen332022年10月10日
大约 7 分钟

VertexFactory 从顶点数据到 Shader (一): C++ VertexFactory

VertexFactory 是 Unreal MeshDrawPipeline 的一部分,主要负责对 Mesh 顶点数据的处理与传递到 VertexShader 和 VertexShader 的部分实现(VertexFactory.ush).

本系列文章将阐述如何自定义VertexFactory以传递顶点数据到Shader, 最终会绘制出一个正方形到场景中. 源码还会附带 Instancing 相关内容, 但文中不会过多展开说明.

本篇文章是本系列文章的第一篇, 主要说明自定义顶点数据, 以及 C++ 端 VertexFactory 的自定义.

流程

顶点数据

首先, 我们需要自定义 VertexBuffer 与 IndexBuffer 结构体.

VertexBuffer

要自定义 VertexBuffer, 我们需要继承 FVertexBuffer 结构体, 先看一下 FVertexBuffer 的代码:

FVertexBuffer-Code

FVertexBuffer 继承了 FRenderResource. RenderResource 类会封装 RHI 资源, 而FVertexBuffer 包含名为 VertexBufferRHI 的成员变量, 并实现了释放资源的函数 ReleaseRHI(). 我们继承了 FVertexBuffer 后, 需要重载 InitRHI() 函数, 在其中使用顶点信息初始化 VertexBufferRHI.

本例中, 我们创建一个表示顶点位置的 VertexBuffer, 单个 Element 类型为 float4.

首先, 在 InitRHI() 中创建 VertexBufferRHI, 如下图所示, 其中 FVertexInput = FVector4f.

CreateVertexBuffer

其中 Usage 的 BUF_Static 对应 DX12 Default Heap 中的 Resource, BUF_VertexBuffer 则标记这个 Buffer 是 VertexBuffer (本人理解: 由于DX12所有Buffer都是ID3DResource*, 所以这个标记没有实质性的作用). 而 ResourceState 则对应 DX12 的 D3D12_RESOURCE_STATE.

接下来向 Buffer 中写入数据, 如下图所示.

WriteDataToBuffer

使用 RHILockBuffer 获取 Buffer 的地址, 并向其中写入数据, 这里我们以先行后列的顺序写入网格顶点的 Position, 最后Unlock.

IndexBuffer

然后定义 IndexBuffer. 定义 IndexBuffer 也需要继承 FIndexBuffer, 其结构与 FVertexBuffer 相似. 创建 IndexBufferRHI 时, 我们使用在创建时就传入Index数据的方式, 如下图所示:

CreateIndexBuffer

将生成的 Index 填入 TResourceArray 后, 将 TResourceArray 设置到 CreateInfo 中, 在 CreateBuffer 时就能将数据上传到 Buffer 里.

完整代码

VertexBuffer 与 IndexBuffer 的完整代码如下:

点击查看完整代码
// TestVertexBuffer
class FTestVertexBuffer: public FVertexBuffer
{
public:
    FTestVertexBuffer(int32 InNumQuadsPerSide): NumQuadsPerSide(InNumQuadsPerSide){}

	using FVertexInput = FVector4f;
    
    virtual void InitRHI() override{
    	const uint32 NumVertsPerSide = NumQuadsPerSide+1;
    	NumVerts = NumVertsPerSide*NumVertsPerSide;
    	void* BufferData = nullptr;

    	FRHIResourceCreateInfo CreateInfo(TEXT("FTestVertexBuffer"));
    	auto Usage = BUF_Static|BUF_VertexBuffer;
    	auto ResourceState = ERHIAccess::VertexOrIndexBuffer;
    	auto VBSize = sizeof(FVertexInput)*NumVerts;
    	VertexBufferRHI = RHICreateBuffer(
    		VBSize, Usage, sizeof(FVertexInput), ResourceState, CreateInfo);
    	BufferData = RHILockBuffer(VertexBufferRHI, 0, VBSize, RLM_WriteOnly);
    	FVertexInput* Contents = static_cast<FVertexInput*>(BufferData);
    	for(uint32 Y = 0;Y<NumVertsPerSide;++Y)
    	{
    		FVertexInput Pos;
    		Pos.Y = (float)Y/NumQuadsPerSide - 0.5f;
    		Pos.Y *= 100;
    		for(uint32 X = 0;X<NumVertsPerSide;++X)
    		{
    			Pos.X = (float)X/NumQuadsPerSide - 0.5f;
    			Pos.X *= 100;
    			
    			Contents[X + Y*NumVertsPerSide] = Pos;
    		}
    	}

    	RHIUnlockBuffer(VertexBufferRHI);
    }
    
    int32 GetVertexCount() const {return NumVerts;}
    
private:
    int32 NumVerts = 0;
    const int32 NumQuadsPerSide = 0;
};

// IndexBuffer
class FTestIndexBuffer: public FIndexBuffer
{
public:
	FTestIndexBuffer(int32 InNumQuadsPerSide) : NumQuadsPerSide(InNumQuadsPerSide){};

	virtual void InitRHI() override
	{
		if(NumQuadsPerSide < 256)
		{
			IndexBufferRHI = CreateIndexBuffer<uint16>();
		}
		else
		{
			IndexBufferRHI = CreateIndexBuffer<uint32>();
		}
	}

	int32 GetIndexCount() const {return NumIndices;}
private:
	template <typename IndexType>
	FBufferRHIRef CreateIndexBuffer()
	{
		TResourceArray<IndexType, INDEXBUFFER_ALIGNMENT> Indices;
		Indices.Reserve(NumQuadsPerSide*NumQuadsPerSide*6);

		// Prepering Indices Array
		for(int32 Morton=0;Morton<NumQuadsPerSide*NumQuadsPerSide;Morton++)
		{
			// Z-Order
			int32 X = FMath::ReverseMortonCode2(Morton);
			int32 Y = FMath::ReverseMortonCode2(Morton >> 1);

			bool ForwardDiagonal = false; // Quad Is Z-Shape

			if (X % 2)
			{
				ForwardDiagonal = !ForwardDiagonal;
			}
			if(Y % 2)
			{
				ForwardDiagonal = !ForwardDiagonal;
			}

			// Z-Order Idx: Left-Up, Right-Up, Left-Down, Right-Down
			int32 Index0 = X + Y * (NumQuadsPerSide+1);
			int32 Index1 = Index0 +1;
			int32 Index2 = Index0 + NumQuadsPerSide + 1;
			int32 Index3 = Index2 + 1;

			Indices.Add(Index3);
			Indices.Add(Index1);
			Indices.Add(ForwardDiagonal? Index2: Index0);
			Indices.Add(Index0);
			Indices.Add(Index2);
			Indices.Add(ForwardDiagonal? Index1: Index3);
		}
		
		NumIndices = Indices.Num();
		const uint32 Size = Indices.GetResourceDataSize();
		const uint32 Stride = sizeof(IndexType);

		FRHIResourceCreateInfo CreateInfo(TEXT("FTestIndexBuffer"), &Indices);
		return RHICreateIndexBuffer(Stride, Size, BUF_Static, CreateInfo);
	}

	int32 NumIndices = 0;
	const int32 NumQuadsPerSide = 0;
}

自定义 VertexFactory

有了顶点数据, 接下来就要告知 Shader 顶点数据的类型, 以及将数据传入到 Shader 中. 通过自定义 VertexFactory 可以完成这两个任务, 主要是在重载的 InitRHI() 函数中进行.

要自定义 VertexFactory, 我们要继承 FVertexFactory 结构体, 并编写对应的 VertexFactory.ush Shader代码.

继承FVertexFactory, 如下图所示:

TestVF

在 VertexFactory 类声明开头加上 DECLARE_VERTEX_FACTORY_TYPE, cpp 文件中加入 IMPLEMENT_VERTEX_FACTORY_TYPE, 如图:

VFHeaderMacroVFMacro!

其中 VertexFactory 的实现宏需要指定 Shader 文件的位置与 Flags. Shader 文件位置可以使用 AddShaderSourceDirectoryMapping 映射来得到, 此处不展开. 对于Flags, 前三个分别是支持材质, 动态光源, 精确的上一帧世界坐标( 目前没有实质作用 ), 而最后一个与 GPUScene 有关.

Vertex/IndexBuffer与VertexDeclaration

在 InitRHI() 初始化 Vertex/IndexBuffer

InitVIBuffer

接下来要创建 VertexDeclaration, 用于定义 VertexInput 的结构. 首先我们需要填充 FVertexStreamComponent 结构体, 这个结构体记录单个 VertexAttribute 的信息, 包括 VertexBuffer 与顶点描述.

VertexStreamComponent

下一步, 用 FVertexStreamComponent 生成 FVertexElemt. FVertexElement 只包含顶点信息, 是之后用来生成 VertexDeclaration 的结构体, 而 AccessStreamComponent 函数会将 FVertexStreamComponent 中的 Vertexbuffer 及附加信息提取出来并加入到 Streams 数组中, 并返回 FVertexElement. Streams 数组保存的数据最终会在绘制时绑定到 VertexShader 上.

InitDeclaration

最后将生成的 FVertexElement 放入数组, 调用 InitDeclaration, VertexFactory 就会在内部自动创建 VertexDeclaration.

UniformBuffer

我们还需要在 VertexFactory 中创建一个 UniformBuffer, 用来控制顶点颜色.

首先在头文件中定义 UniformBuffer 结构, 并在 CPP 文件中实现它. 我们将 FTestVFParameters 结构体绑定到 Shader 的 TestVF 变量上, 这样在 Shader 中可以直接使用类似 TestVF.Color 的代码来访问其数据.

VFParameterTestVFImpl

FTestVFBufferRef 添加到成员变量中, 然后在 InitRHI() 函数中创建 UniformBuffer. 这里设置 Color 为蓝色.

SetupUniformData

完整代码

VertexFactory 的完整代码如下:

点击查看完整代码
// .h
class FTestVertexFactory : public FVertexFactory
{
	DECLARE_VERTEX_FACTORY_TYPE(FTestVertexFactory)
public:
	using Super = FVertexFactory;
	FTestVertexFactory(ERHIFeatureLevel::Type InFeatureLevel, int32 InNumQuadsPerSide);
	virtual ~FTestVertexFactory() override;

	virtual void InitRHI() override;
	virtual void ReleaseRHI() override;
	
	static bool ShouldCompilePermutation(const FVertexFactoryShaderPermutationParameters& Parameters);
	static void ModifyCompilationEnvironment(const FVertexFactoryShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment);
	static void ValidateCompiledResult(const FVertexFactoryType* Type, EShaderPlatform Platform, const FShaderParameterMap& ParameterMap, TArray<FString>& OutErrors);
	
	inline FUniformBufferRHIRef GetTestVFUniformBuffer() const {return UniformBuffer;}

	inline FTestVertexBuffer* GetVertexBuffer() const {return VertexBuffer;}
	inline FTestIndexBuffer* GetIndexBuffer() const {return IndexBuffer;}
protected:
	void SetupUniformData();

	int32 NumQuadsPerSide;

	FTestVertexBuffer* VertexBuffer;
	FTestIndexBuffer* IndexBuffer;
	FTestVFBufferRef UniformBuffer;
public:
};
// .cpp
FTestVertexFactory::FTestVertexFactory(ERHIFeatureLevel::Type InFeatureLevel, int32 InNumQuadsPerSide):
	Super(InFeatureLevel),
	NumQuadsPerSide(InNumQuadsPerSide)
{
	VertexBuffer = new FTestVertexBuffer(NumQuadsPerSide);
	IndexBuffer = new FTestIndexBuffer(NumQuadsPerSide);
}

FTestVertexFactory::~FTestVertexFactory()
{
	delete VertexBuffer;
	delete IndexBuffer;
}

void FTestVertexFactory::InitRHI()
{
	Super::InitRHI();

	// 创建UniformBuffer
	SetupUniformData();

	// 初始化顶点数据
	VertexBuffer->InitResource();
	IndexBuffer->InitResource();

	// 创建顶点结构声明
	check(Streams.Num() == 0);
	FVertexStreamComponent PositionVertexStream;
	PositionVertexStream.VertexBuffer = VertexBuffer;
	PositionVertexStream.Stride = sizeof(FVector4f);
	PositionVertexStream.Offset = 0;
	PositionVertexStream.VertexStreamUsage = EVertexStreamUsage::Default;
	PositionVertexStream.Type = VET_Float4;

	FVertexStreamComponent InstanceDataVertexStream;
	InstanceDataVertexStream.VertexBuffer = nullptr;
	InstanceDataVertexStream.Stride = sizeof(FVector4f);
	InstanceDataVertexStream.Offset = 0;
	InstanceDataVertexStream.VertexStreamUsage = EVertexStreamUsage::Instancing;
	InstanceDataVertexStream.Type = VET_Float4;

	FVertexElement VertexPositionElement(AccessStreamComponent(PositionVertexStream, 0));
	FVertexElement InstanceDataElement(AccessStreamComponent(InstanceDataVertexStream, 1)); // StreamIndex = 1

	FVertexDeclarationElementList Elements;
	Elements.Add(VertexPositionElement);
	Elements.Add(InstanceDataElement);

	InitDeclaration(Elements);
}

void FTestVertexFactory::ReleaseRHI()
{
	if (UniformBuffer)
	{
		UniformBuffer.SafeRelease();
	}
	if(VertexBuffer)
	{
		VertexBuffer->ReleaseResource();
	}
	if(IndexBuffer)
	{
		IndexBuffer->ReleaseResource();
	}
	FVertexFactory::ReleaseRHI();
}

bool FTestVertexFactory::ShouldCompilePermutation(const FVertexFactoryShaderPermutationParameters& Parameters)
{
	return Parameters.MaterialParameters.MaterialDomain == MD_Surface || Parameters.MaterialParameters.bIsSpecialEngineMaterial;
}

void FTestVertexFactory::ModifyCompilationEnvironment(const FVertexFactoryShaderPermutationParameters& Parameters,
	FShaderCompilerEnvironment& OutEnvironment)
{
	OutEnvironment.SetDefine(TEXT("TEST_VF"), 1);
}

void FTestVertexFactory::ValidateCompiledResult(const FVertexFactoryType* Type, EShaderPlatform Platform,
	const FShaderParameterMap& ParameterMap, TArray<FString>& OutErrors)
{
}

void FTestVertexFactory::SetupUniformData()
{
	FTestVFParameters UniformParams;
	UniformParams.Color = FColor::Blue.ReinterpretAsLinear();
	UniformBuffer = FTestVFBufferRef::CreateUniformBufferImmediate(UniformParams, UniformBuffer_MultiFrame);
}

FVertexFactoryShaderParameters

接下来要继承实现 FVertexFactoryShaderParameters 结构, 用于绑定 UniformBuffer 到 Shader 中. 需要实现 Bind 与 GetElementShaderBindings 两个函数

ShaderParameterStruc

首先要在类开头添加 DECLARE_TYPE_LAYOUT 声明

Bind 函数可以将 LAYOUT_FIELD 宏声明的成员变量绑定到 Shader 中. 我们之前已经使用 GLOBAL_SHADER_PARAMETER_STRUCT 宏来绑定, 所以这个函数留空.

GetElementShaderBindings 函数中, 我们使用ShaderBinding.Add(...)将 VertexFactory 的 UniformBuffer 数据绑定到 Shader 上.

最后还需要添加 IMPLEMENT_TYPE_LAYOUTIMPLEMENT_VERTEX_FACTORY_PARAMETER_TYPE 宏, 并将其与 VertexFactory 绑定在一起, 这样 ShaderParameters 中的参数绑定就会自动应用到 VertexFactory 对应的 Shader 上.

ShaderParameterMacro

完整源码如下:

点击查看完整代码
class FTestVFShaderParameters : public FVertexFactoryShaderParameters
{
	DECLARE_TYPE_LAYOUT(FTestVFShaderParameters, NonVirtual);
public:
	void Bind(const FShaderParameterMap& ParameterMap)
	{
	};
	
	void GetElementShaderBindings(
		const class FSceneInterface* Scene,
		const class FSceneView* View,
		const class FMeshMaterialShader* Shader,
		const EVertexInputStreamType InputStreamType,
		ERHIFeatureLevel::Type FeatureLevel,
		const class FVertexFactory* VertexFactory,
		const struct FMeshBatchElement& BatchElement,
		class FMeshDrawSingleShaderBindings& ShaderBindings,
		FVertexInputStreamArray& VertexStreams) const
	{
		const FTestVertexFactory* VF = static_cast<const FTestVertexFactory*>(VertexFactory);
		// 绑定 Shader Parameter(Uniform Buffer)
		ShaderBindings.Add(Shader->GetUniformBufferParameter<FTestVFParameters>(), VF->GetTestVFUniformBuffer());

		//...
		auto InstanceBuffer = static_cast<const FTestInstanceDataBuffers*>(BatchElement.UserData);
		if(VertexStreams.Num()>0)
		{
			FVertexInputStream* InstanceInputStream = VertexStreams.FindByPredicate(
				[](const FVertexInputStream& InStream)
				{
					return InStream.StreamIndex == 1; // Index set at InitRHI()
				});
			check(InstanceInputStream);
			check(InstanceBuffer->GetBuffer());
			InstanceInputStream->VertexBuffer = InstanceBuffer->GetBuffer();
		}
	}
};

结尾

下篇文章会说明如何自定义 VertexFactory.ush 以匹配 C++ 端的 VertexFactory.