ホーム < ゲームつくろー! < Shader編

その3 波:ハイトマップから法線マップを作る方法

〇 使用シェーダ:PS


 オープンワールドな世界に水表現は欠かせません。美しい湖、荒々しい海、力強い川や滝などがある事で世界は美しく彩られます。そんな水は透明で表面反射してうねうねバシャバシャ動くという「流体」の性質があるため、表現するのが大変なマテリアルの一つです。マルペケでも大分昔になりますが水の一部についてちょぴっとだけ記事を上げていました(斜めから見ると底が見えない水面(フレネル反射))。

 ダイナミックな水表現というとやはり「波」です。上のマルペケの記事にあるフレネル反射を適用したプレートは水っぽくは見えるのですが、自然の中であのような全く波が立っていない水は存在しません。フレネル反射に比べ波は動的に動くものなので難易度が非常に高く、実現するまでに知る事ややる事がいっぱい(*_*)。一気にやると目が回ってしまいます。そこで、ここから何章かに分けて少しずつ波について攻めていこうと思います。

 手始めの本章では波の表現に必須となる「ハイトマップ」の扱い方について手短ですが解説致します。



@ ジオメトリレベルとピクセルレベルの波

 波を表現する方法には大きく2つの方針があります。

 一つは平面を細かく分割したメッシュの各頂点を上下(時には水平方向に)動かして凸凹させる「ジオメトリレベルの波」です。この方法の利点は非常に大きなうねりを説得力を持って表現出来る事です。なんせ実際にポリゴンが変形する訳ですから、陰影もそうですし大きな波の後ろにある浮遊物などはZバッファの作用でちゃんと見え隠れします。一方デメリットは細かな波を表現するにはポリゴンを物凄く細かく分割しなければならず、広い面積でそれを行うのはパフォーマンス的に難しく、DirectX11以降のテッセレーションシェーダに頼るしかありません。よって、ジオメトリレベルの波は通常「大まかな波」を作るのに使われます。これについては後の章で見ていきます。

 もう一つの方針はポリゴン表面に陰影を付けて波のような凸凹に見せる「ピクセルレベルの波」です。これはメッシュを実際に凸凹にする訳では無くて陰影をつける事でそう感じさせる方法なので、浅い角度から見ると平面感が目立ってしまうのですが、俯瞰的な角度で見るとジオメトリレベルの波よりはるかに細かなディテイルで表現出来ます。ピクセルレベルの波を実現するにはその波の高さをテクスチャ化した「ハイトマップ」を使いこなす必要があります。この章で焦点を当てているのはこちらです。


A ハイトマップから法線マップへ

 ピクセルレベルの波を表現するには表面の細かな起伏をテクスチャとしてピクセルシェーダに与え陰影を付ける必要があります。そういう目的のテクスチャとして「ハイトマップ」及び「法線マップ」があります。ハイトマップは高さ情報のみを持ったテクスチャで、波の情報を保持するのに最適です。しかし3Dでその高低具合を陰影描画するには高さの情報だけでは扱いにくく、「面がどういう方向を向いているか」というより具体的な情報である「法線マップ」が必要になります。その為、ハイトマップから法線マップへ変換する作業が必要になります。

 ハイトマップはポリゴンの表面の面法線方向の高さを表したテクスチャです。こんな感じの見た目です:


https://area.autodesk.com/downloads/waves_heightmap/

 黒地の箇所は元のポリゴンの高さ、それが白くなる程に高くなることを表現しています。ピクセルシェーダではこれを解釈してブツブツの陰影を付ける事になります。その為にはここから「点法線」を算出する必要があります。

 ハイトマップの一部を拡大します:

 緑色の枠が注目しているポリゴン上の一点で、数字はその位置でのハイトマップの濃度(0〜255)を表しています。緑で囲った点の法線を計算する為に、まずX軸方向(U方向)及びY軸方向(V方向)の値の変化(勾配)を算出します。まずX軸方向を見てみると74→39→16と減少しています。これをグラフにプロットしてみます:

 真ん中の点の勾配とは、その点を起点としたX軸の増加に対する値の増加量の割合です。これはいわゆる微分で求められますが、上のような離散値のままでは微分出来ません。そこで、上の点に2次関数を当てはめます。そのまま連立方程式を作ってガリガリと計算しても良いのですが、両隣とのX座標成分の差が1であるという特性を利用すると凄く簡単になります。真ん中のX座標を0と相対座標化すると、それぞれが(-1, v1=74)、(0, v2=39)、(1, v3=16)という座標になります。これを2次関数に代入して連立方程式を解いてみましょう:

このように2次関数の係数が値の足し引き程度の簡単な計算で求まります。この2次関数からx=0での傾きを求めます。元式を微分して、x=0を代入します:

という事で、真ん中の点の傾き(値の勾配)はその左右の値の差の半分で求まる事がわかりました(^-^)。非常に簡単です。これと全く同じ事はY軸側の傾きでも言えます。

 今求めた勾配はX軸及びY軸に1進んだ時の値の変化です。これは、

という3次元のベクトルで表現出来ます。先程の点での点法線はこの2つのベクトルの外積から求められます。なぜかというと、上の2つのベクトルはその点の「接平面」に含まれているからです。接平面に鉛直なベクトルが法線なので外積で算出できるというわけです。外積は掛ける順番が重要ですが、DirectX等の左手系の場合、

となります(右手系の場合は掛ける順番が逆になります)。色々計算してきましたが、結論は非常にシンプルですよね。

 先のハイトマップから法線マップを作りUnity上で板ポリに貼り付ける(StandardスペキュラシェーダのNormalMapにドラッグして色をそれっぽく調整しただけ)とこんな感じになります:

 ポリゴンは真っ平らのはずなのにボッコボコです(^-^)。こういうのを簡単にテスト出来るのがUnityの良い所ですね。ちなみに、ここまで説明してきて何なのですが、Unityにはハイトマップから法線マップを生成する仕組みがあります(^-^;;;。イメージのインスペクタのTexture Typeから「Normal」を選択して、そこにある「Create From Grayscale」にチェックを入れてApplyすると法線マップ化されます。ただし元のハイトマップを上書きするので注意。

 家の壁のレンガ模様など変化しない物であれば静的に作成した法線マップを使えば良いのですが、波は常に動いているためその手が通用しません。波の場合「毎フレーム動的にハイトマップを作成→法線マップ化して描画」を繰り返す必要があります。ではどうやって波を動的に作り続けるのか?この後の章はもっぱらそのお話となります。