TerrainRendering00

学習用にAgentCodingで生成したデモです

プロシージャルに生成した地形を、カメラに追従する共有グリッドで描画します。標高は1024²固定のFP16 Height Clipmapへ保持し、移動で露出した帯だけをGPUで更新します。標高データファイルを配布せず、地形生成・差分更新・カリングをGPUで行う構成を試しています。

操作方法

  • 「実行」で開始。初期カメラはXYZ=(0, 200, 0)m、水平向きです
  • ビューをクリックし、WASDで前後左右、Q/Eで上下、Shiftで加速。ドラッグ・矢印キーで回転、ホイールで前後移動します
  • シード・標高振幅・山スケールを変更し、「地形を再生成」で反映します。同じパラメータなら同じ場所に同じ地形が生成されます
  • セルスナップ、法線、ワイヤーフレーム、タイル色分けを切り替えられます。初期ワイヤーはOFF、カバー半径は16kmです
  • 「カリング・配置をフリーズ」は標高キャッシュも固定します。カメラだけ動かして、固定された可視タイルを外から観察できます

WebGPU対応ブラウザで実行してください。操作はキーボードとマウスを使うPC向けです。初回と地形再生成時は標高窓全体を計算するため、表示まで少し待つことがあります。

初期値はシード 12343725、標高振幅 2000m、山スケール 3100m。振幅は地形関数のスケールであり、標高の最大値そのものではありません。灰色から白の濃淡で標高を表し、陰影で起伏を確認できます。カメラは地形との衝突判定を行わないので、地中に入った場合はQ/Eで高度を変えるか「カメラを戻す」を使ってください。

比較するときの見方

  • タイル色分け+ワイヤーフレーム:近くは細かく、遠くは粗い格子になる配置と接続を確認します
  • セルスナップOFF:カメラ移動に合わせて標高の参照位置が連続変化する様子を比較します。ONでは各段のセル間隔に合わせて配置を動かします
  • タイル密度:同じ範囲を4×4、8×8、16×16のタイルで覆います。1タイル64×64セルは固定で、密度を上げるとワールド空間のセルが細かくなります。標高キャッシュの最細間隔は1mのままです
  • カバー半径:指定範囲を覆う最小限のLOD段数を使います。段数は離散的なので、実際の配置範囲は指定値より広くなる場合があります
  • フラスタムカリング+フリーズ:フリーズ後にカメラを回すと、元の視点から見えなかったタイルが描画対象から外れていることを観察できます。フリーズ中は標高窓も移動しないため、地形を追いかける通常の移動には解除してください
  • 法線表示・可視リスト集約:法線の接続状態や、subgroup経路とworkgroup経路の描画結果を比較できます。機能の切替だけで速度差を断定できるものではありません

ビューをクリックして操作:W/S 前後・A/D 左右・Q/E 下降/上昇・Shift 加速
ドラッグ/矢印キーで視点回転・ホイールで前後移動。地形との衝突判定はありません

カメラ位置:未初期化

シード・標高振幅・山スケールの変更は「地形を再生成」で反映します。格納先は循環しますが、地形は繰り返しません

実行ボタンで開始します

標高の生成

heights[word] = pack2x16float(vec2f(first, second));

Compute Shaderで標高を計算し、2サンプルを1個のuint32へ詰めます。格納形式はIEEE 754のFP16、計算はFP32です。shader-f16や追加のテクスチャ形式機能は要求しません。VSではunpack2x16floatでFP32へ戻し、双線形補間します。テクスチャのハードウェア補間は使いません。

1 invocationがX方向に隣接する2サンプルの書込先を所有します。窓原点のXも2サンプル刻みにし、矩形の境目で同じuint32を競合更新しないようにしています。標高生成にはAtomicを使いません。

地形関数は低周波の山塊、Domain Warp、Ridged noiseの複数帯域から構成します。粗い標高段ではサンプル間隔に対して高すぎる周波数を滑らかに落とし、不要なノイズ帯域の評価も省きます。これは生成関数の帯域を抑える近似であり、厳密な低域通過フィルタではありません。高周波を省いた分で残りの振幅を再正規化せず、大きな地形の振幅を維持します。

CPUは矩形・シード・スケールなどの生成条件を渡すだけです。CPUで標高を作ったり、標高面を読み戻して描画したりする処理はありません。

1024²の固定窓とトロイダル更新

const rects = updateRects(previous, next);

各標高段は常に1024×1024サンプルです。原点をセル単位で移動し、前の窓と重なる領域を再利用します。露出したX方向の帯と、その帯を除いたZ方向の帯を生成するため、斜め移動でも二重書き込みはありません。1窓分以上の移動とパラメータ変更時は全生成し、静止中の更新は0です。

let physical = vec2u(cell) & vec2u(1023u);

世界の標高格子を循環バッファの位置へ写します。1024²なので剰余をビットマスクにできます。負座標も同じ規則で扱います。循環するのは格納場所だけで、地形の周期ではありません。生成関数は常に世界座標とシードを参照するため、窓が動いても同じ場所の値は変わりません。

更新条件は48バイト/矩形、動的Uniformの配置間隔は256バイトです。最大2矩形/段を同じCompute Passへ発行します。矩形間の書込先は重なりません。生成Pass完了後にタイルカリングと描画を行い、更新途中の標高をVSから読みません。

メッシュLODと標高LOD

メッシュと標高窓は別に管理します。各段の半幅を128mから倍増させるメッシュLODに対して、標高の最細間隔は1mです。

メッシュ段標高段標高間隔
0・101m
212m
324m
438m
5416m
6532m
7664m

各窓は対応するメッシュ範囲より広く取り、段別スナップ・双線形補間・法線参照の余裕を持ちます。タイル密度4/8/16を変えても、同じメッシュ段で使う標高窓の解像度は変えません。

FP16は1段2 MiBです。初期設定の半径16kmはメッシュ8段・標高7段で、標高本体は14 MiBです。カバー半径を縮めて必要段数が変わると、キャッシュも必要な段数へ再確保します。再確保時は古い描画の完了後に旧資源を破棄します。一時的には新旧の資源が共存します。

FP16の標高精度は値の大きさに依存します。例えば512〜1024m付近の刻みは0.5mであり、標高や法線に量子化の影響が出ます。元データファイルを置かないことと、GPU上のキャッシュ容量・精度は別の問題です。

単一格子と外周の接続

Frank Losasso・Hugues Hoppeによる Geometry Clipmaps: Terrain Rendering Using Nested Regular Grids をベースにした、タイルClipmap方式です。視点を中心に解像度の異なる規則格子を入れ子に配置し、移動に伴って必要な領域だけを更新する考え方を参考にしています。このデモでは各段を64×64セルの共有タイルで構成し、VBなしの頂点座標復元、GPUカリング、Indirect Instanced Draw、FP16標高キャッシュを組み合わせています。原論文の実装をそのまま再現したものではなく、境界の頂点補正や標高生成もこの実験の構成に合わせています。

全タイルは64×64セル、65×65=4,225頂点IDです。VBはなく、vertex_indexから格子座標を復元します。タイル描画用の共有uint16 IBは49,152バイト。タイルは1本のIndirect Instanced Drawで描画します。IBをフレームごとに変更しません。

let tile = unpackTile(descriptor);
let grid = worldXZ(id, tile);

可視リストのpacked uint32から、ビット演算で座標・メッシュ段・外周辺を復元します。ビット0〜10は候補ID、11〜14はX+8、15〜18はZ+8、19〜21は段番号、22〜25は外周N/E/S/W、26〜31は予約です。可視リストは最大1,600件・6,400バイトのままです。

細かいメッシュの外周4行で、次の段の原点差と接線方向の偶奇頂点collapseを計算します。格子の対角線をチェッカーボード状に交互にし、四隅も同じ2軸の規則で扱います。追加のスティッチ頂点や角タイル、専用スティッチVSはありません。

return mix(fineHeight, sampleHeight(xz, coarse), weight);

標高段ごとに高周波が異なるため、外周ではXZだけでなく標高も粗い段の値へ合わせます。混合率は補正後の世界位置から求めます。集約された頂点の混合率も一致させ、XZは同じなのに高さだけ異なる面を防ぎます。最外周の重みは1となり、粗い側と同じ位置・同じ標高を参照します。内側へ向けて細部を戻し、法線取得の間隔も粗い段へ合わせます。同じ標高段を使う場合は追加の参照を省きます。

通常部・接続部の標高・法線・行列変換は同じVS経路を使います。法線計算では補正域の位置ごとの重みを固定して差分を取るため、補正重みの空間微分まで含む厳密な面法線ではありません。ワイヤーフレームは位置計算を共通化し、不要な法線を計算しません。

GPUカリングと描画

Computeで候補タイルの保守的AABBとフラスタム6平面を判定します。Y範囲は生成関数の振幅から保守的に求め、GPUからMin/Maxを読み戻しません。地形による遮蔽カリングはありません。

subgroups対応時はprefix sumと代表laneのAtomicAddで可視リストを詰めます。未対応時はworkgroup共有メモリのBlelloch scanで集約します。空の集約単位はAtomicを発行せず、不可視・範囲外laneも集約には参加します。可視タイルごとのグローバルAtomicには戻していません。

タイル描画用とワイヤーフレーム用のIndirect引数は各20バイトです。可視タイル数を1回集計し、確定後にワイヤーフレーム用の引数へ4バイトGPUコピーします。描画のためのCPU読戻しはなく、画面の可視数だけ40バイトを非同期に読み戻します。

生成地形には有限DEMの外周がないため、外周の切り抜き処理はありません。タイル描画・ワイヤーフレームのPSにはdiscard/clip、深度出力、Storage書込を置きません。LOD接続の不要面は頂点の集約で縮退します。

リソースと制約

リソース初期16km設定
標高Clipmap1024²×7段、FP16で14 MiB
標高生成Uniform最大14矩形×256バイト=3,584バイト
タイル描画用共有IB49,152バイト
ワイヤーフレーム用共有IB49,664バイト
可視タイル最大6,400バイト
Scene Uniform480バイト
Indirect / 表示用Readback各40バイト

Sceneの末尾112バイトに標高窓7段分の原点を格納します。物理アドレスは世界格子から直接求めるので、VSは窓原点によるUV移動を繰り返しません。必要な標高参照領域が窓内へ収まることはCPU側のテストで確認します。

カメラ静止中は連続描画しません。フリーズ中は可視リスト・配置・標高窓と内容をまとめて固定し、カメラ行列だけを変更します。再生成条件と配置条件の操作はロックします。時間方向のLODポッピング除去、衝突判定、Shadow Map、法線テクスチャは未実装です。メモリ量や更新サンプル数とGPU時間の改善倍率は同一視しません。

このデモはハイトフィールドを対象とし、オーバーハングや洞窟は表現しません。地形テクスチャや外部モデルの読込も行いません。

ソース

直接表示