33
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DirectX12 Mesh ShaderとAmplification Shader を使ってみる

Posted at

Mesh Shaderとは?

最近UnrealEngine5のテクニカルデモがPlayStation5で動作する様子を初公開しました。
そこではシーン内に数十億ポリゴンが存在し、それを画面内に存在するポリゴンのみを極限まで絞り2000万ポリゴンまで減らして描画しています。

これは今までのリアルタイムレンダリングでは非常に難しいものでした。
それが次世代リアルタイムレンダリングでは可能になるのです。
これを実現するための技術の一つとしてMesh Shader 1 と呼ばれる技術を使うことで大量のポリゴンを扱うことができます。
まだ詳細な技術情報が公開されていませんが、UE5はマイクロポリゴン 2 を使用したソフトウェアラスタライザ 3 を使用しているそうです。

今回は技術的な解説は一旦置いておいて、サンプルプログラムを実行してMesh Shaderがいかなるものか検証していきたいと思います。(こちらは別途まとめたいと思います。)

UE5 PS5 Demo
Unreal Engine 5 Feature Highlights | Next-Gen Real-Time Demo Running on PlayStation 5 (日本語字幕付き)

サンプルプログラムを実行する環境構築をする

まずはハードウェアとしてNvidiaのGPUが必要です。
正式リリースされればいずれAMDのGPUでも利用可能になると思われますが、現時点ではNvidiaの開発者バージョンのドライバを利用する必要があります。

今回検証した開発環境

  • CPU
    • Ryzen 3950X
  • Memory
    • 64GB
  • GPU
    • Nvidia RTX 2060

この記事を執筆した現在では正式機能としてリリースされていません。
そのため以下のような手順を踏む必要があります。

  1. Windows 10 version 2004 (also called or 20H1) 以上をインストールする
  2. Windows Insider Preview SDK version 19582 以上をインストールする
  3. DirectX 12 GPU with compatible drivers version 450.56 以上をインストールする

Windows 10のプレビューバージョンを手に入れるには「Windows Insider Program」に登録する必要があります。
GPU DriverもNvidiaの開発者向け登録が必要です。
正式バージョンがリリースされればこれらの開発者登録は不要になります。

そのほか、VisualStudioなどの開発環境も必要ですがこれらは特別なバージョンを入手する必要はありません。
詳しくは DirectX 12 Ultimate Getting Started Guideに詳しく記載されています。

D3D12 Mesh Shader Samplesリポジトリからサンプルコードを入手する

DirectX12にはサンプルプログラムとライブラリを集めた公式リポジトリがあります。
この中にMesh Shaderのソースも公開されています。
DirectX-Graphics-Samples

現在Mesh Shaderのサンプルリポジトリが公開されています。
現時点ではDevelopブランチに属しており、正式リリースされればMasterブランチに移行されるものと思われます。
D3D12 Mesh Shader Samples

開発環境が揃ったらリポジトリをクローンするかダウンロードしてサンプルを入手しましょう。

D3D12 Mesh Shader Samplesを実行する

サンプルの詳しい構成の話などはいったん抜きにしてとりあえず実行してみましょう。
リポジトリ内のDirectX-Graphics-Samples/Samples/Desktop/D3D12MeshShaders/src/D3D12MeshShaders.slnがVisualStudioのソリューションになります。
ソリューションを開くと3つのプロジェクトがありますが、まずソリューションを右クリックして「ソリューションの再ターゲット」をクリックします。
Windows SDK バージョンが「最新のインストールされているバージョン」になっていることを確認します。
次にMeshletRendererをスタートアッププロジェクトにして準備完了です。
あとはビルドして実行するとカラフルなドラゴンが表示されます。

MeshShader_dragon.png Windowsのプレビューバージョンを使用しているため、Windows SDK バージョンが古いままなどになっているとコンパイルに失敗してしまいます。 さて次からはサンプルプロジェクトのより詳しいことについて見ていきます。

サンプルプロジェクトの構成

サンプルプロジェクトには3つのプロジェクトが含まれています。

  • MeshletGenerator
    • Mesh Shaderで利用するためのMeshletを生成するためのライブラリです。
  • MeshletRender
    • Mesh Shaderを用いてモデルを表示するDirectX12アプリケーションです。
  • WavefrontConverter
    • MeshletGeneratorを用いて.objの3Dモデルをランタイムアプリケーションで扱いやすいバイナリファイルに変換するためのコマンドラインアプリケーションです。

Amplification Shaderを使用してみる

残念ながら現在のサンプルはMesh Shaderを用いたものだけで、Mesh Shaderを大きく活用する肝であるAmplification Shaderを用いたサンプルがありません。
ドキュメントや紹介を見る限りではAmplification Shaderも利用できるはずなのでサンプルを改造してジオメトリシェーダーのようにシェーダー内でポリゴンを増やしてみようと思います。

Amplification Shaderを追加する

MeshletRenderプロジェクトのShadersフォルダにはMesh ShaderとPixel Shaderの二つが入っています。
ここにAmplification Shaderを追加します。名前はMeshletAS.hlslとしました。
次にプロパティからシェーダーコンパイル設定を行います。
シェーダーモデルを6.3に設定し、Amplification Shader用のコンパイルオプションを設定します。
「その他のオプション」に /Tas_6_5 を追加します。以下のようになりました。
MeshShader_as_settings.png

C++コードに Amplification Shaderを読み込む処理を書く

続いてC++コード内でAmplification Shaderを読み込む処理を追加します。
まずはシェーダーを読み込むための定数を既存の定義に追加します。

D3D12MeshletRender.cpp 45行目あたり
const wchar_t* D3D12MeshletRender::c_meshFilename = L"Dragon_LOD0.bin";
const wchar_t* D3D12MeshletRender::c_meshShaderFilename = L"MeshletMS.cso";
const wchar_t* D3D12MeshletRender::c_amplificationShaderFilename = L"MeshletAS.cso";// 新しく追加
const wchar_t* D3D12MeshletRender::c_pixelShaderFilename = L"MeshletPS.cso";

MeshletAS.csoを新しく追加しました。拡張子が.csoなのはシェーダーファイルがあらかじめプリコンパイルされてバイナリ化されるためです。
シェーダーはVisualStudioの設定でプロジェクトビルド時にコンパイルされます。
続いてシェーダーバイナリを読み込むコードを追加します。

D3D12MeshletRender.cpp 265行目あたり
        struct { 
            byte* data; 
            uint32_t size; 
        } meshShader, amplificationShader, pixelShader;// "amplificationShader"を新しく追加

        ReadDataFromFile(GetAssetFullPath(c_meshShaderFilename).c_str(), &meshShader.data, &meshShader.size);
        ReadDataFromFile(GetAssetFullPath(c_amplificationShaderFilename).c_str(), &amplificationShader.data, &amplificationShader.size);// 新しく追加
        ReadDataFromFile(GetAssetFullPath(c_pixelShaderFilename).c_str(), &pixelShader.data, &pixelShader.size);

amplificationShaderの定義を追加して他のシェーダーのように読み込みます。
続いてPipeline StateにAmplification Shaderを追加します。

D3D12MeshletRender.cpp 280行目あたり
        MeshShaderPsoDesc psoDesc = {};
        psoDesc.pRootSignature = m_rootSignature.Get();
        psoDesc.MS = { meshShader.data, meshShader.size };
        psoDesc.AS = { amplificationShader.data, amplificationShader.size };// 新しく追加
        psoDesc.PS = { pixelShader.data, pixelShader.size };
        psoDesc.RTFormats = rtvFormats;
        psoDesc.DepthFormat = m_depthStencil->GetDesc().Format;
        psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
        psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
        psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
        psoDesc.SampleMask = UINT_MAX;
        psoDesc.SampleDesc = DefaultSampleDesc();

Pipeline StateにAmplification Shaderを追加しました。
Mesh ShaderではAmplification Shaderのバインドは任意となっており、バインドしない場合は単にMesh Shaderが実行されるのみとなります。
上記のようにAmplification Shaderをバインドした場合はAmplification Shaderを経由してMesh Shaderが起動されます。
これでC++コードにAmplification Shaderを追加する作業は終わりです。次にAmplification Shaderの中身を書いていきます。

シェーダーの中身を書く

シェーダーの中身を書きます。まずはシンプルに1つだけ3Dモデルを描画するような実装を行います。(つまり描画結果は今までと変わらない)

MeshletAS.hlsl 
struct payloadStruct { 
    uint myArbitraryData; 
}; 
 
[numthreads(1,1,1)] 
void main(in uint3 groupID : SV_GroupID) { 
    payloadStruct p; 
    p.myArbitraryData = groupID.x; 
    DispatchMesh(1,1,1,p);
}

シェーダーの中身はこんな感じです。DispatchMeshがAmplification ShaderからMesh Shaderを発行しています。
つまりDispatchMeshの引数はC++側で呼び出していたDispatchMesh関数とほぼ同じです。
ただし、Amplification Shaderから呼び出す際は4番目の引数であるpayloadを指定する必要があります。
これはAmplification ShaderからMesh Shaderへと情報を渡すための構造体を指定します。
今回はC++側から呼ばれた際のグループIDを渡しています。

MeshletMS.hlsl 55行目あたり
struct PayloadStruct{ 
    uint myArbitraryData; 
}; 

Amplification Shaderのpayloadと同じ構造体を定義します。

MeshletMS.hlsl 125行目あたり
[RootSignature(ROOT_SIG)]
[NumThreads(128, 1, 1)]
[OutputTopology("triangle")]
void main(
    uint gtid : SV_GroupThreadID,
    //uint gid : SV_GroupID, この行を削除
    in payload PayloadStruct meshPayload, // 新しく追加
    out indices uint3 tris[126],
    out vertices VertexOut verts[64]
){
    uint gid = meshPayload.myArbitraryData;

先ほどAmplification ShaderのDispatchMesh関数で4番目に指定した引数ですが、MeshShaderの入力として受け取ることができます。
省略は不可能で、ない場合でもシェーダのコンパイルは通りますがランタイムエラーで描画されません。
payloadはグループIDとして利用するので変更前のようにgid変数に代入しておきます。

これでMesh Shader単体と同じ描画結果のAmplification Shaderが完成しました!
実行すると以前と同じ画面が出ます。
何も変わらず、つまらないですが感動はもうすぐそこです。先に進みましょう。
MeshShader_dragon.png

ドラゴンを複数体表示する

さて、基礎はできたので次はこのドラゴンを増やしてみましょう。
数を増やすにはAmplification ShaderのDispatchMesh関数引数のX / Y / Zの値を増やします。
今回は10体表示させます。

MeshletAS.hlsl 7行目あたり
[numthreads(1,1,1)] 
void main(in uint3 groupID : SV_GroupID) { 
    payloadStruct p; 
    p.myArbitraryData = groupID.x; 
    DispatchMesh(10,1,1,p);// Xを10に増やす
}

これでMesh Shaderが10回呼ばれるようになり、ドラゴンをたくさん出すことができます。
しかしこのままでは同じ場所にドラゴンが表示されてしまうので少しづつずらしてて表示してみます。

MeshletMS.hlsl 105行目あたり
VertexOut GetVertexAttributes(uint meshletIndex, uint vertexIndex, float3 offset){  // float3 offsetを追加
    Vertex v = Vertices[vertexIndex];
    v.Position += offset; // 引数の値を加算

    VertexOut vout;
    vout.PositionVS = mul(float4(v.Position, 1), Globals.WorldView).xyz;
    vout.PositionHS = mul(float4(v.Position, 1), Globals.WorldViewProj);
    vout.Normal = mul(float4(v.Normal, 0), Globals.World).xyz;
    vout.MeshletIndex = meshletIndex;

    return vout;
}

GetVertexAttributes関数にoffset引数を追加しました。頂点座標に引数分オフセットを入れるだけです。
続いてメイン関数を改造です。

MeshletMS.hlsl 125行目あたり
[RootSignature(ROOT_SIG)]
[NumThreads(128, 1, 1)]
[OutputTopology("triangle")]
void main(
    uint gtid : SV_GroupThreadID,
    uint gid : SV_GroupID, // 復活
    in payload PayloadStruct meshPayload,
    out indices uint3 tris[126],
    out vertices VertexOut verts[64]
) {
    //uint gid = meshPayload.myArbitraryData;
    uint arbitraryData = meshPayload.myArbitraryData; //gidからarbitraryDataにリネーム
    Meshlet m = Meshlets[MeshInfo.MeshletOffset + arbitraryData];

    SetMeshOutputCounts(m.VertCount, m.PrimCount);

    if (gtid < m.PrimCount) {
        tris[gtid] = GetPrimitive(m, gtid);
    }

    if (gtid < m.VertCount) {
        uint vertexIndex = GetVertexIndex(m, gtid);
        float offsetValue = 120;
        float3 positionOffset = float3(gid * offsetValue, 0 ,0); // グループIDでXが増加するオフセットを定義
        verts[gtid] = GetVertexAttributes(arbitraryData, vertexIndex, positionOffset);
    }
}

いろいろ変わってしまったので関数全体を載せます。
uint gid : SV_GroupID にはAmplification ShaderでDispatchMesh関数を呼んだ際のグループIDが返ってきます。
そのグループIDによってX座標をオフセットしているわけです。
これで完成です!ジオメトリシェーダーのようにシェーダーでポリゴン数を増やすのでC++コードの編集は必要ありません。
さっそく実行してみます。

MeshShader_dragon_many.png ドラゴンが10体になりました! ドラゴン一体につきポリゴン数が201,972なのでこれでざっと200万ポリゴンです。 UE5のテクニカルデモではシーンには数十億ポリゴン存在し、それがカリングされて2000万ポリゴンほどになっています。 なのでドラゴンを100体に増やしてみましょう。 MeshShader_dragon_many_100.png まだDebugビルドなんですが、60fpsを安定してキープしています。 ちょっと調子に乗って1000体に増やします。

MeshShader_dragon_many_1000.png
2億ポリゴンです。
これはさすがにDebugビルドでは動きませんでした。Releaseビルドです。ついでにGPUの負荷も載せてみました。
V-Syncによって30fpsになってしましたが安定しています。
さすがに頂点負荷が高すぎてGPUの性能は使い切れませんが、従来のレンダリング機能では不可能に近いことができるのは驚きです。

ほかにもいくつかモデルを試してみます。
ハッピーブッダ版 108万ポリゴン
MeshShader_dragon_many_100.png
Mayaでのプレビュー

MeshShader_buddha.png メッシュレット化された様子 MeshShader_dragon_many_100.png 100体表示 1億ポリゴン

より詳細なデバッグ方法

いつものDirectX12などを用いたグラフィックス開発であればNvidia NsightやMicrosoft Pixなどを用いてより詳しくデバッグしたり、詳細なパフォーマンスを調査するところですがまだプレビュー機能のためちゃんと動きません。
現在のPIXではキャプチャするところまでは可能でしたが、描画コマンドの詳細を見ようとするとサポートされていないAPIがあるとの旨が出て詳細を知ることはできませんでした。
いずれDirectX Ultimate対応のPIXがリリースされたときに利用できるようになるそうです。

参考資料

DirectX-Specs Mesh Shader
Introduction to Turing Mesh Shader
Announcing DirectX 12 Ultimate
Coming to DirectX 12— Mesh Shaders and Amplification Shaders: Reinventing the Geometry Pipeline
Geometry Reinvented with Mesh Shading
SIGGRAPH 2018: Turing - Mesh Shaders

  1. PlayStation5では同様の機能がPrimitive Shaderと呼ばれます。

  2. 描画するポリゴンを1ピクセル未満の小さなポリゴンにして描画する方法。

  3. 通常はGPUの固定機能として実行されるものをコンピュートシェーダーなどを用いて自前で実装する方法。

33
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?