# 前言在 ue 里面实现多 pass 比我想象中要简单,并不需要改引擎既可以实现。多 pass 本质上其实就是模型多画一次,所以 ue 需要多 pass 时候只要把模型多复制一份出来就好了,但这样显得有些些蠢,而且 drawcall 也会增加,所以这篇文章就是介绍基于图元多插入一个或多个 Material 来实现多 pass。
# 讲解首先介绍一下 UE 的模型渲染机制 首先可以看到上图,网格体渲染从 FPrimitiveSceneProxy 开始,FPrimitiveSceneProxy 负责通过对 GetDynamicMeshElements 和 DrawStaticElements 的回调将 FMeshBatch 提交给渲染器。从名字就可以看出这个 FPrimitiveSceneProxy 就是图元场景代理,是 UPrimitiveComponent 在渲染器的代表,镜像了 UPrimitiveComponent 在渲染线程的状态。
UE 渲染大致的过程是渲染器会遍历场景的所有经过了可见性测试的 PrimitiveSceneProxy 对象,利用其接口收集不同的 FMeshBatch,加入渲染队列中,所以我们自定义自己的渲染数据类型只需要继承 FPrimitiveSceneProxy 创建 FMeshBatch 就可以。这里只是粗略的介绍了渲染机制,更加详细的话可以看这篇文章
FPrimitiveSceneProxy 有两个生成 FMeshBatches 的路径:动态路径和缓存路径,就是 GetDynamicMeshElements 和 DrawStaticElements 这两个函数去生成 FMeshBatches,然后 FPrimitiveSceneProxy 通过 GetViewRelevance ()函数控制每个帧使用的路径。
缓存路径每一帧不会重构,可以缓存 FMeshBatches,比如 Static Mesh,性能好,但可拓展性弱,当一个 FPrimitiveSceneProxy 被添加到场景中时会调用此函数。然后储存到 FPrimitiveSceneInfo::StaticMeshes 中,之后通过 AddToDrawLists 将 FPrimitiveSceneInfo 里面的东西交给渲染列表。
动态路径会每帧动态重建 FMeshBatch 数据,因此可拓展性强,例如 ProceduralMesh、waterbody、地形、粒子特效、骨骼动画这些,但效率最低
由 FPrimitiveSceneProxy::GetViewRelevance 决定由那个途径生成 FMeshBatch,其中还有其他关于渲染属性的一些开关:
"" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 uint32 bStaticRelevance : 是否静态路劲生成 uint32 bDynamicRelevance : 是否动态路劲生成 uint32 bDrawRelevance : 是否要渲染 uint32 bShadowRelevance : 是否产生投影 uint32 bVelocityRelevance : 是否要产生速度,用于motion blur或者其他计算 uint32 bRenderCustomDepth : 是否自定义深度 uint32 bRenderInDepthPass : 是否渲染到DepthPass,即使不渲染到MainPass uint32 bRenderInMainPass : 是否渲染到MainPass uint32 bEditorPrimitiveRelevance : 仅在编辑器中绘制,并在后期处理后合成到场景中 uint32 bEditorVisualizeLevelInstanceRelevance : 图元的元素属于一个 LevelInstance,它在可视化 LevelInstance 过程中再次被编辑和渲染 uint32 bEditorStaticSelectionRelevance :被选中时候再次渲染OutlinePass uint32 bEditorNoDepthTestPrimitiveRelevance : 仅在编辑器中绘制,并在后期处理后合成到场景中,不适用深度测试 uint32 bHasSimpleLights : 图元收集简单的灯光是否被GatherSimpleLights 调用 uint32 bUsesLightingChannels : 是否使用灯光Channels uint32 bTranslucentSelfShadow : 是否使用半透明自阴影
还有一些在 4.25 以上改名的开关
"" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 UE_DEPRECATED (4.25 , "ShadingModelMaskRelevance has been renamed ShadingModelMask" ) uint16 ShadingModelMaskRelevance;UE_DEPRECATED (4.25 , "bOpaqueRelevance has been renamed bOpaque" ) uint32 bOpaqueRelevance : 1 ;UE_DEPRECATED (4.25 , "bMaskedRelevance has been renamed bMasked" ) uint32 bMaskedRelevance : 1 ;UE_DEPRECATED (4.25 , "bTranslucentVelocityRelevance has been renamed bOutputsTranslucentVelocity" ) uint32 bTranslucentVelocityRelevance : 1 ;UE_DEPRECATED (4.25 , "bDistortionRelevance has been renamed bDistortion" ) uint32 bDistortionRelevance : 1 ;UE_DEPRECATED (4.25 , "bSeparateTranslucencyRelevance has been renamed bSeparateTranslucency" ) uint32 bSeparateTranslucencyRelevance : 1 ;UE_DEPRECATED (4.25 , "bNormalTranslucencyRelevance has been renamed bNormalTranslucency" ) uint32 bNormalTranslucencyRelevance : 1 ;UE_DEPRECATED (4.25 , "bHairStrandsRelevance has been renamed bHairStrands" ) uint32 bHairStrandsRelevance : 1 ;
# 实现简单介绍完 FPrimitiveSceneProxy,然后接了下来说一下实现,为了代码从简,直接用 DrawStaticElements,什么情况都不考虑直接用 lod0 这部分可以参考 StaticMeshRender.cpp,SkeletalMesh 则可以参考 SkeletalMesh.cpp
Myproxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 class FMultiDrawSceneProxy : public FPrimitiveSceneProxy {public : FMultiDrawSceneProxy (UMultiDrawMeshComponent* InComponent) : FPrimitiveSceneProxy (InComponent) , MultiDrawMeshComponent (InComponent) { } UMultiDrawMeshComponent* MultiDrawMeshComponent; virtual uint32 GetMemoryFootprint (void ) const { return (sizeof (*this ) + GetAllocatedSize ()); } SIZE_T GetTypeHash (void ) const override { static size_t UniquePointer; return reinterpret_cast <size_t >(&UniquePointer); } virtual void DrawStaticElements (FStaticPrimitiveDrawInterface* PDI) override { const FStaticMeshRenderData* RenderData = MultiDrawMeshComponent->StaticDrawMesh->GetRenderData (); const FStaticMeshLODResources& Lods = RenderData ->LODResources[0 ]; int DrawMaterialNum = MultiDrawMeshComponent->DrawMaterial.Num (); int DrawPassNum = 0 ; PDI->ReserveMemoryForMeshes (1 ); for (UMaterialInterface* MaterialIndex : MultiDrawMeshComponent->DrawMaterial) { DrawPassNum++; FMaterialRenderProxy* MaterialProxy; if (MaterialIndex && RenderData) { if (MaterialIndex == NULL && DrawPassNum == 1 ) { MaterialProxy = UMaterial::GetDefaultMaterial (MD_Surface)->GetRenderProxy (); } MaterialProxy = MaterialIndex->GetRenderProxy (); FMeshBatch Mesh; FMeshBatchElement& BatchElement = Mesh.Elements[0 ]; BatchElement.IndexBuffer = (FIndexBuffer*)&Lods.IndexBuffer; Mesh.VertexFactory = &RenderData->LODVertexFactories[0 ].VertexFactory; Mesh.MaterialRenderProxy = MaterialProxy; BatchElement.FirstIndex = 0 ; BatchElement.NumPrimitives = Lods.IndexBuffer.GetNumIndices () / 3 ; BatchElement.MinVertexIndex = 0 ; BatchElement.MaxVertexIndex = Lods.IndexBuffer.GetNumIndices () - 1 ; Mesh.ReverseCulling = IsLocalToWorldDeterminantNegative (); if (DrawPassNum > 1 ) { if (MultiDrawMeshComponent->bNeedBackCull) { Mesh.ReverseCulling = true ; } Mesh.CastShadow = MultiDrawMeshComponent->bNeedOtherCastShadow; } Mesh.Type = PT_TriangleList; Mesh.DepthPriorityGroup = SDPG_World; Mesh.LODIndex = 0 ; PDI->DrawMesh (Mesh,RenderData->ScreenSize[0 ].GetValue ()); } } } virtual FPrimitiveViewRelevance GetViewRelevance (const FSceneView* View) const override { FPrimitiveViewRelevance Result; Result.bDrawRelevance = IsShown (View); Result.bShadowRelevance = IsShadowCast (View); Result.bDynamicRelevance = false ; Result.bStaticRelevance = true ; Result.bRenderInMainPass = ShouldRenderInMainPass (); Result.bRenderInDepthPass = ShouldRenderInDepthPass (); Result.bUsesLightingChannels = GetLightingChannelMask () != GetDefaultLightingChannelMask (); Result.bRenderCustomDepth = ShouldRenderCustomDepth (); Result.bTranslucentSelfShadow = bCastVolumetricTranslucentShadow; Result.bVelocityRelevance = DrawsVelocity () && Result.bOpaque && Result.bRenderInMainPass; return Result; } virtual bool CanBeOccluded () const override { return true ; } uint32 GetAllocatedSize (void ) const { return (FPrimitiveSceneProxy::GetAllocatedSize ()); } };
一个最简单的 FPrimitiveSceneProxy 就完成了,然后实现我们的 component
头文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 UCLASS (editinlinenew, meta = (BlueprintSpawnableComponent), ClassGroup = Rendering, HideCategories = (Physics, Object, Activation, "Components|Activation" ), meta = (DisplayName = "MultiDrawMesh" ))class MULTIPASSDRAW_API UMultiDrawMeshComponent : public UMeshComponent { GENERATED_BODY ()public : UPROPERTY (EditAnywhere, BlueprintReadWrite, Category = "MultiDraw Static Mesh" ) class UStaticMesh * StaticDrawMesh; UPROPERTY (EditAnywhere, BlueprintReadWrite, Category = "MultiDraw Static Mesh" ) TArray<UMaterialInterface*> DrawMaterial; UPROPERTY (EditAnywhere, BlueprintReadWrite, Category = "MultiDraw Static Mesh" ) bool bNeedBackCull = false ; UPROPERTY (EditAnywhere, BlueprintReadWrite, Category = "MultiDraw Static Mesh" ,meta=(Displayname = "Need Other Cast Shadow" )) bool bNeedOtherCastShadow = false ; virtual void GetUsedMaterials (TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials = false ) const override ; virtual bool GetMaterialStreamingData (int32 MaterialIndex, FPrimitiveMaterialInfo& MaterialData) const override ; virtual void GetStreamingRenderAssetInfo (FStreamingTextureLevelContext& LevelContext, TArray<FStreamingRenderAssetPrimitiveInfo>& OutStreamingRenderAssets) const override ; virtual class UBodySetup* GetBodySetup () override {return nullptr ;} virtual FBoxSphereBounds CalcBounds (const FTransform& LocalToWorld) const ; public : virtual FPrimitiveSceneProxy* CreateSceneProxy () override ;
cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 FPrimitiveSceneProxy* UMultiDrawMeshComponent::CreateSceneProxy () { if (StaticDrawMesh) { if (!StaticDrawMesh->GetRenderData ()->IsInitialized ()) { UE_LOG (LogStaticMesh, Verbose, TEXT ("Skipping CreateSceneProxy for StaticMeshComponent %s (RenderData is not initialized)" ), *GetFullName ()); return nullptr ; } return new FMultiDrawSceneProxy (this ); } return nullptr ; }void UMultiDrawMeshComponent::GetUsedMaterials (TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials) const { for (UMaterialInterface* material : DrawMaterial) OutMaterials.Add (material); }bool UMultiDrawMeshComponent::GetMaterialStreamingData (int32 MaterialIndex, FPrimitiveMaterialInfo& MaterialData) const { MaterialData.Material = GetMaterial (MaterialIndex); MaterialData.UVChannelData = StaticDrawMesh->GetUVChannelData (MaterialIndex); MaterialData.PackedRelativeBox = PackedRelativeBox_Identity; return MaterialData.IsValid (); }void UMultiDrawMeshComponent::GetStreamingRenderAssetInfo (FStreamingTextureLevelContext& LevelContext, TArray<FStreamingRenderAssetPrimitiveInfo>& OutStreamingRenderAssets) const { GetStreamingTextureInfoInner (LevelContext, nullptr , GetComponentTransform ().GetMaximumAxisScale (), OutStreamingRenderAssets); }FBoxSphereBounds UMultiDrawMeshComponent::CalcBounds (const FTransform& LocalToWorld) const { if (StaticDrawMesh) { FBoxSphereBounds MeshBounds = StaticDrawMesh->GetBounds (); return MeshBounds.TransformBy (LocalToWorld); } FBoxSphereBounds DummyBounds = FBoxSphereBounds (FVector (0 , 0 , 0 ), FVector (0 , 0 , 0 ), 0 ); return DummyBounds.TransformBy (LocalToWorld); }
一个非常简单的 component 就完成了,更多情况可以参考 StaticMeshComponent.h 和 SkeletalMeshCompnent.h
在 drawcall 方面分别用了复制四个模型和自己的 Component 放到场景对比
最后测试出来的结果是