Zack.Zhang Game Developer

UE4 静态网格渲染源码详解

2022-11-28
zack.zhang
UE4

本文主要解析了 UE4 的整套渲染流程,通过文章解析我们可以对 UE4 怎么渲染出模型有一个概括且完整的认识。为了理解得更加详细具体,这里选用静态网格模型从数据导入到最终渲染出图形的过程为例,来详细描述整个渲染流程。

1. 构建渲染数据

通常来说,要渲染一个静态网格需要 Shader、材质、顶点缓冲区、索引缓冲区和顶点格式声明,那么本节内容主要看 UE4 是如何将导入引擎的 FBX 文件转化为引擎需要的数据。

这里我们从 UReimportFbxSceneFactory::ImportStaticMesh 开始往底层代码看,也就没必要从 UAssetToolsImpl::ImportAssets 等这么上层的代码往下看了。

EReimportResult::Type UReimportFbxSceneFactory::ImportStaticMesh(void* VoidFbxImporter, TSharedPtr<FFbxMeshInfo> MeshInfo, TSharedPtr<FFbxSceneInfo> SceneInfoPtr)
{
    UnFbx::FFbxImporter* FbxImporter = (UnFbx::FFbxImporter*)VoidFbxImporter;
    // 获得第一个Geometry节点
    for (int idx = 0; idx < FbxImporter->Scene->GetGeometryCount(); ++idx)
    {
        FbxGeometry *Geometry = FbxImporter->Scene->GetGeometry(idx);
        if (Geometry->GetUniqueID() == MeshInfo->UniqueId)
        {
            GeometryParentNode = Geometry->GetNode();
            break;
        }
    }
    
    (...)
    
    // 获得UPackage对象和静态网格模型的名字
    FString PackageName = MeshInfo->GetImportPath();
    FString StaticMeshName;
    UPackage* Pkg = CreatePackageForNode(PackageName, StaticMeshName);
    
    (...)
    
    UStaticMesh *NewObject = nullptr;
    FbxNode* NodeParent = FbxImporter->RecursiveFindParentLodGroup(GeometryParentNode->GetParent());
    if (NodeParent && NodeParent->GetNodeAttribute() && NodeParent->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
    {
        TArray<FbxNode*> AllNodeInLod;
        // 通过FBXSDK获取第0个子节点的LOD节点
        FbxImporter->FindAllLODGroupNode(AllNodeInLod, NodeParent, 0);
        // 生成UStaticMesh,也就是静态网格模型数据
        NewObject = FbxImporter->ImportStaticMeshAsSingle(Pkg, AllNodeInLod, StaticMeshFName, RF_Public | RF_Standalone, StaticMeshImportData, nullptr, 0);
        // 如果第0个子节点生成静态模型数据成功了再生成其他子节点的LOD模型,否则就没必要继续生成了
        if (NewObject)
        {
            for (int32 LODIndex = 1; LODIndex < NodeParent->GetChildCount(); LODIndex++)
            {
                AllNodeInLod.Empty();
                // 通过FBXSDK获取指定子节点的LOD节点
                FbxImporter->FindAllLODGroupNode(AllNodeInLod, NodeParent, LODIndex);
                // 生成UStaticMesh,也就是静态网格模型数据
                FbxImporter->ImportStaticMeshAsSingle(Pkg, AllNodeInLod, StaticMeshFName, RF_Public | RF_Standalone, StaticMeshImportData, NewObject, LODIndex);
            }
            FbxImporter->FindAllLODGroupNode(AllNodeInLod, NodeParent, 0);
            // 导入后处理:主要是构建渲染用的网格数据
            FbxImporter->PostImportStaticMesh(NewObject, AllNodeInLod);
        }
    }
    else
    {
        // 生成只有一层LOD的UStaticMesh,其内部也是调用了ImportStaticMeshAsSingle函数
        NewObject = FbxImporter->ImportStaticMesh(Pkg, GeometryParentNode, StaticMeshFName, RF_Public | RF_Standalone, StaticMeshImportData);
        if (NewObject != nullptr)
        {
            (...)
            // 导入后处理:主要是构建渲染用的网格数据
            FbxImporter->PostImportStaticMesh(NewObject, AllNodeInLod);
        }
    }
    
    (...)
    
    return EReimportResult::Succeeded;
}

代码第 3 行的 UnFbx::FFbxImporter 对象主要存储了当前导入的 FBX 的所有节点,这个类主要是对 FBX SDK 的一个封装。

代码第 32 行、42 行和 52 行调用的 ImportStaticMeshAsSingle 函数,会生成一个 UStaticMesh 对象,将在下文 1.1 节介绍。

代码第 46 行和 57 行调用的 PostImportStaticMesh 函数,主要是构建了 UStaticMesh 内部用于提供给渲染过程的一些数据,将在 1.2 节介绍。

1.1. 生成 UStaticMesh 对象

ImportStaticMeshAsSingle 函数会根据 FBX SDK 提供的 FbxNode 生成带多个 LOD 层次的 UStaticMesh 对象。

UStaticMesh* UnFbx::FFbxImporter::ImportStaticMeshAsSingle(UObject* InParent, TArray<FbxNode*>& MeshNodeArray, const FName InName, EObjectFlags Flags, UFbxStaticMeshImportData* TemplateImportData, UStaticMesh* InStaticMesh, int LODIndex, const FExistingStaticMeshData* ExistMeshDataPtr)
{
    (...)
    
    UStaticMesh* StaticMesh = NULL;
    
    (...)
    
    // 只有LODIndex为0的情况下InStaticMesh才是NULL
    if( InStaticMesh != NULL && LODIndex > 0 )
    {
        StaticMesh = InStaticMesh;
        // 只有在创建新资产时才取消操作,因为我们还不支持已有资产Restore。
        ImportOptions->bIsImportCancelable = false;
    }
    else
    {
        StaticMesh = NewObject<UStaticMesh>(Package, FName(*MeshName), Flags | RF_Public);
        CreatedObjects.Add(StaticMesh);
    }
    
    if (StaticMesh->GetNumSourceModels() < LODIndex+1)
    {
        // 创建一个LOD模型数据,每一个LOD模型数据都存在一个FStaticMeshSourceModel对象里
        StaticMesh->AddSourceModel();
        
        if (StaticMesh->GetNumSourceModels() < LODIndex+1)
        {
            LODIndex = StaticMesh->GetNumSourceModels() - 1;
        }
    }
    
    FStaticMeshSourceModel& SrcModel = StaticMesh->GetSourceModel(LODIndex);

    FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(LODIndex);
    // 如果MeshDescription不存在,则创建一个
    if (MeshDescription == nullptr)
    {
        // 实际上调用了FStaticMeshSourceModel::CreateMeshDescription函数
        MeshDescription = StaticMesh->CreateMeshDescription(LODIndex);
        (...)
    }
    
    (...)
    
    for (int32 MeshIndex = 0; MeshIndex < MeshNodeArray.Num(); MeshIndex++)
    {
        (...)
        FbxNode* Node = MeshNodeArray[MeshIndex];
        if (Node->GetMesh())
        {
            (...)
            if (!BuildStaticMeshFromGeometry(Node, StaticMesh, MeshMaterials, LODIndex,
                VertexColorImportOption, ExistingVertexColorData, ImportOptions->VertexOverrideColor))
            {
                bBuildStatus = false;
                break;
            }
        }
    }
    
    (...)
    
    return StaticMesh;
}

代码第 18 行通过调用 NewObject<UStaticMesh> 函数创建了一个空的 UStaticMesh 对象。

代码第 25 行通过调用 AddSourceModel 函数,为当前 LOD 创建了一个 FStaticMeshSourceModel 对象。FStaticMeshSourceModel 类后续 1.1.1 小节会详细解析,类里面保存模型数据的成员主要是 TUniquePtr<FMeshDescription> MeshDescriptionFMeshDescription 类也会在后续 1.1.2 小节详细解析。

代码第 53 行调用的 BuildStaticMeshFromGeometry 函数主要就是填充 FStaticMeshSourceModel,也就是填充 FMeshDescription。该函数调用会在后续 1.1.3 小节详细解析。

1.1.1. FStaticMeshSourceModel 类

该类用于保存一层 LOD 的模型数据。

USTRUCT()
struct FStaticMeshSourceModel
{
    GENERATED_USTRUCT_BODY()
    
    (...)
    
    TUniquePtr<FMeshDescription> MeshDescription;
    
    (...)
    
    /** 创建一个新的MeshDescription对象 */
    FMeshDescription* CreateMeshDescription();
}

代码第 8 行是保存模型数据用的 FMeshDescription 类的对象 MeshDescription

代码第 13 行的 CreateMeshDescription 函数是用来创建 FMeshDescription 对象的。

1.1.2. FMeshDescription 类

该类保存了模型的所有顶点的所有属性,比如位置、UV、三角形索引等。

struct MESHDESCRIPTION_API FMeshDescription
{
    (...)

    FVertexArray VertexArray;
    FVertexInstanceArray VertexInstanceArray;
    FEdgeArray EdgeArray;
    FTriangleArray TriangleArray;
    FPolygonArray PolygonArray;
    FPolygonGroupArray PolygonGroupArray;

    TAttributesSet<FVertexID> VertexAttributesSet;
    TAttributesSet<FVertexInstanceID> VertexInstanceAttributesSet;
    TAttributesSet<FEdgeID> EdgeAttributesSet;
    TAttributesSet<FTriangleID> TriangleAttributesSet;
    TAttributesSet<FPolygonID> PolygonAttributesSet;
    TAttributesSet<FPolygonGroupID> PolygonGroupAttributesSet;
}

其中 VertexArrayVertexInstanceArray 保存了每个顶点的 ID,而 VertexAttributesSetVertexInstanceAttributesSet 保存了每个顶点的具体属性。访问具体的顶点属性需要用到 FStaticMeshAttributes 类,这个即将在 1.1.3 小节看到。

1.1.3. BuildStaticMeshFromGeometry 函数

回到 ImportStaticMeshAsSingle 函数的代码第 53 行,调用了 BuildStaticMeshFromGeometry 函数,这个用来填充 UStaticMesh 的每一层 LOD 对应的 FMeshDescription 类所代表的模型数据。

bool UnFbx::FFbxImporter::BuildStaticMeshFromGeometry(FbxNode* Node, UStaticMesh* StaticMesh, TArray<FFbxMaterial>& MeshMaterials, int32 LODIndex,
    EVertexColorImportOption::Type VertexColorImportOption, const TMap<FVector, FColor>& ExistingVertexColorData, const FColor& VertexOverrideColor)
{
    (...)
    FbxMesh* Mesh = Node->GetMesh();
    (...)
    
    FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(LODIndex);
    // FStaticMeshAttributes是一个工具类,专门用来填充MeshDescription的顶点属性。
    FStaticMeshAttributes Attributes(*MeshDescription);
    
    (...)
    
    // 通过FStaticMeshAttributes工具类的对象,访问MeshDescription对象各个属性数组的引用
    TVertexAttributesRef<FVector> VertexPositions = Attributes.GetVertexPositions();
    TVertexInstanceAttributesRef<FVector> VertexInstanceNormals = Attributes.GetVertexInstanceNormals();
    TVertexInstanceAttributesRef<FVector> VertexInstanceTangents = Attributes.GetVertexInstanceTangents();
    TVertexInstanceAttributesRef<float> VertexInstanceBinormalSigns = Attributes.GetVertexInstanceBinormalSigns();
    TVertexInstanceAttributesRef<FVector4> VertexInstanceColors = Attributes.GetVertexInstanceColors();
    TVertexInstanceAttributesRef<FVector2D> VertexInstanceUVs = Attributes.GetVertexInstanceUVs();
    
    // 填充MeshDescription的顶点位置
    int32 VertexCount = Mesh->GetControlPointsCount();
    for (int32 VertexIndex = 0; VertexIndex < VertexCount; ++VertexIndex)
    {
        // 获取FBX的顶点位置
        FbxVector4 FbxPosition = Mesh->GetControlPoints()[VertexIndex];
        FbxPosition = TotalMatrix.MultT(FbxPosition);
        const FVector VertexPosition = Converter.ConvertPos(FbxPosition);
        
        // 获取顶点ID
        FVertexID AddedVertexId = MeshDescription->CreateVertex();
        // 用FBX的顶点位置,赋值给指定顶点ID的顶点
        VertexPositions[AddedVertexId] = VertexPosition;
    }
    
    (...)
    
    return bIsValidMesh;
}

代码第 8 行获取对应 LOD 的 FMeshDescription 对象,也就是对应 LOD 的模型数据的指针。

代码第 10 行用 FStaticMeshAttributes 类来封装 FMeshDescription 对象指针,生成一个名为 Attributes 的对象,这个对象可以用来访问 FMeshDescription 对象的所有属性。

代码第 15 行到 20 行,也就是使用 FStaticMeshAttributes 对象来访问 FMeshDescription 对象各个属性数组的引用,后续代码修改这个引用数组也就是修改 FMeshDescription 对象本身,可以说 FStaticMeshAttributes 就是 FMeshDescription 的代理。

代码第 23 行到 35 行就是使用 FStaticMeshAttributes 对象来填充模型数据的一个例子,这里以填充顶点位置属性为例说明 FStaticMeshAttributes 这个代理是如何帮助 FMeshDescription 填充顶点属性的。

1.2. 构建 UStaticMesh 渲染数据

通过开篇 UReimportFbxSceneFactory::ImportStaticMesh 函数的代码可以知道,构建 UStaticMesh 渲染数据的函数是 PostImportStaticMesh

void UnFbx::FFbxImporter::PostImportStaticMesh(UStaticMesh* StaticMesh, TArray<FbxNode*>& MeshNodeArray, int32 LODIndex)
{
    (...)
    
    StaticMesh->Build(false, &BuildErrors);
    
    (...)
}

这个函数原本很长,但是我们关心的只有一句话,也就是代码第 5 行的 Build 函数。

下面代码从 Build 函数开始往底层调用,由于调用层级比较深,所以把多个函数调用合并到一个代码块去写,类似于调用栈。

void UStaticMesh::Build(bool bInSilent, TArray<FText>* OutErrors)
{
    void UStaticMesh::BatchBuild(const TArray<UStaticMesh*>& InStaticMeshes, bool bInSilent, TFunction<bool(UStaticMesh*)> InProgressCallback, TArray<FText>* OutErrors)
    {
        bool UStaticMesh::BuildInternal(bool bInSilent, TArray<FText> * OutErrors)
        {
            void UStaticMesh::CacheDerivedData()
            {
                (...)
                
                RenderData = MakeUnique<FStaticMeshRenderData>();
                RenderData->Cache(RunningPlatform, this, LODSettings);
                
                (...)
            }
            
            // 如果当前Application是可以渲染东西的话
            if(FApp::CanEverRender())
            {
                // 重新初始化静态网格模型的资源。
                InitResources();
            }
        }
    }
}

代码第 11 行的 FStaticMeshRenderData 类型的 RenderData 对象,也就是保存渲染线程所需的所有数据的地方。

代码第 12 行调用了 Cache 函数,该函数的主要工作就是生成并存放渲染所需的所有数据,主要工作是将 1.1.2 小节生成的 FMeshDescription 类的对象转化成 RenderData 里面的顶点缓冲和索引缓冲。这个 RenderData 对象是一个很重要的对象,将作为 FStaticMeshSceneProxy 类和 UStaticMeshComponent 类中保存渲染数据的关键成员字段,并在渲染线程里用到。

代码第 21 行执行调用 InitResources 函数的时候,UStaticMesh 的渲染数据已经都构建好了,所以这部分将在 1.3 小节详细介绍。

下面看看 RenderDataCache 函数如何实现。

void FStaticMeshRenderData::Cache(const ITargetPlatform* TargetPlatform, UStaticMesh* Owner, const FStaticMeshLODSettings& LODSettings)
{
    (...)
    
    IMeshBuilderModule& MeshBuilderModule = IMeshBuilderModule::GetForPlatform(TargetPlatform);
    if (!MeshBuilderModule.BuildMesh(*this, Owner, LODGroup))
    {
        UE_LOG(LogStaticMesh, Error, TEXT("Failed to build static mesh. See previous line(s) for details."));
        return;
    }
    
    (...)
}

代码第 5 行获得一个实现了 IMeshBuilderModule 接口的对象,这里获取出来实际就是<p style="color:blue">Engine/Source/Developer/MeshBuilder/Private/MeshBuilderModule.cpp</p>里面定义的 FMeshBuilderModule 类的对象。那么具体看一下 BuildMesh 函数的实现。

bool FMeshBuilderModule::BuildMesh(FStaticMeshRenderData& OutRenderData, class UObject* Mesh, const FStaticMeshLODGroup& LODGroup)
{
    UStaticMesh* StaticMesh = Cast<UStaticMesh>(Mesh);
    if (StaticMesh != nullptr)
    {
        // 调用静态网格模型的Builder
        return FStaticMeshBuilder().Build(OutRenderData, StaticMesh, LODGroup);
        
        // 依然是为了简化文章,所以多个函数调用写到一个代码段里,实际C++代码不是这样的。
        bool FStaticMeshBuilder::Build(FStaticMeshRenderData& StaticMeshRenderData, UStaticMesh* StaticMesh, const FStaticMeshLODGroup& LODGroup)
        {
            (...)
            
            FStaticMeshLODResources& StaticMeshLOD = StaticMeshRenderData.LODResources[LodIndex];
            
            (...)
            
            // 构建顶点和索引缓冲
            BuildVertexBuffer(StaticMesh, LodIndex, MeshDescriptions[LodIndex], StaticMeshLOD, LODBuildSettings, IndexBuffer, WedgeMap, PerSectionIndices, StaticMeshBuildVertices, MeshDescriptionHelper.GetOverlappingCorners(), VertexComparisonThreshold, RemapVerts);
            
            (...)
        }
    }
    return false;
}

代码第 14 行获取出 RenderData 的对应 LOD 层的 LODResources 数组对象,接下来的主要工作就是要初始化这个 FStaticMeshLODResources 类型的对象。

代码第 19 行的 BuildVertexBuffer 是一个全局函数,用来构建 UStaticMesh 的顶点和索引缓冲。

void BuildVertexBuffer(
          UStaticMesh *StaticMesh
        , int32 LodIndex
        , const FMeshDescription& MeshDescription
        , FStaticMeshLODResources& StaticMeshLOD
        , const FMeshBuildSettings& LODBuildSettings
        , TArray< uint32 >& IndexBuffer
        , TArray<int32>& OutWedgeMap
        , TArray<TArray<uint32> >& OutPerSectionIndices
        , TArray< FStaticMeshBuildVertex >& StaticMeshBuildVertices
        , const FOverlappingCorners& OverlappingCorners
        , float VertexComparisonThreshold
        , TArray<int32>& RemapVerts)
{
    (...)
    
    // 初始化顶点缓冲的所有元素用的
    StaticMeshBuildVertices.Reserve(VertexInstances.GetArraySize());

    // 取出MeshDescription的所有属性
    TPolygonGroupAttributesConstRef<FName> PolygonGroupImportedMaterialSlotNames = MeshDescription.PolygonGroupAttributes().GetAttributesRef<FName>(MeshAttribute::PolygonGroup::ImportedMaterialSlotName);
    TVertexAttributesConstRef<FVector> VertexPositions = MeshDescription.VertexAttributes().GetAttributesRef<FVector>( MeshAttribute::Vertex::Position );
    TVertexInstanceAttributesConstRef<FVector> VertexInstanceNormals = MeshDescription.VertexInstanceAttributes().GetAttributesRef<FVector>( MeshAttribute::VertexInstance::Normal );
    TVertexInstanceAttributesConstRef<FVector> VertexInstanceTangents = MeshDescription.VertexInstanceAttributes().GetAttributesRef<FVector>( MeshAttribute::VertexInstance::Tangent );
    TVertexInstanceAttributesConstRef<float> VertexInstanceBinormalSigns = MeshDescription.VertexInstanceAttributes().GetAttributesRef<float>( MeshAttribute::VertexInstance::BinormalSign );
    TVertexInstanceAttributesConstRef<FVector4> VertexInstanceColors = MeshDescription.VertexInstanceAttributes().GetAttributesRef<FVector4>( MeshAttribute::VertexInstance::Color );
    TVertexInstanceAttributesConstRef<FVector2D> VertexInstanceUVs = MeshDescription.VertexInstanceAttributes().GetAttributesRef<FVector2D>( MeshAttribute::VertexInstance::TextureCoordinate );
    
    for (const FPolygonID PolygonID : MeshDescription.Polygons().GetElementIDs())
    {
        (...)
        const TArray<FTriangleID>& TriangleIDs = MeshDescription.GetPolygonTriangleIDs(PolygonID);
        (...)
        for (int32 TriangleIndex = 0; TriangleIndex < TriangleIDs.Num(); ++TriangleIndex)
        {
            const FTriangleID TriangleID = TriangleIDs[TriangleIndex];
            (...)
            for (int32 TriVert = 0; TriVert < 3; ++TriVert, ++WedgeIndex)
            {
                const FVertexInstanceID VertexInstanceID = MeshDescription.GetTriangleVertexInstance(TriangleID, TriVert);
                (...)
                const FVector& VertexPosition = CornerPositions[TriVert];
                const FVector& VertexInstanceNormal = VertexInstanceNormals[VertexInstanceID];
                const FVector& VertexInstanceTangent = VertexInstanceTangents[VertexInstanceID];
                const float VertexInstanceBinormalSign = VertexInstanceBinormalSigns[VertexInstanceID];
                
                FStaticMeshBuildVertex StaticMeshVertex;
                
                StaticMeshVertex.Position = VertexPosition * LODBuildSettings.BuildScale3D;
                StaticMeshVertex.TangentX = ScaleMatrix.TransformVector(VertexInstanceTangent).GetSafeNormal();
                StaticMeshVertex.TangentY = ScaleMatrix.TransformVector(FVector::CrossProduct(VertexInstanceNormal, VertexInstanceTangent).GetSafeNormal() * VertexInstanceBinormalSign).GetSafeNormal();
                StaticMeshVertex.TangentZ = ScaleMatrix.TransformVector(VertexInstanceNormal).GetSafeNormal();
                
                (...)
                
                Index = StaticMeshBuildVertices.Add(StaticMeshVertex);
            }
        }
    }
    
    StaticMeshLOD.VertexBuffers.StaticMeshVertexBuffer.SetUseHighPrecisionTangentBasis(LODBuildSettings.bUseHighPrecisionTangentBasis);
    StaticMeshLOD.VertexBuffers.StaticMeshVertexBuffer.SetUseFullPrecisionUVs(LODBuildSettings.bUseFullPrecisionUVs);
    StaticMeshLOD.VertexBuffers.StaticMeshVertexBuffer.Init(StaticMeshBuildVertices, NumTextureCoord);
    StaticMeshLOD.VertexBuffers.PositionVertexBuffer.Init(StaticMeshBuildVertices);
    StaticMeshLOD.VertexBuffers.ColorVertexBuffer.Init(StaticMeshBuildVertices);
}

代码第 5 行的 FStaticMeshLODResources& StaticMeshLOD 就是这个函数要返回的关键字段,为了初始化这个字段,代码第 18 行初始化了一个 StaticMeshBuildVertices 对象,代码第 61 行到 65 行使用这个 StaticMeshBuildVertices 对象来初始化 StaticMeshLOD 对象。

代码第 20 行到第 59 行,都是使用 UStaticMesh 类对应 LOD 的 MeshDescription 成员对象来填充 StaticMeshBuildVertices 对象。

那么下一节将开始介绍 RenderData 相关的类,以及渲染数据在这些类中是如何传递的。

1.3. 初始化 FStaticMeshRenderData 类及相关 RHI 对象

还记得 1.2 小节 UStaticMesh::Build 函数中调用的 InitResources 函数的功能,是要留到这一小节来讲的吗?下面再次贴上代码回顾一下。

void UStaticMesh::CacheDerivedData()
{
    (...)
    
    // 如果当前Application是可以渲染东西的话
    if(FApp::CanEverRender())
    {
        // Reinitialize the static mesh's resources.
        InitResources();
    }
}

也就是到代码第 3 行的时候 UStaticMesh 的渲染内存数据已经都构建好了,代码第 9 行要开始调用 UStaticMesh::InitResources 函数初始化 UStaticMesh 类的 RHI 资源数据了。

在介绍 UStaticMesh::InitResources 函数之前,先总的看一下 UStaticMesh 渲染时最主要用到的一些类的 UML 图吧!

UStaticMesh-UML

从图中“推荐从这里开始看”的地方看起,第一个类就是上文提到过的 UStaticMesh 类。

接下来,UStaticMesh 类里面最重要的一个成员就是我们之前介绍过的 FStaticMeshRenderData* RenderData

FStaticMeshRenderData 类里面有两个重要的成员(UML 图里均已标位<p style="color:red">红色</p>):一个类型为 FStaticMeshLODResources,这个在之前的 FBX 导入过程中已经详细描述过;另一个类型为 FStaticMeshVertexFactories,这个类里面有一个重要的成员,这个成员的类型是 FLocalVertexFactory,我们称其为“顶点工厂”类。到目前为止,我们没有创建过任何顶点工厂相关的对象,也就是说它现在还是未初始化的状态。

在讲顶点工厂初始化代码之前,先了解一下顶点工厂的作用。

从 UML 图中可以看到,顶点工厂类有一个 FDataType 类型的对象,这个对象有以下成员属性(UML 图里均已标为<p style="color:green">绿色</p>):PositionComponentTangentBasisComponents[2]TextureCoordinatesLightMapCoordinateComponentColorComponent,这些正好与渲染模型时候所需的顶点属性的类型吻合。而且注意看每一个属性都是 FVertexStreamComponent 类型的,这个类型又有一个 FVertexBuffer 类型的成员。

所以从结论上说,FLocalVertexFactory 这个顶点工厂,已经包含了在渲染时候所有需要的顶点缓冲区(Vertex Buffer)顶点声明(Vertex Declaration)

那么下面回到 UStaticMesh::InitResources 函数看看如何创建顶点缓冲区和顶点声明的,又如何初始化顶点工厂的。

void UStaticMesh::InitResources()
{
    (...)
    
    if (RenderData)
    {
        UWorld* World = GetWorld();
        RenderData->InitResources(World ? World->FeatureLevel.GetValue() : ERHIFeatureLevel::Num, this);
    }
    
    (...)
}

从代码看这个函数主要调用了 FStaticMeshRenderData 类的 InitResources 函数。

void FStaticMeshRenderData::InitResources(ERHIFeatureLevel::Type InFeatureLevel, UStaticMesh* Owner)
{
    (...)
    for (int32 LODIndex = 0; LODIndex < LODResources.Num(); ++LODIndex)
    {
        // 跳过已经剥离了渲染数据的LOD
        if (LODResources[LODIndex].VertexBuffers.StaticMeshVertexBuffer.GetNumVertices() > 0)
        {
            LODResources[LODIndex].InitResources(Owner);
            LODVertexFactories[LODIndex].InitResources(LODResources[LODIndex], LODIndex, Owner);
        }
        (...)
    }
    (...)
}

从代码第 9 行看,主要调用了 FStaticMeshLODResourcesInitResources 函数。

从代码第 10 行看,主要调用了 FStaticMeshVertexFactoriesInitResources 函数。

下面分别来看两个函数做了什么。

1.3.1. 初始化 FStaticMeshLODResources 资源

首先是 FStaticMeshLODResourcesInitResources 函数。

void FStaticMeshLODResources::InitResources(UStaticMesh* Parent)
{
    (...)
    BeginInitResource(&IndexBuffer);
    if(bHasWireframeIndices)
    {
        BeginInitResource(&AdditionalIndexBuffers->WireframeIndexBuffer);
    }
    BeginInitResource(&VertexBuffers.StaticMeshVertexBuffer);
    BeginInitResource(&VertexBuffers.PositionVertexBuffer);
    if(bHasColorVertexData)
    {
        BeginInitResource(&VertexBuffers.ColorVertexBuffer);
    }
    
    (...)
}

从代码可以看到,该函数主要是调用 BeginInitResource 函数去初始化从 FBX 生成的各个缓冲区,比如索引缓冲区、位置缓冲区等。

下面简单看一下 BeginInitResource 函数是怎么实现的。

void BeginInitResource(FRenderResource* Resource)
{
    ENQUEUE_RENDER_COMMAND(InitCommand)(
        [Resource](FRHICommandListImmediate& RHICmdList)
        {
            Resource->InitResource();
        });
}

BeginInitResource 的函数实现上来看,就是把各个缓冲区的初始化函数 InitResource 放到渲染线程去执行了。

什么是渲染线程,请参看上一篇文章 UE4 多线程渲染源码详解

下面以位置的顶点缓冲区(FPositionVertexBuffer 类)为例,看一下这些缓冲区的 InitResource 函数内部都做了什么。

由于这些缓冲区都是渲染线程的资源,所以他们都继承于 FRenderResource 类,InitResource 函数的实现就是写在这个基类里面。

void FRenderResource::InitResource()
{
    (...)
    
    if (GIsRHIInitialized)
    {
        (...)
        InitDynamicRHI();
        InitRHI();
    }
    
    (...)
}

从代码第 8 行和第 9 行可以得知,FRenderResource 类的 InitResource 函数内部是会去调用 InitRHI 函数的。

那么我们再回到 FPositionVertexBuffer 类看看 InitRHI 函数的实现。

void FPositionVertexBuffer::InitRHI()
{
    VertexBufferRHI = CreateRHIBuffer_RenderThread();
    (...)
}

代码第 3 行调用了 CreateRHIBuffer_RenderThread 函数去初始化一个 FRHIVertexBuffer 类型的引用。

这里我们就不往 CreateRHIBuffer_RenderThread 函数内部去探究了。因为会涉及到不同的渲染 API,为了控制篇幅这里以 DX12 为例简单介绍一下 CreateRHIBuffer_RenderThread 函数内部实现。

首先,从先前的介绍里我们知道这个函数是在渲染线程里执行的。然后,通过 DX12 的 API 函数 CreatePlacedResource 去创建一个占位资源立即返回到函数返回值。最后,在 RHI 线程里,调用 CopyBufferRegion 将包含有缓冲数据的 UploadBuffer 填充到缓冲区内。有兴趣可以去看 FD3D12DynamicRHI::RHICreateVertexBuffer 的实现。

到这里 FPositionVertexBuffer 的 RHI 资源就已经初始化完成了,FStaticMeshLODResources 的其他缓冲区(UML 图的标为<p style="color:blue">蓝色</p>的那些类)也是类似的初始化过程就不再赘述。

另外还有一个疑问,为什么要给每一个顶点属性都创建一个顶点缓冲区?比如上面的代码,为位置属性创建一个顶点缓冲区,又为颜色属性创建一个顶点缓冲区……具体原因可以参看储存顶点数据:交错还是不交错?(翻译)

至此,FStaticMeshLODResourcesInitResources 函数完成了它的工作。

1.3.2. 初始化 FStaticMeshVertexFactories 资源

最后是 FStaticMeshVertexFactoriesInitResources 函数。

void FStaticMeshVertexFactories::InitResources(const FStaticMeshLODResources& LodResources, uint32 LODIndex, const UStaticMesh* Parent)
{
        InitVertexFactory(LodResources, VertexFactory, LODIndex, Parent, false);
        BeginInitResource(&VertexFactory);

        (...)
}

从代码看这个函数总共调用了两个函数:InitVertexFactoryBeginInitResource。下面先看看 InitVertexFactory 函数的实现。

void FStaticMeshVertexFactories::InitVertexFactory(
    const FStaticMeshLODResources& LodResources,
    FLocalVertexFactory& InOutVertexFactory,
    uint32 LODIndex,
    const UStaticMesh* InParentMesh,
    bool bInOverrideColorVertexBuffer
    )
{
    struct InitStaticMeshVertexFactoryParams
    {
        FLocalVertexFactory* VertexFactory;
        const FStaticMeshLODResources* LODResources;
        bool bOverrideColorVertexBuffer;
        uint32 LightMapCoordinateIndex;
        uint32 LODIndex;
    } Params;
    (...)
    Params.VertexFactory = &InOutVertexFactory;
    Params.LODResources = &LodResources;
    Params.bOverrideColorVertexBuffer = bInOverrideColorVertexBuffer;
    Params.LightMapCoordinateIndex = LightMapCoordinateIndex;
    Params.LODIndex = LODIndex;
    
    // 初始化静态网格模型的顶点工厂
    ENQUEUE_RENDER_COMMAND(InitStaticMeshVertexFactory)(
        [Params](FRHICommandListImmediate& RHICmdList)
        {
            FLocalVertexFactory::FDataType Data;

            Params.LODResources->VertexBuffers.PositionVertexBuffer.BindPositionVertexBuffer(Params.VertexFactory, Data);
            Params.LODResources->VertexBuffers.StaticMeshVertexBuffer.BindTangentVertexBuffer(Params.VertexFactory, Data);
            Params.LODResources->VertexBuffers.StaticMeshVertexBuffer.BindPackedTexCoordVertexBuffer(Params.VertexFactory, Data);
            Params.LODResources->VertexBuffers.StaticMeshVertexBuffer.BindLightMapVertexBuffer(Params.VertexFactory, Data, Params.LightMapCoordinateIndex);
            
            (...)
            
            Params.VertexFactory->SetData(Data);
            Params.VertexFactory->InitResource();
        });
}

代码第 16 行声明并定义了一个 InitStaticMeshVertexFactoryParams 类的对象 Params

代码第 19 行将 FStaticMeshLODResources 的对象传递给 Params 的成员 LODResources

代码第 30 行到 33 行是在渲染线程里执行的,做的事情是用已经初始化完成的 FStaticMeshLODResources 内存对象和顶点工厂做一个“绑定”。

PositionVertexBuffer 为例看看具体是如何“绑定”的。

void FPositionVertexBuffer::BindPositionVertexBuffer(const FVertexFactory* VertexFactory, FStaticMeshDataType& StaticMeshData) const
{
    StaticMeshData.PositionComponent = FVertexStreamComponent(
        this,
        STRUCT_OFFSET(FPositionVertex, Position),
        GetStride(),
        VET_Float3
    );
    StaticMeshData.PositionComponentSRV = PositionComponentSRV;
}

代码第 3 行的 StaticMeshData 对象其实就是 FLocalVertexFactory 类的 Data 成员。

从上面整个代码看,这个 BindPositionVertexBuffer 函数就是用已经创建好的 RHI 对象来创建顶点工厂的 FVertexStreamComponent,使每一个 FVertexStreamComponent 都指向已创建好的 RHI 对象,可以说它们就是 FStaticMeshVertexBuffers 里每一个缓冲区在渲染线程里的代理。

下面回到 FStaticMeshVertexFactories::InitResources 函数看看 BeginInitResource(&VertexFactory) 做了什么。

根据前文看过的代码可以知道 BeginInitResource 函数实际上就是在渲染线程调用参数对象的 InitRHI 函数,所以直接看一下 FLocalVertexFactoryInitRHI 函数。

void FLocalVertexFactory::InitRHI()
{
    (...)
    FVertexDeclarationElementList Elements;
    if(Data.PositionComponent.VertexBuffer != NULL)
    {
        Elements.Add(AccessStreamComponent(Data.PositionComponent,0));
    }
    
    (...)
    
    InitDeclaration(Elements);
    (...)
    
    const int32 DefaultBaseVertexIndex = 0;
    const int32 DefaultPreSkinBaseVertexIndex = 0;
    if (RHISupportsManualVertexFetch(GMaxRHIShaderPlatform) || bCanUseGPUScene)
    {
        (...)
        UniformBuffer = CreateLocalVFUniformBuffer(this, Data.LODLightmapDataIndex, nullptr, DefaultBaseVertexIndex, DefaultPreSkinBaseVertexIndex);
    }
    (...)
}

代码第 12 行调用 InitDeclaration 函数,根据从 Data 收集来的顶点属性生成顶点声明(Vertex Declaration)。

代码第 7 行还同时还调用了 AccessStreamComponent 这个函数,通过这个函数可以将 StaticMeshData 里面的每一个顶点属性都添加到 FVertexFactory::Streams 这个成员数组里面了,在后续生成绘制命令时主要用的就是这个成员对象。

FVertexElement FVertexFactory::AccessStreamComponent(const FVertexStreamComponent& Component, uint8 AttributeIndex)
{
    FVertexStream VertexStream;
    VertexStream.VertexBuffer = Component.VertexBuffer;
    VertexStream.Stride = Component.Stride;
    VertexStream.Offset = Component.StreamOffset;
    VertexStream.VertexStreamUsage = Component.VertexStreamUsage;

    return FVertexElement(Streams.AddUnique(VertexStream),Component.Offset,Component.Type,AttributeIndex,VertexStream.Stride, EnumHasAnyFlags(EVertexStreamUsage::Instancing, VertexStream.VertexStreamUsage));
}

代码第 9 行的 Streams.AddUnique 也就是将这个 FVertexStreamComponent 的引用同时保存到 Streams 数组里。

1.3.4. 小结

至此,静态网格模型渲染所需要的 RHI 缓冲区已经备齐。至于材质和 Shader 的相关流程以后的文章会专门地解析,这里不再详述。

2. 渲染线程处理渲染数据

第 1 节内容描述了创建静态模型的完整过程,接下来就可以把上一节中创建的数据输入到渲染线程来处理了。

2.1. 引擎模块和渲染模块的主要类型

在介绍具体处理过程之前,先理解几个概念。前文 1.3 小节的 UML 图里,我们可以看到有两个当时忽略的类 FStaticMeshSceneProxyUStaticMeshComponent,它们都同时有一个 UStaticMesh 类型的成员。那么这两个类是什么,它们又是如何创建的?

UStaticMeshComponent 类我们很熟悉,在虚幻编辑器里我们经常用到这个组建,并将其附加到一个 AActor 上使用。这里我们需要知道:

  • FStaticMeshSceneProxy 的基类是 FPrimitiveSceneProxy
  • UStaticMeshComponent 的基类是 UPrimitiveComponent

在虚幻编辑器里不仅 UStaticMeshComponent 继承于 UPrimitiveComponent,所有可渲染的组件都要继承于 UPrimitiveComponent。那么下面整理了这些基类的一些概念,方便做对比。

类型 功能 工作线程 模块
UWorld 引擎里所有关卡和 Actor 的容器 游戏线程 引擎模块
FScene UWorld 在渲染模块里的代理,UWorld 的渲染工作全部交给 FScene 来实现。 渲染线程 渲染模块
UPrimitiveComponent 引擎里所有可渲染的组件父类 游戏线程 引擎模块
FPrimitiveSceneProxy UPrimitiveComponent 在渲染线程的代理,记录了 UPrimitiveComponent 的渲染数据 渲染线程 引擎模块
FPrimitiveSceneInfo UPrimitiveComponent 在渲染模块的状态,包含了 UPrimitiveComponent 和 FPrimitiveSceneProxy 渲染线程 渲染模块

也就是说,在引擎里我们给 AActor 加上了 UStaticMeshComponent 组件,为了渲染这个组件包含的 UStaticMesh 对象里面的渲染数据,需要将 UStaticMesh 传递给渲染模块的代理类 FStaticMeshSceneProxy,交由这个代理类在渲染线程里面处理数据。

为了下文描述方便,我们称 Primitive 为“基元”。

2.2. 将渲染数据提交到渲染模块

UStaticMeshComponent 的基类之一 UActorComponent 类,里面有一个函数是处理渲染数据传递到渲染线程相关逻辑的,那就是 DoDeferredRenderUpdates_Concurrent 函数。

/**
 * 使用bRenderStateDirty/bRenderTransformDirty在该组件上执行必要的工作。
 * 不要直接调用这个函数,调用MarkRenderStateDirty, markrenderdynamicdatadity来触发
 *
 * @注意,这是在多个线程上并发调用的(但是绝不能同时调用相同的组件)
 */
void UActorComponent::DoDeferredRenderUpdates_Concurrent()
{
    (...)
    
    if(bRenderStateDirty)
    {
        (...)
        RecreateRenderState_Concurrent();
        (...)
    }
    else
    {
        if(bRenderTransformDirty)
        {
            SendRenderTransform_Concurrent();
        }

        if(bRenderDynamicDataDirty)
        {
            SendRenderDynamicData_Concurrent();
        }
    }
}

从代码注释看,大意就是这个 DoDeferredRenderUpdates_Concurrent 函数的触发点比较多,当有渲染状态改变时就会触发。这个函数内部最终都会调用到 SendRenderTransform_Concurrent 函数,而 SendRenderTransform_Concurrent 是一个虚函数,那么看看子类 UPrimitiveComponent 里面的 SendRenderTransform_Concurrent 函数实现。

void UPrimitiveComponent::SendRenderTransform_Concurrent()
{
    if( bDetailModeAllowsRendering && (ShouldRender() || bCastHiddenShadow))
    {
        // 为这个Primitive更新场景信息
        GetWorld()->Scene->UpdatePrimitiveTransform(this);
    }

    Super::SendRenderTransform_Concurrent();
}

代码第 6 行用到了 GetWorld()->Scene 这个 FScene 类型的对象,并将 this 指针传到这个对象的函数里面。已知 FScene 是渲染模块的对象,所以正是通过 UpdatePrimitiveTransform 函数将渲染数据从引擎模块传递到渲染模块的。

void FScene::UpdatePrimitiveTransform(UPrimitiveComponent* Primitive)
{
    (...)
    
    // 如果Primitive的代理为空,则创建一个
    if(Primitive->SceneProxy)
    {
        // 检查Primitive是否需要为transform更新重新创建代理
        if(Primitive->ShouldRecreateProxyOnUpdateTransform())
        {
            RemovePrimitive(Primitive);
            AddPrimitive(Primitive);
        }
    }
}

代码 12 行调用 AddPrimitive 函数,将当前 UPrimitiveComponent 添加到 FScene 里,也就是真正将渲染数据传递到渲染线程的函数。

void FScene::AddPrimitive(UPrimitiveComponent* Primitive)
{
    // 创建Primitive的场景代理。
    FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
    Primitive->SceneProxy = PrimitiveSceneProxy;
    if(!PrimitiveSceneProxy)
    {
        return;
    }

    // 创建Primitive的场景信息。
    FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
    PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;
    
    (...)
    
    FScene* Scene = this;
    (...)
    
    ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
        [Params = MoveTemp(Params), Scene, PrimitiveSceneInfo, PreviousTransform = MoveTemp(PreviousTransform)](FRHICommandListImmediate& RHICmdList)
        {
            (...)

            Scene->AddPrimitiveSceneInfo_RenderThread(PrimitiveSceneInfo, PreviousTransform);
        });
}

// AddPrimitiveSceneInfo_RenderThread的实现
void FScene::AddPrimitiveSceneInfo_RenderThread(FPrimitiveSceneInfo* PrimitiveSceneInfo, const TOptional<FTransform>& PreviousTransform)
{
    (...)
    AddedPrimitiveSceneInfos.Add(PrimitiveSceneInfo);
    (...)
}

代码第 4 行调用 PrimitiveCreateSceneProxy 函数创建了一个 Primitive 的场景代理。CreateSceneProxy 函数是个虚函数,如果这里是静态网格模型,那么就需要详细看静态网格模型类对应的 CreateSceneProxy 函数,也就是 FStaticMeshSceneProxy 的构造函数。

代码第 12 行调用 FPrimitiveSceneInfo 的构造函数,创建了一个 Primitive 的场景信息对象。

代码第 25 行调用 AddPrimitiveSceneInfo_RenderThread 函数,将创建好的场景信息 PrimitiveSceneInfo 添加到 AddedPrimitiveSceneInfos 集合中,等待渲染线程主循环的时候使用,到时候再真正加入到 FScene 中(详细请看 FScene::UpdateAllPrimitiveSceneInfos 函数,这部分内容将会在 2.3.4.3 小节介绍)。

最后看一下 FStaticMeshSceneProxy 的构造函数的实现。

FStaticMeshSceneProxy::FStaticMeshSceneProxy(UStaticMeshComponent* InComponent, bool bForceLODsShareStaticLighting)
    : FPrimitiveSceneProxy(InComponent, InComponent->GetStaticMesh()->GetFName())
    , RenderData(InComponent->GetStaticMesh()->RenderData.Get())
    (...)

代码第 3 行可以看出,静态网格的代理 FStaticMeshSceneProxy 类直接将 UStaticMeshComponent 的渲染数据 RenderData 拷贝了一份到渲染模块里。至此,渲染数据已经提交到渲染模块。

2.3. 视图可见性计算

在详解视图可见性计算之前,先了解一下什么是视图。

视图其实可以理解为场景渲染出来的一个画面,或者理解为一个相机,同一个场景(也就是 FScene)允许有多个视图,也就是说可以渲染出多个画面。

  • 在引擎模块,视图用 FSceneView 类来表示;
  • 在渲染模块,视图用 FViewInfo 类来表示。
类型 功能 工作线程 模块
FSceneView 引擎里单个场景内可以有多个 View。每一帧都会创建新的 View 实例。 渲染线程 引擎模块
FViewInfo FSceneView 在渲染模块里的代理 渲染线程 渲染模块

视图的概念似乎和渲染网格模型没有什么关系,似乎有点跑题。实际上并没有,UE4 渲染模块每一帧的渲染都是以视图来组织的,一个视图包渲染了许多个基元渲染数据,所以了解视图的可见性计算是有必要的,它决定了那些基元要渲染哪些不渲染,还根据基元的绘制路径来决定如何管理绘制命令。

下面用代码的形式看一下视图可见性计算的调用栈。

void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
    void UGameEngine::RedrawViewports( bool bShouldPresent /*= true*/ )
    {
        void UGameViewportClient::Draw(FViewport* InViewport, FCanvas* SceneCanvas)
        {
            void FRendererModule::BeginRenderingViewFamily(FCanvas* Canvas, FSceneViewFamily* ViewFamily)
            {
                ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
                    [SceneRenderer, DrawSceneEnqueue](FRHICommandListImmediate& RHICmdList)
                    {
                        static void RenderViewFamily_RenderThread(FRHICommandListImmediate& RHICmdList, FSceneRenderer* SceneRenderer)
                        {
                            void FMobileSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
                            {
                                void FMobileSceneRenderer::InitViews(FRHICommandListImmediate& RHICmdList)
                                {
                                    PreVisibilityFrameSetup(RHICmdList);
                                    ComputeViewVisibility(RHICmdList, BasePassDepthStencilAccess, ViewCommandsPerView, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer);
                                    PostVisibilityFrameSetup(ILCTaskData);
                                }
                            }
                        }
                    });
            }
        }
    }
}

代码第 9 行的 FDrawSceneCommand 渲染线程命令表明接下来的函数调用都是在渲染线程里执行。

代码第 19 行调用的 ComputeViewVisibility 函数就是计算视图可见性的函数,下面看一下这个函数的具体实现。

void FSceneRenderer::ComputeViewVisibility(FRHICommandListImmediate& RHICmdList, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, FViewVisibleCommandsPerView& ViewCommandsPerView, 
    FGlobalDynamicIndexBuffer& DynamicIndexBuffer, FGlobalDynamicVertexBuffer& DynamicVertexBuffer, FGlobalDynamicReadBuffer& DynamicReadBuffer)
{
    (...)
// 第一步(2.3.1),初始化用于检查可见性的缓冲区
    int32 NumPrimitives = Scene->Primitives.Num();
    // 标记动态绘制路径基元的二进制位
    FPrimitiveViewMasks HasDynamicMeshElementsMasks;
    HasDynamicMeshElementsMasks.AddZeroed(NumPrimitives);
    
    (...)
    
    uint8 ViewBit = 0x1;
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex, ViewBit <<= 1)
    {
        FViewInfo& View = Views[ViewIndex];
        FViewCommands& ViewCommands = ViewCommandsPerView[ViewIndex];
        
        // 为当前视图分配可见性map,视图有多少个基元,就有多少二进制位
        View.PrimitiveVisibilityMap.Init(false,Scene->Primitives.Num());
        
        (...)
        
        if (bNeedsFrustumCulling)
        {
// 第二步(2.3.2),根据HLOD系统,更新基元可见性
            FLODSceneTree& HLODTree = Scene->SceneLODHierarchy;
            // 如果HLOD系统开启的话
            if (HLODTree.IsActive())
            {
                HLODTree.UpdateVisibilityStates(View);
            }
            else
            {
                HLODTree.ClearVisibilityState(View);
            }
            (...)
// 第三步(2.3.3),视椎体剪裁,更新PrimitiveVisibilityMap
            NumCulledPrimitivesForView = bUseFastIntersect ? FrustumCull<false, true, true>(Scene, View) : FrustumCull<false, true, false>(Scene, View);
            (...)
        }
        
        (...)
        
// 第四步(2.3.4),计算绘制路径并标记视图相关性
        ComputeAndMarkRelevanceForViewParallel(RHICmdList, Scene, View, ViewCommands, ViewBit, HasDynamicMeshElementsMasks, HasDynamicEditorMeshElementsMasks);
        
        (...)
    }
    
// 第五步(2.3.5),收集动态绘制路径下的绘制批次
    {
        GatherDynamicMeshElements(Views, Scene, ViewFamily, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer,
                HasDynamicMeshElementsMasks, HasDynamicEditorMeshElementsMasks, MeshCollector);
    }
    
    (...)
    
// 第六步(2.3.6),生成视图绘制命令,设置渲染批次
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        FViewInfo& View = Views[ViewIndex];
        (...)
        SetupMeshPass(View, BasePassDepthStencilAccess, ViewCommands);
    }
    (...)
}

FSceneRenderer::ComputeViewVisibility 函数的代码比较长,按照注释将整个可见性计算分成六步,下面我们将第一步放在 2.3.1 小节介绍,第二步放在 2.3.2 小节介绍,依此类推。

2.3.1. 初始化可见性缓冲区

FSceneRenderer::ComputeViewVisibility 函数的代码第 6 行记录了当前场景的基元数量。代码第 9 行初始化一个 Bit 位的集合 HasDynamicMeshElementsMasks,这个集合总共有与基元数量同样多个 Bit 位,每一位 Bit 的值代表这个基元是否在动态绘制路径下绘制(下文会介绍什么是绘制路径),如果是则设为 1,否则设为 0。

代码第 14 行通过 for 循环遍历所有视图。

代码第 20 行初始化一个 Bit 位的集合 PrimitiveVisibilityMap,同样这个集合有与基元数量同样多个 Bit 位,每一位 Bit 的值代表这个基元是否能被当前视图看见,默认全部位都为 0,也就是不能被视图看见。

2.3.2. 更新 HLOD 系统的可见性

FSceneRenderer::ComputeViewVisibility 函数的代码第 31 行,当场景开启 HLOD 系统时候会去设置物体显隐,具体代码在 FLODSceneTree::UpdateVisibilityStates 函数里,作用就是将远处的模型合成一个来渲染,提高渲染性能,这里不再详述。

2.3.3. 视椎剪裁

FSceneRenderer::ComputeViewVisibility 函数第 39 行调用 FrustumCull 函数来做视椎剪裁。下面详细看一下 FrustumCull 函数的实现。

template<bool UseCustomCulling, bool bAlsoUseSphereTest, bool bUseFastIntersect>
static int32 FrustumCull(const FScene* Scene, FViewInfo& View)
{
    (...)
    // 使用TaskGraph系统来并行计算,并且通过Event来阻塞宿主线程,与宿主线程同步
    ParallelFor(NumTasks, 
        [&NumCulledPrimitives, Scene, &View, MaxDrawDistanceScale, HLODState](int32 TaskIndex)
    {
        (...)
        // PrimitiveVisibilityMap的Bit位数
        const int32 BitArrayNumInner = View.PrimitiveVisibilityMap.Num();
    
        (...)
        
        // #define NumBitsPerDWORD ((int32)32)
        // NumBitsPerDWORD的声明表明PrimitiveVisibilityMap是由若干个int32组合成的Bit位集合,每个int32被称为一个Word
        // 也就是说一个Word包含32个Bit
        
        // 每个线程遍历处理FrustumCullNumWordsPerTask个Word
        const int32 TaskWordOffset = TaskIndex * FrustumCullNumWordsPerTask;
        for (int32 WordIndex = TaskWordOffset; WordIndex < TaskWordOffset + FrustumCullNumWordsPerTask && WordIndex * NumBitsPerDWORD < BitArrayNumInner; WordIndex++)
        {
            (...)
            uint32 Mask = 0x1;
            uint32 VisBits = 0;
            (...)
            // 遍历当前Word中的每一个Bit来计算
            for (int32 BitSubIndex = 0; BitSubIndex < NumBitsPerDWORD && WordIndex * NumBitsPerDWORD + BitSubIndex < BitArrayNumInner; BitSubIndex++, Mask <<= 1)
            {
                int32 Index = WordIndex * NumBitsPerDWORD + BitSubIndex;
                const FPrimitiveBounds& Bounds = Scene->PrimitiveBounds[Index];
                // 基元包围盒离视图的距离
                float DistanceSquared = (Bounds.BoxSphereBounds.Origin - ViewOriginForDistanceCulling).SizeSquared();
                (...)
                // 基元包围盒的最大剪裁距离
                float MaxDrawDistance = Bounds.MaxCullDistance < FLT_MAX ? Bounds.MaxCullDistance * MaxDrawDistanceScale : FLT_MAX;
                (...)
                // 是否能被距离剪裁
                bool bDistanceCulled = DistanceSquared > FMath::Square(MaxDrawDistance + FadeRadius) || (DistanceSquared < MinDrawDistanceSq);
                
                if (bDistanceCulled ||
                    // 如果是自定义剪裁,且没有触发剪裁
                    (UseCustomCulling && !View.CustomVisibilityQuery->IsVisible(VisibilityId, FBoxSphereBounds(Bounds.BoxSphereBounds.Origin, Bounds.BoxSphereBounds.BoxExtent, Bounds.BoxSphereBounds.SphereRadius))) ||
                    // 如果支持包围球检测,优先用视椎体和包围球作碰撞检测,且这里判断没有碰撞,也就不触发剪裁
                    (bAlsoUseSphereTest && View.ViewFrustum.IntersectSphere(Bounds.BoxSphereBounds.Origin, Bounds.BoxSphereBounds.SphereRadius) == false) ||
                    // 如果不在视椎体的8个面内部
                    (bUseFastIntersect ? IntersectBox8Plane(Bounds.BoxSphereBounds.Origin, Bounds.BoxSphereBounds.BoxExtent, PermutedPlanePtr) : View.ViewFrustum.IntersectBox(Bounds.BoxSphereBounds.Origin, Bounds.BoxSphereBounds.BoxExtent)) == false)
                {
                    // 剪裁数加一
                    STAT(NumCulledPrimitives.Increment());
                }
                else
                {
                    (...)
                    // 设置当前基元可见
                    VisBits |= Mask;
                    (...)
                }
            }
            (...)
            if (VisBits)
            {
                check(!View.PrimitiveVisibilityMap.GetData()[WordIndex]); // 这个应该初始为0
                // 如果当前基元可见,则将对应的Bit位设为1
                View.PrimitiveVisibilityMap.GetData()[WordIndex] = VisBits;
            }
            (...)
        }
    };
    
    return NumCulledPrimitives.GetValue();
}

代码第 6 行使用 ParallelFor 函数去执行 NumTasks 个线程任务,它基于 TaskGraph 系统实现,并行执行 Lamba 表达式里的过程,充分用上了多核计算,且所有子线程结束前,当前线程都处于阻塞状态。代码执行虽然使用了多线程,但是上层用法上并不能感知到并发的存在,而是感觉到这是一个单线程串行逻辑。详细请看 ParallelFor 的实现。

代码第 20 行和 21 行之所以提出 Word 的概念,是因为 View.PrimitiveVisibilityMap 的实现就是由若干个 32 位的整型数字组成的,我们称呼每一个整型数字为一个 Word,所以这里只有先遍历 Word 才能遍历每个 Word 里的 Bit 位。详细请看 FSceneBitArray 类的实现。

视椎剪裁的核心代码是第 41 行到第 58 行,这些代码的逻辑简单概括一下就是:如果当前遍历到的基元没有满足剪裁条件,也就是视图可见,则 VisBits 对应的 Bit 位设为 1,否则 NumCulledPrimitives 自增 1。而 VisBits 对应的 Bit 位设为 1,也就意味着 View.PrimitiveVisibilityMap 的对应 Bit 位设为 1,也就是说这个基元不会被剪裁掉。

2.3.4. 计算绘制路径并标记视图相关性

之前的章节里也多次提到绘制路径,这里需要先了解一下什么是绘制路径。

2.3.4.1. 绘制路径

UE4 的模型绘制路径的过程:

  • 首先是遍历当前视图的所有 FPrimitiveSceneProxy,收集提取出 FMeshBatch 类的对象来处理模型批次;
  • 然后再将渲染过程封装为一个模型绘制命令,也就是 FMeshDrawCommand
  • 最后传递给 RHI 线程。

因为模型批处理本身有内存分配等各种消耗,为了优化性能,渲染模块实现了两种网格绘制路径:动态路径缓存路径。详细内容请参看官方文档《网格体绘制管道》

DrawPath

橙色箭头表示每帧都必须执行的操作,而蓝色箭头表示执行一次就缓存的操作。

图中第一行“Dynamic Relevance”(动态相关)属于动态路径,表示完全不缓存网格模型批次,也不缓存绘制命令。这种绘制路径适合粒子特效、水体这类物体,因为模型状态时刻在改变。

图中第二行和第三行的两个“Static Relevance”(静态相关)属于缓存路径,第二行是只缓存网格模型的批次 FMeshBatch,第三行是除了缓存网格模型批次还缓存绘制命令 FMeshDrawCommand。由于静态物体绘制状态极少变化,根据其变化程度选用合适的缓存策略,在网格模型加入到渲染场景的时候批处理一次就可以了,这样对于优化绘制路径的性能是有帮助的。

2.3.4.2. 计算并标注相关性

理解了绘制路径后接着来看 FSceneRenderer::ComputeViewVisibility 函数的代码第 46 行,这里调用了 ComputeAndMarkRelevanceForViewParallel 函数。

static void ComputeAndMarkRelevanceForViewParallel(
    FRHICommandListImmediate& RHICmdList,
    const FScene* Scene,
    FViewInfo& View,
    FViewCommands& ViewCommands,
    uint8 ViewBit,
    FPrimitiveViewMasks& OutHasDynamicMeshElementsMasks,
    FPrimitiveViewMasks& OutHasDynamicEditorMeshElementsMasks
    )
{
    (...)
    int32 EstimateOfNumPackets = NumMesh / (FRelevancePrimSet<int32>::MaxInputPrims * 4);

    TArray<FRelevancePacket*,SceneRenderingAllocator> Packets;
    Packets.Reserve(EstimateOfNumPackets);
    
    {
        FSceneSetBitIterator BitIt(View.PrimitiveVisibilityMap);
        if (BitIt)
        {
            FRelevancePacket* Packet = new(FMemStack::Get()) FRelevancePacket(
                RHICmdList,
                Scene, 
                View, 
                ViewCommands,
                ViewBit,
                ViewData,
                OutHasDynamicMeshElementsMasks,
                OutHasDynamicEditorMeshElementsMasks,
                MarkMasks);
            Packets.Add(Packet);
            while (1)
            {
                Packet->Input.AddPrim(BitIt.GetIndex());
                ++BitIt;
                (...)
            }
        }
    }
    {
        ParallelFor(Packets.Num(), 
            [&Packets](int32 Index)
            {
                Packets[Index]->AnyThreadTask();
            },
            !WillExecuteInParallel
        );
    }
    
    (...)
}

代码第 5 行的形参 FViewCommands& ViewCommands 是这个函数最重要的一个返回值,因为这个返回值包含了所有静态绘制路径的绘制命令。

代码第 7 行的形参 FPrimitiveViewMasks& OutHasDynamicMeshElementsMasks 也是一个返回值,这个返回值就是 2.3.1 小节的 HasDynamicMeshElementsMasks,用于标记哪些基元是在动态绘制路径下处理的。

代码第 21 行到第 38 行的代码过程是为了构建若干 FRelevancePacket 对象,以备后面执行它们的 AnyThreadTask 函数。从代码第 25 行和第 28 行可以看到,构建 FRelevancePacket 时候传入的参数也是 AnyThreadTask 函数要计算得出的主要返回值。

代码第 44 行在并行线程里逐个调用 FRelevancePacket 对象的 AnyThreadTask 函数,来执行计算和标注相关性的核心工作。

下面看一下 AnyThreadTask 函数的执行过程。

void AnyThreadTask()
{
    ComputeRelevance();
    MarkRelevant();
}

代码第 3 行调用了 ComputeRelevance 函数计算出所有基元的视图相关性(View Relevance),包括是否静态相关(Static Relevance)和是否动态相关(Dynamic Relevance)。视图相关性指的是这个基元在视图里渲染时候的一些特性的集合。

代码第 4 行调用了 MarkRelevant 函数,根据 ComputeRelevance 函数计算出来的视图相关性,处理每一个基元,收集绘制命令,以及标记基元。

下面先看一下 ComputeRelevance 函数的实现。

void ComputeRelevance()
{
    (...)
    
    for (int32 Index = 0; Index < Input.NumPrims; Index++)
    {
        int32 BitIndex = Input.Prims[Index];
        FPrimitiveSceneInfo* PrimitiveSceneInfo = Scene->Primitives[BitIndex];
        FPrimitiveViewRelevance& ViewRelevance = const_cast<FPrimitiveViewRelevance&>(View.PrimitiveViewRelevanceMap[BitIndex]);
        // 获取每一个基元的视图相关性
        ViewRelevance = PrimitiveSceneInfo->Proxy->GetViewRelevance(&View);
        (...)
        
        // 获得绘制路径相关的视图相关性
        const bool bStaticRelevance = ViewRelevance.bStaticRelevance;
        (...)
        const bool bDynamicRelevance = ViewRelevance.bDynamicRelevance;
        (...)
        
        // 如果是静态相关
        if (bStaticRelevance && (bDrawRelevance || bShadowRelevance))
        {
            RelevantStaticPrimitives.AddPrim(BitIndex);
        }
        
        (...)
        
        // 如果是动态相关
        (...)
        else if(bDynamicRelevance)
        {
            // Keep track of visible dynamic primitives.
            ++NumVisibleDynamicPrimitives;
            OutHasDynamicMeshElementsMasks[BitIndex] |= ViewBit;

            (...)
        }
    }
}

代码第 5 行的 for 循环是遍历每一个基元。

代码第 11 行,通过调用 FPrimitiveSceneProxy 类的 GetViewRelevance 函数,去获取这个基元的所有视图相关性。继承于 FPrimitiveSceneProxy 类的不同子类都重写了一样的 GetViewRelevance 函数。

代码第 23 行判断如果当前基元是静态相关的,那么就将该基元加入到 RelevantStaticPrimitives 里面,留到 MarkRelevant 函数里处理,也就是说这个基元要基于缓存绘制路径来处理。

代码第 34 行判断如果当前基元是动态相关的,那么就给 OutHasDynamicMeshElementsMasks 赋值,设置当前基元对应的 Bit 位为 1,表明这个基元需要基于动态绘制路径处理。这个 OutHasDynamicMeshElementsMasks 数组最终要返回给 FSceneRenderer::ComputeViewVisibility 函数的局部变量——Bit 集合 HasDynamicMeshElementsMasks

如果忘记的话可以回 2.3.1 小节去看。

这里我们只关心动态相关性和静态相关性,其他的相关性在 ComputeRelevance 函数里也有计算处理,但是这里不关心就先省略。

那么接下来再看一下 MarkRelevant 函数的实现。

void MarkRelevant()
{
    (...)
    
    for (int32 StaticPrimIndex = 0, Num = RelevantStaticPrimitives.NumPrims; StaticPrimIndex < Num; ++StaticPrimIndex)
    {
        int32 PrimitiveIndex = RelevantStaticPrimitives.Prims[StaticPrimIndex];
        const FPrimitiveSceneInfo* RESTRICT PrimitiveSceneInfo = Scene->Primitives[PrimitiveIndex];
        
        (...)
        
        const int32 NumStaticMeshes = PrimitiveSceneInfo->StaticMeshRelevances.Num();
        for(int32 MeshIndex = 0;MeshIndex < NumStaticMeshes;MeshIndex++)
        {
            const FStaticMeshBatchRelevance& StaticMeshRelevance = PrimitiveSceneInfo->StaticMeshRelevances[MeshIndex];
            const FStaticMeshBatch& StaticMesh = PrimitiveSceneInfo->StaticMeshes[MeshIndex];
            if (ViewRelevance.bDrawRelevance)
            {
                (...)
                if (StaticMeshRelevance.bUseForMaterial && (ViewRelevance.bRenderInMainPass || ViewRelevance.bRenderCustomDepth))
                {
                    DrawCommandPacket.AddCommandsForMesh(PrimitiveIndex, PrimitiveSceneInfo, StaticMeshRelevance, StaticMesh, Scene, bCanCache, EMeshPass::BasePass);
                    (...)
                }
                (...)
            }
            (...)
        }
        (...)
    }
    (...)
}

代码第 7 行遍历所有记录在 RelevantStaticPrimitives 里的基元。

代码第 12 行到第 16 行取出所有基元已经缓存好的静态模型批次。这就是在介绍绘制路径时候,缓存路径里面缓存批处理模型的那种情况。具体静态模型如何生成模型批次、如何缓存到 PrimitiveSceneInfo->StaticMeshes 里,2.3.4.3.1 小节会详细介绍。

代码第 22 行是整个 MarkRelevant 函数的核心,AddCommandsForMesh 函数会去判断指定的 Pass 是否支持缓存绘制命令,如果支持就去取出绘制命令并添加到一个记录命令的数组里。

下面看一下 AddCommandsForMesh 函数的实现。

void AddCommandsForMesh(
    int32 PrimitiveIndex, 
    const FPrimitiveSceneInfo* InPrimitiveSceneInfo,
    const FStaticMeshBatchRelevance& RESTRICT StaticMeshRelevance, 
    const FStaticMeshBatch& RESTRICT StaticMesh, 
    const FScene* RESTRICT Scene, 
    bool bCanCache, 
    EMeshPass::Type PassType)
{
    (...)
    
    if (bUseCachedMeshCommand)
    {
        const int32 StaticMeshCommandInfoIndex = StaticMeshRelevance.GetStaticMeshCommandInfoIndex(PassType);
        if (StaticMeshCommandInfoIndex >= 0)
        {
            // 两个缓存容器
            const FCachedMeshDrawCommandInfo& CachedMeshDrawCommand = InPrimitiveSceneInfo->StaticMeshCommandInfos[StaticMeshCommandInfoIndex];
            const FCachedPassMeshDrawList& SceneDrawList = Scene->CachedDrawLists[PassType];

            // 添加一个新的绘制命令的记录到VisibleCachedDrawCommands里
            VisibleCachedDrawCommands[(uint32)PassType].AddUninitialized();
            // 获取出新添加的绘制命令记录的引用,下面为其赋值
            FVisibleMeshDrawCommand& NewVisibleMeshDrawCommand = VisibleCachedDrawCommands[(uint32)PassType].Last();
            
            // 从已经缓存的绘制命令集合里取出当前基元、当前Pass对应的绘制命令
            const FMeshDrawCommand* MeshDrawCommand = CachedMeshDrawCommand.StateBucketId >= 0
                ? &Scene->CachedMeshDrawCommandStateBuckets[PassType].GetByElementId(CachedMeshDrawCommand.StateBucketId).Key
                : &SceneDrawList.MeshDrawCommands[CachedMeshDrawCommand.CommandIndex];
            
            // 用当前基元、当前Pass对应的绘制命令来填充绘制命令记录
            NewVisibleMeshDrawCommand.Setup(
                MeshDrawCommand,
                PrimitiveIndex,
                PrimitiveIndex,
                CachedMeshDrawCommand.StateBucketId,
                CachedMeshDrawCommand.MeshFillMode,
                CachedMeshDrawCommand.MeshCullMode,
                CachedMeshDrawCommand.SortKey);
        }
    }
    (...)
}

代码第 22 行的 VisibleCachedDrawCommands 是用于记录静态模型的绘制命令的对象的数组,这一行代码就是为记录绘制命令开辟一个新的记录位。

代码第 27 行,去场景的全局记录缓存容器里找当前基元、当前 Pass 对应的绘制命令,并在代码第 33 行填充到记录位里面。那么是从哪里缓存的绘制命令呢?2.3.4.3.2 小节会详细介绍。

这里缓存路径已经获得了所有已缓存的静态模型的绘制命令,并记录下来了,接下来如何把这些命令返回给渲染逻辑呢?这里就需要把这些记录好的命令添加到上文 2.3.4.2 小节开篇提到的最重要的返回值 FViewCommands& ViewCommands 里面。这个工作需要返回 ComputeAndMarkRelevanceForViewParallel 函数的实现代码看看,其实那段代码的第 50 行省略的部分代码就是在处理返回。下面展开看看省略掉的代码。

static void ComputeAndMarkRelevanceForViewParallel(
    FRHICommandListImmediate& RHICmdList,
    const FScene* Scene,
    FViewInfo& View,
    FViewCommands& ViewCommands,
    uint8 ViewBit,
    FPrimitiveViewMasks& OutHasDynamicMeshElementsMasks,
    FPrimitiveViewMasks& OutHasDynamicEditorMeshElementsMasks
    )
{
    (...)
    
    // 这里省略已经分析过的代码,下面是之前省略掉的代码
    
    {
        for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
        {
            int32 NumVisibleCachedMeshDrawCommands = 0;
            (...)
            for (auto Packet : Packets)
            {
                NumVisibleCachedMeshDrawCommands += Packet->DrawCommandPacket.VisibleCachedDrawCommands[PassIndex].Num();
                (...)
            }
            ViewCommands.MeshCommands[PassIndex].Reserve(NumVisibleCachedMeshDrawCommands);
            (...)
        }
        
        for (auto Packet : Packets)
        {
            Packet->RenderThreadFinalize();
            Packet->~FRelevancePacket();
        }

        Packets.Empty();
    }
    (...)
}

代码第 22 行汇总所有 FRelevancePacket 里记录的绘制命令的数量,代码第 25 行为返回值 ViewCommands 分配对应数量的命令空间。

代码第 31 行调用 RenderThreadFinalize 函数,主要功能是将记录的绘制命令拷贝到 ViewCommands 里面。

void RenderThreadFinalize()
{
    FViewInfo& WriteView = const_cast<FViewInfo&>(View);
    FViewCommands& WriteViewCommands = const_cast<FViewCommands&>(ViewCommands);
    
    for (int32 Index = 0; Index < NotDrawRelevant.NumPrims; Index++)
    {
        WriteView.PrimitiveVisibilityMap[NotDrawRelevant.Prims[Index]] = false;
    }
    
    (...)
    
    for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
    {
        // 从命令记录位的里取出所有绘制命令
        FPassDrawCommandArray& SrcCommands = DrawCommandPacket.VisibleCachedDrawCommands[PassIndex];
        // 从返回值里取出对应Pass的命令列表,等待填充
        FMeshCommandOneFrameArray& DstCommands = WriteViewCommands.MeshCommands[PassIndex];
        if (SrcCommands.Num() > 0)
        {
            static_assert(sizeof(SrcCommands[0]) == sizeof(DstCommands[0]), "Memcpy sizes must match.");
            const int32 PrevNum = DstCommands.AddUninitialized(SrcCommands.Num());
            // 填充返回值的命令列表
            FMemory::Memcpy(&DstCommands[PrevNum], &SrcCommands[0], SrcCommands.Num() * sizeof(SrcCommands[0]));
        }
        (...)
    }
    
    (...)
}

代码通过遍历所有 Pass 的记录命令列表记录,在代码第 24 行处,填充到最终返回值 ViewCommands 里面。

2.3.4.3. 缓存绘制路径的缓存

2.2 节解析 FScene::AddPrimitive 函数代码时候提到过,这个函数会把创建好的场景信息 PrimitiveSceneInfo 添加到 AddedPrimitiveSceneInfos 集合中,AddedPrimitiveSceneInfos 这个集合会在 FScene::UpdateAllPrimitiveSceneInfos 函数中使用到,下面可以看看这个函数的调用栈。

void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
    void UGameEngine::RedrawViewports( bool bShouldPresent /*= true*/ )
    {
        void UGameViewportClient::Draw(FViewport* InViewport, FCanvas* SceneCanvas)
        {
            void FRendererModule::BeginRenderingViewFamily(FCanvas* Canvas, FSceneViewFamily* ViewFamily)
            {
                ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
                    [SceneRenderer, DrawSceneEnqueue](FRHICommandListImmediate& RHICmdList)
                    {
                        static void RenderViewFamily_RenderThread(FRHICommandListImmediate& RHICmdList, FSceneRenderer* SceneRenderer)
                        {
                            void FMobileSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
                            {
                                Scene->UpdateAllPrimitiveSceneInfos(RHICmdList);
                            }
                        }
                    });
            }
        }
    }
}

这个调用栈可以看到 FScene::UpdateAllPrimitiveSceneInfos 函数是渲染线程主循环里每帧调用的函数。下面看看这个函数的具体实现。

void FScene::UpdateAllPrimitiveSceneInfos(FRHICommandListImmediate& RHICmdList, bool bAsyncCreateLPIs)
{
    (...)
    
    const bool bAddToDrawLists = !(CVarDoLazyStaticMeshUpdate.GetValueOnRenderThread());
    if (bAddToDrawLists)
    {
        FPrimitiveSceneInfo::AddToScene(RHICmdList, this, TArrayView<FPrimitiveSceneInfo*>(&AddedLocalPrimitiveSceneInfos[StartIndex], AddedLocalPrimitiveSceneInfos.Num() - StartIndex), true, true, bAsyncCreateLPIs);
    }
    else
    {
        FPrimitiveSceneInfo::AddToScene(RHICmdList, this, TArrayView<FPrimitiveSceneInfo*>(&AddedLocalPrimitiveSceneInfos[StartIndex], AddedLocalPrimitiveSceneInfos.Num() - StartIndex), true, false, bAsyncCreateLPIs);
        (...)
    }
    
    (...)
}

FScene::UpdateAllPrimitiveSceneInfos 函数处理的事情很多,这里我们主要关注 FPrimitiveSceneInfo::AddToScene 函数的调用。

void FPrimitiveSceneInfo::AddToScene(FRHICommandListImmediate& RHICmdList, FScene* Scene, const TArrayView<FPrimitiveSceneInfo*>& SceneInfos, bool bUpdateStaticDrawLists, bool bAddToStaticDrawLists, bool bAsyncCreateLPIs)
{
    check(IsInRenderingThread());
    
    (...)
    
    {
        SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_AddToScene_AddStaticMeshes, FColor::Magenta);
        if (bUpdateStaticDrawLists)
        {
            AddStaticMeshes(RHICmdList, Scene, SceneInfos, bAddToStaticDrawLists);
        }
    }
    
    (...)
}

FPrimitiveSceneInfo::AddToScene 函数里面,我们也挑重点关心的函数介绍,也就是代码第 11 行的 AddStaticMeshes 函数。

void FPrimitiveSceneInfo::AddStaticMeshes(FRHICommandListImmediate& RHICmdList, FScene* Scene, const TArrayView<FPrimitiveSceneInfo*>& SceneInfos, bool bAddToStaticDrawLists)
{
        LLM_SCOPE(ELLMTag::StaticMesh);

        {
            ParallelForTemplate(SceneInfos.Num(), [Scene, &SceneInfos](int32 Index)
            {
                FPrimitiveSceneInfo* SceneInfo = SceneInfos[Index];
                // 缓存基元的静态模型批处理元素。
                FBatchingSPDI BatchingSPDI(SceneInfo);
                BatchingSPDI.SetHitProxy(SceneInfo->DefaultDynamicHitProxy);
                SceneInfo->Proxy->DrawStaticElements(&BatchingSPDI);
                SceneInfo->StaticMeshes.Shrink();
                SceneInfo->StaticMeshRelevances.Shrink();
            });
        }

        {
            for (FPrimitiveSceneInfo* SceneInfo : SceneInfos)
            {
                for (int32 MeshIndex = 0; MeshIndex < SceneInfo->StaticMeshes.Num(); MeshIndex++)
                {
                    FStaticMeshBatchRelevance& MeshRelevance = SceneInfo->StaticMeshRelevances[MeshIndex];
                    FStaticMeshBatch& Mesh = SceneInfo->StaticMeshes[MeshIndex];

                    // 添加静态网格模型到场景的静态网格模型列表里。
                    FSparseArrayAllocationInfo SceneArrayAllocation = Scene->StaticMeshes.AddUninitialized();
                    Scene->StaticMeshes[SceneArrayAllocation.Index] = &Mesh;
                    Mesh.Id = SceneArrayAllocation.Index;
                    MeshRelevance.Id = SceneArrayAllocation.Index;
                }
            }
        }

        if (bAddToStaticDrawLists)
        {
            CacheMeshDrawCommands(RHICmdList, Scene, SceneInfos);
        }
}

代码第 12 行调用了 FPrimitiveSceneProxy 类的 DrawStaticElements 函数,这个函数的主要功能是收集网格模型的顶点、索引、材质等数据,记录到 SceneInfo->StaticMeshes 中。这个收集和记录的过程,本小节的下文会详细介绍。

代码第 28 行的 Mesh 就是从上面提到的 SceneInfo->StaticMeshes 中取出来的,这一行将取出来的 FStaticMeshBatch 缓存到 Scene->StaticMeshes 中。

代码第 37 行调用了 CacheMeshDrawCommands 函数来生成并缓存绘制命令,缓存好绘制命令以后就没必要每帧重新生成绘制命令了。这个函数本小节的下文也会详细介绍。

2.3.4.3.1. DrawStaticElements 函数

下面先看 FStaticMeshSceneProxy 类的 DrawStaticElements 函数实现。

/**
 * 绘制基元的静态元素。在创建SceneProxy时,这将会在渲染线程里调用一次。
 * 只有当GetViewRelevance()声明静态相关性时,静态元素才会被渲染。
 * @param PDI - 接收基元的接口。
 */
void FStaticMeshSceneProxy::DrawStaticElements(FStaticPrimitiveDrawInterface* PDI)
{
    (...)
    
    const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
    // Draw the static mesh elements.
    for(int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
    {
        (...)
        const int32 NumBatches = GetNumMeshBatches();
        for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
        {
            FMeshBatch BaseMeshBatch;
            
            if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, BaseMeshBatch))
            {
                (...)
                PDI->DrawMesh(MeshBatch, FLT_MAX);
                (...)
            }
        }
    }
}

代码第 20 行通过调用 GetMeshElement 函数收集网格模型的信息,构建第 18 行的 BaseMeshBatch 对象。

代码第 23 行,将构建好的 FMeshBatch 对象缓存到 PrimitiveSceneInfo->StaticMeshes 中。

GetMeshElement 函数的实现如下所示。

/** 为特定的LOD和元素建立FMeshBatch。 */
bool FStaticMeshSceneProxy::GetMeshElement(
    int32 LODIndex, 
    int32 BatchIndex, 
    int32 SectionIndex, 
    uint8 InDepthPriorityGroup, 
    bool bUseSelectionOutline,
    bool bAllowPreCulledIndices, 
    FMeshBatch& OutMeshBatch) const
{
    const FStaticMeshLODResources& LOD = RenderData->LODResources[LODIndex];
    const FStaticMeshVertexFactories& VFs = RenderData->LODVertexFactories[LODIndex];
    const FStaticMeshSection& Section = LOD.Sections[SectionIndex];
    const FLODInfo& ProxyLODInfo = LODs[LODIndex];
    
    (...)
    
    const FVertexFactory* VertexFactory = nullptr;

    FMeshBatchElement& OutMeshBatchElement = OutMeshBatch.Elements[0];
    
    (...)
    
    VertexFactory = &VFs.VertexFactory;
    OutMeshBatchElement.VertexFactoryUserData = VFs.VertexFactory.GetUniformBuffer();
    
    const uint32 NumPrimitives = SetMeshElementGeometrySource(LODIndex, SectionIndex, bWireframe, bRequiresAdjacencyInformation, bUseReversedIndices, bAllowPreCulledIndices, VertexFactory, OutMeshBatch);
    uint32 FStaticMeshSceneProxy::SetMeshElementGeometrySource(
        int32 LODIndex,
        int32 SectionIndex,
        bool bWireframe,
        bool bRequiresAdjacencyInformation,
        bool bUseReversedIndices,
        bool bAllowPreCulledIndices,
        const FVertexFactory* VertexFactory,
        FMeshBatch& OutMeshBatch) const
    {
        (...)
        OutMeshBatchElement.IndexBuffer = bUseReversedIndices ? &LODModel.AdditionalIndexBuffers->ReversedIndexBuffer : &LODModel.IndexBuffer;
        OutMeshBatchElement.FirstIndex = Section.FirstIndex;
        NumPrimitives = Section.NumTriangles;
        (...)
        OutMeshBatchElement.NumPrimitives = NumPrimitives;
        OutMeshBatch.VertexFactory = VertexFactory;
        
        return NumPrimitives;
    }
    
    (...)
    
    return true;
}

由于静态模型只有一个元素,所以代码第 20 行仅仅取出 FMeshBatch 的第 0 号元素来初始化。

代码第 27 行调用 SetMeshElementGeometrySource 函数来将索引缓冲区和顶点工厂设置给 OutMeshBatchElement

这样就构建好一个 FMeshBatch 对象了,下面通过 DrawMesh 来缓存到 PrimitiveSceneInfo->StaticMeshes 中。

virtual void DrawMesh(const FMeshBatch& Mesh, float ScreenSize) final override
{
    (...)
    FStaticMeshBatch* StaticMesh = new(PrimitiveSceneInfo->StaticMeshes) FStaticMeshBatch(
        PrimitiveSceneInfo,
        Mesh,
        CurrentHitProxy ? CurrentHitProxy->Id : FHitProxyId()
    );
    
    (...)
    
    FStaticMeshBatchRelevance* StaticMeshRelevance = new(PrimitiveSceneInfo->StaticMeshRelevances) FStaticMeshBatchRelevance(
        *StaticMesh, 
        ScreenSize, 
        bSupportsCachingMeshDrawCommands,
        bUseSkyMaterial,
        bUseSingleLayerWaterMaterial,
        bUseAnisotropy,
        FeatureLevel
        );
}

代码第 4 行通过调用 new 操作符的重载函数,将新构建出来的 FStaticMeshBatch 对象包裹着 FMeshBatch 对象,缓存到 PrimitiveSceneInfo->StaticMeshes。这也是 2.3.4.2 小节介绍 MarkRelevant 函数时候,提到的静态模型缓存。

2.3.4.3.2. CacheMeshDrawCommands 函数

下面再看一下 AddStaticMeshes 函数里面调用的 CacheMeshDrawCommands 函数的实现。

void FPrimitiveSceneInfo::CacheMeshDrawCommands(FRHICommandListImmediate& RHICmdList, FScene* Scene, const TArrayView<FPrimitiveSceneInfo*>& SceneInfos)
{
    (...)
    for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
    {
        const EShadingPath ShadingPath = Scene->GetShadingPath();
        EMeshPass::Type PassType = (EMeshPass::Type)PassIndex;

        if ((FPassProcessorManager::GetPassFlags(ShadingPath, PassType) & EMeshPassFlags::CachedMeshCommands) != EMeshPassFlags::None)
        {
            FCachedMeshDrawCommandInfo CommandInfo(PassType);

            FCriticalSection& CachedMeshDrawCommandLock = Scene->CachedMeshDrawCommandLock[PassType];
            // 缓存的关键容器1:Scene->CachedDrawLists
            FCachedPassMeshDrawList& SceneDrawList = Scene->CachedDrawLists[PassType];
            // 缓存的关键容器2:Scene->CachedMeshDrawCommandStateBuckets
            FStateBucketMap& CachedMeshDrawCommandStateBuckets = Scene->CachedMeshDrawCommandStateBuckets[PassType];
            // 创建一个FCachedPassMeshDrawListContext对象,里面承载了SceneDrawList和CachedMeshDrawCommandStateBuckets
            FCachedPassMeshDrawListContext CachedPassMeshDrawListContext(CommandInfo, CachedMeshDrawCommandLock, SceneDrawList, CachedMeshDrawCommandStateBuckets, *Scene);
            
            PassProcessorCreateFunction CreateFunction = FPassProcessorManager::GetCreateFunction(ShadingPath, PassType);
            FMeshPassProcessor* PassMeshProcessor = CreateFunction(Scene, nullptr, &CachedPassMeshDrawListContext);
            
            if (PassMeshProcessor != nullptr)
            {
                for (const FMeshInfoAndIndex& MeshAndInfo : MeshBatches)
                {
                    FStaticMeshBatch& Mesh = SceneInfo->StaticMeshes[MeshAndInfo.MeshIndex];
                    
                    uint64 BatchElementMask = ~0ull;
                    // 填充代码第15行和第17行的容器
                    PassMeshProcessor->AddMeshBatch(Mesh, BatchElementMask, SceneInfo->Proxy);
                    (...)
                }
            }
        }
    }
}

代码第 15 行和第 17 行的两个绘制命令列表,也就是 2.3.4.2 小节里 AddCommandsForMesh 函数里面提到的两个缓存容器。

代码第 18 行通过构建一个 FCachedPassMeshDrawListContext 对象,承载了上述两个命令缓存容器,一并传给 FMeshPassProcessorFMeshPassProcessor 后续 2.4 节会详细讲解。

代码第 32 行通过调用 FMeshPassProcessorAddMeshBatch 函数,生成绘制命令并填充到 Scene->CachedDrawListsScene->CachedMeshDrawCommandStateBuckets 里面。

至此,引申出来的内容,即缓存绘制路径用到的所有缓存的创建、填充都已经介绍完毕。

2.3.4.4. 小结

至此计算并标注相关性工作完成,ComputeAndMarkRelevanceForViewParallel 函数的主要功能就是识别出所有基元是基于缓存路径还是动态路径处理的,分别处理两种绘制路径下的基元:

  • 动态路径基元就标记 HasDynamicMeshElementsMasks 对应 Bit 位的值为 1;
  • 缓存路径的基元就取出已经缓存的批处理网格模型和绘制命令,返回给 ViewCommands

2.3.5. 收集动态绘制路径的绘制批次

FSceneRenderer::ComputeViewVisibility 函数的代码第 53 行调用 GatherDynamicMeshElements 函数去收集动态绘制路径下的绘制批次。动态绘制路径在 2.3.4.2 小节介绍过了,下面看看这个函数是如何实现的。

void FSceneRenderer::GatherDynamicMeshElements(
    TArray<FViewInfo>& InViews, 
    const FScene* InScene, 
    const FSceneViewFamily& InViewFamily, 
    FGlobalDynamicIndexBuffer& DynamicIndexBuffer,
    FGlobalDynamicVertexBuffer& DynamicVertexBuffer,
    FGlobalDynamicReadBuffer& DynamicReadBuffer,
    const FPrimitiveViewMasks& HasDynamicMeshElementsMasks, 
    const FPrimitiveViewMasks& HasDynamicEditorMeshElementsMasks,
    FMeshElementCollector& Collector)
{
    (...)
    
    for (int32 PrimitiveIndex = 0; PrimitiveIndex < NumPrimitives; ++PrimitiveIndex)
    {
        const uint8 ViewMask = HasDynamicMeshElementsMasks[PrimitiveIndex];
        
        if (ViewMask != 0)
        {
            (...)
            FPrimitiveSceneInfo* PrimitiveSceneInfo = InScene->Primitives[PrimitiveIndex];
            (...)
            PrimitiveSceneInfo->Proxy->GetDynamicMeshElements(InViewFamily.Views, InViewFamily, ViewMaskFinal, Collector);
            (...)
        }
    }
}

代码第 10 行的 FMeshElementCollector& Collector 即为 FSceneRenderer::GatherDynamicMeshElements 函数最终要返回的对象,这个对象收集了所有动态路径下的模型元素。

代码第 23 行调用了 FPrimitiveSceneProxy 类的 GetDynamicMeshElements 函数来获取所有动态路径下的模型元素。每个 FPrimitiveSceneProxy 的基类都有其 GetDynamicMeshElements 函数的具体实现。

这里我们重点看 FStaticMeshSceneProxyGetDynamicMeshElements 函数实现。

void FStaticMeshSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const
{
    (...)
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        const FSceneView* View = Views[ViewIndex];
        (...)
        for (int32 LODIndex = 0; LODIndex < RenderData->LODResources.Num(); LODIndex++)
        {
            const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
            (...)
            for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
            {
                const int32 NumBatches = GetNumMeshBatches();
                
                for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
                {
                    (...)
                    FMeshBatch& MeshElement = Collector.AllocateMesh();
                    (...)
                    if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, SDPG_World, bSectionIsSelected, true, MeshElement))
                    {
                        (...)
                        Collector.AddMesh(ViewIndex, MeshElement);
                    }
                    (...)
                }
            }
        }
    }
}

FStaticMeshSceneProxy::GetDynamicMeshElements 函数的最终目的依然是返回收集完所有动态路径下的模型元素的 Collector

从代码看 for 循环了很多层,最终只是为了调用第 21 行的 GetMeshElement 函数来初始化第 19 行构建的 FMeshBatchGetMeshElement 函数就不再重复介绍,因为这个函数已经在 2.3.4.3.1 小节介绍过。

代码第 24 行将初始化好的 FMeshBatch 添加到 Collector 中,本函数的任务就完成了。

2.3.6. 根据 Pass 生成绘制命令

FSceneRenderer::ComputeViewVisibility 函数的第 64 行调用了 SetupMeshPass 函数。

这里就不看 SetupMeshPass 函数的具体实现了,因为这部分内容不再属于视图可见性计算的内容,我们将会在 2.4 节详细解析。

这里我们只需要知道,传入 SetupMeshPass 函数的第三个参数 ViewCommands,就是在 2.3.4 小节 ComputeAndMarkRelevanceForViewParallel 函数返回的 FViewCommands& ViewCommands 参数,也就是缓存路径下的所有已缓存的绘制命令。

2.3.7. 小结

至此,所有可渲染的物体的视图可见性已经确定,下面是整个函数的调用流程。

ComputeViewVisibility-Flow

2.4. 生成绘制命令

生成渲染线程的绘制命令是在 FSceneRenderer::SetupMeshPass 函数里实现的,下面看看这个函数的实现。

void FSceneRenderer::SetupMeshPass(FViewInfo& View, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, FViewCommands& ViewCommands)
{
    const EShadingPath ShadingPath = Scene->GetShadingPath();
    
    for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
    {
        const EMeshPass::Type PassType = (EMeshPass::Type)PassIndex;
        
        (...)
        
        PassProcessorCreateFunction CreateFunction = FPassProcessorManager::GetCreateFunction(ShadingPath, PassType);
        FMeshPassProcessor* MeshPassProcessor = CreateFunction(Scene, &View, nullptr);
        
        FParallelMeshDrawCommandPass& Pass = View.ParallelMeshDrawCommandPasses[PassIndex];
        (...)
        Pass.DispatchPassSetup(
            Scene,
            View,
            PassType,
            BasePassDepthStencilAccess,
            MeshPassProcessor,
            View.DynamicMeshElements,
            &View.DynamicMeshElementsPassRelevance,
            View.NumVisibleDynamicMeshElements[PassType],
            ViewCommands.DynamicMeshCommandBuildRequests[PassType],
            ViewCommands.NumDynamicMeshCommandBuildRequestElements[PassType],
            ViewCommands.MeshCommands[PassIndex]);
    }
}

代码第 5 行表明,不同的 Pass 生成的命令用的同一套模型批次,但是用了不同的绘制命令。

代码第 11 行通过传入不同的 Pass 类型,调用 FPassProcessorManager 里面已经注册好的构造函数,生成不同的 FMeshPassProcessor 类的子类对象。FPassProcessorManager 的注册代码不再详述,有兴趣的可以去看一下 FPassProcessorManager 的具体实现。

代码第 12 行创建了一个 FMeshPassProcessor 类的对象,后面我们将用这个类去绑定 Shader,绑定 Pipline State Object,最终生成绘制命令。这个类是网格通道处理器的基类,特定的通道(也就是 Pass)网格体处理器派生自 FMeshPassProcessor 基类,负责将 FMeshBatch 转换为用于给定通道的网格体绘制命令。本文我们将主要以 BasePassRendering 这个子类为例来介绍。

代码第 14 行的 FParallelMeshDrawCommandPass 是一个很重要的容器类,这个类有一个 FMeshDrawCommandPassSetupTaskContext 类型的上下文成员,这个上下文对象里就保存了最终生成的绘制命令。

代码第 16 行调用 DispatchPassSetup 函数才是真正生成绘制命令的主过程。

2.4.1. 生成绘制命令的主要类

在解析生成绘制命令的主过程之前,先看一下生成绘制命令的这三个重要的类之间的关系。

GenCmd-UML

每个 Pass 都对应有一个 FParallelMeshDrawCommandPass 类。这个类有三个作用。

  • 包含了所有已经生成的绘制命令,其他模块想访问某个 Pass 的绘制命令通过访问这个类的成员就可以实现。
  • 提供触发建立绘制命令的入口。
  • 根据绘制命令,生成 RHI 指令,然后提交 RHI 指令到 RHI 线程。

FMeshDrawCommandPassSetupTaskContext 类是一个保存数据用的类,由于生成绘制命令的过程很长,所以在一层一层的函数调用中,需要有一个上下文类保存关键数据,贯穿整个绘制命令生成过程。

FMeshPassProcessor 是针对不同 Pass 实现的不同模型通道处理器类,例如有以下 Pass 对应的模型通道处理器。

  • FBasePassMeshProcessor:几何通道网格处理器,对应 EMeshPass::BasePass
  • FDepthPassMeshProcessor:深度通道网格处理器,对应 EMeshPass::DepthPass
  • FCustomDepthPassMeshProcessor:自定义深度通道网格处理器,对应 EMeshPass::CustomDepth
  • ……

FMeshPassDrawContext 类也是一个上下文类,它有两个子类 FDynamicPassMeshDrawListContextFCachedPassMeshDrawListContext

  • FDynamicPassMeshDrawListContext 用于动态绘制路径下的绘制命令的保存。
  • FCachedPassMeshDrawListContext 用于缓存绘制路径下的绘制命令的保存。

那么 FMeshPassDrawContextFMeshDrawCommandPassSetupTaskContext 都是上下文类,他们有什么区别呢?

注意看 FMeshPassDrawContext 的子类的成员对象,它们都是引用类型的,实际上它们都是指向 FMeshDrawCommandPassSetupTaskContext 对象的对应字段的。换句话说,FMeshPassDrawContext 类是 FMeshDrawCommandPassSetupTaskContext 类的引用。为什么要做这种引用关系?

从 UML 图中可以看到 FMeshDrawCommandPassSetupTaskContext 已经有一个 FMeshPassProcessor 类型的成员对象了,如果 FMeshPassProcessor 生成命令过程中要保存命令,总不能在 FMeshPassProcessor 类里也添加一个 FMeshDrawCommandPassSetupTaskContext 类型的成员对象吧?这样就形成互相依赖了,违反了 OO 设计原则。这里使用 FMeshPassDrawContext 作为 FMeshDrawCommandPassSetupTaskContext 的代理来进行解耦。

2.4.2. 生成绘制命令的过程

下面我们接着看 DispatchPassSetup 函数的实现。

void FParallelMeshDrawCommandPass::DispatchPassSetup(
    FScene* Scene,
    const FViewInfo& View,
    EMeshPass::Type PassType,
    FExclusiveDepthStencil::Type BasePassDepthStencilAccess,
    FMeshPassProcessor* MeshPassProcessor,
    const TArray<FMeshBatchAndRelevance, SceneRenderingAllocator>& DynamicMeshElements,
    const TArray<FMeshPassMask, SceneRenderingAllocator>* DynamicMeshElementsPassRelevance,
    int32 NumDynamicMeshElements,
    TArray<const FStaticMeshBatch*, SceneRenderingAllocator>& InOutDynamicMeshCommandBuildRequests,
    int32 NumDynamicMeshCommandBuildRequestElements,
    FMeshCommandOneFrameArray& InOutMeshDrawCommands,
    FMeshPassProcessor* MobileBasePassCSMMeshPassProcessor,
    FMeshCommandOneFrameArray* InOutMobileBasePassCSMMeshDrawCommands
)
{
    (...)
    // 初始化上下文
    TaskContext.MeshPassProcessor = MeshPassProcessor;
    TaskContext.DynamicMeshElements = &DynamicMeshElements;
    TaskContext.PassType = PassType;
    (...)
    // 初始上下文的MeshDrawCommands,将缓存绘制路径下的绘制命令先填充进去
    FMemory::Memswap(&TaskContext.MeshDrawCommands, &InOutMeshDrawCommands, sizeof(InOutMeshDrawCommands));
    
    (...)
    // 通过TaskGraph系统,依次执行FMeshDrawCommandPassSetupTask和FMeshDrawCommandInitResourcesTask两个任务
    FGraphEventArray DependentGraphEvents;
    DependentGraphEvents.Add(TGraphTask<FMeshDrawCommandPassSetupTask>::CreateTask(nullptr, ENamedThreads::GetRenderThread()).ConstructAndDispatchWhenReady(TaskContext));
    TaskEventRef = TGraphTask<FMeshDrawCommandInitResourcesTask>::CreateTask(&DependentGraphEvents, ENamedThreads::GetRenderThread()).ConstructAndDispatchWhenReady(TaskContext);
    (...)
}

代码第 18 行到第 24 行,都是在给 TaskContext 的成员对象赋值,也就是在初始化 TaskContext

其中代码第 24 行将传入本函数的 InOutMeshDrawCommands 直接填充到还是空的 TaskContext.MeshDrawCommands 里面,而根据前文已知 InOutMeshDrawCommands 里面已经填充了缓存路径下的静态绘制命令了,所以这里就把静态绘制命令先提交到上下文里面。

代码第 28 到第 30 行通过 TaskGraph 系统,依次执行 FMeshDrawCommandPassSetupTaskFMeshDrawCommandInitResourcesTask 两个任务。

下面先看一下 FMeshDrawCommandPassSetupTask 类的实现。

/**
 * 任务用于并行建立网格绘制命令。包括生成动态网格绘制命令,排序,合并等。
 */
class FMeshDrawCommandPassSetupTask
{
public:
    FMeshDrawCommandPassSetupTask(FMeshDrawCommandPassSetupTaskContext& InContext)
        : Context(InContext)
    {
    }
    
    (...)
    
    void AnyThreadTask()
    {
        (...)
        // 生成动态绘制路径的绘制命令
        GenerateDynamicMeshDrawCommands(
            *Context.View,
            Context.ShadingPath,
            Context.PassType,
            Context.MeshPassProcessor,
            *Context.DynamicMeshElements,
            Context.DynamicMeshElementsPassRelevance,
            Context.NumDynamicMeshElements,
            Context.DynamicMeshCommandBuildRequests,
            Context.NumDynamicMeshCommandBuildRequestElements,
            Context.MeshDrawCommands,
            Context.MeshDrawCommandStorage,
            Context.MinimalPipelineStatePassSet,
            Context.NeedsShaderInitialisation
        );
        
        (...)
        
        // 更新绘制命令排序用的Key
        {
            UpdateTranslucentMeshSortKeys(
                Context.TranslucentSortPolicy,
                Context.TranslucentSortAxis,
                Context.ViewOrigin,
                Context.ViewMatrix,
                *Context.PrimitiveBounds,
                Context.TranslucencyPass,
                Context.MeshDrawCommands
            );
        }
        
        // 根据排序的Key对绘制命令进行排序
        {
            (...)
            Context.MeshDrawCommands.Sort(FCompareFMeshDrawCommands());
        }
        (...)
    }
    
    void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
    {
        AnyThreadTask();
    }
    
private:
    FMeshDrawCommandPassSetupTaskContext& Context;
}

代码第 7 行和第 8 行的构造函数可以看出来,FMeshDrawCommandPassSetupTaskContext 上下文被传进 FMeshDrawCommandPassSetupTask 类里面参与接下来的过程的执行,这就是上下文该干的事。

代码第 57 行到第 60 行的 DoTask 函数正是 TaskGraph 系统中任务的真正执行函数,代码第 59 行表明这个类的真正执行过程写在 AnyThreadTask 函数里面了。

AnyThreadTask 函数的实现过程中可以知道,这个函数主要做了两件事:

  • 代码第 18 行生成动态绘制路径的绘制命令;
  • 代码第 37 行到第 53 行,对绘制命令进行排序。

2.4.2.1. 生成动态绘制路径的绘制命令

下面看一下 GenerateDynamicMeshDrawCommands 的实现。

/**
 * 将每个FMeshBatch转换为一组用于特定网格通道类型的FMeshDrawCommands。
 */
void GenerateDynamicMeshDrawCommands(
    const FViewInfo& View,
    EShadingPath ShadingPath,
    EMeshPass::Type PassType,
    FMeshPassProcessor* PassMeshProcessor,
    const TArray<FMeshBatchAndRelevance, SceneRenderingAllocator>& DynamicMeshElements,
    const TArray<FMeshPassMask, SceneRenderingAllocator>* DynamicMeshElementsPassRelevance,
    int32 MaxNumDynamicMeshElements,
    const TArray<const FStaticMeshBatch*, SceneRenderingAllocator>& DynamicMeshCommandBuildRequests,
    int32 MaxNumBuildRequestElements,
    FMeshCommandOneFrameArray& VisibleCommands,
    FDynamicMeshDrawCommandStorage& MeshDrawCommandStorage,
    FGraphicsMinimalPipelineStateSet& MinimalPipelineStatePassSet,
    bool& NeedsShaderInitialisation
)
{
    (...)
    FDynamicPassMeshDrawListContext DynamicPassMeshDrawListContext(
        MeshDrawCommandStorage,
        VisibleCommands,
        MinimalPipelineStatePassSet,
        NeedsShaderInitialisation
    );
    PassMeshProcessor->SetDrawListContext(&DynamicPassMeshDrawListContext);
    
    {
        const int32 NumDynamicMeshBatches = DynamicMeshElements.Num();

        for (int32 MeshIndex = 0; MeshIndex < NumDynamicMeshBatches; MeshIndex++)
        {
            if (!DynamicMeshElementsPassRelevance || (*DynamicMeshElementsPassRelevance)[MeshIndex].Get(PassType))
            {
                const FMeshBatchAndRelevance& MeshAndRelevance = DynamicMeshElements[MeshIndex];
                const uint64 BatchElementMask = ~0ull;

                PassMeshProcessor->AddMeshBatch(*MeshAndRelevance.Mesh, BatchElementMask, MeshAndRelevance.PrimitiveSceneProxy);
            }
        }
    }
    
    (...)
}

代码第 21 行创建了一个 FDynamicPassMeshDrawListContext 类型的上下文,这个上下文通过传入引用到构造函数,称为了 FMeshDrawCommandPassSetupTaskContext 的代理。

代码第 27 行,将 FDynamicPassMeshDrawListContext 这个代理类的对象设置到 FMeshPassProcessor 的成员里。

代码第 39 行,通过遍历所有 FMeshBatch,调用 FMeshPassProcessorAddMeshBatch 函数将每一个 FMeshBatch 转换成绘制命令。

那么下面看看 AddMeshBatch 函数的实现,因为 FMeshPassProcessor 类的 AddMeshBatch 函数是个纯虚函数,所以这里主要看子类 FBasePassMeshProcessor 里面的实现。

void FBasePassMeshProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId)
{
    (...)
    
    Process< FUniformLightMapPolicy >(
        MeshBatch,
        BatchElementMask,
        StaticMeshId,
        PrimitiveSceneProxy,
        MaterialRenderProxy,
        Material,
        BlendMode,
        ShadingModels,
        FUniformLightMapPolicy(LMP_NO_LIGHTMAP),
        MeshBatch.LCI,
        MeshFillMode,
        MeshCullMode);
    
    (...)
}

这个函数其他部分已经省略,最主要的调用就是 Process 函数,下面看看 Process 函数的具体实现。

template<typename LightMapPolicyType>
void FBasePassMeshProcessor::Process(
    const FMeshBatch& RESTRICT MeshBatch,
    uint64 BatchElementMask,
    int32 StaticMeshId,
    const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
    const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
    const FMaterial& RESTRICT MaterialResource,
    EBlendMode BlendMode,
    FMaterialShadingModelField ShadingModels,
    const LightMapPolicyType& RESTRICT LightMapPolicy,
    const typename LightMapPolicyType::ElementDataType& RESTRICT LightMapElementData,
    ERasterizerFillMode MeshFillMode,
    ERasterizerCullMode MeshCullMode)
{
    const FVertexFactory* VertexFactory = MeshBatch.VertexFactory;
    
    (...)
    
    // 获取Shader
    TMeshProcessorShaders<
        TBasePassVertexShaderPolicyParamType<LightMapPolicyType>,
        FBaseHS,
        FBaseDS,
        TBasePassPixelShaderPolicyParamType<LightMapPolicyType>> BasePassShaders;
    
    GetBasePassShaders<LightMapPolicyType>(
        MaterialResource,
        VertexFactory->GetType(),
        LightMapPolicy,
        FeatureLevel,
        bRenderAtmosphericFog,
        bRenderSkylight,
        Get128BitRequirement(),
        BasePassShaders.HullShader,
        BasePassShaders.DomainShader,
        BasePassShaders.VertexShader,
        BasePassShaders.PixelShader
        );
    
    // 获取/设置渲染状态
    FMeshPassProcessorRenderState DrawRenderState(PassDrawRenderState);
    
    SetDepthStencilStateForBasePass(
        ViewIfDynamicMeshCommand,
        DrawRenderState,
        FeatureLevel,
        MeshBatch,
        StaticMeshId,
        PrimitiveSceneProxy,
        bEnableReceiveDecalOutput);

    if (bTranslucentBasePass)
    {
        SetTranslucentRenderState(DrawRenderState, MaterialResource, GShaderPlatformForFeatureLevel[FeatureLevel], TranslucencyPassType);
    }
    
    (...)
    
    // 计算当前绘制命令的排序Key
    FMeshDrawCommandSortKey SortKey = FMeshDrawCommandSortKey::Default;

    if (bTranslucentBasePass)
    {
        SortKey = CalculateTranslucentMeshStaticSortKey(PrimitiveSceneProxy, MeshBatch.MeshIdInPrimitive);
    }
    else
    {
        SortKey = CalculateBasePassMeshStaticSortKey(EarlyZPassMode, BlendMode, BasePassShaders.VertexShader.GetShader(), BasePassShaders.PixelShader.GetShader());
    }
    
    // 构建绘制命令
    BuildMeshDrawCommands(
        MeshBatch,
        BatchElementMask,
        PrimitiveSceneProxy,
        MaterialRenderProxy,
        MaterialResource,
        DrawRenderState,
        BasePassShaders,
        MeshFillMode,
        MeshCullMode,
        SortKey,
        EMeshPassFeatures::Default,
        ShaderElementData);
}

代码第 21 行到第 39 行,通过调用 GetBasePassShaders 函数,获取出适合的所有 Shader。

代码第 42 行到第 56 行,根据当前 Pass 的当前模型,计算出合适的渲染状态并设置给 FMeshPassProcessorRenderState 对象。其中代码第 44 行是获取深度/模板缓冲区的描述(例如 DX12 里的 D3D12_DEPTH_STENCIL_DESC1);代码第 55 行是设置透明混合状态的(例如 DX12 里的 D3D12_BLEND_DESC)。

代码第 61 行到第 70 行,计算出当前绘制命令的排序 Key,后续用这个 Key 为绘制命令排序,也就能确定物体渲染的先后顺序了。比如透明物体,这里的 CalculateTranslucentMeshStaticSortKey 函数给 Key 赋值了渲染优先级和基元 ID 来帮助排序,这里没有设置离相机的距离是因为后面会设置,这里统一设为 0(详细代码请参看 CalculateTranslucentMeshStaticSortKey 函数以及 FMeshDrawCommandSortKey 类的实现,这里不再详述)。

代码第 73 行调用的 BuildMeshDrawCommands 就是生成绘制命令并存储到上下文里。

template<typename PassShadersType, typename ShaderElementDataType>
void FMeshPassProcessor::BuildMeshDrawCommands(
    const FMeshBatch& RESTRICT MeshBatch,
    uint64 BatchElementMask,
    const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
    const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
    const FMaterial& RESTRICT MaterialResource,
    const FMeshPassProcessorRenderState& RESTRICT DrawRenderState,
    PassShadersType PassShaders,
    ERasterizerFillMode MeshFillMode,
    ERasterizerCullMode MeshCullMode,
    FMeshDrawCommandSortKey SortKey,
    EMeshPassFeatures MeshPassFeatures,
    const ShaderElementDataType& ShaderElementData)
{
    const FVertexFactory* RESTRICT VertexFactory = MeshBatch.VertexFactory;
    
    // 创建当前FMeshBatch所有Element都通用的绘制命令
    FMeshDrawCommand SharedMeshDrawCommand;
    
    // 设置模板参考值给到绘制命令
    SharedMeshDrawCommand.SetStencilRef(DrawRenderState.GetStencilRef());
    
    // 创建PSO的初始化器
    FGraphicsMinimalPipelineStateInitializer PipelineState;
    PipelineState.PrimitiveType = (EPrimitiveType)MeshBatch.Type;
    PipelineState.ImmutableSamplerState = MaterialRenderProxy.ImmutableSamplerState;
    
    // InputStreamType赋值为Default,表明FVertexFactory里面的顶点缓冲流用的是Streams这个数组
    EVertexInputStreamType InputStreamType = EVertexInputStreamType::Default;
    (...)
    
    // 获取顶点声明(Vertex Declaration)
    FRHIVertexDeclaration* VertexDeclaration = VertexFactory->GetDeclaration(InputStreamType);
    
    // 将Shader、顶点声明赋值给PSO初始化器,并且初始化Shader绑定
    SharedMeshDrawCommand.SetShaders(VertexDeclaration, PassShaders.GetUntypedShaders(), PipelineState);
    
    // 设置模型的填充模式(线框/顶点/实体填充)和剔除模式(不剔除/正面剔除/背面剔除)给到PSO初始化器
    PipelineState.RasterizerState = GetStaticRasterizerState<true>(MeshFillMode, MeshCullMode);
    
    // 设置透明混合状态、模板/深度描述、着色率给到PSO初始化器
    PipelineState.BlendState = DrawRenderState.GetBlendState();
    PipelineState.DepthStencilState = DrawRenderState.GetDepthStencilState();
    PipelineState.DrawShadingRate = GetShadingRateFromMaterial(MaterialResource.GetShadingRate());
    
    // 设置顶点工厂的顶点缓冲区给到绘制命令
    VertexFactory->GetStreams(FeatureLevel, InputStreamType, SharedMeshDrawCommand.VertexStreams);
    
    int32 DataOffset = 0;
    // 获取不同类型的Shader的绑定
    if (PassShaders.VertexShader.IsValid())
    {
        FMeshDrawSingleShaderBindings ShaderBindings = SharedMeshDrawCommand.ShaderBindings.GetSingleShaderBindings(SF_Vertex, DataOffset);
        PassShaders.VertexShader->GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, MaterialResource, DrawRenderState, ShaderElementData, ShaderBindings);
    }
    (...)
    
    const int32 NumElements = MeshBatch.Elements.Num();
    // 遍历每一个Element
    for (int32 BatchElementIndex = 0; BatchElementIndex < NumElements; BatchElementIndex++)
    {
        (...)
        
        const FMeshBatchElement& BatchElement = MeshBatch.Elements[BatchElementIndex];
        // 创建并添加绘制命令FMeshDrawCommand到上下文的MeshDrawCommandStorage中去
        FMeshDrawCommand& MeshDrawCommand = DrawListContext->AddCommand(SharedMeshDrawCommand, NumElements);
        
        DataOffset = 0;
        // 根据不同顶点工厂给不同类型Shader添加绑定
        if (PassShaders.VertexShader.IsValid())
        {
            // 调用到具体顶点工厂的获取绑定函数,并添加Shader绑定。例如FLocalVertexFactory的FLocalVertexFactoryShaderParametersBase::GetElementShaderBindingsBase函数
            FMeshDrawSingleShaderBindings VertexShaderBindings = MeshDrawCommand.ShaderBindings.GetSingleShaderBindings(SF_Vertex, DataOffset);
            FMeshMaterialShader::GetElementShaderBindings(PassShaders.VertexShader, Scene, ViewIfDynamicMeshCommand, VertexFactory, InputStreamType, FeatureLevel, PrimitiveSceneProxy, MeshBatch, BatchElement, ShaderElementData, VertexShaderBindings, MeshDrawCommand.VertexStreams);
        }
        
        (...)
        
        // 创建并添加绘制命令FVisibleMeshDrawCommand到上下文的MeshDrawCommands中去
        DrawListContext->FinalizeCommand(MeshBatch, BatchElementIndex, DrawPrimitiveId, ScenePrimitiveId, MeshFillMode, MeshCullMode, SortKey, PipelineState, &ShadersForDebugging, MeshDrawCommand);
    }
}

代码虽然很长,但是要重点关心的语句不多。

代码第 19 行创建了绘制命令对象,代码第 25 行创建了 PSO(Pipline State Object)初始化器,这两个对象是这个函数要创建和初始化的最重要的对象。

代码第 19 行到第 56 行都是在对这两个对象做初始化。

代码第 67 行和 81 行都是创建并添加绘制命令的语句,区别在于:第 67 行创建的是原始的绘制命令 FMeshDrawCommand,没有经历过过滤和重新排序,最终不一定参与渲染;第 81 行创建的绘制命令 FVisibleMeshDrawCommand 最终都会参与渲染。

FVisibleMeshDrawCommand 里面包含了一个 FMeshDrawCommandFMeshDrawCommand 才是渲染时候真正用到的类,FVisibleMeshDrawCommand 只是一个对 FMeshDrawCommand 的包装。

这里还要特别说一下代码第 81 行的 FinalizeCommand 函数,这个函数除了创建并添加绘制命令到上下文的 MeshDrawCommands,还将当前 Element 的索引缓冲区和 PSO 初始化器赋值给了 FMeshDrawCommand

至此,绘制命令已经生成完成。

2.4.2.2. 绘制命令排序

回到 FMeshDrawCommandPassSetupTask 类的代码,第 38 行调用的 UpdateTranslucentMeshSortKeys 函数就是在更新绘制命令排序用的 Key。

/**
* Update mesh sort keys with view dependent data.
*/
void UpdateTranslucentMeshSortKeys(
    ETranslucentSortPolicy::Type TranslucentSortPolicy,
    const FVector& TranslucentSortAxis,
    const FVector& ViewOrigin,
    const FMatrix& ViewMatrix,
    const TArray<struct FPrimitiveBounds>& PrimitiveBounds,
    ETranslucencyPass::Type TranslucencyPass, 
    FMeshCommandOneFrameArray& VisibleMeshCommands
    )
{
    for (int32 CommandIndex = 0; CommandIndex < VisibleMeshCommands.Num(); ++CommandIndex)
    {
        FVisibleMeshDrawCommand& VisibleCommand = VisibleMeshCommands[CommandIndex];
        
        // 获取包围盒原点
        const int32 PrimitiveIndex = VisibleCommand.ScenePrimitiveId;
        const FVector BoundsOrigin = PrimitiveIndex >= 0 ? PrimitiveBounds[PrimitiveIndex].BoxSphereBounds.Origin : FVector::ZeroVector;
        
        float Distance = 0.0f;
        if (TranslucentSortPolicy == ETranslucentSortPolicy::SortByDistance)
        {
            // 基于到视图原点的距离排序,视图旋转不考虑
            Distance = (BoundsOrigin - ViewOrigin).Size();
        }
        else if (TranslucentSortPolicy == ETranslucentSortPolicy::SortAlongAxis)
        {
            // 基于正交距离排序
            const FVector CameraToObject = BoundsOrigin - ViewOrigin;
            Distance = FVector::DotProduct(CameraToObject, TranslucentSortAxis);
        }
        else
        {
            // 基于投影Z距离排序
            check(TranslucentSortPolicy == ETranslucentSortPolicy::SortByProjectedZ);
            Distance = ViewMatrix.TransformPosition(BoundsOrigin).Z;
        }
        
        // 更新Key的值,也就是PackedData
        FMeshDrawCommandSortKey SortKey;
        SortKey.PackedData = VisibleCommand.SortKey.PackedData;
        SortKey.Translucent.Distance = (uint32)~BitInvertIfNegativeFloat(*((uint32*)&Distance));
        VisibleCommand.SortKey.PackedData = SortKey.PackedData;
    }
}

代码第 23 行到 39 行基于不同排序策略计算出 Distance 值。

代码第 44 行使用计算出来的 Distance 值来更新 KeyTranslucent 值,也就是更新了 PackedData 值,因为 PackedDataTranslucent 是同一个联合体的不同表现形式。

再次回到 FMeshDrawCommandPassSetupTask 类的代码第 52 行调用的 Context.MeshDrawCommands.Sort(FCompareFMeshDrawCommands())。从代码可以看出绘制命令根据 FCompareFMeshDrawCommands 的规则进行了排序,那么 FCompareFMeshDrawCommands 的规则是什么呢?

struct FCompareFMeshDrawCommands
{
    FORCEINLINE bool operator() (FVisibleMeshDrawCommand A, FVisibleMeshDrawCommand B) const
    {
        // 首先通过排序Key排序。
        if (A.SortKey != B.SortKey)
        {
            return A.SortKey < B.SortKey;
        }

        // 其次通过实例桶进行排序。
        if (A.StateBucketId != B.StateBucketId)
        {
            return A.StateBucketId < B.StateBucketId;
        }

        return false;
    }
};

原理很简单,看代码注释就知道,首先根据排序 Key 排序,如果排序 Key 相同,那么根据实例桶进行排序。

2.4.3. 小结

至此,渲染线程的绘制命令就已经生成完成,并保存到 FParallelMeshDrawCommandPass 里面了。

其生成过程简单描述一下:

  • 首先将先前已经生成缓存绘制路径下的绘制命令拷贝到 FParallelMeshDrawCommandPass 里面;
  • 接下来 FParallelMeshDrawCommandPass 类驱动不同 Pass 的 FMeshPassProcessor 执行生成动态绘制路径下的绘制命令并存储;
  • 最后将绘制命令进行排序。

3. 渲染线程向 RHI 线程提交数据

渲染线程向 RHI 提交数据的绘制命令的函数在前文已经提到过,是通过 FMeshDrawCommand::SubmitDraw 这个函数去做的提交。下面看一下该函数的实现。

/** 向RHI命令列表提交命令来绘制MeshDrawCommand。 */
void FMeshDrawCommand::SubmitDraw(
    const FMeshDrawCommand& RESTRICT MeshDrawCommand, 
    const FGraphicsMinimalPipelineStateSet& GraphicsMinimalPipelineStateSet,
    FRHIVertexBuffer* ScenePrimitiveIdsBuffer,
    int32 PrimitiveIdOffset,
    uint32 InstanceFactor,
    FRHICommandList& RHICmdList,
    FMeshDrawCommandStateCache& RESTRICT StateCache)
{
    (...)
    
    // 设置PSO和PSO管线到CommandList
    if (MeshDrawCommand.CachedPipelineId.GetId() != StateCache.PipelineId)
    {
        FGraphicsPipelineStateInitializer GraphicsPSOInit = MeshPipelineState.AsGraphicsPipelineStateInitializer();
        RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
        SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
        StateCache.SetPipelineState(MeshDrawCommand.CachedPipelineId.GetId());
    }
    
    // 设置模板参考值到CommandList
    if (MeshDrawCommand.StencilRef != StateCache.StencilRef)
    {
        RHICmdList.SetStencilRef(MeshDrawCommand.StencilRef);
        StateCache.StencilRef = MeshDrawCommand.StencilRef;
    }
    
    // 设置顶点缓冲流到CommandList
    for (int32 VertexBindingIndex = 0; VertexBindingIndex < MeshDrawCommand.VertexStreams.Num(); VertexBindingIndex++)
    {
        const FVertexInputStream& Stream = MeshDrawCommand.VertexStreams[VertexBindingIndex];

        if (MeshDrawCommand.PrimitiveIdStreamIndex != -1 && Stream.StreamIndex == MeshDrawCommand.PrimitiveIdStreamIndex)
        {
            RHICmdList.SetStreamSource(Stream.StreamIndex, ScenePrimitiveIdsBuffer, PrimitiveIdOffset);
            StateCache.VertexStreams[Stream.StreamIndex] = Stream;
        }
        else if (StateCache.VertexStreams[Stream.StreamIndex] != Stream)
        {
            RHICmdList.SetStreamSource(Stream.StreamIndex, Stream.VertexBuffer, Stream.Offset);
            StateCache.VertexStreams[Stream.StreamIndex] = Stream;
        }
    }
    
    // 设置Shader绑定到CommandList
    MeshDrawCommand.ShaderBindings.SetOnCommandList(RHICmdList, MeshPipelineState.BoundShaderState.AsBoundShaderState(), StateCache.ShaderBindings);
    
    (...)
    
    // 从CommandList里请求绘制命令
    RHICmdList.DrawIndexedPrimitive(
        MeshDrawCommand.IndexBuffer,
        MeshDrawCommand.VertexParams.BaseVertexIndex,
        0,
        MeshDrawCommand.VertexParams.NumVertices,
        MeshDrawCommand.FirstIndex,
        MeshDrawCommand.NumPrimitives,
        MeshDrawCommand.NumInstances * InstanceFactor
    );
    (...)
}

这个过程是不是很接近渲染 API 的调用了?既然很类似了就不用再解释更多,有兴趣了解各个渲染 API(DX9、Vulkan、OpenGL 等)如何实现上面这些调用的可以去详细看每一个函数调用的具体实现。

那么是从哪里调用的 FMeshDrawCommand::SubmitDraw 呢?这个函数的入口非常多,要看具体通道的实现需要,那么下面我们列一个调用栈作为例子来说明一下。

void FMobileSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    InitViews(RHICmdList);
    
    (...)
    
    FRHITexture* FMobileSceneRenderer::RenderForward(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> ViewList)
    {
        void FMobileSceneRenderer::RenderMobileBasePass(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> PassViews)
        {
            void FParallelMeshDrawCommandPass::DispatchDraw(FParallelCommandListSet* ParallelCommandListSet, FRHICommandList& RHICmdList) const
            {
                void SubmitMeshDrawCommandsRange(
                    const FMeshCommandOneFrameArray& VisibleMeshDrawCommands,
                    const FGraphicsMinimalPipelineStateSet& GraphicsMinimalPipelineStateSet,
                    FRHIVertexBuffer* PrimitiveIdsBuffer,
                    int32 BasePrimitiveIdsOffset,
                    bool bDynamicInstancing,
                    int32 StartIndex,
                    int32 NumMeshDrawCommands,
                    uint32 InstanceFactor,
                    FRHICommandList& RHICmdList)
                {
                    (...)
                    FMeshDrawCommand::SubmitDraw(*VisibleMeshDrawCommand.MeshDrawCommand, GraphicsMinimalPipelineStateSet, PrimitiveIdsBuffer, PrimitiveIdBufferOffset, InstanceFactor, RHICmdList, StateCache);
                }
            }
        }
    }
}

代码第 1 行的 FMobileSceneRenderer::Render 想必已经很熟悉了,这个就是渲染线程的主循环函数之一。

代码第 3 行就是第 2 章详细解析的内容。

代码第 7 行一直到到第 25 行就是一般基通道里的调用栈。

4. 总结

至此,一个静态模型从 FBX 到渲染 API 的全过程已经完全展现出来,里面有一些细节限于篇幅没有详细去解释后续有机会再继续整理。整个调用过程由于细节很多,描述得很复杂需要花时间去阅读、研究,文章也可能会在后续研究中动态进行调整。


Similar Posts

评论