このページに含まれる情報は、カスタム メッシュ パスを追加するプログラマーや、Unreal Engine のメッシュ描画パフォーマンス特性を理解する必要のあるプログラマーを対象としています。
メッシュ描画パイプライン は、リテインド モードの概念に基づいています。このモードでは、すべてのシーン描画を、毎フレーム作成するのではなく、事前に準備します。あまり変化しない、複数のフレームで再利用できるスタティック メッシュのプロパティを活用するために、積極的キャッシュと描画呼び出しマージも備えています。

描画の過程。
メッシュ描画は FPrimitiveSceneProxy
から始まります。これは、ゲーム スレッドの UPrimitiveComponent
のレンダリング スレッド表現です。FPrimitiveSceneProxy
は、GetDynamicMeshElements
と DrawStaticElements
のコールバックを通じて FMeshBatch をレンダラに送信する役割を担います。
FMeshBatch
は FPrimitiveSceneProxy
実装 (ユーザー コード) をメッシュ パス (プライベート レンダラ モジュール) から切り離します。これには、パスが最終的なシェーダ バインディングとレンダリング ステートを把握するために必要な情報がすべて含まれているため、レンダリングされるパスをプロキシが認識することはありません。
次のステップは、FMeshBatch
をメッシュ パス固有の FMeshDrawCommand
に変換することです。FMeshDrawCommand
は FMeshBatch
と RHI の間のインターフェースです。これは、完全にステートレスな描画記述であり、RHI がメッシュ描画について知る必要のあるすべての情報が格納されています。
-
使用するシェーダ
-
リソース バインディング
-
描画呼び出しパラメータ
これにより、RHI レベルのすぐ上で描画呼び出しのキャッシュとマージが可能となります。FMeshDrawCommand
はメッシュ パス固有の FMeshPassProcessor
により FMeshBatch
から作成されます。
最後に、SubmitMeshDrawCommands
を使用して FMeshDrawCommand
が、RHICommandList 上に設定される一連の RHI コマンドに変換されます。
キャッシュされたメッシュ バッチとダイナミック メッシュ バッチ
FPrimitiveSceneProxy
には、FMeshBatches
を生成するためのパスが 2 つあります。キャッシュされたパスとダイナミック パスです。FPrimitiveSceneProxy
の実装は、GetViewRelevance()
関数を通じて各フレームで使用するパスを制御します。

FMeshBatch コード パス。オレンジの矢印は毎フレーム行う必要のある操作を表し、青の矢印はキャッシュ前に 1 回行う必要のある操作を示しています。
キャッシュされたパスは、FMeshBatch
を構築、再利用します。スタティック メッシュなどの毎フレーム変化するわけではない描画を迅速にレンダリングする場合にお勧めです。DrawStaticElements
によって実装されており、これはプロキシがシーンに追加されるときに呼び出されます。作成された FMeshBatches
は FPrimitiveSceneInfo::StaticMeshes
内に格納され、プロキシがシーンから削除されるまですべてのフレームで再利用されます。
ダイナミック パスでは FMeshBatch
をフレームごとに再作成します。これは最も柔軟なパスであり、フレームごとに変化することが多いパーティクルなどの描画に使用されます。GetDynamicMeshElements
によって実装されています。この関数は毎フレーム InitViews から呼び出され、すべてのビューに一時的な FMeshBatch
を作成します。
FMeshPassProcessor
具体的なパス メッシュ プロセッサは FMeshPassProcessor
基本クラスから派生し、FMeshBatch
を所定のパスに対するメッシュ描画コマンドに変換する役割を担います。ここで、最終的な描画フィルタリング、適切なシェーダの選択、シェーダ バインディングの収集が行われます。
カスタム メッシュ パス プロセッサを作成するために、FMeshPassProcessor
から派生している必要があります。また、AddMeshBatch
関数をオーバーライドする必要があります。
AddMeshBatch
は以下を実装しています。
-
描画フィルタリング - マテリアルに透過描画モードがあり、そのマテリアルを
FDepthPassMeshProcessor
で処理しない場合など -
シェーダとパイプラインのステート (深度/ステンシル/ブレンド ステート) を選択する
-
最終的に
BuildMeshDrawCommands()
の呼び出しを行い、パス/マテリアル/頂点ファクトリ/ プリミティブのシェーダ バインディングを収集し、適切なリストに新しい描画コマンドを追加する
シェーダ バインディング
Unreal Engine のシェーダ バインディングは、ユニフォーム バッファ、サンプラー、テクスチャ、ShaderResourceView、または緩いパラメータ (FShaderParameter
) のいずれかになります。
FMeshPassProcessor
は RHICmdList.SetShaderParameter
を使用してシェーダ バインディングを直接 RHI には送信せず、単に FMeshDrawSingleShaderBindings
クラスに記録します。すべてのパス間の共有コードである BuildMeshDrawCommands()
関数は、パス シェーダ上で GetShaderBindings()
を呼び出します。
シェーダ バインディングは次のいくつかのカテゴリのいずれかに該当します。
-
ViewUniformBuffer
やDepthPassUniformBuffer
などのパス定数ユニフォーム バッファ -
頂点ファクトリ バインディング
-
マテリアル バインディング
-
プリミティブ バインディング
-
描画ごとに変化するパス固有のバインディング
描画ごとに異なるバインディングを設定すると、描画呼び出しのマージは行われません。緩いパラメータ (ユニフォーム バッファに含まれないシェーダ パラメータ) を設定する場合も描画呼び出しのマージは行われず、描画間で低速の定数バッファ更新が強制されます。
各 FMeshPassProcessor
は、BuildMeshDrawCommands()
を実行してパス シェーダの GetShaderBindings()
を呼び出す必要があるので、FMeshPassProcessor
から GetShaderBindings()
呼び出しに任意のデータを渡すメカニズムが必要です。これは、BuildMeshDrawCommands()
の ShaderElementData
パラメータによって実現できます。
FMeshDrawCommand のパフォーマンス上の危険性
FMeshDrawCommand
では、余分なヒープ割り当てを行うことなく可変長の配列を格納するために、数多くのインライン アロケータが使用されています。これらがオーバーフローすると、コマンドのトラバースのキャッシュ ミスと合わせて各メッシュ描画コマンドがヒープ割り当てを構築/破棄/コピーしなければならないため、パフォーマンス上の危険が発生します。
FMeshDrawShaderBindings
は 2 個のシェーダ周波数 (頂点 + ピクセル) を前提としています。
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 が割り当てられます。これにより、FScene が AddToScene 時にパスのメッシュ描画コマンドをキャッシュできるようになります。FMeshPassProcessor は FRegisterPassProcessorCreateFunction によって列挙型に登録される必要があります。パスのセットアップとディスパッチはタスクで行われます。 |
手動パス | 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 内でセットアップできる場合だけです。

メッシュ描画コマンドのキャッシュ パス。オレンジの矢印は毎フレーム行う必要のある操作を表し、青の矢印は 1 回行ってキャッシュする操作を示しています。
キャッシュされたメッシュ描画コマンドをサポートするためには以下の要件があります。
-
パスは
EMeshPass::Type
のエントリを使用する必要がある。 -
カスタム メッシュ プロセッサを登録する際に
EMeshPassFlags::CachedMeshCommands
フラグを渡す必要がある。 -
メッシュ パス プロセッサは、キャッシュ中に null になるので、
FSceneView
に依存することなくすべてのシェーダ バインディングをセットアップできる必要がある。
シェーダがキャッシュされたメッシュ描画コマンドを使用してフレームごとのデータにアクセスできるようにするために、シーン全体のユニフォーム バッファ (FScene::UniformBuffers
参照) をバインドしてから、RHIUpdateUniformBuffer
を使用して描画前にそのコンテンツを変更します。
現在キャッシュできるのは FLocalVertexFactory (UStaticMeshComponent)
だけです。それ以外の頂点ファクトリはすべて、シェーダ バインディングをセットアップするためのビューが必要なためです。
キャッシュの無効化
メッシュ パス プロセッサが AddMeshBatch
で読み取るデータは、キャッシュされたメッシュ描画コマンドの依存関係です。この依存関係が変化したら、キャッシュされたコマンドは無効化されなければなりません。単一プリミティブのキャッシュされたコマンドは、FPrimitiveSceneInfo::BeginDeferredUpdateStaticMeshes
を使用して無効にできます。シーン全体のキャッシュされたコマンドは、Scene->bScenesPrimitivesNeedStaticMeshElementUpdate
を true
に設定することで無効にできます。これは大掛かりな操作であり、大規模なシーンで処理落ちを引き起こすため、ゲームプレイ中には避ける必要があります。
たとえば、FBasePassMeshProcessor::AddMeshBatch
は Scene->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.ParallelPassSetup を 0 に設定すると、パス セットアップ タスクが無効になり、作業がレンダリング スレッドで行われるようになります。このようにすると、デバッグに役立ちます。
-
r.RHICmdBasePassDeferredContexts を 0 に設定すると、ベース パス描画ディスパッチの並列処理タスクが無効になり、そのタスクがレンダリング スレッドで行われるようになります。
これらのタスクは、フレームのレンダリング スレッドで並列実行できるように、依存関係チェーンによって可能な限り早く開始されます。レンダリング スレッドは FSceneRenderer::WaitForTasksClearSnapshotsAndDeleteSceneRenderer
のフレームの最後でそれらのタスクが完了するときにのみブロックします。
コンソール変数
以下に、メッシュ描画パイプラインの問題の診断に役立つコンソール変数をいくつか示します。
コンソール変数 | 説明 |
---|---|
r.MeshDrawCommands.ParallelPassSetup |
メッシュ描画コマンド処理タスクを切り替えます。メッシュ パスのスレッディング問題の診断に役立ちます。 |
r.MeshDrawCommands.UseCachedCommands |
無効にすると、すべてのメッシュ描画コマンドを強制的に動的にします。キャッシュされたメッシュ描画コマンド内の古いデータの問題の診断に役立ちます。 |
r.MeshDrawCommands.DynamicInstancing |
動的インスタンス化を切り替えます。動的インスタンス化の問題の診断に役立ちます。 |
r.MeshDrawCommands.LogDynamicInstancingStats |
動的インスタンス化の効率の調査に役立ちます。 |
r.GPUScene.UploadEveryFrame |
GPU シーンを強制的に毎フレーム完全に更新します。古い GPU シーン データの問題の診断に役立ちます。 |
r.GPUScene.ValidatePrimitiveBuffer |
GPU シーンを CPU にダウンロードし、プリミティブ ユニフォーム バッファと対照してコンテンツを検証します。 |