DirectXの話 第160回

AnyHitShaderによるアルファテスト

19/04/29 up

平成最後の更新はAnyHitShaderを利用したアルファテストとその他いくつかについて書いておこうと思います。

なお、今回は新しいサンプルは作成しておらず、Sample011を修正する形になっています。

GitHub

AnyHitShaderを使ってアルファテストをする

DirectX Raytracingのシェーダはシェーダタイプとして5つのタイプが存在します。

ここまで使っていたのは RayGenerationShader、MissShader、IntersectionShader、ClosestHitShader の4つです。

この中で IntersectionShader と ClosestHitShader の2つは Hit Group というグループでまとめて使用していました。

このグループは RayGenerationShader で生成されて発射されたレイがヒットしたか、何にヒットしたか、ヒットした際にどういう処理を行うかをまとめたものです。

実はこの Hit Group にはもう1つのシェーダを登録することが出来、これが AnyHitShader です。

IntersectionShader は通常形状との衝突を検出するシェーダで、未登録の場合はトライアングルとレイの衝突を検出するシェーダが自動で設定されます。

普通の三角ポリゴンとの衝突判定ということなので、普通にメッシュを処理する場合は IntersectionShader を設定しません。

このシェーダではポリゴンとの衝突を判定しますが、ポリゴンと衝突したからその瞬間に ClosestHitShader を起動して衝突検出後処理を行っていいかというとそういうわけでもない場合があります。

それはアルファテストを行う場合です。

上の画像を見てもらえるとわかると思いますが、草などのメッシュは板ポリゴンに草や葉の画像を適用し、その形状にアルファ値を設定、0の部分は描画しないというのが基本になります。

この場合、ポリゴンとの衝突判定を検出しただけでは不十分で、ポリゴンのUVに応じてアルファ値をチェックしなければならないわけです。

これを実現する方法として IntersectionShader でアルファテストを行う方法もあるかもしれませんが、大量に発生しやすい衝突検出処理で毎回毎回アルファ値をテクスチャから呼び出すのはコストが掛かりすぎます。

また、ClosestHitShader でアルファテストを行い、抜き状態であればレイをそのまま進行させるという手段もあるでしょう。

この場合はレイトレースがどんどんネスト化していく可能性が高くなり、やはり現実的ではありません。

そこで登場するのが AnyHitShader です。

AnyHitShader では Payload の更新も行えますが RepotHit() 命令、TraceRay() 命令を呼び出すことは出来ません。

しかし、AnyHitShader でのみ IgnoreHit() 命令を使用することが出来ます。

その名の通り、衝突を無視する命令で、この命令が発行された場合は IntersectionShader による衝突検出を無効化します。

無効化されるのでレイはそのまま直進を続け、ClosestHitShader は呼ばれません。

つまり、AnyHitShader 内でアルファテストを行い、抜きと判断されたら IgnoreHit() 命令を呼び出せばアルファテストが実現できるというわけです。

というわけで実際の AnyHitShader のコードは以下です。

衝突したプリミティブインデックスからUV座標を求め、テクスチャをサンプリングし、そのアルファ値をチェックして IgnoreHit() 命令を呼び出しているだけです。

なお、シェーダ関数の引数は ClosestHitShader と同じです。

これで上の画像のように草などのアルファテストがキチンと行われます。

以前の画像と比べてもらえればわかると思いますが、以前はアルファテストが行われていませんでした。

レイのバウンス回数を自在に制御する

以前の記事にも書きましたが、DXRではレイの再帰回数は最大数が決まっていて、その最大数以上に再起させようとするとGPUがクラッシュしたりしました。

この再帰最大回数はパイプラインステートを作成する際に D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG というサブオブジェクトのパラメータとして設定する必要がありました。

この値は 0~31 の値を設定する必要があり、つまり再帰回数はどんなに頑張っても最大31回ということになります。

再帰回数とはそもそも何を担っているのかというと、パストレーサーの場合は基本的にレイのバウンス回数です。

レイがシーンに衝突した場合、そこからさらにレイを飛ばして、そこからさらに…という感じでレイトレースを行います。

バウンス回数が増えれば間接光の影響は複雑になっていきますが、その分映像のリアリティは増します。

ただしバウンス回数が増えればその分速度が犠牲になるので、映像クオリティと速度のトレードオフを行う1つの要素がバウンス回数です。

正直な話、31回も再帰ができれば十分といえば十分なのですが、どうしてもクオリティを高くしたいという場合はそれ以上のバウンスがほしい場面もあるかと思います。

そのような場合、APIの仕様で31回までで抑えられてしまうのは困りものです。

これに1つの回答を見せたのが Unreal Engine 4.22 のパストレーサーの実装で、今回はそちらを参考にさせてもらっています。

バウンス処理を再帰する理由はレイトレースがカメラ側から行われるからです。

カメラから見えるあるポイントにどのような光が入ってくるうかを逆に追跡していくわけなので、結果を計算するのに以前の結果が必要になり、さらにその以前の結果を求めるのに更に前の結果が必要になり…という感じになります。

このような処理は再帰が最も扱いやすい処理方法なのですが、大抵の再帰処理はループで書き直すことが可能です。

そしてそれは簡単なパストレーサーも例外ではありません。

再帰をループで処理する場合、ループの大本は RayGenerationShader に置きます。

ここから TraceRay() でレイトレースしますが、ClosestHitShader ではライティング計算は行いません。

ClosestHitShader では衝突したマテリアルの情報だけを返し、ライティング計算は RayGenerationShader で行います。

以下がその実装例です。

RayGenerationShader 内にループが存在し、その内部で TraceRay() を2回行っています。

1回目はマテリアル情報の取得に用いられていて、ここでミスしていない場合はシャドウ検出用の TraceRay() 命令を発行、それらの結果からライティングを行い、衝突点から反射したレイを次にトレースするレイとして設定するという感じです。

ループ回数はシーン情報として渡されていて、GUIで変更可能なようにしています。

回数が増えるとあからさまに実行速度が増えていくので楽しいですね!

なお、ライティング計算部分ですが、このような計算になる理由はまあ、前回までのプログラムを数式に展開すればわかるかと。

ClosestHitShader の方はマテリアル情報を取得するだけなので特に提示はしません。

気になる方はサンプルをご覧ください。

ClosestHitShaderを使わないレイトレース

ClosestHitShader はマテリアル情報の取得やライティング計算に用いられますが、そもそも不要な場合が存在します。

それは遮蔽情報を求める場合で、遮蔽されているかいないかの 0/1 の情報がほしいだけなら Payload の値をデフォルト 0 にして、MissShaderに入ってきたら 1 にするという形でも対応できるわけです。

遮蔽情報を求める場合とは、シャドウやAOのチェックということになります。

このような場合、TraceRay() の第2引数のフラグに RAY_FLAG_SKIP_CLOSEST_HIT_SHADER フラグを利用することをおすすめします。

以前のサンプルを作った時には知らなかったのですが、このフラグを立てることである程度高速化が見込める場合があるようです。

上のコードでもシャドウ用のレイトレース時にこのフラグを使っています。

特にハイブリッドレンダリングでAOやシャドウを行う場合は使っておくとよいかと思います。

これから

これからのレイトレースネタとしてはライトマッパーを作りたいなと思ったりしています。

まずは簡単に頂点単位で作成し、その後はテクスチャ作成に行きたいなと。

ハイブリッドレンダリングでも現実的な落とし所を試してみたいので、やることはいっぱいありますね!

令和になってもよろしくお願いします!