2012/07/15

XNA CDLOD Terrain

XNA へ移植していた Continuous Distance-Dependent Level of Detail (CDLOD) の実装を概ね終えました。
CDLOD: Filip Strugar: Oh no, another terrain rendering paper!
デモ コードでは、Perlin noise を用いて実行時に height map を生成し、CDLOD を用いて地形の描画を行なっています。
ソース コード: https://github.com/willcraftia/TestXna/tree/master/TerrainDemo
一応、ざっくりと CDLOD を説明しておきます (深く説明する知識がないのでざっくり)。

CDLOD では、まず quadtree を用いて height map が表す地形情報を level of dedail (LOD) のレベルごとにノードへ分割しておきます。そして、視点からどの程度の距離でどの LOD レベルを用いるかを示すリストを用意しておき、このリストに基いて quadtree から表示対象とするノードを選択します。なお、各ノードは描画で必要とする頂点情報は持たず、対応する height map 上での位置と height map から得られる高さを持つのみです。

続いて、1 つの矩形メッシュを用意しておき、GPU へ矩形メッシュとノードの情報を渡し、vertex shader  で適切な座標へ変換しながら描画します。この時、ある LOD レベルのノードで描画するメッシュが、次に粗い LOD レベルのノードで描画するメッシュの形状に近づくように、視点からノードまでの距離に応じて頂点位置をモーフィングさせながら描画します。これにより、LOD の違いによる繋ぎ目や t-junction の発生がなくなるという仕組みです。

こんな感じでしょうか?なお、地形の分割とモーフィングの説明としては、以下のサイトが分かりやすい気がします。
Mistal Research: GPU Terrain Subdivision and Tessellation
僕のコードは、そのままの移植ではなく、幾つかの改変が加えられています。
一番大きな点には、HW インスタンシングの利用、および、そのためのノード選択ロジックの変更があります (オリジナル コードは各ノードごとに DrawIndexedPrimitives() を呼び出す)。ただし、改善と呼べるかどうかは分かりません。

まず、オリジナル コードでは、単一の矩形メッシュを用意するのみであるものの、描画時には矩形メッシュの中で必要とする区画のみを描画しています。これには、必要十分な細かい LOD のノードを選択するというロジックが関与しています。

あるノードには 4 区画が存在し、各区画には対応する子ノード (より細かい LOD のノード) がぶら下がります。ノード選択において、全区画で子ノードが選択されなければ、そのノードをそのまま選択することになりますが、一部の区画では子ノードが選択される (より細かい LOD が選択される) という状況も生まれます。

ここで、親ノードを N、その子ノードを n0、n1、n2、n3 とし、ノード選択において n0、n1 が選択されるとします。オリジナルでは、N、n0、n1 を選択状態にし、n0 と n1 はそのまま矩形メッシュを描画しますが、N は n0 と n1 の区画を含むため、n0 と n1 に重なる区画を描画しないようにインデックス データを調整してから矩形メッシュを描画します。

一方、僕のコードでは、HW インスタンシングを行うために、n0、n1 が選択されたら n2、n3 も強制的に選択し、N を破棄します。つまり、全ノードを同一メッシュで描画するために、N が矩形ではなくなる状態を回避しています。しかし、これは、定義した LOD レベルの範囲を超えたノード選択を行なっている (選択されるノード数がオリジナルよりも増える) ことにもなります。

細かい変更としては、Draw() 呼び出しごとに渡さずに済む Effect パラメータを事前に設定したり、選択する LOD の範囲を定めるロジックをわずかに簡易な物にしたりなどしています。

ただし、改善すべき点は、簡単に気付く所でもまだまだ残されています。僕のコードでは (オリジナルもそうですが)、view frustum とノードの AABB との単純な交差判定で frustum calling を行なっています。これには、事前に球同士の交差判定で除外するなどの余地があります。また、CDLOD で期待する描画を維持するには、かなり遠方まで距離と LOD レベルの関係を定義をしておく必要があり、僕のコードではそれをそのまま用いているために、視覚範囲を越えた部分のノードまで判定してしまいます。

一応、動画を YouTube とニコニコ動画へ上げてあります。ニコ動版は、どうもアップロード時に画質が劣化してしまったようで (もうニコ動は投稿が面倒すぎて直す気が起きません)、YouTube 版を見た方が良い気がします。





実行画面におけるワイヤフレームの立方体は、ノードの AABB を表しています。その色は LOD レベルを表しており、レベル %= 4 で 4 階調にしたものです。ある地点に近づくにつれ、より大きな AABB がより小さな AABB に置き換えられていく (LOD レベルが切り替わる) 様子を見ることができると思います。
また、ワイヤフレーム表示時には、モーフィングしていく様子を見ることができると思います (解像度の問題で若干見辛いですが・・・)。

Height map は、実行時に Perlin noise で作成しています。動画でのサイズは 4096x4096 ですが、実際に使う場合には、(512 + 1)x(512 + 1) や (1024 + 1)x(1024 + 1) などにするかと思います。なお、CDLOD では height map をテクスチャとしてシェーダへ渡す必要があり、このテクスチャは SurfaceFormat.Single となるわけですが、 XNA HiDef プロファイルで扱える最大サイズが 4096x4096 であるらしく、その限界で heigh map を作成してみたという所です。

地形描画は、その高度情報に応じた色分けとブレンドで済ませています。リアルな地形を望むならばテクスチャをどのように貼るかを考えなければならない所ですが、今のところ僕には興味が無いため無視しています。

この後は、複数の height map を用いて、実行時にロード/アンロードを繰り返しながら、より大きな地形を描画するためのコードを書こうと思っています。

0 件のコメント:

コメントを投稿

libgdx いじり

Google が提供している Java 版の Tango Examples は Rajawali をベースにしているため、自分が仕事で開発する Tango アプリも Rajawali ベースとしていましたが、最近は libGDX への移行を進めています。一応、要点については移行が...