第1章
このデモとは
「あの斜め見下ろしの Civ みたいな世界地図、3Dモデルなしで、絵(スプライト)を並べるだけで作れるの?」に、動く実装で答えるサンプルです。
このデモは、Babylon.js ファミリーの WebGPU 専用ランタイム Babylon Lite の公式リポジトリに含まれる、freeciv(フリーシブ)——オープンソースの Civ クローン「Freeciv」の見た目を借りた、アイソメトリック(斜め見下ろし)の世界地図です。本物の Freeciv タイルセット(amplio2)を読み込んで、ダイヤ形のタイルを何千枚も敷き詰め、大陸を自動生成し、都市・道・川・野生動物、そして霧・昼夜・雲・きらめく水面まで描き込みます。
いちばんの驚きは、このデモがシーンもカメラもメッシュもライトも作らないこと。Babylon Lite の2Dスプライト描画の経路だけで、この立体感あふれる世界を組み上げています。「奥行き」は3Dではなく、ダイヤ形タイルを正しい順で重ねることで生まれる“錯覚”です。
プログラムとしての見どころは、次の3点です。
Freeciv の .spec ファイル(タイル配置の設計図)を自前で解析し、1枚のPNGから何百ものタイル絵を切り出す。“データ考古学”の実践です。
ノイズと緯度・湿度から、海・草原・砂漠・森・山・ツンドラを描き分け、川を流し、都市を置き、道でつなぐ。シード固定で毎回同じ世界に。
戦場の霧・昼夜・雲と影・水面のさざなみ・太陽のきらめき——すべて“世界に貼りついた1枚のシェーダ板”として、15レイヤーに重ねます。
第2章
あそびかた
コードを読む前に、まず地図をぐりぐり動かすのが近道です。マップ探索と、1体だけ動かせる斥候(スカウト)がいます。
babylonjs.com のライブデモを開く →(WebGPU 対応ブラウザが必要です。初回はタイルセットの読み込みに少し時間がかかります)
| 操作 | すること |
|---|---|
| ドラッグ | 地図をスクロール(パン) |
| マウスホイール | カーソル位置を中心にズーム(倍率 0.5〜8) |
| ホバー | タイルを水色の枠で強調+地形名のツールチップ |
| 斥候をクリック → タイルをクリック | 斥候を選択し、経路探索で目的地まで移動 |
| N | 昼 ⇄ 夜 の切り替え(5秒かけてなめらかに) |
| ミニマップをクリック/ドラッグ | その場所へ表示を移動 |
生成される世界の中心には、必ず首都 Babylon(人口5)が置かれます。「Babylon 5 へのオマージュ」というコメント付き。起動時のカメラは、この首都に寄って始まります。ほかにも Carthage・Paris・Kyoto が固定配置され、残りは自動で名付けられます。
第3章
用語ミニ辞典
この先の章で出てくることばを先にまとめて紹介します。分からなくなったらここに戻ってきてください。
斜め45°の見下ろし視点。正方形のマスを2:1のダイヤ形に描くことで、3Dなしに立体的な地形を表現します。
世界を敷き詰めるダイヤ形のマス1枚。このデモでは横96×縦48ピクセルの菱形で、96×96マスの世界を作ります。
スプライト=1枚の絵、アトラス=たくさんの絵を1枚に詰め込んだ画像とその切り出し表。GPUに優しい定番手法。
.spec=Freeciv の「どのタイル絵がシートのどこにあるか」を書いた設計図。amplio2=その定番タイルセットの名前です。
「奥のものから先に描く」画家の手法。アイソメトリックでは x+y の小さい順に描けば、手前が正しく上に重なります。
海岸線などを、タイルを4つの角セルに割って隣接地形ごとに描き分ける Freeciv の技。境目をなめらかにつなぎます。
乱数を滑らかに補間した“自然な凸凹”(値ノイズ)を、周波数を変えて重ねたもの(fBm)。地形・雲・霧の生成に使います。
まだ見ていない土地を霞ませる演出。斥候や都市の視界の中だけが晴れます。1枚の連続したモヤとして描きます。
実際より高い解像度で描いてから縮小する手法。ダイヤの継ぎ目に隙間(クラック)が出ない、なめらかなズームの要です。
WGSL=WebGPU のシェーダ言語。このデモは霧・雲・水などを「世界に貼った1枚の板+自前シェーダ」で、ピクセルごとに合成します。
スプライトを描く重ね順。海(0)→水面(0.5)→海岸(1)→地形…→都市(10)→ユニット(11)→霧(13)→雲(40)→夜(45)…と積みます。
ダイクストラ=斥候の最短経路探索。最小全域木(MST)=都市を最短でつなぐ道路網の作り方です。
第4章
全体の地図
エントリの freeciv.ts(664行)が司令塔。タイルセットを読み、世界を生み、レイヤーを積み、演出を配線して、1本のループを回します。
flowchart TD
ENTRY["freeciv.ts
司令塔(2Dスプライトのみ)"]
subgraph DATA["読む"]
SPEC["spec-parser + atlas
10枚の.spec→絵の切り出し"]
WGEN["worldgen
96×96の大陸生成"]
end
subgraph DRAW["描く"]
TMAP["tilemap
15レイヤーに配置"]
FX["fog/daynight/atmosphere
water/glints/dust…"]
PRES["present
なめらかズーム合成"]
end
LIVE["live
斥候・野生動物"]
ENTRY --> DATA
ENTRY --> DRAW
ENTRY --> LIVE
WGEN --> TMAP
SPEC --> TMAP
TMAP --> PRES
FX --> PRES
矢印は「上が下を利用する」向きです。freeciv.ts は、10枚のタイルシートを並列で読み込み→世界を生成し→15枚のスプライトレイヤーを積み上げ→動くものと演出を配線し→毎フレーム、シミュレーション更新・ズーム補間・各演出の更新を回します。「読む」「描く」がはっきり分かれているのが最初の目印です。
3D の奥行きバッファは使いません。かわりに、海・水面・海岸・地形・森・山・川・道・都市・ユニット…と15枚のレイヤーを決まった順で重ねることと、各レイヤー内では x+y の小さいタイルから描く「画家のアルゴリズム」で、立体的な前後関係を作り出します。
第5章
タイルセットを読む
担当:spec-parser.ts と atlas.ts。本物の Freeciv タイルセットを、コードで読み解きます。
Freeciv のタイルは、1枚の大きなPNGに格子状に並んでいて、その「どのマスがどのタイルか」を .spec という INI 風のテキストが定義します。spec-parser.ts はこれを自前で解析——シート画像のパス、格子の原点・セル幅・間隔、そして「行・列・タグ名」の対応表を読み取ります。
flowchart LR
SPEC[".spec テキスト"] --> PARSE["parseSpec
格子geometry+タグ表"]
PNG["シートPNG"] --> ATLAS["loadFreecivSheet
共有テクスチャ+アトラス"]
PARSE --> ATLAS
ATLAS --> FRAME["frameOf(タグ)
= 行×列数+列"]
atlas.ts は、その設計図に沿ってPNGを GPU テクスチャに載せ、グリッドごとにタグ名 → フレーム番号の引き表を作ります。テクスチャはニアレスト(最近傍)フィルタ——ドット絵をくっきり保ち、1px の境界を越えて色がにじまないようにします。10枚のシート(地形・丘・山・海・水・都市・ユニット・動物・選択枠…)から、15枚の描画レイヤーが作られます。
1つのシートに複数の格子が同居することもあります。たとえば water.spec は、川用の 96×48 グリッドと、海岸セル用の 48×24 グリッドを別々に持ちます(海岸グリッドはシート内の x=1, y=437 から始まる)。パーサはこのグリッドごとの独立した原点を正しく扱います。
第6章
斜め格子の座標
担当:iso.ts(79行)。マス目 (x, y) と、画面のピクセル座標を行き来する数学です。
タイルは横 TILE_W = 96・縦 TILE_H = 48 の 2:1 ダイヤ形。マス (x, y) の画面中心は、次の一行で決まります。
// マス目 → 画面ピクセル(アイソメトリック変換) isoCentre(x, y) = [ (x - y) * 48, // = TILE_W/2 (x + y) * 24 ]; // = TILE_H/2 // 逆変換 worldToTile はこれを解いて四捨五入 → 点の下のダイヤに吸着
この式のおかげで、隣り合うマスが画面上で綺麗な菱形の格子になります。iso.ts はさらに、8方向の隣接マス(DIR8)や、ダイヤの辺を共有する4方向(EDGES=川や海岸で使う“実質的な東西南北”)、画面方向を Freeciv のスプライト向き記号に変換する表(SPRITE_DIR)を提供します。これらは世界生成と描画の両方が共通で使う、いわば“方位磁針”です。
第7章
世界を生成する
担当:worldgen.ts(526行)。シードから、海に囲まれた1つの大陸を組み上げます。
まず高さの地図を作ります。5オクターブの値ノイズ(fBm)に、中心からの距離で効く放射状の減衰(1 - 距離²×1.15)を掛ける——だから縁は海に沈み、中央が陸になります(高さ < 0.32 は海)。そこへ別の低周波ノイズ=湿度と、赤道からの緯度を組み合わせて、11種類の地形を振り分けます。
// 地形の振り分け(抜粋・実際のしきい値) if (elevation < 0.32) → Ocean // 海面 if (lat > 0.86) → Arctic // 極地 if (lat > 0.72) → Tundra / Mountains if (elevation > 0.78) → Mountains if (elevation > 0.62) → Hills // 残りは湿度で: 乾→砂漠/平原、湿→湿地/密林/森、中間→草原
地形ができたら、生活を吹き込みます。川は高地(高さ > 0.66)から湧き、辺づたいに一番低い隣へ流れ下って海に達したものだけ残す(最大8本/“取り残された水たまり”は消す)。都市は草原か平原に、間隔6マスで最大9つ——中心には首都 Babylon(人口5)、Carthage・Paris・Kyoto は固定座標に。道路は都市どうしを最小全域木で最短につなぎ、約9%の陸地には地形に合った資源や灌漑・鉱山を配置します。
すべての乱数は整数ハッシュから作られ、seed: 7 固定。だから何度開いてもまったく同じ大陸が再現されます。手続き的生成の「決定性」の教科書です。
第8章
レイヤーで描く
担当:tilemap.ts(368行)。生成された世界を、正しい順でタイル絵に置き換えていきます。
描画は奥から手前へ(x+y の昇順=画家のアルゴリズム)。1マスは、複数レイヤーにまたがる絵の積み重ねとして表現されます。
| 順 | レイヤー | 内容 |
|---|---|---|
| 0〜1 | ocean / water / coast | 深海・浅瀬・きらめく水面・海岸線 |
| 2〜5 | terrain / forest / hills / mountains | 基本地形と、せり上がる森・丘・山 |
| 6〜9 | river / road / improvement / special | 川・道・灌漑や鉱山・資源 |
| 10〜12 | city / unit / animals | 都市・ユニット・野生動物(足元を接地) |
| 15 | highlight | ホバー中のタイル強調枠 |
ただ地形ごとに絵を置くだけでは、海と陸の境がカクカクします。そこで CELL_CORNER:浅瀬のタイルを4つの角セルに割り、各角で出会う3マスが陸か水かを見て、境界専用の絵を選ぶ。森・丘・山は CELL_WHOLE の“マッチ”方式で、4辺の隣が同じ地形かをビットにして、輪郭のつながった絵を選びます。道路は「つながっている方向ごとに1枚ずつ」重ね描き。本物の Freeciv と同じタイリング規則を再現しています。
第9章
なめらかズーム
担当:present.ts(167行)。ドット絵タイルを、隙間なく・なめらかに拡大縮小する巧妙な仕掛けです。
問題はこうです。ダイヤ形タイルを中途半端な倍率(例:1.7倍)で直接描くと、タイルの継ぎ目に髪の毛のような隙間(クラック)が出ます。かといってピクセルを整数倍でしか拡大しないと、ズームがガクガクします。
解決は2段構え。まずタイルを、2倍のスーパーサンプリングをかけたオフスクリーンの画像に、継ぎ目が出ない“きりのいい倍率”(0.5/1/2/4/8 のいずれか)で描く。次にその画像を、1枚の板として、実際に見せたい中途半端な倍率へ滑らかに縮小して画面に貼る。こうして「くっきり」と「なめらか」を両立します。描画は ワールド → 合成板 → HUD の3パスで流れます。
第10章
動くもの
担当:live.ts(285行)と dust.ts。止まった地図に、命を吹き込む層です。
動くものは2種類。1つはプレイヤーが操る斥候(スカウト)——命令があるまで待機し、目的地が指示されると、経路を1マスずつ約 620ms/マス で歩きます(滑らかに補間)。選択中は大きさは変えず、透明度がゆっくり明滅して「選ばれている」ことを示します。もう1つは6体の野生動物(狼・熊・豹・虎・ワニ・ゴリラ・蛇から)——都市以外の隣マスへ気ままに歩き回ります。すべてシード固定で、毎回同じ動き。
dust.ts は、斥候が歩いている間だけ、足元に 95ms ごとに土ぼこりの粒(最大18個のプール)を落とします。粒はワールド座標に固定されるので、斥候は自分の足跡から離れていく。しかも描画順をユニットより下・夜の陰りより下に置くことで、斥候が土煙を踏み越え、夜には足跡もろとも暗くなる——手のかからない自然さです。
第11章
クリックの手ごたえ
担当:pick.ts と commandfx.ts。操作に“手ごたえ”を返す、ゲームフィールの層です。
マウスを乗せると、pick.ts がそのタイルに水色の枠を置き、地形・資源・改良・川/道・座標をまとめたツールチップを出します(3Dのピッキング機能ではなく、座標計算だけで実現)。斥候を選んで行き先をクリックすると、freeciv.ts のダイクストラ経路探索(道の上は移動コスト 1/3 に割引)が最短路を求め、斥候がそこへ歩き出します。
その瞬間の視覚フィードバックが commandfx.ts。すべて WGSL シェーダで1枚の板に描かれます。
| 演出 | 中身 |
|---|---|
| 目的地のリング | 暖色の点線(マーチングアンツ)が回転(7本の破線が流れる) |
| クリックのさざ波 | 淡いシアンのピング(波紋)が広がって 520ms で消える(同時5つまで) |
| 選択中の斥候 | (ここではなく)ユニット自身の明滅で表現 |
第12章
戦場の霧
担当:fog.ts(256行)。未踏の土地を霞ませる、あの“霧”です。
ポイントは、霧をタイルごとの絵で置かないこと。もしそうすると、ダイヤの継ぎ目に不自然な段差が出ます。代わりに、世界に貼りついた1枚の連続したモヤの板を用意し、そのシェーダが「タイルごとの視界の強さ」を記録した小さなテクスチャをなめらかに(バイリニアで)読んで、モヤの濃さを決めます。
CPU 側は「踏破済み・現在視界・霧の濃さ」を配列で管理。地図は最初から全域が踏破済みで、見ていない土地は薄い霧(0.72)、視界に入ると 450ms かけて晴れます。視界の半径は都市の周り2マス・斥候の周り3マス。さらにシェーダは、fBm ノイズで霧のふちを揺らして、風にたなびくように見せます。
第13章
昼と夜
担当:daynight.ts(215行)。N キーで、世界が昼と夜を行き来します。
N を押すと、5000ms かけて昼 ⇄ 夜へなめらかに遷移し、到達した側でとどまります(自動ループはしません)。夜は、画面全体を覆う1枚の暗い青の板(rgb(0.04, 0.06, 0.18))の不透明度を、時間に比例して最大 0.62 まで上げるだけ。そして日が傾くと、各都市が暖色の光(rgb(1.0, 0.82, 0.5))を加算でにじませ、街の灯がともります。
コメントによれば、以前は夕暮れに暖色を差す演出があったものの、地平線を越える瞬間に「明るくなってから暗くなる」不自然な脈動が出たため、あえて外して一定の寒色に統一したとのこと。判断の理由まで読めるのが、このコードの良さです。
第14章
空・雲・水・きらめき
担当:atmosphere.ts・water.ts・glints.ts。空と海の“生きた”質感です。
5オクターブの fBm で流れる雲を1枚の板に描き、少しずらした雲の影を地面に落とす。どちらもズームで寄ると消える(俯瞰のときだけの演出)。夜は影が消えます。
海タイル1枚ごとに、3つのサイン波を足し合わせたコースティック(水の揺らぎ)を GPU で描画。タイルごとに位相をずらし、隣どうしが同時に光らないように。
外洋にだけ、固定した太陽の方角へ伸びる細い光の筋がチカチカ瞬く。夜(daylight)になると消灯します。
これらに共通するのが、「世界に貼った1枚の板+自前 WGSL シェーダ」という作り。板のスプライトの色(tint)に「画面上の世界の矩形」や「タイル位置+位相」を忍ばせ、ピクセルごとに模様を合成します。霧・雲・水・きらめきはすべてこの共通テクニックで、CPU の負担をほぼ増やしません。
第15章
古地図と縁取り
担当:backdrop.ts・vignette.ts・minimap.ts。世界の“額縁”と俯瞰図です。
backdrop.ts は、島の背後に1569年の Mercator 世界地図(パブリックドメイン)を敷きます。暗く落とし、紙のような色をかぶせて“古びた抽象背景”に加工。さらに海岸が外洋へ溶け込むよう、青いにじみ(sea halo)を島の縁に足します。静的で、地図と一緒にパン・ズームします。
vignette.ts は、画面の四隅を暗く落とす周辺減光。中央 50% は明るいまま、隅へ向かってなめらかに暗くして、背景の“余白”を目立たなくします。minimap.ts は右下のミニマップ(長辺180px)——世界を1タイル=1ピクセルのデータ画像として一度に描き、地形色・都市の点・現在の表示範囲(回転した四角形をはみ出さないよう切り抜き)を重ね、クリック/ドラッグでそこへ表示を飛ばせます。
第16章
設計テーマ
19ファイルに散らばって見えて、じつは一貫した4つの考え方が通っています。
シーンもカメラもメッシュもライトも無し。奥行きは「レイヤー順」と「x+yの描画順」で作る。3Dに頼らず立体を見せる潔さ。
Freeciv の .spec を自前で解析し、CELL_CORNER や CELL_WHOLE といった本家のタイリング規則を再現。データに忠実だから“らしく”見える。
霧・雲・水・きらめきは、世界に貼った1枚の板と自前WGSLで合成。CPUを増やさず、GPUに任せて豊かな空気感を出す。
世界も動物の動きもシード固定。整数ハッシュ由来の乱数で、毎回まったく同じ世界・同じ挙動を再現できる。
このコードのコメントには「なぜそうしたか」がていねいに書かれています。夕暮れの脈動を消して一定の寒色にした理由/霧を連続した板にして継ぎ目を消す判断/ズームのクラックをスーパーサンプリングで防ぐ工夫——判断の理由ごと読めるのが、このサンプルの教材価値です。Minecraft・Platformer・Tetris・Sandblox・DOOM デモと共通する“コメント文化”が、ここにも息づいています。
第17章
ファイル一覧(全19ファイル)
コードリーディングのおともに。行数は master ブランチのソース実測値です。freeciv.ts が最大で、合計およそ4,342行。
| ファイル | 行数 | やくわり |
|---|---|---|
| エントリ | ||
freeciv.ts | 664 | 司令塔。読込・生成・レイヤー・演出・入力・ズーム・経路探索を配線 |
| データ・座標 | ||
worldgen.ts | 526 | 大陸の手続き生成(地形・川・都市・道・資源) |
spec-parser.ts | 160 | Freeciv の .spec タイル設計図を解析 |
atlas.ts | 118 | PNG+.spec からアトラスとタグ→フレーム表を構築 |
iso.ts | 79 | アイソメトリック座標変換と8方向テーブル |
| 描画・合成 | ||
tilemap.ts | 368 | 世界を15レイヤーのタイル絵に配置(海岸・森・山の境界処理) |
present.ts | 167 | 2×スーパーサンプルのオフスクリーンでクラックのないズーム合成 |
| 動き・操作 | ||
live.ts | 285 | 斥候(プレイヤー操作)と6体の野生動物のアニメ |
minimap.ts | 267 | 右下のミニマップと、クリックで表示移動 |
dust.ts | 187 | 歩く斥候の足元に舞う土ぼこりの粒 |
commandfx.ts | 173 | 目的地の点線リングとクリックの波紋(WGSL) |
pick.ts | 127 | ホバータイルの強調枠と地形ツールチップ |
| 環境・演出 | ||
atmosphere.ts | 216 | 流れる雲と地面に落ちる雲の影(fBm・5オクターブ) |
daynight.ts | 215 | Nキーで昼夜(5秒遷移)と都市の灯り |
fog.ts | 256 | 連続した1枚の板で描く戦場の霧(視界2〜3マス) |
glints.ts | 176 | 外洋の太陽のきらめき(昼のみ) |
backdrop.ts | 145 | 背景の Mercator 1569 世界地図と海のにじみ |
water.ts | 120 | 海タイルの水面のさざなみ(GPU コースティック) |
vignette.ts | 93 | 画面四隅の周辺減光(背景の余白を隠す) |
行数は master ブランチ(2026年時点)のソース実測値です。合計およそ4,342行(19ファイル)。