第1章
このデモとは
「Minecraft みたいなゲームって、どう作られているの?」に、動くコードで答えてくれるサンプルです。
このデモは、Babylon.js ファミリーの新しい WebGPU 専用ランタイム Babylon Lite の公式リポジトリに含まれるサンプルの1つです。ブラウザの中に草原・森・砂漠・雪山・海のある世界が生成され、歩き回ってブロックを壊したり置いたりできます。時間が経つと夕焼けになり、夜には星空の下でグロウストーンが光ります。
プログラムとしての特徴は次の3点です。
地形データを保存せず、seed(種になる数字)から毎回同じ世界を計算で再現します。だからセーブデータが極端に小さい。
ブロックの絵はフリー素材(Kenney Voxel Pack, CC0)を実行時に取得。動物は箱を組み合わせてコードだけで作ります。
重い処理はぜんぶ「1フレームに◯回まで」と上限が決まっていて、どんなに忙しくてもカクつかない設計です。
第2章
あそびかた
コードを読む前に、まず遊んでみるのがいちばんの近道です。画面をクリックするとマウスが視点操作モードになります。
babylonjs.com のライブデモを開く →(WebGPU 対応ブラウザが必要です)
| 操作 | すること |
|---|---|
| W A S D | 歩く(Shift 押しっぱなしでダッシュ) |
| Space | ジャンプ。水の中では押しっぱなしで浮上、離すとゆっくり沈む |
| マウス移動 | 視点を動かす |
| 左クリック | ブロックを壊す(岩盤だけは壊せない) |
| 右クリック | 手に持ったブロックを置く |
| 1〜0 / Tab | ホットバー(手持ちブロック10種)の切り替え |
| Ctrl+S / Ctrl+O | 世界をファイルに保存 / ファイルから読み込み |
| F3 | デバッグ表示(FPS・座標・チャンク数など)の切り替え |
URL の後ろに ?time=0.75 を付けて開くと夕焼けから、?time=0 なら真夜中からスタートします(0〜1 で1日を表します。0.25 が日の出、0.5 が正午)。
第3章
用語ミニ辞典
この先の章で出てくることばを先にまとめて紹介します。分からなくなったらここに戻ってきてください。
「立体のピクセル」。この世界はすべて 1m 角のブロック(ボクセル)でできていて、あるかないか・何のブロックかを数字1つで表します。
世界を 16×16 ブロックの「土地の区画」に分けたもの(高さは96)。区画ごとに生成・描画・破棄することで、無限に続く世界を有限のメモリで扱えます。
GPU が描ける形式=三角形の集まり。ブロックのデータ(数字の並び)は、描く前に必ずメッシュへ変換します。
ブロック同士がくっついて隠れている面は、絶対に見えないので三角形を作らない工夫。地中の面をぜんぶ省けるので、三角形の数が劇的に減ります。
「隅っこは暗くなる」現象の再現。ブロックの角や溝がうっすら暗くなるだけで、絵に立体感が生まれます。
なめらかなランダム模様を作る計算。周波数を変えて何層も重ねる(fractional Brownian motion)と、大陸→丘→凸凹という自然な地形になります。
水面に石を落とした波紋のように、近い場所から順に広げていく探索。このデモでは光が広がる様子の計算に使います。
GPU の上で動く小さなプログラム。WGSL は WebGPU 用のシェーダ言語です。頂点を動かしたり(水の波)、色を決めたり(霧・夕焼け)します。
たくさんの小さな画像を1枚の大きな画像(シール台紙のようなもの)にまとめたもの。テクスチャの切り替えが不要になり、描画が速くなります。
同じ入力なら必ず同じ結果になる性質。このデモの地形は seed から決定論的に計算されるので、「保存」も「隣のチャンクとの整合」もタダで手に入ります。
第4章
全体の地図
エントリポイントの minecraft.ts はたった 236 行。オーケストラの指揮者のように、実際の演奏(実装)は 23 のコンポーネントに任せています。
flowchart TD
MAIN["minecraft.ts(236行)
ぜんぶの組み立てとフレームループ"]
subgraph FEATURE["機能・ゲームロジック層 ─ 約1,860行"]
direction LR
CTRL["controls
操作と移動"]
WSIM["water-sim
水の流れ"]
FALL["falling-blocks
砂の落下"]
MOBS["mobs
動物"]
LGT["lighting
昼夜の司令塔"]
end
subgraph RENDER["描画・マテリアル層 ─ 約1,384行"]
direction LR
CR["chunk-renderer
チャンク出し入れ係"]
MESH["mesher
三角形づくり"]
VMAT["voxel-material
WGSLシェーダ"]
ENV["sky / clouds /
particles / highlight"]
end
subgraph WORLDL["ワールド生成・照明層 ─ 714行"]
direction LR
WORLD["world
ブロック保管庫"]
GEN["worldgen
地形の計算"]
WLGT["world-light
明るさの計算"]
end
subgraph BASE["基盤層 ─ 476行"]
direction LR
BLK["blocks
ブロック図鑑"]
NOISE["noise
決定論ノイズ"]
UTIL["raycast / audio /
constants"]
end
MAIN --> FEATURE
FEATURE --> RENDER
RENDER --> WORLDL
WORLDL --> BASE
矢印は「上の層が下の層を利用する」という依存の向きです。下の層ほど土台に近く、constants.ts(チャンクの寸法などを決める定数、わずか18行)と noise.ts は他に何も依存しません。逆に minecraft.ts は組み立てるだけで、ゲームのルールを1つも持っていません。この「薄い入口+役割ごとの部品」という形は、大きなプログラムを読むときの目印になります。
おすすめの読書順は、①constants.ts(18行)→ ②blocks.ts → ③worldgen.ts → ④mesher.ts → ⑤minecraft.ts。小さい部品から読むと、指揮者のコードが「知ってる部品の組み立て」に見えてきます。
第5章
起動の流れ
ページを開いてから世界が表示されるまで、プログラムは5つのステップを順番に進みます。
flowchart TD
A["① エンジンとシーンを用意
createEngine / createSceneContext"]
B["② テクスチャの台紙づくり
Kenney の PNG を fetch して1枚のアトラスに合成"]
C["③ 世界と部品を生成
World・レンダラー・水・落下・動物・HUD…"]
D["④ warmAround:準備運動
海を先に満たし、周囲13×13チャンクを一気にメッシュ化"]
E["⑤ registerScene → startEngine
描画ループ開始!"]
A --> B --> C --> D --> E
ポイントは④の warmAround() です。ふだんチャンクのメッシュ化は「1フレームに3個まで」という予算で少しずつ進みますが、最初の1回だけは予算を無制限にして、プレイヤーの周囲をぜんぶ作り切ってからループを始めます。おかげで、世界が虫食いのままチラチラ現れる「ポップイン」が起きません。
同時に、海の底に口を開けた洞窟へ先に水を流し込んでおきます(プリフラッド)。これをサボると、海中なのに空気で満たされた不自然な「乾いた穴」が最初のフレームに見えてしまうのです。
第6章
地形のつくりかた
担当:worldgen.ts(197行)と noise.ts(82行)。世界のすべては seed という1つの数字(このデモでは 1337)から計算されます。
flowchart TD
S["seed と座標 (x, z)"] --> H["高さを決める
大陸+丘+細かい凸凹の3層ノイズ"]
S --> T["気温と湿度を決める
べつの低周波ノイズ2枚"]
H --> B["バイオーム判定
海・砂浜・砂漠・平原・森・雪"]
T --> B
B --> F["柱を下から積む
岩盤→石(鉱石まじり)→土→表面ブロック"]
F --> C["洞窟をくり抜く
3Dノイズが濃い場所を空気に"]
C --> W["海面より低ければ水を注ぐ"]
W --> V["木とサボテンを植える"]
バイオームの決まりかた
高さと「気温・湿度」の2つのノイズから、上から順に条件を当てはめて決まります。
| 条件(上から順に判定) | バイオーム |
|---|---|
| 高さが海面より低い | 🌊 海 |
| 海面すれすれ(±1ブロック) | 🏖️ 砂浜 |
| 海面から38ブロック以上高い | 🏔️ 雪山(山頂だけ雪化粧) |
| 気温 < 0.28 | ❄️ 雪原 |
| 気温 > 0.55 かつ 湿度 < 0.42 | 🏜️ 砂漠 |
| 湿度 > 0.55 | 🌲 森 |
| それ以外 | 🌾 平原 |
木の幹が隣のチャンクに生えていても、葉っぱはこちらのチャンクにはみ出してきます。このデモは自分のチャンクの外周2ブロックまで「木が生えるか」を余分に調べて、はみ出す葉を正しく描きます。地形の計算が seed と座標だけで決まる純粋な関数なので、隣のチャンクのデータを見なくても答えが必ず一致する──決定論のご利益です。
鉱石は深さで出やすさが変わります。ダイヤは深さ14未満でおよそ0.8%、金は22未満で1%、鉄と石炭はどこでも数%。洞窟は海面+8より深い場所だけに掘られるので、山の中腹に不自然な横穴が開くことはありません。
第7章
光のしくみ
担当:world-light.ts(317行)。洞窟に潜ると暗くなり、グロウストーンを置くと周りが照らされる。あの「Minecraft らしい光」の正体です。
各ボクセルは 0〜15 の明るさを2種類持ちます。空から届く「スカイライト」と、光るブロックが放つ「ブロックライト」です。光は1ブロック進むごとに1ずつ弱まりながら、波紋のように広がります(BFS)。この2つの値を1バイトに詰めて、チャンクごとにキャッシュします。
flowchart TD
A["パスA:柱ごとに「空の見える床」を探す
上から見て最初の不透明ブロックの1つ上"]
B["パスB:床より上を明るさ15で一括塗り"]
C["影との境目のセルだけをBFSの出発点にする
まっ平らな地表は伝播不要なのでスキップ"]
D["BFSで1ずつ弱めながら広げる
真下方向だけは弱まらない(日光の柱)"]
E["中央チャンク分を切り出してキャッシュ"]
A --> B --> C --> D --> E
1チャンクの光を計算するとき、周囲16ブロックのマージンを含めた 48×48 の範囲をまとめて計算します。光は最大でも15ブロックしか届かないので、マージンの外の光がこのチャンクに影響することは原理的にありえません。つまり隣のチャンクの光の計算結果を待つ必要がなく、どんな順番で計算してもチャンクの境目に光の段差(シーム)が出ないのです。
さらに面白いのが _anyEmitters という1個のフラグです。自然の地形には光るブロックが1つも生成されないので、プレイヤーが初めてグロウストーンを置くまで、ブロックライトの計算はまるごとスキップされます。「起きないことは計算しない」──怠惰は美徳、という好例です。
第8章
ブロックが絵になるまで
数字の並び(ブロックID)が画面のピクセルになるまで、データは5つの部品をバケツリレーされます。
flowchart LR
A["worldgen
ノイズから地形"] --> B["world
ブロックの保管庫"]
B --> C["world-light
明るさを計算"]
C --> D["mesher
見える面だけ三角形に"]
D --> E["chunk-renderer
GPUへ登録"]
E --> F["voxel-material
WGSLシェーダで着色"]
mesher:三角形をケチる職人
16×16×96 のチャンクには最大 24,576 個のブロックが入りますが、実際に描くのは「空気に触れている面」だけ(面カリング)。さらに各頂点に、周囲のブロック配置から計算した AO(隅の暗さ)を焼き込みます。芸が細かいのは、四角形を2つの三角形に割るとき、明るい頂点同士を結ぶ向きに対角線をひっくり返すこと。これで AO のグラデーションに折り目が出ません。
頂点カラーは4項目の「メモ用紙」
各頂点の色(RGBA)には、色ではなく計算結果のメモが詰められています。
| チャンネル | 入っているもの |
|---|---|
| R | AO の暗さ(0.5〜1.0 の掛け算係数) |
| G | スカイライト(空からの光がどれだけ届くか) |
| B | ブロックライト(グロウストーンの光) |
| A | 「これは水/揺れる葉っぱ」フラグ |
voxel-material:1本のシェーダ、3つの顔
WGSL シェーダは1本だけ書かれていて、テンプレートの差し替えで3変種が生まれます。opaque(石や土。ふつうに描く)、cutout(葉やサボテン。透明部分を切り抜き、A フラグが立った葉だけ風に揺れる)、blend(水・ガラス・氷。半透明で、A フラグが立った水面だけ頂点が波打つ)。太陽の向きによる面の明暗はシェーダ側で毎フレーム計算するので、夕日が差すと西向きの面だけ赤く染まります。仕上げに距離に応じた霧をかけ、遠くのチャンクの生成境界を上手に隠します。
第9章
毎フレームの仕事
1秒間に約60回、決まった順番で世界が更新されます。順番そのものが設計です。
flowchart TD
P["① player.update
移動・視線・壊す/置く"] --> S["② falling / water
砂の落下と水の流れ"]
S --> Q["③ renderer.processQueue
新しいチャンクを3個までメッシュ化"]
Q --> L["④ lighting.tick
太陽を少し動かし、空と材質の色を更新"]
L --> M["⑤ mobs / clouds / particles
動物・雲・粒の更新"]
M --> H["⑥ 空をカメラに追従・HUD更新"]
H -.次のフレームへ.-> P
「予算」の一覧表
このデモの秘訣は、重い処理すべてに上限(予算)が設定されていることです。仕事が予算を超えたら、残りは翌フレームに持ち越します。
| 処理 | 1フレームの予算 |
|---|---|
| チャンクのメッシュ化 | 3チャンクまで(起動時のみ無制限) |
| 光の計算(3×3領域のフラッド) | 4回まで |
| 水の伝播 | 4ステップ・1ステップ8,192セルまで |
| 落下ブロックの支え確認 | 1,024セルまで |
| 動物 | 同時に10体まで |
タブを裏に回して戻ってくると、経過時間(dt)が何秒にもなります。そのまま物理計算すると砂が地面を突き抜けたりするので、dt は最大 0.1 秒に切り詰めてから使います。ゲームループの定番の安全策です。
第10章
壊す・置くと何が起きる?
左クリックの一瞬に、5つのシステムが連鎖して動きます。ゲームで最も「密度の高い」瞬間です。
flowchart TD
CLICK["左クリック"] --> RAY["raycast:視線の先のブロックを特定
最大7ブロック先まで、格子を1マスずつたどる"]
RAY --> SET["world.setBlock(空気)
変更を「編集差分」として記録"]
SET --> INV["光キャッシュを無効化
チャンクの境目なら隣も"]
SET --> RM["remeshEdit:触れたチャンクを作り直し
境目なら隣も(AOが変わるため)"]
SET --> WN["water.onBreak
穴に水が流れ込むかも?"]
SET --> FN["falling:真上をチェック
砂や砂利なら落ちはじめる"]
SET --> FX["粒エフェクト+効果音
音はWebAudioでその場で合成"]
初期の実装では1ブロック掘るたびに周囲3×3チャンクの光とメッシュを全部作り直していて、掘るたびにカクついたそうです(コメントに経緯が残っています)。現在は「編集がチャンクの端に触れたときだけ、その方向の隣を作り直す」方式。内側の編集なら作り直しは1チャンクで済みます。パフォーマンス改善の実録として読みごたえがあります。
第11章
水と砂の物理
担当:water-sim.ts(331行)と falling-blocks.ts(260行)。どちらも「イベントが起きた場所だけ調べる」省エネ設計です。
水:1リングずつ広がる「世代」方式
flowchart LR
E["ブロックを壊した/置いた"] --> Q["そのセルと隣6方向を
キューに積む"]
Q --> S["0.2秒ごとに
今の世代をまとめて評価"]
S --> N["新しく水になったセルの隣は
「次の世代」へ繰り越し"]
N -.1ブロックの輪ずつ広がる.-> S
毎フレーム全部の水を調べるのではなく、変化が起きた場所の周りだけをキューに積み、0.2秒に1回「1世代」ずつ進めます。新しく水になったセルの影響はわざと次の世代に持ち越すので、海底に穴を掘ると水が1ブロックずつ輪になって迫ってくる、あの Minecraft らしい見た目になります。
横方向への広がりは「海面より下」に限定されているのもポイントです。おかげで氾濫は必ず壁と海面で止まり、世界の果てまで水が暴走することがありません。
砂と砂利:エンティティになって落ちる
支えを失った砂は、ブロックのデータから一度取り除かれて「落下中の小さな箱メッシュ」に変身し、重力で落ちて、着地した瞬間にまたブロックへ戻ります。コードのコメントには、ボクセルゲームの落下物にありがちな3大バグへの対策が明記されています。
1フレームで大きく落ちても、着地点は「下に向かって最初の固体」を走査して決めるので、地面を突き抜けません。
置く直前にもう一度そのマスが空きか確認。2粒が同時に着地しても同じマスを取り合いません。
低い位置の粒から順に処理し、着地は即座にブロック化。砂の柱が1フレーム内で正しく積み上がります。
第12章
動物たち
担当:mobs.ts(491行、最大のファイル)。牛・豚・羊・にわとりが草原をのんびり歩き回ります。
驚くべきことに、動物の画像素材は1枚もありません。胴体・頭・足という「箱」をコードで組み立て、単色の頂点カラーで塗っています。足は股関節(ピボット)を軸に前後へ振れるので、ちゃんと歩いて見えます。まさに Minecraft の動物のレシピそのものです。
| 仕様 | 値 |
|---|---|
| 種類 | 牛・豚・羊・にわとり の4種 |
| 同時に存在できる数 | 最大10体 |
| 現れる場所 | プレイヤーから10〜44ブロック先の地面(約1.4秒ごとに抽選) |
| いなくなる距離 | 96ブロック以上離れると消える |
専用シェーダの mob-material.ts は地形と同じ太陽・環境光で動物を照らしますが、1つだけ違いがあります。動物は向きを変えて回転するので、法線(面の向き)をワールド行列で変換してから光を計算するのです。地形のチャンクは絶対に回転しないためこの計算を省略できる──「なぜ地形側にはこの処理がないのか」の答えまでコメントに書かれています。
第13章
セーブ&ロード
担当:save-load.ts(118行)。世界まるごとの保存なのに、ファイルは驚くほど小さくなります。
// セーブデータの中身、これで全部! interface SaveData { v: 1; // フォーマットのバージョン seed: number; // 地形の「種」 time: number; // 時刻(0〜1) player: PlayerState;// 位置と視線の向き edits: number[]; // [x, y, z, ブロックID, ...] 編集した場所だけ }
地形は seed から毎回同じものが計算できるので、保存する必要がありません。保存するのはプレイヤーが手を加えた差分だけ。1万個ブロックを積んでも、数値4個×1万=わずか数百KBです。水の氾濫や動物の位置すら保存しません──読み込み後にルール通り再計算すれば同じ状態に戻るからです。
ファイル保存には新しい File System Access API を使い、対応していないブラウザ(Firefox や Safari)では自動的にダウンロード方式へ切り替わります。読み込み後は起動時と同じ warmAround() を呼び直すだけで、「水は満ちていてメッシュも完成している」という保証がそのまま再現されます。
第14章
3つの設計テーマ
4,669行を貫くのは、この3つの考え方です。自分のプロジェクトにもそのまま持ち帰れます。
地形は seed から必ず同じに再現される。だからセーブは差分だけでよく、チャンクの継ぎ目は自然に一致し、捨てたチャンクも安心して作り直せる。1つの性質が3つの問題を同時に解いています。
テクスチャは CC0 素材を実行時に取得、動物は箱の組み立て、効果音は WebAudio で合成。リポジトリにバイナリを1つも増やさない潔さです。
メッシュ化・光・水・落下、すべてに「1フレーム◯回まで」。どんな大仕事も数フレームに分割され、体感のなめらかさが最優先されます。
このコードのコメントには「以前は◯◯していたが、それが最大のボトルネックだったので△△に変えた」という改善の経緯が随所に残されています。完成形だけでなく試行錯誤ごと読めるのが、このサンプル最大の教材価値かもしれません。
第15章
ファイル一覧(全25ファイル)
コードリーディングのおともに。行数は目安として。
ファイル名は GitHub 上の該当ファイルへのリンクです(別タブで開きます)。
| ファイル | 行数 | やくわり |
|---|---|---|
| エントリポイント | ||
minecraft.ts | 236 | 全部品の組み立てとフレームループ。ゲームのルールは持たない |
| 基盤層 | ||
constants.ts | 18 | チャンク寸法(16×16×96)や海面の高さ(30)などの定数 |
noise.ts | 82 | seed から同じ模様を再現する決定論ノイズ(fBm) |
blocks.ts | 180 | ブロック図鑑。23種類の見た目と性質をフラグで宣言 |
raycast.ts | 71 | 視線の先のブロックを探す格子レイキャスト |
audio.ts | 125 | 壊す・置く・足音の効果音を WebAudio でその場で合成 |
| ワールド生成・照明層 | ||
worldgen.ts | 197 | ノイズ→バイオーム→地形・洞窟・鉱石・木の生成 |
world.ts | 200 | チャンクの保管庫。プレイヤーの編集を差分として記録 |
world-light.ts | 317 | スカイライト+ブロックライトの2チャネル光計算 |
| 描画・マテリアル層 | ||
atlas.ts | 97 | ブロックのPNGを1枚のテクスチャ台紙にまとめる |
mesher.ts | 223 | 見える面だけ三角形化。頂点AOと対角線の工夫つき |
voxel-material.ts | 234 | WGSLシェーダ本体。不透明・切り抜き・半透明の3変種 |
chunk-renderer.ts | 255 | チャンクの出し入れ係。予算管理と安全な後片付け |
sky.ts | 121 | 空のドーム。地平線から天頂へのグラデーションと太陽 |
clouds.ts | 127 | 頭上を流れる雲。1枚の板にノイズで模様を描く |
particles.ts | 132 | ブロック破壊時に飛び散る小さな粒 |
highlight.ts | 78 | ねらったブロックを囲う光る枠 |
mob-material.ts | 117 | 動物用シェーダ。頂点カラーをそのまま体の色に |
| 機能・ゲームロジック層 | ||
controls.ts | 374 | 操作・歩行・ジャンプ・泳ぎ・ブロックとの衝突判定 |
water-sim.ts | 331 | 水の流れ。1リングずつ広がる世代方式 |
falling-blocks.ts | 260 | 砂・砂利の落下。すり抜け/重なり/柱くずれ対策つき |
mobs.ts | 491 | 動物の組み立て・出現・お散歩AI・歩行アニメ |
lighting.ts | 149 | 昼夜サイクルの司令塔。太陽・空・霧の色を一元管理 |
hud.ts | 136 | 照準・ホットバー・デバッグ表示。DOMだけで実装 |
save-load.ts | 118 | 世界のファイル保存と読み込み |