DirectXの話 第183

Surface Gradient-Based Bump Mapping

23/04/13 up

ノーマルのブレンド

現在のゲームのマテリアルでは、Detailマッピング、Triplanarマッピング、デカールなどでノーマルをブレンドすることが割とあります。
最も簡単なブレンド方法はウェイト値を用いた加重平均を取ることで、地形のレイヤードマテリアルなんかはこの方法を採用することが多いのではないかと思います。
草原と砂利道のマテリアルはお互いに干渉しませんが、その切り替わり部分をなめらかにするためにブレンドするといった場合はウェイト値をどのように求めるのかはともかくとして、基本的には加重平均になるでしょう。

しかし、ベースとなるノーマルマップが存在し、そこにディテールを追加するといった場合に加重平均を取ってしまうと、ベースのノーマルが失われてしまう結果になります。

これを避けるため、いくつかのDetailマッピング用のブレンド手法が存在します。
それらの手法の詳細は以下のサイトが参考になることでしょう。

https://blog.selfshadow.com/publications/blending-in-detail/

もう10年も前の記事ですが、今でも参考にしている方はいるのではないでしょうか。

このサイトで紹介されている手法はいずれもTangent Spaceでのブレンドとなります。
Tangent SpaceでブレンドしてからWorld Spaceへと変更します。

しかしTangent Space以外でもDetailマッピングやそれと似たような手法を用いたい場合があります。
可能であればどの空間においてもベースとなるノーマルに対して付け足すようなノーマルを設定できるようにしたいですよね。

そんなときに出てきたのが以下のペーパー『Surface Gradient-Based Bump Mapping Framework』です。

https://jcgt.org/published/0009/03/04/

こちらはUnity社に所属するMorten S.Mikkelsen氏によって執筆されたものです。
従来までのTBNを用いただけのノーマルマッピングアプローチは、Tangent Spaceをメッシュに対して1種類しか持たないのが普通であったり、複数の手法(Triplanarやデカール)によるブレンドに対応しづらかったりするため、より良いフレームワークが必要だということです。

このペーパーで提示されている手法は表面上の勾配としてノーマルを表現することで、ベースとなるノーマルからその勾配を線形結合してディテールやレイヤーの適用をするというものと認識しています。
まあ、正直なところ、イマイチよくわかってないのですが…

よくわかっていないのですが、ペーパーに部分部分のコード片があるため、実装は容易だったりします。
今回のサンプルではDetailマッピング、TriplanarマッピングをSurface Gradientを用いる手法で実装しています。

サンプルは前回のVisibility Bufferサンプルを拡張して実装しました。

VisibilityBufferサンプル

マウス右ドラッグでカメラ回転、WASDでカメラ移動ができます。
TriplanarはメッシュタイプがSponzaの場合にのみ配置されるため、起動時引数で"-mesh 1"を指定してください。
Triplanarを適用した球のメッシュはSponza中央の布の上部に存在します。

DetailマッピングについてはSurface GradientとUDNブレンドと呼ばれる手法を実装しています。
Triplanarマッピングはスタンダードなブレンド方法とSurface Gradientを実装しています。
手法を切り替えてどのような差が出るか試してみてください。

Detailマッピング

特に解説するようなコードはないのですが、ペーパーに提示されているコードは surface_gradient.hlsli ファイルに記述しています。
デカールなんかもやりたかったのですが、デカールのシステム作るのめんどく…ゲフンゲフン

Detailマッピングについては mesh.p.hlsl を見ていただくと、41行目くらいから処理を記述しています。
基本的にはDetailノーマルマップからTangent Spaceノーマルをサンプリング、ここから2次元のDerivativeを求めます。
DerivativeとTangent/Binormalを利用することでSurface Gradientが求まりますので、それをベースノーマルに適用します。
ベースノーマルはベースノーマルマップから取得したTangent SpaceノーマルをWorld Spaceに変換したものとなります。

UDNブレンドの実装は60行目から。こちらはTangent Spaceノーマルに対して処理を行っています。

また、Detailノーマルから求めたDerivativeを予めR8G8テクスチャに格納したらどうなるかという実験も行っています。
Detail TypeをSurface Gradient Texにするとその結果を得られますが、結果は失敗です。
Derivativeは-128~128の値になるのですが、そのほとんどは0付近の値となります。
これを8ビットUNORMに線形に格納してしまうと精度がまったく足りなくなってしまいます。
なお、16ビットUNORMの場合は特に問題がなかったのですが、それであれば通常のノーマルマップでR8G8にXYを格納しておくほうがテクスチャサイズは下がります。

以下は3種類のDetailマッピングの結果です。

UDNとSurface Gradientに違いはほとんどありません。若干影の濃さに違いがあるように見えますが、サンプルで切り替える分にはわかりません。
テクスチャを用いたものは明らかにおかしいのがわかりますね。

Triplanarマッピング

こちらはスタンダードなブレンド実装とSurface Gradientの実装を行っています。
ブレンドウェイト値についてはどちらの手法でも同様で、これもペーパーに書かれていたものを利用していますが、Powを使うだけでも良いです。

この2つは明確な違いがわかりやすく出ています。

角に当たる部分はブレンドによって複数方向が入り混じっているのはどちらも同じなのですが、明確に違うのはスタンダードブレンドは妙に扁平な形状になってしまっています。

スタンダードブレンドのコードは以下のようになります。

XYZ軸それぞれの方向のノーマルはノーマルマップから取得したものをそれぞれの軸方向に向けているだけです。
元の形状(今回は球)のノーマルはウェイト値の計算と各軸の正負に利用されていますが、主体となるノーマルはノーマルマップからのものです。
そしてこのノーマルマップは各軸に垂直に平面投影されているわけですので、ある軸についてブレンドウェイトが1.0に近い値であれば、ノーマルはほぼ平面になるというわけです。
ウェイト値を求める際にPowを利用しなければ滑らかに球状になるのですが、その分別軸のノーマルがかなりの間影響を与えてしまうのため、ブレンドされているのがまるわかりになってしまいます。

これに対してSurface Gradientは、World Spaceで生成された各軸のノーマルとウェイト値からSurface Gradientを求め、それをベースノーマル(今回は球のノーマル)に対して適用されています。
これによってベースノーマルを残しつつノーマルマップを適用できるのです。

Detailマッピングに関してはUDNブレンドでも基本的には問題ないと言えますが、Triplanarについては明らかにSurface Gradientが上です。

最後に

今回のサンプルではSurface Gradientは1レイヤーでのみ適用という形にしていますが、処理する空間が同じであれば複数を線形結合で処理できるというのが非常に大きな利点と言えます。
つまり、Triplanarマッピングの上にDetailマッピングを適用し、さらにデカールも貼り付ける、ということが可能になります。
Surface Gradientに強度を乗算することで、あとから追加するノーマルの強度も簡単に設定できます。

ただし、最終的には加算後にNormalizeが必要になるため、GBufferとしてノーマルを格納しているところにどんどん線形結合するだけでは成立しません。
GBufferのノーマルのフォーマット次第ですが、場合によってはNormalize前にバッファが飽和してしまうかもしれません。
デカールに使う場合は、デカールバッファのような一時バッファが必要になるかもしれません。

また、Detailのようにベースノーマルに対して適用するにはとても良い手法なのですが、普通にブレンドするのは難しいです。
普通にブレンドしたいデカールと、Detailのように使いたいデカールをどちらもサポートするとデカールバッファ周りの対応が難しそうです。

とは言え、技術的には比較的簡単な割に結果は良好ですので、おすすめできる手法ではないかと思います。