DirectXの話 第167回
DXRで生成するSigned Distance Field
19/12/22 up
今回はこちらのお猿さん(スザンヌさん)に犠牲になってもらって”Signed Distance Field”に挑戦してみました。
サンプルはいつもどおりにGitHubにアップしてあります。
Sample019が対象となるサンプルです。
サンプルではカメラを動かすことも可能ですし、SDF Resoを変更してApplyボタンを押せば解像度を変更した SDF テクスチャを作り直せます。
テクスチャ解像度による生成速度の違いとレンダリングクオリティの違いを確認してみてください。
Signed Distance Fieldとは?
Signed Distance Field は日本語に訳すなら”符号付き距離場”というのが妥当なのでしょうか?
その名の通り、符号が付いた距離を示した場、です。
通常は SDF という省略が用いられますが、”Signed Distance Function”も SDF と省略されるのでちょっとわかりにくいかも。
この記事では以降、Signed Distance Field を SDF と省略して呼びます。
この場(というか空間?)は距離で満たされている場であると考えてください。
距離と言っても何の距離かって話ですが、基本は場にある物体との距離です。
例えば下図のように空間に複数の物体(図では円2つと角丸のボックス)があるとすると、任意の点を設定した際にこの場から得られるものは最も近い場所にある物体との距離です。
図の Pa を選択すれば la の距離が、Pb を選択すれば lb の距離が得られます。
距離は物体の内部と外部を分けています。
通常は物体内部を正、物体外部を負とするものですが、今回は逆にしてます。
特に意味はありませんが、プロファイラでボリュームテクスチャを見た場合にこちらの方がわかりやすいかな?という程度の理由だったりします。
さて、この SDF は何に使われるかというと、レイトレーシング手法の1つであるレイマーチで使われることが多いです。
レイマーチは始点と方向を持つレイを飛ばすという点では通常のレイトレーシングと同じですが、コリジョン判定のとり方が独特です。
通常は物体とレイのコリジョンは数式で解いていくのですが、レイマーチの場合は SDF から取得された距離を利用して複数回のレイ移動によってレイトレーシングを実現します。
使い方は非常に簡単で、レイの始点で距離を検索→その距離分だけレイを方向の向きに移動、というこれを繰り返すだけです。
SDF があればレイの始点と距離は簡単に求められるので、その分だけ移動させるわけです。
SDF から得られる距離は最も近い距離ですので、少なくともこの距離分だけ移動させても絶対に物体内部に入らないはずです。最接近距離なので当たり前ですが。
そして移動した先でも同じように求めて移動させればやっぱり物体内部に入らないわけで、これを複数回繰り返すことで物体にかなり近い部分まで接近できます。
無限に繰り返せば理論上ではほぼ物体表面と言っていい部分まで近づいているはずです。
図は示しませんが、SDF やレイマーチについて知らない方はググってみてください。
いっぱい出てきますし、概念的にもわかりやすいです。
ボリュームテクスチャに距離情報を格納する
SDF を利用する場合、多くの場面では符号付き距離関数(Signed Distance Function)を用います。
この関数は関数1つで距離を表すことができるもので、引数は座標、戻り値は符号付き距離となります。
この関数は関数で表せるもの(球やボックスなどの単純形状やそれらを組み合わせたもの)を表現するのにはとても向いてますし、うまくやればそれらの単純形状を組み合わせて任意のモデルを作成することも出来ます。
三角ポリゴンとの距離も求められるので、任意のポリゴンモデルを作成することだって問題はないです。
しかし、任意のポリゴンモデルとの距離を毎フレーム、各ピクセルで処理するというのは流石にナンセンスです。
うまく空間分割できていたとしても、それならわざわざ SDF を使わずに普通にレイとポリゴンの衝突判定を取ったほうが高速でしょう。
そこで登場するのがボリュームテクスチャ、いわゆる3Dテクスチャです。
2Dテクスチャを利用した SDF としてはフォントやUIなどで解像度が変わってもなめらかに描画したいものによく使われますが、これを3Dに拡張したものです。
2Dでは画像に対してしか使えませんでしたが、3Dにすればモデルも対応できるよね!ということです。
なお、2Dの SDF を用いたフォント描画なんかはヘキサドライブさんの記事がわかりやすいと思います。
https://hexadrive.jp/lab/demo/610/
現在のUIフォントはほとんどこの技術を使ってるんじゃないでしょうか?
PCゲームやスマホゲーム、各種コンソールでも複数解像度が当たり前になってきていて固定解像度のみでフォントを利用するという機会はほとんどなくなってしまっていますので。
ただし、ボリュームテクスチャで作成する SDF はそこまできれいに形状は出ません。
特に薄いもの、細いものは解像度によっては消えてしまいがちですので、使用する場合は注意しましょう。
SDFテクスチャの作り方
まずボリュームテクスチャですが、これはメッシュに対して1つ、オフラインで生成します。
今回はオンラインでメッシュを読み込んだ後に生成するようにしていますが、リアルタイムには生成していません。
そのため、変形するモデル(スケルタルメッシュやワールド位置変異など)には対応していません。
ボリュームテクスチャはメッシュを内包する立方体のAABBと同じサイズに対して作成されます。
正確にはこのAABBに適当な係数をかけて膨らませています。
解像度も立方体なので3辺とも同一の解像度です。
SDF テクスチャを作るのに使用したのは DirectX Raytracing です。
ボリュームテクスチャの1つのVoxelに対して、Voxel中心から複数のレイを飛ばして衝突判定を取り、最接近距離を求めています。
最接近距離ですので絶対値の最小値を利用し、その距離を1~-1の値になるように正規化します。
あるAABB内で最大の距離は対角線の距離になりますが、面倒だったのでAABBの1辺の長さの3倍を今回は用いています。
Ray Generation Shaderは以下のとおりです。
TraceRay() でレイトレーシングして、ヒットしたら最接近距離に更新するだけです。
Closest Hit Shader や Miss Shader については距離を格納するくらいしか行ってないので割愛。
今回 DXR を利用して SDF を生成した理由としては、Maskedマテリアルに対応するというのが1つの理由です。
今回のサンプルでは不要だったので対応していませんが、ゲームではMaskedマテリアルは結構いろいろなところで使用されます。
例えば木や草などで使用された場合、四角ポリゴンにテクスチャが貼られただけ、というのもよくあります。
これらの SDF を作成した場合、単純なポリゴンとの距離だとMaskedなテクスチャが考慮されず、レイマーチの結果として四角ポリゴンが生まれてしまいます。
これは困るのでレイトレを使っています。
なお、同様の手法として、UE4ではオフラインで Intel Embree を用いて実装されています。
Embree なしでの実装もありますが、興味がある方はソースコード読んでみてもいいかもしれません。
レイマーチの仕方
割愛!
まあ、特に難しいことはしてません。
強いて言うならAABB外の点との距離はまずAABB上の最接近点を求めて、ここからボリュームテクスチャを参照して距離を求め、最接近点までの距離と加算するという手法を用いています。
この手法はあまり効率はよくありません。
もし1つのメッシュに対してレイマーチを行うのであれば、一旦バウンディングボックスとのレイトレーシングを行い、接触した点からレイマーチを行うべきです。
が、今回は2つのメッシュでメタボールっぽい表現をやりたかったのでこの手法を採用しました。
SDFを何に使うか?
SDF を何に使うべきかはUE4の実装がわかりやすいのではないかと思います。
UE4 では SDF はメッシュ単位でオフラインで生成され、レベル内では Global Distance Field という形でカメラ周辺にグローバルな SDF が生成されます。
Global Distance Field はマテリアルなんかで使用できるようですが、近距離、ローカル、そこまで正確性を求めないもので使用されています。
更新にはメッシュ単位の SDF ボリュームテクスチャが用いられているようです。
メッシュ単位の SDF ボリュームテクスチャは Global Distance Field の更新の他にも Distance Field Shadow にも用いられています。
通常、ゲームのシャドウには Cascade Shadow Map が使用されますが、この手法はカメラの近くに対してはきれいなシャドウを出せますが、遠距離ではシャドウの解像度が下がり、安定性も下がってしまうのでチラツキやアーティファクトの原因になります。
そのため、多くのゲームでは一定距離以上のシャドウを消すなどの手法と用いていたりしますが、オープンワールドで広い範囲を描画するゲームの場合はこれが結構気になります。
UE4 ではこれに対応する手法として、一定以上の距離ではメッシュ単位の SDF テクスチャを用いてレイマーチによってシャドウを生成しています。
実装方法としては、まずメッシュ SDF テクスチャを1つのボリュームテクスチャにアトラス化して格納します。
そしてそのアトラス情報(どのメッシュがどの部分に格納されているかなど)を構造化バッファに格納します。
DFシャドウを使う場合はこの情報とメッシュのインスタンス情報からスクリーンのタイル単位でバウンディングボリュームのカリングを行います。
そして各タイルの各ピクセルごとに対応する SDF テクスチャを使ってレイマーチし、衝突しているかを検出しています。
DFシャドウは遠距離で使われることが前提なので SDF テクスチャはそこまで大きな解像度を必要としません。
大まかな形状さえ取れれば十分ですし、その割に遮蔽情報は安定しているのでシャドウマップ特有のアーティファクトが発生しないという利点があります。
ただ、配置されているメッシュインスタンスが多いとその分処理コストが上がりますし、当然 SDF テクスチャの解像度が大きすぎるのも問題です。
特に SDF テクスチャの解像度はある程度調整できるので、何でもかんでも正確なシャドウが欲しい!とかいって無闇に解像度を上げると痛い目を見るので注意が必要です。
また、DFシャドウの場合はシャドウマップと違って影が動きません。
草木が風で揺れていてもDFシャドウは動かないので注意です。
とはいえ、十分離れていれば動かなくても気になりませんがね。
まとめ
・SDF テクスチャの生成は簡単
DXR ならテクスチャも簡単に対応できるが、パイプライン生成がちょっと面倒
テクスチャに対応できているなら Intel Embree を使うほうがいいが、割とどちらでも構わない
・レイマーチも簡単
サンプルより高速な実装は可能だし、使いみちは結構あると思う
・SDF テクスチャ作成には時間がかかる
DXR でもテクスチャ解像度が高いと結構時間がかかる
オフラインでやるのが吉
・データサイズが不安
高解像度の SDF テクスチャは避けないと、オフラインで生成していてもロードに時間がかかるので注意
もちろんメモリ容量的にもキツイ