メッシュ描画パイプライン

カスタム メッシュ パスの追加、及び Unreal Engine のメッシュ描画パフォーマンス特性を理解するためのガイド

このページに含まれる情報は、カスタム メッシュ パスを追加するプログラマーや、Unreal Engine のメッシュ描画パフォーマンス特性を理解する必要のあるプログラマーを対象としています。

メッシュ描画パイプライン は、リテインド モードの概念に基づいています。このモードでは、すべてのシーン描画を、毎フレーム作成するのではなく、事前に準備します。あまり変化しない、複数のフレームで再利用できるスタティック メッシュのプロパティを活用するために、積極的キャッシュと描画呼び出しマージも備えています。

MeshPipelineOverview_1.png

描画の過程。

メッシュ描画は FPrimitiveSceneProxy から始まります。これは、ゲーム スレッドの UPrimitiveComponent のレンダリング スレッド表現です。FPrimitiveSceneProxy は、GetDynamicMeshElements と DrawStaticElements のコールバックを通じて FMeshBatch をレンダラに送信する役割を担います。

FMeshBatch は FPrimitiveSceneProxy 実装 (ユーザー コード) をメッシュ パス (プライベート レンダラ モジュール) から切り離します。これには、パスが最終的なシェーダ バインディングとレンダリング ステートを把握するために必要な情報がすべて含まれているため、レンダリングされるパスをプロキシが認識することはありません。

次のステップは、FMeshBatch をメッシュ パス固有の FMeshDrawCommand に変換することです。FMeshDrawCommandFMeshBatch と RHI の間のインターフェースです。これは、完全にステートレスな描画記述であり、RHI がメッシュ描画について知る必要のあるすべての情報が格納されています。

  • 使用するシェーダ

  • リソース バインディング

  • 描画呼び出しパラメータ

これにより、RHI レベルのすぐ上で描画呼び出しのキャッシュとマージが可能となります。FMeshDrawCommand はメッシュ パス固有の FMeshPassProcessor により FMeshBatch から作成されます。

最後に、SubmitMeshDrawCommands を使用して FMeshDrawCommand が、RHICommandList 上に設定される一連の RHI コマンドに変換されます。

キャッシュされたメッシュ バッチとダイナミック メッシュ バッチ

FPrimitiveSceneProxy には、FMeshBatches を生成するためのパスが 2 つあります。キャッシュされたパスとダイナミック パスです。FPrimitiveSceneProxy の実装は、GetViewRelevance() 関数を通じて各フレームで使用するパスを制御します。

MeshPipelineOverview_2.png

FMeshBatch コード パス。オレンジの矢印は毎フレーム行う必要のある操作を表し、青の矢印はキャッシュ前に 1 回行う必要のある操作を示しています。

キャッシュされたパスは、FMeshBatch を構築、再利用します。スタティック メッシュなどの毎フレーム変化するわけではない描画を迅速にレンダリングする場合にお勧めです。DrawStaticElements によって実装されており、これはプロキシがシーンに追加されるときに呼び出されます。作成された FMeshBatchesFPrimitiveSceneInfo::StaticMeshes 内に格納され、プロキシがシーンから削除されるまですべてのフレームで再利用されます。

ダイナミック パスでは FMeshBatch をフレームごとに再作成します。これは最も柔軟なパスであり、フレームごとに変化することが多いパーティクルなどの描画に使用されます。GetDynamicMeshElements によって実装されています。この関数は毎フレーム InitViews から呼び出され、すべてのビューに一時的な FMeshBatch を作成します。

FMeshPassProcessor

具体的なパス メッシュ プロセッサは FMeshPassProcessor 基本クラスから派生し、FMeshBatch を所定のパスに対するメッシュ描画コマンドに変換する役割を担います。ここで、最終的な描画フィルタリング、適切なシェーダの選択、シェーダ バインディングの収集が行われます。

カスタム メッシュ パス プロセッサを作成するために、FMeshPassProcessor から派生している必要があります。また、AddMeshBatch 関数をオーバーライドする必要があります。

AddMeshBatch は以下を実装しています。

  • 描画フィルタリング - マテリアルに透過描画モードがあり、そのマテリアルを FDepthPassMeshProcessor で処理しない場合など

  • シェーダとパイプラインのステート (深度/ステンシル/ブレンド ステート) を選択する

  • 最終的に BuildMeshDrawCommands() の呼び出しを行い、パス/マテリアル/頂点ファクトリ/ プリミティブのシェーダ バインディングを収集し、適切なリストに新しい描画コマンドを追加する

シェーダ バインディング

Unreal Engine のシェーダ バインディングは、ユニフォーム バッファ、サンプラー、テクスチャ、ShaderResourceView、または緩いパラメータ (FShaderParameter) のいずれかになります。

FMeshPassProcessorRHICmdList.SetShaderParameter を使用してシェーダ バインディングを直接 RHI には送信せず、単に FMeshDrawSingleShaderBindings クラスに記録します。すべてのパス間の共有コードである BuildMeshDrawCommands() 関数は、パス シェーダ上で GetShaderBindings() を呼び出します。

シェーダ バインディングは次のいくつかのカテゴリのいずれかに該当します。

  • ViewUniformBufferDepthPassUniformBuffer などのパス定数ユニフォーム バッファ

  • 頂点ファクトリ バインディング

  • マテリアル バインディング

  • プリミティブ バインディング

  • 描画ごとに変化するパス固有のバインディング

描画ごとに異なるバインディングを設定すると、描画呼び出しのマージは行われません。緩いパラメータ (ユニフォーム バッファに含まれないシェーダ パラメータ) を設定する場合も描画呼び出しのマージは行われず、描画間で低速の定数バッファ更新が強制されます。

FMeshPassProcessor は、BuildMeshDrawCommands() を実行してパス シェーダの GetShaderBindings() を呼び出す必要があるので、FMeshPassProcessor から GetShaderBindings() 呼び出しに任意のデータを渡すメカニズムが必要です。これは、BuildMeshDrawCommands()ShaderElementData パラメータによって実現できます。

FMeshDrawCommand のパフォーマンス上の危険性

FMeshDrawCommand では、余分なヒープ割り当てを行うことなく可変長の配列を格納するために、数多くのインライン アロケータが使用されています。これらがオーバーフローすると、コマンドのトラバースのキャッシュ ミスと合わせて各メッシュ描画コマンドがヒープ割り当てを構築/破棄/コピーしなければならないため、パフォーマンス上の危険が発生します。

FMeshDrawShaderBindings2 個のシェーダ周波数 (頂点 + ピクセル) を前提としています。

    TArray<FMeshDrawShaderBindingsLayout, TInlineAllocator<2>>ShaderLayouts

FMeshDrawCommand はすべての周波数の中で 10 個のシェーダ バインディングを前提としています。

    const int32 NumInlineShaderBindings = 10;

FMeshDrawCommand は頂点ファクトリから 4 個の頂点ストリームを前提としています。

    typedef TArray<FVertexInputStream, TInlineAllocator<4>>FVertexInputStreamArray;

パス タイプ

FMeshPassProcessor を使用して描画を行う方法は 3 つあります。

パス タイプ

説明

EMeshPass::Type 列挙型

ここにエントリを追加すると、FScene 内に FParallelMeshDrawCommandPass が割り当てられます。これにより、FSceneAddToScene 時にパスのメッシュ描画コマンドをキャッシュできるようになります。FMeshPassProcessorFRegisterPassProcessorCreateFunction によって列挙型に登録される必要があります。パスのセットアップとディスパッチはタスクで行われます。

手動パス

FParallelMeshDrawCommandPass が任意のクラスで変数として格納されている場合に手動パスを使用します。これは、各フレームのパスの数が可変の場合に使用します (例: シャドウ深度パス)。このタイプのパスは、FScene::AddToScene 時にコマンドをキャッシュできませんが、タスクで行われるパスのセットアップとディスパッチの利点を享受できます。

DrawDynamicMeshPass

これは即時モードの描画に使用され、最も低速ですが、最も便利なアプローチです。パスのセットアップとディスパッチは、呼び出し元スレッド内で即時行われます。

レンダラは、この時点でプラグインに拡張できるようにはなっておらず、DrawDynamicMeshPass を例外として、新しいパスを追加するにはレンダラ モジュール コードの変更が必要です。

FParallelMeshDrawCommandPass

カスタム メッシュ パスを追加するには、まず、新しいエントリを EMeshPass 列挙型に追加する必要があります。次に、FRelevancePacket::MarkRelevant() 内で、関連性フラグに基づいて、スタティック メッシュを可視メッシュ描画コマンド リストに追加します。たとえば、次のスニペットは、メッシュ描画コマンドが深度パスに関連している場合、それを深度パスに追加します。

    if (StaticMeshRelevance.bUseForDepthPass)
    {
        DrawCommandPacket.AddCommandsForMesh(PrimitiveIndex, PrimitiveSceneInfo, StaticMeshRelevance, StaticMesh, Scene, bCanCache, EMeshPass::DepthPass);
    }

ComputeDynamicMeshRelevance 内で EMeshPass の動的描画の関連性をマークします。

    if (ViewRelevance.bDrawRelevance && (ViewRelevance.bRenderInMainPass|| ViewRelevance.bRenderCustomDepth))
    {
        PassMask.Set(EMeshPass::DepthPass);
        View.NumVisibleDynamicMeshElements[EMeshPass::DepthPass] += NumElements;
    }

FParallelMeshDrawCommandPass::DispatchDraw を使用してこの特定のパスを描画します。

    View.ParallelMeshDrawCommandPasses[EMeshPass::DepthPass].DispatchDraw(nullptr, RHICmdList);

このパスを並列で描画するために、並列コマンド リスト セットをセットアップすることもできます。

    FPrePassParallelCommandListSet ParallelCommandListSet(View, this, ParentCmdList, true, DrawRenderState);
    View.ParallelMeshDrawCommandPasses[EMeshPass::DepthPass].DispatchDraw(&ParallelCommandListSet, ParentCmdList);

DrawDynamicMeshPass

FParallelMeshDrawCommandPass は一般的なメッシュ パスのデフォルト パスです。これはメッシュ描画コマンド キャッシュと並列レンダリングをサポートする唯一のパスなので、パフォーマンスが重要なメッシュ パスで使用します。一方で、パフォーマンス要件によって、非常に厳密なデザインが強制されます。たとえば、InitViews の後でメッシュ描画コマンドやシェーダ バインディングを変更することはできません。

エディタ内でいくつかのメッシュを描画するといった特定のユースケースでは、DrawDynamicMeshPass がシンプルなソリューションとなる可能性があります。これは、即時モードのメッシュ描画を提供し、最も柔軟性の高いレンダリング パスです。Unreal Engine では、一部のエディタ専用パスとキャンバスのレンダリングに DrawDynamicMeshPass を使用します。

DrawDynamicMeshPass を使用した描画は非常にシンプルで、必要なのは、メッシュ描画コマンドの一時的なリストを埋めるラムダを渡すことだけです。

    DrawDynamicMeshPass(View, RHICmdList, [&View, CurrentDecalStage, RenderTargetMode](FDynamicPassMeshDrawListContext* DynamicMeshPassContext)
    {
        FMeshDecalMeshProcessor PassMeshProcessor(
            View.Family->Scene->GetRenderScene(),
            &View,
            CurrentDecalStage,
            RenderTargetMode,
            DynamicMeshPassContext);

        for (int32 MeshBatchIndex = 0; MeshBatchIndex < View.MeshDecalBatches.Num(); ++MeshBatchIndex)
        {
            const FMeshBatch* Mesh = View.MeshDecalBatches[MeshBatchIndex].Mesh;
            const FPrimitiveSceneProxy* PrimitiveSceneProxy = View.MeshDecalBatches[MeshBatchIndex].Proxy;
            const uint64 DefaultBatchElementMask = ~0ull;

            PassMeshProcessor.AddMeshBatch(*Mesh, DefaultBatchElementMask, PrimitiveSceneProxy);
        }
    });

キャッシュされたメッシュ描画コマンド

キャッシュされたメッシュ描画コマンドは、FPrimitiveSceneInfo::CacheMeshDrawCommands 内の組み込みの FPrimitiveSceneInfo::AddToScene です。これらを使用して描画すると、事前にビルドされた適切なコマンドを毎フレーム選択するだけなので、非常に効率的です (FDrawCommandRelevancePacket::AddCommandsForMesh)。キャッシュされた描画コマンドを使用できるのは、描画ステートが毎フレーム変化せず、すべてのシェーダ バインディングを AddToScene 内でセットアップできる場合だけです。

MeshPipelineOverview_3.png

メッシュ描画コマンドのキャッシュ パス。オレンジの矢印は毎フレーム行う必要のある操作を表し、青の矢印は 1 回行ってキャッシュする操作を示しています。

キャッシュされたメッシュ描画コマンドをサポートするためには以下の要件があります。

  • パスは EMeshPass::Type のエントリを使用する必要がある。

  • カスタム メッシュ プロセッサを登録する際に EMeshPassFlags::CachedMeshCommands フラグを渡す必要がある。

  • メッシュ パス プロセッサは、キャッシュ中に null になるので、FSceneView に依存することなくすべてのシェーダ バインディングをセットアップできる必要がある。

シェーダがキャッシュされたメッシュ描画コマンドを使用してフレームごとのデータにアクセスできるようにするために、シーン全体のユニフォーム バッファ (FScene::UniformBuffers 参照) をバインドしてから、RHIUpdateUniformBuffer を使用して描画前にそのコンテンツを変更します。

現在キャッシュできるのは FLocalVertexFactory (UStaticMeshComponent) だけです。それ以外の頂点ファクトリはすべて、シェーダ バインディングをセットアップするためのビューが必要なためです。

キャッシュの無効化

メッシュ パス プロセッサが AddMeshBatch で読み取るデータは、キャッシュされたメッシュ描画コマンドの依存関係です。この依存関係が変化したら、キャッシュされたコマンドは無効化されなければなりません。単一プリミティブのキャッシュされたコマンドは、FPrimitiveSceneInfo::BeginDeferredUpdateStaticMeshes を使用して無効にできます。シーン全体のキャッシュされたコマンドは、Scene->bScenesPrimitivesNeedStaticMeshElementUpdatetrue に設定することで無効にできます。これは大掛かりな操作であり、大規模なシーンで処理落ちを引き起こすため、ゲームプレイ中には避ける必要があります。

たとえば、FBasePassMeshProcessor::AddMeshBatchScene->SkyLight を使用してスカイライト シェーダ順列を選択するかどうかを決定します。Scene-SkyLight が変化したら、キャッシュされたメッシュ描画コマンドを無効にする必要があります。

このキャッシング スキームで高いパフォーマンスを達成するには、データを永続的なユニフォーム バッファに配置することが重要です。その後は、キャッシュされたコマンドを頻繁に無効にするのではなく、それらのバッファを更新するようにします。たとえば、スカイライトのケースは、別のシェーダ順列を選択するのではなく、PassUniformBuffer のコンテンツに基づいてシェーダで動的ブランチに変更される場合があります。

リソースの存続期間管理

FMeshDrawCommand は参照しているリソースの存続期間の管理を行わないため、キャッシュされたメッシュ描画コマンドに特別な配慮を行って、特定のリソースを参照する可能性のあるコマンドを無効にする必要があります。たとえば、キャッシュされたメッシュ描画コマンドによって参照されるユニフォーム バッファを再作成し、そのキャッシュされたメッシュ描画コマンドをレンダリングのためにトラバースするとクラッシュが発生します。ユニフォーム バッファを更新するか、キャッシュされたメッシュ描画コマンドを無効にする必要があります。

VALIDATE_UNIFORM_BUFFER_LIFETIME を使用して、キャッシュされたメッシュ描画コマンドによってまだ参照されているユニフォーム バッファを削除している場所を特定できます。

描画呼び出しのマージ

FMeshDrawCommands によって RHI レベルのすぐ上での描画に必要なすべてのステートがキャプチャされるので、描画呼び出しのマージとの互換性を簡単に比較できます。現在実装されている描画呼び出しのマージの唯一の形式は、D3D11 機能セットに基づいています。この機能セットによって、同じシェーダ バインディングを持つ描画呼び出しをインスタンス化描画にマージできます。D3D12 などのより高度な RHI を使用すると、より積極的な描画のマージが可能ですが、これはまだ実装されていません。

動的インスタンス化

2 つの描画を 1 つのインスタンス化描画にマージするには、描画に同じシェーダ バインディング (FMeshDrawCommand::MatchesForDynamicInstancing) がなければなりません。異なるのは、シェーダの InstanceID だけです。または、インスタンス周波数の頂点ストリーム セットアップです。

動的インスタンス化を実現するには、シェーダ パラメータを慎重に作成する必要があります。その方法は、パラメータ周波数によってさまざまです。

パス タイプ

説明

パス パラメータ

パス ユニフォーム バッファ内に配置されています。このバッファで、パス内の描画をマージできます。

FLocalVertexFactory パラメータ

同じ UStaticMesh を持つ描画をマージできる UStaticMesh が所有するユニフォーム バッファ内に配置されています。

マテリアル インスタンス パラメータ

同じマテリアル インスタンスを使用する描画をマージできるマテリアル ユニフォーム バッファ内に配置されています。

ライトマップ リソース パラメータ

同じ LightmapTexture を使用する描画をマージできる LightmapResourceCluster ユニフォーム バッファ内に配置されています。

プリミティブ パラメータ

GPUScene と呼ばれるシーン全体のプリミティブ データ内に配置され、PrimitiveID を使用してシェーダ内でインデックス付けされます。

GPU シーン

プリミティブ固有パラメータを持つ同じインスタンス化描画に異なるプリミティブを設定するために、サポートしているプラットフォーム (UseGPUScene) がそれらをシーン全体のバッファ (UpdateGPUScene) にアップロードして、PrimitiveId を使用してインデックスを付けます。FLocalVertexFactory の場合、PrimitiveId はインスタンス周波数の頂点入力ストリームから得られます。これはピクセル シェーダに渡す必要があります。ピクセル シェーダでは、プリミティブ ユニフォーム バッファ (Primitive.Member) に直接アクセスするのではなく、GetPrimitiveData(Parameters.PrimitiveId).Member を使用してプリミティブ シェーダ パラメータにアクセスする必要があります。

インスタンス化の効率

現在、キャッシュされたメッシュ描画コマンドしか動的インスタンス化でマージできません。そのため、動的インスタンス化は FLocalVertexFactory に限定されています。

以下のような一部のエッジケースもマージを妨げています。

  • 小さなテクスチャを生み出すライトマップ — DefaultEngine.ini で MaxLightmapRadius を調整する

  • コンポーネント毎の頂点カラー

  • SpeedTree Wind ノード

あるレベルにおける動的インスタンス化の効率を調査するには、r.MeshDrawCommands.LogDynamicInstancingStats 1 コンソール コマンドを使用して、ログの出力を調査します。

Depth Prepass および Shadow Depth パスを使用すると、可能な限り、デフォルト マテリアルのシェーダで頻繁にオーバーライドが行われるため、高いマージ効率が実現します。

メッシュ描画並列処理

メッシュ描画作業の大半は、レンダリング スレッドのクリティカル パスを避けるタスクで行われます。 レンダリング スレッド フレームの最初の InitViews で、FParallelMeshDrawCommandPass は、パスのセットアップのために 1 つのパスにつき 1 つのタスクを発行します (動的コマンド生成、ソート、描画呼び出しのマージ)。 レンダリング スレッドがフレームの処理を進めてメッシュ パス (RenderBasePass など) に到達すると、描画ディスパッチ (RHICmdList の記録) のために、システムのコア数とディスパッチする描画数に応じて、パスごとに複数の FDrawVisibleMeshCommandsAnyThreadTasks を開始します。

  • r.MeshDrawCommands.ParallelPassSetup0 に設定すると、パス セットアップ タスクが無効になり、作業がレンダリング スレッドで行われるようになります。このようにすると、デバッグに役立ちます。

  • r.RHICmdBasePassDeferredContexts0 に設定すると、ベース パス描画ディスパッチの並列処理タスクが無効になり、そのタスクがレンダリング スレッドで行われるようになります。

これらのタスクは、フレームのレンダリング スレッドで並列実行できるように、依存関係チェーンによって可能な限り早く開始されます。レンダリング スレッドは FSceneRenderer::WaitForTasksClearSnapshotsAndDeleteSceneRenderer のフレームの最後でそれらのタスクが完了するときにのみブロックします。

コンソール変数

以下に、メッシュ描画パイプラインの問題の診断に役立つコンソール変数をいくつか示します。

コンソール変数

説明

r.MeshDrawCommands.ParallelPassSetup

メッシュ描画コマンド処理タスクを切り替えます。メッシュ パスのスレッディング問題の診断に役立ちます。

r.MeshDrawCommands.UseCachedCommands

無効にすると、すべてのメッシュ描画コマンドを強制的に動的にします。キャッシュされたメッシュ描画コマンド内の古いデータの問題の診断に役立ちます。

r.MeshDrawCommands.DynamicInstancing

動的インスタンス化を切り替えます。動的インスタンス化の問題の診断に役立ちます。

r.MeshDrawCommands.LogDynamicInstancingStats

動的インスタンス化の効率の調査に役立ちます。

r.GPUScene.UploadEveryFrame

GPU シーンを強制的に毎フレーム完全に更新します。古い GPU シーン データの問題の診断に役立ちます。

r.GPUScene.ValidatePrimitiveBuffer

GPU シーンを CPU にダウンロードし、プリミティブ ユニフォーム バッファと対照してコンテンツを検証します。

Unreal Engine のドキュメントを改善するために協力をお願いします!どのような改善を望んでいるかご意見をお聞かせください。
調査に参加する
キャンセル