voxelized-js 技術仕様書
概要: ボクセル空間をリアルタイム描画するためのストリーミングエンジン
voxelized-js は、大規模なボクセル空間を Web ブラウザ上でリアルタイム描画するためのライブラリである。 256³ ボクセル単位の「Region」を空間分割の基本単位とし、カメラ位置に応じて必要な Region だけを動的にロード・描画する仕組みを提供する。 Web Worker による非同期処理と優先度付きタスクキューにより、メインスレッドをブロックせずに Atlas 画像の取得・デコード・メッシュ生成を行う。
┌────────────────────────────────────────────────────────────┐
│ voxelized-js Architecture │
├────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ │
│ │ Camera │ │
│ │ (viewport) │ │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Scene │ │
│ │ ┌──────────┐ ┌──────┐ ┌──────────┐ │ │
│ │ │ Vis │ │ Mesh │ │ Slots │ │ │
│ │ │(culling) │ │(vtx) │ │(tex ctrl)│ │ │
│ │ └──────────┘ └──────┘ └──────────┘ │ │
│ │ ┌──────────┐ │ │
│ │ │ Store │ │ │
│ │ │(Rgn ctrl)│ │ │
│ │ └────┬─────┘ │ │
│ └───────┼────────────────────────────────┘ │
│ ┌────┴────┐ │
│ ▼ ▼ │
│ ┌────────┐ ┌─────────┐ │
│ │ Queue │ │ Worker │──▶ CDN/R2 Storage │
│ │(Task) │ │(off-thr)│ (Atlas delivery) │
│ └────┬───┘ └─────────┘ │
│ ▼ │
│ ┌─────────┐ │
│ │ Region │ │
│ │(unit) │ │
│ └─────────┘ │
└────────────────────────────────────────────────────────────┘
空間モデル: Web Mercator タイルとボクセル Region の対応関係
空間は Web Mercator 座標系のタイル (z=17) と 1 対 1 で対応する Region に分割される。 各 Region は 256×256×256 ボクセルを保持し、1 ボクセルは実世界で約 1m に相当する。 Region の識別子は Web Mercator の (i, j) 座標から一意に決定される。
Web Mercator Tile Coordinates (z=17)
┌────────────────────────────────────────────────────────┐
│ (116358, 51619) ─────────────── (116467, 51619) │
│ │ │ │
│ │ ┌─────┬─────┬─────┐ │ │
│ │ │ R │ R │ R │ │ ← Each cell │
│ │ ├─────┼─────┼─────┤ │ is 1 Region │
│ │ │ R │ cam │ R │ │ (256³) │
│ │ ├─────┼─────┼─────┤ │ │
│ │ │ R │ R │ R │ │ │
│ │ └─────┴─────┴─────┘ │ │
│ │ │ │
│ (116358, 51626) ─────────────── (116467, 51626) │
└────────────────────────────────────────────────────────┘
| 定数名 | 値 | 説明 |
|---|---|---|
| REGION | 256 | 1 Region の一辺のボクセル数 |
| SLOT | 16 | 同時に保持可能な Region テクスチャ数 |
| PREBUILD | 4 | カメラ外でメッシュを事前生成する Region 数 |
| PREFETCH | 4 | カメラ外で画像を事前取得する Region 数 |
| PREPURGE | 32 | メモリに保持する Region の最大数 |
| MAX_RETRY | 3 | 永続エラー状態になるまでのリトライ回数 |
データフロー: Atlas 画像からメッシュ描画までの変換過程
Atlas 画像は 3D Morton 曲線を 2D Morton 曲線でマッピングした PNG である。 Worker スレッドで画像をデコードし、Greedy Meshing によりインスタンス描画用データを生成する。
┌────────────────────────────────────────────────────────────────────────────┐
│ Data Transformation Pipeline │
├────────────────────────────────────────────────────────────────────────────┤
│ CDN/R2 Worker Thread Main Thread │
│ ────── ───────────── ─────────── │
│ ┌─────────┐ ┌──────────────────────────┐ ┌─────────────────────┐ │
│ │ Atlas │ ───▶ │ 1. fetch (get PNG) │ │ 6. merge (combine) │ │
│ │ PNG │ │ 2. createImageBitmap │ │ 7. commit (finalize)│ │
│ │4096×4096│ │ 3. getImageData │ ───▶ │ 8. draw (render) │ │
│ └─────────┘ │ 4. atlas2occ (Morton inv)│ └─────────────────────┘ │
│ │ 5. greedyMesh (WASM) │ │
│ └──────────────────────────┘ │
│ Output Format: │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ bitmap: ImageBitmap (for texture) │ │
│ │ occ: Uint8Array[256³] (for collision detection) │ │
│ │ mesh: { pos: Float32Array, scl: Float32Array, cnt: number } │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
優先度スケジューリング: カメラ位置に基づく動的タスク制御
Queue はタスクを high/low の 2 つのバケットで管理し、カメラ移動に応じて優先度を動的に変更する。 visible (カメラ内) > prebuild (カメラ外近距離) > prefetch (カメラ外遠距離) の順で処理される。
┌───────────────────────────────────────────────────────────────────────┐
│ Priority and State Transitions │
├───────────────────────────────────────────────────────────────────────┤
│ Priority │
│ 3 ┌─────────────────────────────────────┐ │
│ │ visible (in camera) │ ← full mode, immediate │
│ 2 ├─────────────────────────────────────┤ │
│ │ prebuild (pre-generate mesh) │ ← full mode, up to 4 │
│ 1 ├─────────────────────────────────────┤ │
│ │ prefetch (pre-fetch image) │ ← image mode, up to 4 │
│ 0 ├─────────────────────────────────────┤ │
│ │ (no task) │ │
│ -1 ├─────────────────────────────────────┤ │
│ │ abort (cancel task) │ ← moved away from camera │
│ └─────────────────────────────────────┘ │
│ │
│ Concurrency Limits: │
│ high (priority > 0): max 4 concurrent tasks │
│ low (priority ≤ 0): max 1 concurrent task │
└───────────────────────────────────────────────────────────────────────┘
| 優先度 | 状態 | モード | 処理内容 |
|---|---|---|---|
| 3 | visible | full | 画像取得 + メッシュ生成 + 描画 |
| 2 | prebuild | full | 画像取得 + メッシュ生成 (描画待ち) |
| 1 | prefetch | image | 画像取得のみ |
| -1 | abort | - | 進行中タスクを中止 |
Region ライフサイクル: 生成から破棄までの状態管理
Region は level (処理完了度)、request (現在の要求)、isError (永続エラー) の内部状態を持つ。 level は 'none' → 'image' → 'full' と進行し、MAX_RETRY (3) 回失敗すると 'none' → 'error' になる。 カメラから離れると dispose により 'none' に戻る。
┌───────────────────────────────────────────────────────────────────────┐
│ Region State Transition Diagram │
├───────────────────────────────────────────────────────────────────────┤
│ tune('image', 1) │
│ ┌───────────────────────────────────────────────┐ │
│ │ ▼ │
│ ┌────┴──┐ tune('full', 2) ┌──────────┐ Worker ┌───────┐ │
│ │ none │ ───────────────────────▶ │ fetching │ ──────────▶ │ image │ │
│ └───────┘ └────┬─────┘ └───┬───┘ │
│ ▲ dispose() │ tune('full', 3) │ │
│ │ ▼ ▼ │
│ ┌────┴───┐ tune('none', -1) ┌──────────┐ Worker ┌──────┐ │
│ │ purged │ ◀────────────────────── │ building │ ──────────▶ │ full │ │
│ └────────┘ └────┬─────┘ └──────┘ │
│ ▲ │ fail 3x │
│ │ dispose() ▼ │
│ │ ┌───────┐ │
│ └─────────────────────────────│ error │ ← skip render │
│ └───────┘ │
│ Internal Variables: │
│ level = 'none' | 'image' | 'full' | 'error' ← completion state │
│ request = 'none' | 'image' | 'full' ← current request │
│ ticket = number ← request ID (ignore stale) │
│ isError = boolean ← permanent error flag │
│ retry = number ← failures before error │
└───────────────────────────────────────────────────────────────────────┘
Slot 管理: テクスチャユニットの割り当てと再利用
Slot は Region とテクスチャスロットの対応を 管理する (デフォルト 16 スロット)。 visible な Region に順次割り当て、SlotUpdate オブジェクトを生成する。 テクスチャのアップロードは利用側 (glre, THREE.js 等) が SlotUpdate を受け取って実行する。 カメラ外に出た Region のスロットは解放され、新しい Region に再割り当てされる。
┌────────────────────────────────────────────────────────────────────────┐
│ Slot Allocation Structure │
├────────────────────────────────────────────────────────────────────────┤
│ Slot Array Regions │
│ ────────── ──────── │
│ slot[0] ◀───────────────────────▶ Region(i,j) │
│ slot[1] ◀───────────────────────▶ Region(i,j) │
│ ... │
│ slot[15] ◀───────────────────────▶ Region(i,j) │
│ │
│ SlotUpdate Output: { at: number, atlas: ImageBitmap, offset: vec3 } │
│ │
│ Processing Flow (step function): │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 1. Iterate through pending array │ │
│ │ 2. Skip fetching Regions (hasPending = true) │ │
│ │ 3. Skip error Regions (if isError() is true) │ │
│ │ 4. Find empty slot and assign Region │ │
│ │ 5. Produce SlotUpdate { at, atlas, offset } for consumer │ │
│ │ 6. Combine vertex data with mesh.merge │ │
│ │ 7. On all Regions complete: mesh.commit → reflect in render │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘