虚幻引擎渲染机制学习笔记

向往大佬有关虚幻渲染机制的博客

  1. 虚幻引擎分为GameThread和RenderThread,UPrimitiveComponent是可渲染物体在GameThread的数据,UPrimitiveSceneProxy是可渲染物体在RenderThread的数据
    (为什么分为两个,个人认为是为了避免线程之间数据读写带来的一系列问题)

  2. 有关于类似模型之类的渲染对象的渲染数据用FMeshBatchElement表示,FMeshBatch则是一系列FMeshBatchElement的数据。这两者都是RenderThread中的对象

  3. 对于可渲染的对象UPrimitiveComponent,分为静态绘制路径和动态绘制路径,可以理解为:像StaticMesh这种不会每帧改变的物体,一般使用静态绘制路径,如果是类似某些水体之类的每帧都要改变的物体则需要动态绘制路径

  4. DrawStaticElements:以地形渲染为例
    地形渲染的图元组件分别为:ULandscapeComponent和FLandscapeComponentSceneProxy
    地形是我们导入或者使用UE的地形编辑工具生成的一个Mesh,地形渲染基本上走的是静态绘制路径。
    对于静态绘制路径中UPrimitiveSceneProxy来说,最重要的是virtual void DrawStaticElements(FStaticPrimitiveDrawInterface* PDI)这个接口。
    在这个接口中,我们把各级LOD代表的Mesh加到PDI中,并且还要告诉PDI当模型的包围盒在屏幕中的占比为多少时,选择哪级LOD。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void FLandscapeComponentSceneProxy::DrawStaticElements(FStaticPrimitiveDrawInterface* PDI)  
    {
    ...
    for (int32 LODIndex = FirstLOD; LODIndex <= LastLOD; LODIndex++)
    { FMeshBatch MeshBatch;

    if (GetStaticMeshElement(LODIndex, false, MeshBatch, StaticBatchParamArray))
    { PDI->DrawMesh(MeshBatch, LODIndex == FirstLOD ? FLT_MAX : (FMath::Sqrt(LODScreenRatioSquared[LODIndex]) * 2.0f));
    } }
    check(StaticBatchParamArray.Num() <= TotalBatchCount);
    }

    在FLandscapeComponentSceneProxy我们可以看到,地形首先会事先构建好各级LOD对应的包围盒屏占比,然后再DrawStaticElements中先构建MeshBatch然后再通过DrawMesh把各级LOD渲染需要的信息告诉PDI

  5. GetDynamicMeshElements:接口的样式为
    virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const
    DrawStaticElements一样本质上是要把MeshBatch加到Collector中,这里以ShowCollision时,简单碰撞的凸包的绘制为例:

    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
    if(bDrawSolid)  
    {
    // Cache collision vertex/index buffer
    if(!RenderInfo)
    { //这里要自己构建顶点数据以及索引,然后把这些数据组建成MeshBatch提供信息给Collector
    FKAggregateGeom& ThisGeom = const_cast<FKAggregateGeom&>(*this);
    ThisGeom.RenderInfo = new FKConvexGeomRenderInfo();
    ThisGeom.RenderInfo->VertexBuffers = new FStaticMeshVertexBuffers();
    ThisGeom.RenderInfo->IndexBuffer = new FDynamicMeshIndexBuffer32();

    TArray<FDynamicMeshVertex> OutVerts;
    for(int32 i=0; i<ConvexElems.Num(); i++)
    { // Get vertices/triangles from this hull.
    //构建IndexBuffer
    ConvexElems[i].AddCachedSolidConvexGeom(OutVerts, ThisGeom.RenderInfo->IndexBuffer->Indices, FColor::White);
    }
    // Only continue if we actually got some valid geometry
    // Will crash if we try to init buffers with no data if(ThisGeom.RenderInfo->VertexBuffers
    && ThisGeom.RenderInfo->IndexBuffer
    && OutVerts.Num() > 0
    && ThisGeom.RenderInfo->IndexBuffer->Indices.Num() > 0)
    { ThisGeom.RenderInfo->IndexBuffer->InitResource();

    ThisGeom.RenderInfo->CollisionVertexFactory = new FLocalVertexFactory(Collector.GetFeatureLevel(), "FKAggregateGeom");
    //把顶点数据告诉CollisionVertexFactory
    ThisGeom.RenderInfo->VertexBuffers->InitFromDynamicVertex(ThisGeom.RenderInfo->CollisionVertexFactory, OutVerts);

    } }
    // If we have geometry to draw, do so
    if(RenderInfo->HasValidGeometry())
    { // Calculate transform
    FTransform LocalToWorld = FTransform(FQuat::Identity, FVector::ZeroVector, Scale3D) * ParentTM;

    // Draw the mesh.
    FMeshBatch& Mesh = Collector.AllocateMesh();
    FMeshBatchElement& BatchElement = Mesh.Elements[0];
    BatchElement.IndexBuffer = RenderInfo->IndexBuffer;
    Mesh.VertexFactory = RenderInfo->CollisionVertexFactory;
    Mesh.MaterialRenderProxy = MatInst;
    FBoxSphereBounds WorldBounds, LocalBounds;
    CalcBoxSphereBounds(WorldBounds, LocalToWorld);
    CalcBoxSphereBounds(LocalBounds, FTransform::Identity);

    FDynamicPrimitiveUniformBuffer& DynamicPrimitiveUniformBuffer = Collector.AllocateOneFrameResource<FDynamicPrimitiveUniformBuffer>();
    DynamicPrimitiveUniformBuffer.Set(LocalToWorld.ToMatrixWithScale(), LocalToWorld.ToMatrixWithScale(), WorldBounds, LocalBounds, true, false, bOutputVelocity);
    BatchElement.PrimitiveUniformBufferResource = &DynamicPrimitiveUniformBuffer.UniformBuffer;

    // previous l2w not used so treat as static
    BatchElement.FirstIndex = 0;
    BatchElement.NumPrimitives = RenderInfo->IndexBuffer->Indices.Num() / 3;
    BatchElement.MinVertexIndex = 0;
    BatchElement.MaxVertexIndex = RenderInfo->VertexBuffers->PositionVertexBuffer.GetNumVertices() - 1;
    Mesh.ReverseCulling = LocalToWorld.GetDeterminant() < 0.0f ? true : false;
    Mesh.Type = PT_TriangleList;
    Mesh.DepthPriorityGroup = SDPG_World;
    Mesh.bCanApplyViewModeOverrides = false;
    Collector.AddMesh(ViewIndex, Mesh);
    }}
    else
    {
    //这里是简单地通过DrawLine画出一个个三角形的线框模式
    for(int32 i=0; i<ConvexElems.Num(); i++)
    { FColor ConvexColor = bPerHullColor ? DebugUtilColor[i%NUM_DEBUG_UTIL_COLORS] : Color;
    FTransform ElemTM = ConvexElems[i].GetTransform();
    ElemTM *= Transform;
    ConvexElems[i].DrawElemWire(Collector.GetPDI(ViewIndex), ElemTM, 1.f, ConvexColor); //we pass in 1 for scale because the ElemTM already has the scale baked into it
    }
    }

    参考这一段将凸包数据画出来的代码,分为两种情况:一种是简单地通过DrawLine画出一个个三角形的线框模式;还有一种是要自己构建顶点数据以及索引,然后把这些数据组建成MeshBatch提供信息给Collector
    其中顶点相关的数据是存在VertexFactory中,而索引的Buffer需要自己另外构建
    这里要注意的一点是,由于碰撞数据在模型中不同于渲染数据,因此我要画这些东西,可能需要自己构造相应的IndexBuffer和VertexBuffer告诉MeshBatch顶点和索引的信息

  6. 除上述两个接口之外还有一个很重要的接口virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const这个接口会决定你的Mesh是否会加入某个Pass的绘制(举例就是我在仿照UE源码添加一个MeshPass的时候,忽略了这个接口导致最终结果错误orz)

  7. 综上,对于研究一个图元组件渲染相关的一些参数,可以通过研究它的MeshBatch结构入手

  8. 有些时候,可能希望通过一些指令动态改变渲染的一些参数,如果是动态绘制路径还好,毕竟每帧都会调用,但是因为静态绘制路径是事先缓存好的,因此直接改变可能会不起作用,需要调用对应的UPrimitiveComponent的MarkRenderStateDirty()接口才能触发静态绘制路径的MeshBatch重新加入PDI