(UE4 4.27)自定义PrimitiveComponent

前言

UE4开发中, 通常用到的MeshStaticMesh,SkeletalMesh,ProceduralMesh等等, 他们对应都有相应的渲染组件如UStaticMeshComponent, UProceduralMeshComponent, 本质上这些Mesh组件都继承了UPrimitiveComponent, UPrimitiveComponent通过FPrimitiveSceneProxy渲染代理负责将特定的Mesh的渲染数据(VertexBuffer, IndexBuffer, Material)从游戏线程送往渲染线程. 有时候为了定制某种特殊的Mesh渲染, 我们得自定义新的PrimitiveComponent。(当然UProceduralMeshComponent往往满足了定制新的Mesh需求, 但有时候为了进一步的性能或者进行特殊的MeshPass得定制PrimitiveComponent)。

自定义金字塔 PrimitiveComponent

下面我就以一个八面体为例子介绍自定义PrimitiveComponent

创建渲染代理

创建的渲染代理包含VertexBuffer, IndexBuffer, Material相关数据

class FPyramidSceneProxy final : public FPrimitiveSceneProxy
{
public:
	SIZE_T GetTypeHash() const override
	{
		static size_t UniquePointer;
		return reinterpret_cast<size_t>(&UniquePointer);
	}

	FPyramidSceneProxy(const UPyramidComponent* InComponent);

	virtual ~FPyramidSceneProxy()
	{
		VertexBuffers.PositionVertexBuffer.ReleaseResource();
		VertexBuffers.StaticMeshVertexBuffer.ReleaseResource();
		VertexBuffers.ColorVertexBuffer.ReleaseResource();
		VertexFactory.ReleaseResource();
		IndexBuffer.ReleaseResource();
	}

	virtual void GetDynamicMeshElements(const TArray<const FSceneView *>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, class FMeshElementCollector& Collector) const override;

	virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const;

	void BuildPyramidMesh(const FPyramidMesh* PyramidMeshData, TArray<FDynamicMeshVertex>& OutVertices, TArray<uint32>& OutIndices);
	void SetMeshData_RenderThread(FPyramidMesh* PyramidMeshData);

	uint32 GetAllocatedSize(void) const { return(FPrimitiveSceneProxy::GetAllocatedSize()); }
	virtual uint32 GetMemoryFootprint(void) const override { return(sizeof(*this) + GetAllocatedSize()); }

private:
	/** Vertex buffer for this section */
	FStaticMeshVertexBuffers VertexBuffers;

	/** Index buffer for this section */
	FDynamicMeshIndexBuffer32 IndexBuffer;

	/** Vertex factory for this section */
	FLocalVertexFactory VertexFactory;

	FColor PyramidColor;

	UMaterialInterface* Material;

	FMaterialRelevance MaterialRelevance;
};

初始化VertexBuffer, IndexBuffer, Material

FPyramidSceneProxy::FPyramidSceneProxy(const UPyramidComponent* InComponent)
	: FPrimitiveSceneProxy(InComponent)
	, VertexFactory(GetScene().GetFeatureLevel(), "FPyramidSceneProxy")
	, PyramidColor(InComponent->ShapeColor)
	, MaterialRelevance(InComponent->GetMaterialRelevance(GetScene().GetFeatureLevel()))
{
	// Setup VertexData And IndexData
	TArray<FDynamicMeshVertex> Vertices;
	TArray<uint32> Indices;

	BuildPyramidMesh(&InComponent->PyramidMesh, Vertices, Indices);
    //VertexFactory Bind vertexBuffer
	VertexBuffers.InitFromDynamicVertex(&VertexFactory, Vertices, 1);
	IndexBuffer.Indices = Indices;

	// Enqueue initialization of render resource
	BeginInitResource(&VertexBuffers.PositionVertexBuffer);
	BeginInitResource(&VertexBuffers.StaticMeshVertexBuffer);
	BeginInitResource(&VertexBuffers.ColorVertexBuffer);
	BeginInitResource(&IndexBuffer);
	BeginInitResource(&VertexFactory);

	Material = nullptr == InComponent->Material ? UMaterial::GetDefaultMaterial(MD_Surface) : InComponent->Material;
}

覆盖GetDynamicMeshElements, 进行MeshBatch的提交

Mesh的顶点数据, 索引数据, 顶点工厂,材质Proxy等

void FPyramidSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView *>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, class FMeshElementCollector& Collector) const
{
	QUICK_SCOPE_CYCLE_COUNTER(STAT_PyramidSceneProxy_GetDynamicMeshElements);

	// Set up wireframe material (if needed)
	const bool bWireframe = AllowDebugViewmodes() && ViewFamily.EngineShowFlags.Wireframe;
	
	FColoredMaterialRenderProxy* WireframeMaterialInstance = NULL;
	if (bWireframe)
	{
		WireframeMaterialInstance = new FColoredMaterialRenderProxy(
			GEngine->WireframeMaterial ? GEngine->WireframeMaterial->GetRenderProxy() : NULL,
			FLinearColor(0, 0.5f, 1.f)
		);

		Collector.RegisterOneFrameMaterialProxy(WireframeMaterialInstance);
	}


	FMaterialRenderProxy* MaterialProxy = bWireframe ? WireframeMaterialInstance : Material->GetRenderProxy();

	for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
	{
		if (VisibilityMap & (1 << ViewIndex))
		{
			const FSceneView* View = Views[ViewIndex];
			
			//Draw Mesh
			FMeshBatch& Mesh = Collector.AllocateMesh();
			FMeshBatchElement& BatchElement = Mesh.Elements[0];
			BatchElement.IndexBuffer = &IndexBuffer;
			Mesh.bWireframe = bWireframe;
            //VertexFactory Bind vertexBuffer
			Mesh.VertexFactory = &VertexFactory;
			Mesh.MaterialRenderProxy = MaterialProxy;

			bool bHasPrecomputedVolumetricLightmap;
			FMatrix PreviousLocalToWorld;
			int32 SingleCaptureIndex;
			bool bOutputVelocity;
			GetScene().GetPrimitiveUniformShaderParameters_RenderThread(GetPrimitiveSceneInfo(), bHasPrecomputedVolumetricLightmap, PreviousLocalToWorld, SingleCaptureIndex, bOutputVelocity);

			FDynamicPrimitiveUniformBuffer& DynamicPrimitiveUniformBuffer = Collector.AllocateOneFrameResource<FDynamicPrimitiveUniformBuffer>();
			DynamicPrimitiveUniformBuffer.Set(GetLocalToWorld(), PreviousLocalToWorld, GetBounds(), GetLocalBounds(), true, bHasPrecomputedVolumetricLightmap, DrawsVelocity(), bOutputVelocity);
			BatchElement.PrimitiveUniformBufferResource = &DynamicPrimitiveUniformBuffer.UniformBuffer;
			
			BatchElement.FirstIndex = 0;
			BatchElement.NumPrimitives = IndexBuffer.Indices.Num() / 3;
			BatchElement.MinVertexIndex = 0;
			BatchElement.MaxVertexIndex = VertexBuffers.PositionVertexBuffer.GetNumVertices() - 1;
			Mesh.ReverseCulling = IsLocalToWorldDeterminantNegative();
			Mesh.Type = PT_TriangleList;
			Mesh.DepthPriorityGroup = SDPG_World;
			Mesh.bCanApplyViewModeOverrides = false;
			Collector.AddMesh(ViewIndex, Mesh);
		}
	}
}

覆盖GetViewRelevance,设置Mesh渲染状态信息

FPrimitiveViewRelevance FPyramidSceneProxy::GetViewRelevance(const FSceneView* View) const
{
	FPrimitiveViewRelevance Result;
	Result.bDrawRelevance = IsShown(View);
	Result.bShadowRelevance = IsShadowCast(View);
	Result.bDynamicRelevance = true;
	Result.bRenderInMainPass = ShouldRenderInMainPass();
	Result.bUsesLightingChannels = GetLightingChannelMask() != GetDefaultLightingChannelMask();
	Result.bRenderCustomDepth = ShouldRenderCustomDepth();
	Result.bTranslucentSelfShadow = bCastVolumetricTranslucentShadow;
	MaterialRelevance.SetPrimitiveViewRelevance(Result);
	Result.bVelocityRelevance = IsMovable() && Result.bOpaque && Result.bRenderInMainPass;
	return Result;
}

创建八面体的PrimitiveComponent

UCLASS(meta = (BlueprintSpawnableComponent), ClassGroup = Rendering)
class CUSTOMRENDERCOMPONENT_API UPyramidComponent : public UPrimitiveComponent
{
	GENERATED_BODY()
	
public:
	virtual FPrimitiveSceneProxy* CreateSceneProxy() override;

	//~ Begin UActorComponent Interface.
	virtual void OnRegister() override;

private:
	//~ Begin USceneComponent Interface.
	virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override;
	//~ Begin USceneComponent Interface.

#if WITH_EDITOR
	void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
#endif // WITH_EDITOR

	void RecreateMeshData();
	void UpdateLocalBounds();

public:
	void RecreateMesh();
	void SendMeshDataToRenderThread();

	UFUNCTION(BlueprintCallable)
	void SetCustomMaterial(UMaterialInterface* InMaterial);

	/** Accesses the scene relevance information for the materials applied to the mesh. Valid from game thread only. */
	FMaterialRelevance GetMaterialRelevance(ERHIFeatureLevel::Type InFeatureLevel) const;
	virtual void GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials = false) const override;

	UFUNCTION(BlueprintCallable)
	void SetPyramidHeight(float NewPyramidHeight);

	UFUNCTION(BlueprintCallable)
	void SetPyramidBottomSize(FVector2D NewSize);

private:
	/** Local space bounds of mesh */
	UPROPERTY()
		FBoxSphereBounds LocalBounds;

	UPROPERTY(EditAnywhere)
		float PyramidHeight = 4.0f;

	UPROPERTY(EditAnywhere)
		FVector2D PyramidBottomSize = FVector2D(4.0f, 4.0f);

public:

	/** Color used to draw the shape. */
	UPROPERTY(EditAnywhere)
		FColor ShapeColor;

	UPROPERTY()
		FPyramidMesh PyramidMesh;

	UPROPERTY(EditAnywhere)
		UMaterialInterface* Material;
};

八面体构建Mesh数据函数

void UPyramidComponent::RecreateMeshData()
{
	PyramidMesh.Reset();

	//Vertex Pos
	FVector HeightPos = FVector(0.0f, 0.0f, PyramidHeight);
	FVector LowPos = FVector(0.0f, 0.0f, -PyramidHeight);
	float BoxSizeX = PyramidBottomSize.X / 2.0f;
	float BoxSizeY = PyramidBottomSize.Y / 2.0f;
	FVector RightTopPos = FVector(BoxSizeX, BoxSizeY, 0.0f);
	FVector RightDownPos = FVector(-BoxSizeX, BoxSizeY, 0.0f);
	FVector LeftTopPos = FVector(BoxSizeX, -BoxSizeY, 0.0f);
	FVector LeftDownPos = FVector(-BoxSizeX, -BoxSizeY, 0.0f);

	FVector FaceNormal1 = FVector::CrossProduct(RightTopPos - HeightPos, HeightPos - LeftTopPos);
	FaceNormal1.Normalize();
	FVector FaceNormal2 = FVector::CrossProduct(LeftTopPos - HeightPos, HeightPos - LeftDownPos);
	FaceNormal2.Normalize();
	FVector FaceNormal3 = FVector::CrossProduct(LeftDownPos - HeightPos, HeightPos - RightDownPos);
	FaceNormal3.Normalize();
	FVector FaceNormal4 = FVector::CrossProduct(RightDownPos - HeightPos, HeightPos - RightTopPos);
	FaceNormal4.Normalize();
	FVector FaceNormal5 = FVector::CrossProduct(LeftTopPos - LowPos, LowPos - RightTopPos);
	FaceNormal5.Normalize();
	FVector FaceNormal6 = FVector::CrossProduct(LeftDownPos - LowPos, LowPos - LeftTopPos);
	FaceNormal6.Normalize();
	FVector FaceNormal7 = FVector::CrossProduct(RightDownPos - LowPos, LowPos - LeftDownPos);
	FaceNormal7.Normalize();
	FVector FaceNormal8 = FVector::CrossProduct(RightTopPos - LowPos, LowPos - RightDownPos);
	FaceNormal8.Normalize();

	//Add Vertex
	//Up
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LeftTopPos, FaceNormal1));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(HeightPos, FaceNormal1));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(RightTopPos, FaceNormal1));

	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LeftDownPos, FaceNormal2));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(HeightPos, FaceNormal2));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LeftTopPos, FaceNormal2));

	PyramidMesh.VertexArray.Add(FCustomMeshVertex(RightDownPos, FaceNormal3));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(HeightPos, FaceNormal3));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LeftDownPos, FaceNormal3));

	PyramidMesh.VertexArray.Add(FCustomMeshVertex(RightTopPos, FaceNormal4));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(HeightPos, FaceNormal4));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(RightDownPos, FaceNormal4));

	//Down
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(RightTopPos, FaceNormal5));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LowPos, FaceNormal5));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LeftTopPos, FaceNormal5));

	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LeftTopPos, FaceNormal6));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LowPos, FaceNormal6));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LeftDownPos, FaceNormal6));

	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LeftDownPos, FaceNormal7));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LowPos, FaceNormal7));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(RightDownPos, FaceNormal7));

	PyramidMesh.VertexArray.Add(FCustomMeshVertex(RightDownPos, FaceNormal8));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(LowPos, FaceNormal8));
	PyramidMesh.VertexArray.Add(FCustomMeshVertex(RightTopPos, FaceNormal8));

	for (int32 Index = 0; Index < 24; ++Index)
	{
		PyramidMesh.IndexArray.Add(Index);
	}

	UpdateLocalBounds();
}

覆盖创建渲染代理函数

FPrimitiveSceneProxy* UPyramidComponent::CreateSceneProxy()
{
	return new FPyramidSceneProxy(this);
}

覆盖构建包围体函数

Mesh视椎剔除的包围盒是通过CalcBounds函数传递的, 每次构建新的Mesh数据的时候记得调用UpdateLocalBounds 和 MarkRenderTransformDirty 更新剔除包围盒的大小

FBoxSphereBounds UPyramidComponent::CalcBounds(const FTransform& LocalToWorld) const
{
	FBoxSphereBounds Ret(LocalBounds.TransformBy(LocalToWorld));

	Ret.BoxExtent *= BoundsScale;
	Ret.SphereRadius *= BoundsScale;

	return Ret;
}


void UPyramidComponent::UpdateLocalBounds()
{
	FBox LocalBox(ForceInit);

	for(auto& MeshVertex : PyramidMesh.VertexArray)
	{
		LocalBox += MeshVertex.Position;
	}

	LocalBounds = LocalBox.IsValid ? FBoxSphereBounds(LocalBox) : FBoxSphereBounds(FVector(0, 0, 0), FVector(0, 0, 0), 0); // fallback to reset box sphere bounds

	// Update global bounds
	UpdateBounds();
	// Need to send to render thread
	MarkRenderTransformDirty();
}

UPyramidComponent更新数据给FPrimitiveSceneProxy

当你设置新的八面体高度,相应的顶点数据都会发生改变, 这时候更新数据有两种办法:

(1)进行渲染脏标记, 引起渲染Component再次调佣CreateSceneProxy,创建新的FPrimitiveSceneProxy,重新根据Component的数据构建新的VertexBuffer, IndexBuffer等等。不过这样重复销毁资源和创建资源不太好。设置材质新的时候就是进行了脏标记

(2)另外一种方式是Component通过渲染指令把要进行的顶点缓存更新。 具体是在GameThread直接通过ENQUEUE_RENDER_COMMAND把构建的数据变化传递到渲染线程的FPrimitiveSceneProxy,这种比较适合在频繁改变VertexBuffer, IndexBuffer参数的Mesh.

void UPyramidComponent::SendMeshDataToRenderThread()
{
	FPyramidMesh* NewPyramidMesh = new FPyramidMesh;
	*NewPyramidMesh = PyramidMesh;

	// Enqueue command to set vertex data to renderthread
	FPyramidSceneProxy* PyramidSceneProxy = (FPyramidSceneProxy*)SceneProxy;

	if (PyramidSceneProxy)
	{
		ENQUEUE_RENDER_COMMAND(FPyramidMeshData)(
			[PyramidSceneProxy, NewPyramidMesh](FRHICommandListImmediate& RHICmdList)
			{
				PyramidSceneProxy->SetMeshData_RenderThread(NewPyramidMesh);
			}
		);

	}
}

void FPyramidSceneProxy::SetMeshData_RenderThread(FPyramidMesh* NewPyramidMeshData)
{
	check(IsInRenderingThread());

	//FCustomVertex To FDynMeshVertexildPyramidMesh(NewPyramidMeshData, Vertices, Indices);
	TArray<FDynamicMeshVertex> Vertices;
	TArray<uint32> Indices;
	BuildPyramidMesh(NewPyramidMeshData, Vertices, Indices);

	for (int32 Index = 0; Index < Vertices.Num(); Index++)
	{
		const FDynamicMeshVertex& Vertex = Vertices[Index];

		VertexBuffers.PositionVertexBuffer.VertexPosition(Index) = Vertex.Position;
		VertexBuffers.StaticMeshVertexBuffer.SetVertexTangents(Index, Vertex.TangentX.ToFVector(), Vertex.GetTangentY(), Vertex.TangentZ.ToFVector());
		VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index, 0, Vertex.TextureCoordinate[0]);
		VertexBuffers.ColorVertexBuffer.VertexColor(Index) = Vertex.Color;
	}

	{
		auto& VertexBuffer = VertexBuffers.PositionVertexBuffer;
		void* VertexBufferData = RHILockVertexBuffer(VertexBuffer.VertexBufferRHI, 0, VertexBuffer.GetNumVertices() * VertexBuffer.GetStride(), RLM_WriteOnly);
		FMemory::Memcpy(VertexBufferData, VertexBuffer.GetVertexData(), VertexBuffer.GetNumVertices() * VertexBuffer.GetStride());
		RHIUnlockVertexBuffer(VertexBuffer.VertexBufferRHI);
	}

	{
		auto& VertexBuffer = VertexBuffers.ColorVertexBuffer;
		void* VertexBufferData = RHILockVertexBuffer(VertexBuffer.VertexBufferRHI, 0, VertexBuffer.GetNumVertices() * VertexBuffer.GetStride(), RLM_WriteOnly);
		FMemory::Memcpy(VertexBufferData, VertexBuffer.GetVertexData(), VertexBuffer.GetNumVertices() * VertexBuffer.GetStride());
		RHIUnlockVertexBuffer(VertexBuffer.VertexBufferRHI);
	}

	{
		auto& VertexBuffer = VertexBuffers.StaticMeshVertexBuffer;
		void* VertexBufferData = RHILockVertexBuffer(VertexBuffer.TangentsVertexBuffer.VertexBufferRHI, 0, VertexBuffer.GetTangentSize(), RLM_WriteOnly);
		FMemory::Memcpy(VertexBufferData, VertexBuffer.GetTangentData(), VertexBuffer.GetTangentSize());
		RHIUnlockVertexBuffer(VertexBuffer.TangentsVertexBuffer.VertexBufferRHI);
	}

	{
		auto& VertexBuffer = VertexBuffers.StaticMeshVertexBuffer;
		void* VertexBufferData = RHILockVertexBuffer(VertexBuffer.TexCoordVertexBuffer.VertexBufferRHI, 0, VertexBuffer.GetTexCoordSize(), RLM_WriteOnly);
		FMemory::Memcpy(VertexBufferData, VertexBuffer.GetTexCoordData(), VertexBuffer.GetTexCoordSize());
		RHIUnlockVertexBuffer(VertexBuffer.TexCoordVertexBuffer.VertexBufferRHI);
	}

	if (NewPyramidMeshData)
	{
		delete NewPyramidMeshData;
		NewPyramidMeshData = nullptr;
	}
}

结果显示

这里比较注意的设置材质的时候得进行 MarkRenderStateDirtyMarkRenderStateDirty会使得UPyramidComponent创建新的FPrimitiveSceneProxy。

八面体渲染组件的源码链接

CustomRenderComponent.zip-其他文档类资源-CSDN下载

参考资料

【1】CableComponent.h 和 ProceduralMeshComponent.h

【2】https://medium.com/@lordned/unreal-engine-4-rendering-part-2-shaders-and-vertex-data-80317e1ae5f3

猜你喜欢

转载自blog.csdn.net/qq_29523119/article/details/120806384