DirectXの話 第158回

DXRでglTF描画

19/02/03 up

はじめに

今回はモデルアセット用のフォーマット、glTFを使ってDXRでレンダリングするところまでやってみました。

glTFはKronos Groupが策定しているフォーマットで、USDやFBXと似たようなものですが、基本はリアルタイムレンダリング用のフォーマットです。

特に頂点情報が1つにまとまっている、つまり属性ごとにインデックスを持つようなフォーマットではないため、そのままレンダリングAPIに流しやすくなっています。

また、取り扱うためのライブラリがそれなりにあって、Windowsで使う分にはMicrosoftが提供している glTF-SDK が扱いやすいかと思います。

NuGetでも提供されているため、使うだけならほんとに簡単なのでおすすめです。

今回のサンプルもいつもどおりにGitHubにアップ済みです。

Sample011が対象のサンプルとなります。

D3D12Samples

今回やったこと

    • glTFフォーマットのバイナリ形式 (.glb) ファイルを読み込み、メッシュとテクスチャを利用する

    • 複数のSubmeshが存在するメッシュを読み込み、その情報からシェーダテーブル等を生成する

    • 直接光と間接光を計算する

    • 直接光はシャドウ計算あり

.glbファイルの読み込み

ここについては難しいことはありません。

必要なプロジェクトに glTF-SDK のNuGetパッケージをインストールし、そのライブラリを使用するだけです。

今回は.glbファイルをSubstance Painterで用意しました。

そのため、上記画像の sponza は一旦Substance Painterで読み込んでマテリアル等も軽くですが設定したものです。

.glbファイルはその1ファイルの中にメッシュデータとテクスチャデータを持っているので、テクスチャデータもそのまま利用しています。

テクスチャデータは .png 形式で保存されているので、これについては DirectXTex を利用して読み込んでいます。

頂点情報は読み込んだそのままを利用しています。特に最適化は行っていません。

テクスチャも特にこれと言った処理は行っていません。ミップマップもなしです。

複数のSubmeshが存在するメッシュの描画

実のところ、ここが一番面倒でした。しかし、一度出来てしまえば問題なし。

Submeshはメッシュごとにある小さなメッシュの単位で、Submesh1つに1つのマテリアルが割り当てられているものです。

これをSubmeshと呼ばない方もいらっしゃるかと思いますが、私はSubmeshと呼んでいますので以降もその名前で進めます。

DXRにおいてSubmeshはBottomASに設定されるもので、D3D12_RAYTRACING_GEOMETRY_DESC 構造体1つがSubmeshとなります。

DXRではSubmeshには0からの連番が自動的に割り当てられ、シェーダテーブルを参照する際の情報となります。

これについては 第153回 で解説していますので、詳しくはそちらをご覧ください。

今回サンプルで描画している sponza のメッシュはMSのDXRサンプルでも描画されています。

ただ、Fallbackレイヤーで実装されていたそのサンプルでは、Submeshすべての頂点情報を1つにまとめてシェーダ側に送られていました。

テクスチャも確か配列になっていたと思いますが、私のサンプルではローカルルートシグネチャを利用して各情報を渡すようにしています。

実際のコードを見てみましょう。

まずはBottomAS生成時の処理です。

メッシュのSubmesh数分だけGeometryDescを設定し、それを1つのBottomASとして作成しています。

先程も書いたとおり、GeometryDescの数だけ連番のインデックスが内部的に割り当てられます。

これを利用することでマテリアルの種別を設定することも可能ですが、今回はやっていません。

次はシェーダテーブル生成命令です。

シェーダテーブルは RayGenShader や MissShader にも必要ですが、上の例は HitGroup 用のものだけです。

シェーダテーブルはローカルルートシグネチャに設定するリソース等とシェーダID(使用するシェーダ、ヒットグループのID)を設定します。

今回、シェーダID自体はすべてSubmeshで同じHitGroupを使用していますが、Submeshごとの頂点情報、テクスチャなどはSubmeshごととなります。

ShaderIDが1つのSubmeshにつき2つ存在している理由は後述します。

直接光と間接光の計算

シェーダを見ていただければわかるかと思いますが、ライトは平行光源1つとスカイライト1つです。

平行光源は直接光、スカイライトは間接光として利用されています。

もちろん、平行光源の結果による間接光も存在しています。

バウンス数は8バウンスとなっています。30バウンスしたら流石に重かったので。

ピクセルごとのサンプリング数は最大512sppで、1フレームに1サンプルとなります。

収束にそれなりに時間はかかりますが、さほど遅いということもないでしょう。

ライティング計算は特に面白いことをしていないので割愛。

PBRとかそういうのは考えてません。

直接光のシャドウ

今回、直接校にはシャドウ計算を入れています。

バウンス先でもシャドウ計算はしっかり行って、平行光源の影響の有無をチェックしています。

まず、シャドウを実装するためにシャドウ用のHitGroupを作成しています。

ClosestHitShader、MissShaderはシャドウ用は別です。

シャドウ用のレイトレ時にMissShaderに入ってくるということは遮蔽されていないということになります。

そのため、シャドウ用のMissShaderはカラー1.0(つまり白)を返します。

逆にClosestHitShaderは黒を返します。

HitGroupのシェーダテーブルは各Submeshにつき2つ存在していて、1つはライティング用のHitGroup、もう1つはシャドウ用のHitGroupとなります。

シェーダテーブルの配列は、Submesh0のライティングHG・Submesh0のシャドウHG・Submesh1のライティングHG・Submesh1のシャドウHG…という具合になっています。

このような配列になっている場合、TraceRay() 命令でどのような指定をすれば正しくなるかというと

// RayGenShader、およびバウンス時 TraceRay(Scene, RAY_FLAG_CULL_BACK_FACING_TRIANGLES, ~0, 0, 2, 0, ray, payload);// シャドウ TraceRay(Scene, RAY_FLAG_CULL_BACK_FACING_TRIANGLES, ~0, 1, 2, 1, shadow_ray, shadow_payload);

注目は第4, 5引数です。

HitGroupの選定はSubmeshのインデックスと第5引数の乗算に第4引数が加算される形になります。

そのため、Submeshごとに2つのHitGroupが存在するので、第5引数は常に2となります。

通常のライティング計算時は第4引数は0で、これは各Submesh用のHitGroupの0番がライティング用、1番が者同様だからですね。

シェーダテーブルにShaderIDが2つあったのもこれが原因です。

また、第6引数はMissShaderのインデックスですので、ライティング用は0番を、シャドウ用は1番を選ぶようになっています。

ちなみに私、最初はテーブルの持ち方や各引数を間違えてしまって、シャドウ計算なのにライティング計算してしまって痛い目を見ました。

みなさんもお気をつけください。

最後に

今回のサンプルは sponza のメッシュではありますが、メッシュが変わっても正しく動作することを証明するために他のメッシュも用意しています。

main.cpp の最初の方に USE_MEET_MAT という定義がコメントアウトされていますが、このコメントアウトを外すと別のメッシュが表示されます。

Substance PainterのMeetMatさんですね。

これ以外でも.glbなら読み込んでくれるはずですが、巨大なシーンの場合は処理速度面でかなり問題が出るのではないかと思います。

次回はラスタライザとの連携をやってみたいなぁと思っています。

ライティングはラスタライザで、シャドウとAOはDXRで、って感じの予定。