[Noise 入門 #41] 第5集開幕 — 終わらない世界を錬成する(Minecraft的なボクセル地形とチャンクの基礎)

はじめに

宇宙の創造を終えた私たちは、再び視点を「大地」へと戻します。

これまでの第4集までは、球体や平面の「頂点(Vertex)」をノイズで歪ませることで地形を表現してきました。しかし、今回から始まる第5集「Procedural World編」では、アプローチが根本から変わります。滑らかなポリゴンではなく、離散的なブロック(Voxel)を積み上げて世界を構築するのです。

単なるVFXを超え、「ゲームエンジン開発者」の領域へと足を踏み入れる第一歩。今回は、無限に広がるボクセル世界を生成するための「概念と設計図」を解き明かします。

前回の記事:

ボクセル(Voxel)という新たなキャンバス

神の視点で宇宙を創り上げた私たちは、舞台を再び「大地」へと戻し、今度はその地上を自らの足で歩くための世界を錬成していきます。

その新たなキャンバスとなるのが「ボクセル(Voxel)」です。

ボクセル(Voxel)とは何か?

ボクセルとは、「Volume(体積)」と「Pixel(画素)」を組み合わせた造語です。 スマートフォンの画面や2D画像がピクセルの集まりでできているように、3D空間を均等な立方体の「グリッド(格子)」で分割した最小単位を指します。一番直感的に分かりやすい例は、大ヒットゲーム『Minecraft』の世界構成そのものです。

これまでの第4集まで扱ってきた地形表現と、今回のボクセル表現には、根本的かつ決定的な違いがあります。

  • Surface(これまでの地形) 連続したポリゴンの頂点(Vertex)のY軸座標を、ノイズの値で上下させることで起伏を作ります。一枚の皮(メッシュ)を被せているだけなので、「表面」しか存在しません。もし地形に穴を開けて地下を覗き込んだら、そこにはポリゴンの裏側のスッカスカな空間(虚無)が広がっています。
  • Volume(今回の地形) 空間上の特定の3Dグリッド座標 (x, y, z) に対して、「そこにブロック(データ)が存在するかどうか」、そして「それが何のブロックなのか」を判定して配置します。空間をブロックの集合体として扱うため、「中身(体積)」が存在します。地面を掘ればそこには土があり、さらに掘れば石や鉱石がギッシリと詰まっているのです。

ノイズとグリッドの親和性

ここで重要になるのが、「空間を座標のグリッドとして捉える」という考え方です。

鋭い方はお気づきかもしれませんが、これは第1集で学んだ「Perlin Noise」や「Value Noise」の基礎と完全に一致しています。ノイズのアルゴリズム自体が、そもそも整数のグリッド(格子点)を基準にして、その間の空間を数学的に補間することで滑らかな値を生成していました。

つまり、私たちがこれまで学んできたノイズ関数は、本質的にボクセルという概念と非常に相性が良いのです。

💡 Point: 滑らかさ(連続)からブロック(離散)へ
これまではノイズの滑らかさをそのまま活かして「シームレスな地形や雲」を作ってきました。しかしボクセルの世界では、ノイズが弾き出した滑らかな値を、あえて「ブロックという単位(整数)」に区切る(量子化する)ことで世界を構築していきます。

2Dノイズからブロックを積み上げる(Heightmap)

ボクセル地形を作る最もシンプルかつ王道なアプローチは、第1集・第2集で徹底的に学んだ 2DのFBM(Fractal Brownian Motion) を「ハイトマップ(高さ情報)」として利用することです。

これまでのWebGLやThree.jsでの地形表現(Surface)では、平面の各頂点に対してノイズを適用していました。しかし、ボクセルの世界(Volume)では、空間上の水平なグリッド座標 $(x, z)$ を入力としてノイズを評価し、その値を「ブロックを積み上げる最大の高さ(整数)」に変換します。

これを数式で表すと、以下のようになります。

$$y_{height}=\lfloor\text{FBM}(x,z)\times\text{amplitude}\rfloor$$

このシンプルながら美しい数式が、無限の起伏を生み出す根源です。それぞれの要素を分解してみましょう。

  • $\text{FBM}(x,z)$: 水平方向の座標から、自然な起伏を持つノイズ値(例:0.0 〜 1.0)を取得します。
  • $\text{amplitude}$(振幅): 地形の最大高さを決定します。たとえばこれを 64 に設定すれば、最大で高さ64ブロックの山ができます。
  • $\lfloor\dots\rfloor$(床関数 / Floor): ここが最も重要なポイントです。連続的な(小数点を持つ)ノイズの値を切り捨て、離散的な「整数(グリッド)」に落とし込みます。

連続から離散へ:空間を「配列」に格納する

私たちは $1.73$ 個のブロックを置くことはできません。ブロックは常に $1$ 個、$2$ 個と整数で存在する必要があります。だからこそ、床関数 $\lfloor\dots\rfloor$ による量子化(丸め込み)が不可欠なのです。

最大高さ $y_{height}$ を求めたら、あとはプログラムの力業です。 $y=0$(岩盤)から $y_{height}$(地表)までのすべての座標に対して、ループ処理( for 文など)を回し、愚直にブロックのデータを配列に格納していきます。

【頭の中のアルゴリズム・イメージ】

  1. ある $(x, z)$ 座標の地形の高さ $y_{height}$ をノイズで計算する(例: 24 )。
  2. $y=0$ から $y=24$ まで、縦にブロックを積み上げる処理を行う。
  3. 深い場所($y=0$ 〜 $15$)には「石ブロック」のIDを割り当てる。
  4. 少し浅い場所($y=16$ 〜 $23$)には「土ブロック」のIDを割り当てる。
  5. 一番上の地表($y=24$)には「草ブロック」のIDを割り当てる。

これをすべての $(x, z)$ グリッドで繰り返すことで、中身がギッシリと詰まった「起伏のある大地」が生成されます。

💡 Point: 表面を描画するのではなく、世界を定義する
ノイズを使って「見た目(ポリゴン)」を歪めるのではなく、ノイズを使って「世界のデータ構造(ブロックの配列)」を定義している点に注目してください。これが、グラフィックプログラマーからゲームエンジン開発者へと視点が切り替わった証です。

無限を生み出す「チャンク(Chunk)」の魔法

さて、ブロックの高さ(データ)を配列に格納するロジックは完成しました。 しかし、ここでゲームエンジン開発者として、直面せざるを得ない物理的な限界があります。

それは「メモリ(RAM/VRAM)」と「計算リソース」の枯渇です。

無限に広がる広大な世界を、たった一度のロードで全て生成し、描画することは不可能です。もしそんなことをすれば、あっという間にメモリがパンクし、どんなにハイスペックなPCでもブラウザはクラッシュしてしまいます。

ここで必要になるのが、「チャンク(Chunk)」と呼ばれる空間分割とメモリ管理の概念です。

空間を切り分ける「柱」

チャンクとは、巨大な世界を効率よく管理するために、空間を一定サイズのブロック群に切り分けた単位のことです。Minecraftをはじめとする多くのボクセルゲームで採用されている、デファクトスタンダードな手法です。

  1. 空間の分割(例:16 × 16 × 256) 世界全体を、例えば「横幅16ブロック、奥行き16ブロック、高さ256ブロック」の細長い柱(チャンク)に分割して管理します。1つのチャンクには最大で $65,536$ 個のブロックデータが含まれることになります。
  2. プレイヤー位置の監視とローディング プレイヤーの現在の $(X, Z)$ 座標を常に監視します。そして、プレイヤーの周囲(例えば「半径8チャンク以内」)にあるチャンクだけを、ノイズ関数を使って「オンデマンド(必要な時だけ)」で生成し、メモリにロードして描画します。
  3. 破棄(アンローディング)のサイクル プレイヤーが移動し、遠ざかって視界(あるいは設定した半径)から外れたチャンクは、不要なデータとして速やかにメモリから削除(アンロード)、または非表示にします。

この「生成(ロード)」と「破棄(アンロード)」のサイクルを背後で絶えず回し続けることで、計算上はどこまで歩いても決して終わることのない「無限の地形」が実現するのです。

ノイズの「決定論的」な性質がもたらす奇跡

ここで、第1集で学んだノイズの根源的な性質が、チャンクシステムにおいて圧倒的な強みを発揮します。 それは、ノイズ関数が「決定論的(Deterministic)」であるということです。

ノイズ関数は、同じシード値(Seed)と、同じ座標 $(x, z)$ を入力すれば、未来永劫、何度計算しても「全く同じ値(高さ)」を返します。

これが何を意味するか分かりますか?

プレイヤーがあるチャンクから遠ざかり、そのチャンクがメモリから完全に破棄されたとします。その後、プレイヤーが再びその場所に戻ってきたとき、システムは再び同じ座標 $(x, z)$ をノイズ関数に入力して地形を再計算します。 すると、前回と一寸の狂いもない、全く同じ地形が再び生成されるのです。

💡 Point: 記憶しなくていい。計算すればいい。
ギガバイト単位の膨大な地形データをハードディスクに保存(セーブ)しておく必要はありません。地形の形はすべて「数式(ノイズ関数)」の中に刻み込まれているため、プレイヤーが近づいた瞬間に、その場で数式を解いて世界を復元すれば良いのです。これこそが「プロシージャル生成(Procedural Generation)」の真骨頂です。

Three.js での壁:数万のブロックをどう描画するか?

概念とデータ構造はシンプルです。しかし、いざ Three.js で実装しようとすると、ゲームエンジン開発者として、私たちは「レンダリング・パフォーマンス」という、第4集までとは全く異なる、より巨大で無慈悲な壁に直面することになります。

もし、1つのブロックを1つの THREE.Mesh として、シーンに追加しようとすれば、ブラウザは瞬時にフリーズするでしょう。

ドローコール(Draw Call)の爆発

なぜMeshを愚直に増やすと、フリーズするのでしょうか? その最大の原因は、「ドローコール(Draw Call)」という、CPUからGPUへの描画命令の回数です。

通常、1つの THREE.Mesh を描画するためには、CPUがGPUに対して「このジオメトリ(形状)を、このマテリアル(質感)で、ここに描画して!」という命令(ドローコール)を1回送る必要があります。

ここで、具体的な数字を計算してみましょう。

  • 1チャンク(16 × 16)の「表面」: これをブロックで敷き詰めるだけでも、$16 \times 16 = 256$ 個の Mesh が必要です。つまり、256回のドローコールが発生します。これくらいなら、まだブラウザは動くでしょう。
  • プレイヤーの視界(例:半径8チャンク): チャンクの数は、$17 \times 17 = 289$ チャンクになります。
  • 合計のドローコール: $256(ブロック/チャンク) \times 289(チャンク) \approx$ 7万4千回。

たった1フレームを描画するのに、7万4千回もの命令をCPUがGPUに送り続ける。 これが、ボクセル世界の「壁」です。どんなに優れたGPUでも、CPU側の処理が追いつかず、画面は完全に停止します。これがフリーズの正体です。

魔法の技術:THREE.InstancedMesh

この絶望的な状況を解決するために用意された、Three.jsにおける魔法の技術が THREE.InstancedMesh(インスタンス・メッシュ)です。

これは、「まったく同じ形状(Geometry)と、まったく同じマテリアル(Material)」を使い回しつつ、それぞれの「位置や回転、縮尺(Matrix / 行列)」の情報だけを変えて、GPUに一括で送る技術です。

仕組みはこうです。

  1. 準備: 「 BoxGeometry(立方体)」と「 MeshStandardMaterial(土)」をそれぞれ1つずつ用意します。
  2. 生成: THREE.InstancedMesh を作成し、「数万個分のブロックのデータを入れる枠」をGPU上に確保します。
  3. 配置: 配列からブロックの座標 $(x, y, z)$ を読み取り、それぞれのブロックのMatrixを計算して、GPU上の枠にデータをセットします。
  4. 描画: 最後に、CPUはGPUに対して「たった1回」だけ、「この InstancedMesh の全データを描画して!」と命令を送ります。

7万4千回あったドローコールが、極端には「1回」に削減されます。

ドローコールのボトルネックが解消されたGPUは、本来の力を発揮し、数万個、数十万個のブロックを爆速で、それこそヌルヌルと描画できるようになります。

💡 Point: 個別(Mesh)から一括(Instance)へ これまでは「1つのMesh」が「1つのオブジェクト」として独立していました。しかし、ボクセルの世界では、数万個のブロックを「1つの巨大な群れ(Instance)」として扱い、GPUに一括処理させるエンジニアリングが必要不可欠です。

次回予告:Three.js × InstancedMesh で大地を敷き詰める

第41回、お疲れ様でした。 今回は、これまでの「ノイズで絵を描く」フェーズから一歩踏み出し、「ノイズで世界を構築する」ための設計図を俯瞰しました。ボクセル、ハイトマップ、そしてチャンク。これらは現代のオープンワールド・ゲームを支える、最も基本的で強力な概念です。

しかし、理論だけでは世界は動きません。 次回は、いよいよ手を動かしてコードを書き、ブラウザ上に「実体としてのボクセル世界」を顕現させます。

次回のハイライト

  • InstancedMesh の実践セットアップ: 数万個のブロックを1ドローコールで描画するための、具体的な Three.js の実装コードを解説。
  • FBM と座標の接続: 数式 $y_{height} = \lfloor \text{FBM}(x, z) \times \text{amplitude} \rfloor$ を JavaScript 上でどうループ処理に組み込むか。
  • Matrix(行列)の操作: 膨大な数のブロックを正しい位置に「整列」させるための setMatrixAt の使い方。
  • パフォーマンスの体感: 10万個のブロックが 60 FPS でヌルヌル動く快感を、実際のデモを通して体験します。

「鑑賞するノイズ」から、その中に入り込める「構築するノイズ」へ。 第5集の旅は、ここから一気にエンジニアリングの楽しさが加速します。

あなたのブラウザの中に、最初の大地が誕生する瞬間を一緒に作り上げましょう。 第42回、「Three.js × InstancedMesh 実装編」でお会いしましょう。お楽しみに!