地形のテッセレーション

ワンダと巨像を遊んでいたら地形を作りたくなりました。

テッセレーション

https://msdn.microsoft.com/ja-jp/library/ee417841(v=vs.85).aspx
http://www.nvidia.co.jp/object/tessellation_jp.html

テッセレーションはシェーダーパイプラインのステージの一つで,パッチと呼ばれる1つの面を複数の面に分割します。 頂点シェーダーの後,ピクセルシェーダーの前に実行されます。
例えば四角形のパッチを分割する場合,ハルシェーダーで4つエッジと,四角形の内部のXYについての分割数を指定して, テッセレーターが分割した結果をドメインシェーダーで加工してからピクセルシェーダーに渡します。

f:id:hikita12312:20180224194636p:plain:w400

テッセレーションを地形に使うことで,地形の詳細度を動的に計算して,必要な部分だけ三角形をたくさん生成することで, レンダリングの負荷を下げつつ,滑らかな地形を表現しようということです。

Height Map/Normal Map

まず,地形の元データーである,高さマップと法線マップを作成します。

f:id:hikita12312:20180224110047p:plain:w300
Height Map
f:id:hikita12312:20180224110109p:plain:w300
Normal Map

高さマップはパーリンノイズとフラクタルブラウン運動で作成しました。 http://mrl.nyu.edu/~perlin/noise/
https://thebookofshaders.com/13/

法線マップは高さマップから,隣接する高さの差分を利用して変分すれば作れます

{ \displaystyle
\begin{eqnarray}
\Delta_x y(x,z) &=& \frac{h(x+1, z) - h(x-1, z)}{2 \Delta x} \\
\Delta_z y(x,z) &=& \frac{h(x, z+1) - h(x, z-1)}{2 \Delta z} \\
{\bf u} &=& (1, \Delta_x y(x,z), 0) \\
{\bf v} &=& (0, \Delta_z y(x,z), 1) \\
{\bf n} &=& \frac{{\bf u}}{|{\bf u}|} \times \frac{{\bf v}}{|{\bf v}|}
\end{eqnarray}
}

分割度マップ

先程のHeightMapで表される地形は,32x32個のパッチで表現される地形であるとします。 それぞれのパッチの地形分割度に対応した値を格納した32x32サイズのテクスチャを地形分割度マップとして作成します。

まずは距離適応型のテッセレーションとして,単純にカメラから近い地形ほど分割度が増えるようにしてみます。

実際に出来上がった分割度マップです。

f:id:hikita12312:20180224112208p:plain:w300

赤い色ほど,パッチの分割数が多い事を示しています。 カメラに近い場所ほど赤色になり,カメラからは遠い場所は黒色になります。

地形のテッセレーション

なぜ,わざわざ分割度マップを別パスで作成したかというと,テッセレーションのパッチ間のエッジの切れ目を滑らかに接続するためです。 単純に分割数を各パッチに割り当てるだけだと,隣接するパッチの分割数が異なるときに,その境界が正しく接続しません。
そこで,1パス目で分割度を計算してから,注目するパッチと隣接するパッチの分割度の平均値を,注目するパッチのエッジの分割度として指定します。 こうすることで,隣接し合うパッチの境界の分割度は互いに一致するので,エッジの切れ目は作られないことになります。

f:id:hikita12312:20180224200410p:plain:w500

実際に分割してみます。テッセレーション部分のコードの概略は以下です。 注目するパッチとその上下左右のパッチの分割度を取得するために,Gather命令を利用しているのが工夫ポイントです。

分割して,レンダリングをした結果が以下のものです。

f:id:hikita12312:20180224114028p:plain:w600
f:id:hikita12312:20180224114028p:plain:w300 f:id:hikita12312:20180224114106p:plain:w300

輪郭検出

単純に距離だけで分割度を決定すると,遠くの山の輪郭がカクカクとしたものになってしまいます。 そこで,輪郭となりうる度合いも分割度マップの計算に取り入れてみます。

サンプリング点の法線と,サンプリング点からカメラに向かうベクトルの内積を見て輪郭になりうるかの度合いを組み込んでいます。

f:id:hikita12312:20180224112208p:plain:w300
輪郭検出なし
f:id:hikita12312:20180224114928p:plain:w300
輪郭検出あり
f:id:hikita12312:20180224114959p:plain:w300
輪郭検出なし
f:id:hikita12312:20180224115026p:plain:w300
輪郭検出あり

輪郭検出をいれると,全体的に分割度が上昇するバイアスはかかりますが,カメラ方向に法線が向いているパッチの分割はそのままなのがわかります。

結果

輪郭検出も入れた結果です。

f:id:hikita12312:20180224115302p:plain:w600
f:id:hikita12312:20180224115302p:plain:w300 f:id:hikita12312:20180224115343p:plain:w300
f:id:hikita12312:20180224121533g:plain

輪郭検出の有りと無しの比較です

f:id:hikita12312:20180224114028p:plain:w300
輪郭検出なし
f:id:hikita12312:20180224115302p:plain:w300
輪郭検出あり
f:id:hikita12312:20180224114106p:plain:w300
輪郭検出なし
f:id:hikita12312:20180224115343p:plain:w300
輪郭検出あり

まあそれっぽい気がします。

視錐台カリング

画面に映っていないパッチに対してテッセレーションを行うことは無駄になります。
その為,透視投影をした時に幾何的に画面に表示されないパッチの分割処理をスキップするような処理を入れると多少高速化されます。 ...が実装はしたのですが,パッチのAABBが上手に計算できていないためか視錐台のnear平面あたりでイマイチ不安定なので現在は利用していません。 そのうち修正します。