ゲームエフェクトデザイナーのブログ | A Real-Time VFX Artist's Blog

About Making Materials on UE, Making Tools with C#, etc

C# ハイトマップからノーマルマップを生成してみた

C#による画像処理の練習がてら、ハイトマップからノーマルマップの生成を試してみました。
プルダウンで選んでいる数字が高いほど法線の傾斜が緩くなっています。

f:id:moko_03_25:20190107013038g:plain

こちらをざっくりと解説してみます。

まずRチャンネルの法線を得る方法について。

① 考え方としては、法線を得たい今のピクセルを原点として、左に隣接するピクセルと右に隣接するピクセルのベクトル(座標)を最初に決めます。

Xは左ピクセルなら「-1」、右ピクセルなら「1」とします。とりあえず。
Yはハイトマップでの今のピクセルとの輝度の差「隣接ピクセルの色の値 - 今のピクセルの色の値」を与えます。

f:id:moko_03_25:20190107012738p:plain

② この2つのベクトルを Y+ 方向を向くように90度傾ければそれが法線となります。

f:id:moko_03_25:20190107012744p:plain

③ 左右の法線の丁度中間の角度を求めて‥

f:id:moko_03_25:20190107012749p:plain

④ その角度が 0°~180° なのを 0~255 に落とし込めばノーマルマップにする色の値が決まるという算段です。これが一般的な求め方なのかは知らず、まずは素直に角度を求めてみようと思った次第です。※ちなみにノーマルマップの実際の仕様と値が左右逆なのでコードでは最後に反転させてます

f:id:moko_03_25:20190107012753p:plain

⑤ 具体的にどうするかと言うと、まず左の隣接ピクセルの座標からアークタンジェント関数「Math.Atan2(Y, X)」で簡単に求まります。※引数が Y, X とXYが逆順なので注意

ただし下図の例のようにベクトルが Yマイナスへ向いていると Atan2 で得られるのは 225° ではなく -135° となるので注意が必要です。マイナスかどうかは「Math.Sign(Y)」で判定して、マイナスなら 360°分プラスしてあげればOKです。ここからさらに -90° してあげればそれだけで法線の角度が求まります。

f:id:moko_03_25:20190107012758p:plain

⑥ 右の隣接ピクセルも同様に「Math.Atan2(Y, X)」で角度が分かるので、さらに 90°プラスしてやれば法線の角度が求まります。

f:id:moko_03_25:20190107012804p:plain

⑤と⑥でそれぞれ求まった法線の角度を単純に足して2で割れば中間の角度になります。

角度は基本的にラジアンの単位での計算になるので、最後に 0~255 の範囲に変換してあげてから、Rチャンネルのピクセルに設定してあげれば完了!となります。

これを全ピクセルに行います。Gチャンネルも同様です。

法線の傾斜キツすぎない?


ここで問題となるのが、隣接ピクセルとの輝度差がたった 1 でも 45° の傾斜になるのって「急角度すぎないか?」という点です。

f:id:moko_03_25:20190107012744p:plain

実際に計算してみるとえらく強度のあるノーマルマップになってしまいます。
これでは厳しいので、何かしらの方法で傾斜に補正を与えられるようにしたい訳です。

f:id:moko_03_25:20190107020338p:plain

そこで X に与える値を大きくしてやることで傾斜を緩めてみました。
これが良い方法なのかはサッパリ分かりません。まず思い付いたのがこの方法というだけです。

f:id:moko_03_25:20190107022817p:plain

プルダウンで選んでいる値は X に与える値です。
下図の場合は左のピクセルに X = -50 を、右のピクセルに X = 50 を与えた場合です。
かなり傾斜が緩くなり、良好な結果が得られました。

f:id:moko_03_25:20190107020345p:plain

こちらは同様に 100 を与えた場合。さらに傾斜が緩くなりました。

f:id:moko_03_25:20190107020351p:plain

 

結局、傾斜はどう決めたら良いのか?


隣接ピクセルのベクトルのXに50とか100とか適当な値を入れるのではなく、根拠のある数字を入れてちゃんとした傾斜を算出したいと思った場合、最終的に生成したノーマルマップを貼るモデルを3D空間上に表示した際のテクスチャの1ピクセル(テクセル)の3D空間上での距離をXに与えて、Yはハイトマップの値が最大の255(8bitだったとして)の際に3D空間上でどれくらいの高さを表しているのかを考慮してYの値を求めて与えてあげれば良い訳ですね。

こちら、twitterで TAI YAMAGUCHI(@oteguro)さんからアドバイスいただきました!
なるほどとても納得です!ありがとうございます。。

        *        *        *        *

ちなみに法線はAtan2で求めなくても外積でスマートに求められるっぽいですが、そちらはまた別途試してみたいと思います。

実際のコードはこちらです。人に見せる前提じゃないのでアレですが。。
Ideone.com - 75BfFQ - Online C# Compiler & Debugging Tool