前言
在UE4开发中, 通常用到的Mesh有StaticMesh,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;
}
}
结果显示
这里比较注意的设置材质的时候得进行 MarkRenderStateDirty, MarkRenderStateDirty会使得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