/ Technical Architecture Overview

Gint — Technical Architecture

Geographic Interleaved Binary Format
int = interleave(Mortonビットインターリーブ)· integer(64bit整数コード)· intended(設計意図を持ったLOD)
空間インデックス × LODエンコーディング × GPU描画の統合パイプライン

Table of Contents
  1. 従来手法の問題点
  2. パイプライン概観
  3. Stage 1 — エンコーディング:Mortonオーダー × VWウェイト
  4. Stage 2 — GPU VB 書き込み(一度だけ)
  5. Stage 3 — GPUステンシルテッセレーション
  6. 性能比較
  7. 設計思想

1. 従来手法の問題点

ウェブGISの一般的な実装では、地図タイル(スライプトタイル / MVT)を使い、 ズームレベルごとに独立したファイル群をサーバに事前生成して配信する。

問題原因コスト
タイル数の爆発 zoom z での総タイル数は 4z z=14 で 268 M tiles
ズームごとのネットワーク 各ズームで別タイルを取得 ズームのたびに複数リクエスト
CPU三角形分割 ポリゴンは GPU に直接送れない(凹形・穴) フレームごとに earcut O(n²) 実行
LODの静的管理 複数解像度を別ファイルで事前生成 ストレージ × LOD数だけ増加

Gint はこれら 4 つの問題を「1ファイル × ゼロ追加ネットワーク × CPU三角分割なし」で解決する。

2. パイプライン概観

1
エンコーディング(前処理・一度だけ)
GeoJSON → .gint binary(ブラウザ上で実行)
GINT.html
Morton order — フィーチャをZ曲線でソート(空間クラスタリング)
VW weight — 各頂点に三角形面積(重要度)をタグ付け
binary pack — 座標 + ウェイトを固定長バイナリに詰め込む
2
GPU VB 書き込み(クライアント側・一度だけ)
GeoPBF decode → VWビット展開 → GPU 頂点バッファへ一括転送
GeoPBF decode — バイナリから頂点座標・VWビットを展開(Morton 順保持)
VW bits unpack — 各頂点の VW ウェイトを GPU VB フォーマットへ展開
GPU VB upload — 全頂点を一括で GPU 頂点バッファへ転送(以降 CPU 変換ゼロ)
3
GPU LOD + ステンシルテッセレーション(毎フレーム・CPU処理ゼロ)
vertex shader LOD → WebGL stencil → fill
LOD.htmlST.html
VW threshold(z) — vertex shader: zoom に応じた VW 閾値で頂点を破棄(LOD・追加バイト転送ゼロ)
fan triangles — NDC 原点から全頂点へファントライアングルを生成
stencil XOR — 偶奇カウントで凹部・穴を自動判定
color pass — ステンシル奇数部分のみ塗りつぶし

3. Stage 1 — エンコーディング

3.1 Morton Order(Z順序曲線)

Mortonコードは 2 次元座標 (x, y) を 1 次元の整数に変換する手法で、 X のビットと Y のビットを インターリーブ(交互配置) することで生成される。

X = col 3
0
1
1
← 3ビット(8×8グリッドの場合)
Y = row 5
1
0
1
Morton
1
0
0
1
1
1
= 0b100111 = 39
Y₂ X₂ Y₁ X₁ Y₀ X₀ (MSBから交互配置)
// 2D → Morton コード(32bit座標対応)
function morton2D(x, y) {
	let r = 0;
	for (let i = 0; i < 32; i++) {
		r |= ((x >> i) & 1) << (2*i);      // X bit at even positions
		r |= ((y >> i) & 1) << (2*i + 1);  // Y bit at odd positions
	}
	return r;
}

// ビューポート内フィーチャの抽出(ズームレベル z のビットマスク)
// z=6 なら上位12ビット(2×6)がタイルアドレス
const mask = ~(0xFFFFFFFFFFFFFFFFn >> BigInt(2*z));
const tileKey = mortonCode & mask;  // → タイル内の全フィーチャが同じ prefix を共有
Key Property

空間的に近い 2 点は Morton コードが近くなる(必ずしも連続ではないが同一プレフィックスを共有)。 これにより「ビューポート内のフィーチャ」は Morton コード範囲の二分探索で O(log n) で特定できる。 タイル境界を跨ぐ場合も最大 4 回の範囲クエリで対応可能(Z曲線の再帰的分割構造)。

3.2 Visvalingam-Whyatt Weight(LODウェイト)

各頂点に「削除されたとき形状が失う面積」をウェイトとして事前計算し、バイナリに埋め込む。 低ズーム時は高ウェイト頂点のみ使用し、ズームインにつれて閾値を下げ、より細かい頂点を加える。

Area = 大
岬・湾入口(急な方向変化)
High weight → always kept
Area = 中
小湾・入り江
Medium weight
Area ≈ 0
ほぼ直線上の細部
Low weight → LOD max only
weight(vi) = | (xi-1(yi−yi+1) + xi(yi+1−yi-1) + xi+1(yi-1−yi)) / 2 |
← 前後の頂点と作る三角形の面積(px²)
// エンコード時に全頂点のウェイトを計算して保存
function computeVWWeights(polygon) {
	const n = polygon.length;
	return polygon.map((_, i) => triangleArea(
		polygon[(i - 1 + n) % n],
		polygon[i],
		polygon[(i + 1) % n]
	));
}

// ズームレベルに対応する頂点フィルタ(デコード時)
// ズームレベル z における 1px の地図単位換算
// (例: メルカトル投影、タイルサイズ 256px の場合)
const metersPerPx = (40075016 / (256 * Math.pow(2, z)));
const threshold   = metersPerPx * metersPerPx;  // 1px² → これ未満の頂点はサブピクセル
const activeVerts  = vertices.filter(v => v.weight >= threshold);
LOD 閾値は「1px」が自動的に決める

VW ウェイトの単位は スクリーンピクセルの面積(px²) に対応する。 ズームレベル z において、三角形面積が 1px² 未満の頂点は視覚的に サブピクセル ——つまり削除しても描画上の差がゼロ——であり、含める必要がない。 したがってズームごとの LOD 閾値は 「1px が地図座標系で何単位に相当するか」から 物理的に自動導出 される。 設計者がズームレベルごとに手動でしきい値を調整する必要はなく、 スケールが変わるたびに必要十分な頂点数が自然に決まる。

LOD 0(アンカー)と上位 LOD の関係

LOD 0 のアンカー頂点は岬・湾口など形状の骨格を定義し、常に保持される。 LOD 1 以降の頂点は、既存の辺の途中に挿入されるものであり、アンカーを置き換えるわけではない。 そのため各 LOD レベルは前の LOD の厳密な上位集合になっており、 どの閾値でフィルタしても「アンカーが作る骨格」を崩さない有効なポリゴンが得られる。

また VW ウェイトを計算すると、ミッドポイント分割の深さ(LOD 0=アンカー, LOD 4=細部)と 自然に一致する。VW アルゴリズムが「重要な折れ点から順に保持する」設計であるため、 アンカーが最高ウェイトを持ち、細部ほどウェイトが小さくなる。

4. Stage 2 — GPU VB 書き込み(一度だけ)

クライアントはサーバから GeoPBF を受け取り、一度だけ gint 形式(Morton順ソート済み・VWビット付き)にデコードして GPU 頂点バッファへ転送する。 以降のズーム・パン操作では追加ネットワークリクエストは発生しない。LOD(頂点の間引き)は Stage 3 の vertex shader が毎フレーム VW 閾値で処理する。

4.1 Morton ビットシフトによるビューポートクエリ

// ズームレベル z でのビューポート内フィーチャ検索
// 64bit Morton コードの上位 2z ビットがタイルアドレスを示す

const tileAddr  = morton2D(tileX, tileY);     // ビューポート左上のタイル
const shiftBits = 64 - 2 * z;                    // 右シフト量
const prefix    = tileAddr >> BigInt(shiftBits);  // 上位 2z ビット

// ソート済み配列に対して二分探索
const lo = lowerBound(features, prefix << BigInt(shiftBits));
const hi = upperBound(features, ((prefix + 1n) << BigInt(shiftBits)) - 1n);
const viewport = features.slice(lo, hi);  // O(log n) で絞り込み完了

4.2 ズームレベルと LOD の対応

ZoomLODVW Threshold(例) 有効頂点数(例)タイル方式ならば
z = 2LOD 0 maxW × 0.50 22 vertices 1 tile fetch
z = 4LOD 1 maxW × 0.20 44 vertices 4 tile fetches
z = 6LOD 2 maxW × 0.07 88 vertices 16 tile fetches
z = 8LOD 3 maxW × 0.018 176 vertices 64 tile fetches
z = 10LOD 4 0(全頂点) 352 vertices 256 tile fetches
Zero Network Overhead

ズームレベルが変わるたびにタイル方式では追加のネットワークリクエストが発生するが、 gint はすべての LOD を単一ファイルに内包している。 ズーム操作は GPU uniform の zoom 値を更新するだけで、 vertex shader が vw_bits > zoom の頂点を毎フレーム破棄する(CPU 処理ゼロ)。 初回ロード後は追加バイト転送 +0 bytes

5. Stage 3 — GPU ステンシルテッセレーション

WebGL でポリゴンを描画するには、通常 CPU で三角形分割(Earcut 等)を行ってから GPU に送る必要がある。 凹ポリゴン・穴あきポリゴンは特に複雑で、高フレームレートを維持することが難しい。

ステンシルテッセレーションは、CPU による三角形分割を完全に排除し、 頂点列をそのまま GPU に渡して 2 パスで描画する手法である。

5.1 Pass 1: ステンシルパス(偶奇カウント)

// 頂点列から NDC 原点(0,0) をファン中心としたトライアングルを生成
// 頂点数 N のポリゴン → N 個のトライアングル(CPU 処理不要)

for (let i = 0; i < N; i++) {
	drawTriangle(
		[0, 0],          // NDC 原点(固定)
		vertices[i],       // 現在の頂点
		vertices[i+1]    // 次の頂点
	);
}

// WebGL ステンシル設定
gl.stencilOp(gl.KEEP, gl.KEEP, gl.INVERT);  // 重なるたびに XOR(0→1→0→1...)
gl.stencilFunc(gl.ALWAYS, 0, 0xFF);
gl.colorMask(false, false, false, false); // カラーバッファは書かない

各ファントライアングルが重なった回数が 奇数 のピクセル = ポリゴン内部、 偶数 = ポリゴン外部(穴・背景)という even-odd ルールが自然に成立する。

5.2 Pass 2: カラーパス

// ステンシルが奇数(内部)のピクセルのみ描画
gl.stencilFunc(gl.EQUAL, 1, 0x01);  // ビット0 = 1 のピクセルのみ通過
gl.colorMask(true, true, true, true);
drawFullscreenQuad(fillColor);     // 全画面クワッドを塗るだけ(GPU 爆速)

5.3 凹ポリゴン・穴の自動処理

従来手法(CPU Earcut)
earcut: 3 triangles CPU O(n) before upload
GPU ステンシル法(Stencil Fan)
NDC(0,0) 5 fan triangles GPU only, zero CPU
ステンシルカウント(偶奇)
count=2 count=1 count=1 odd=fill, even=hole 任意トポロジ自動解決
なぜ凹形・穴が自動で解決するか

ファントライアングルが重なる回数は、その点がポリゴン輪郭をどちら向きに何回横切るかに対応する。 凹部分(ポリゴンが自分自身と「重なる」部分)では 2 回カウントされ偶数 → 穴として処理される。 これは ray casting アルゴリズムと同じ even-odd ルールを GPU のハードウェアステンシルで実現したものである。

5.4 球体カリング — 地平線またぎポリゴン

ファンステンシルは 2D 平面レンダリングでは正しく機能する。しかし正射影(orthographic)の地球儀では、 ポリゴンが地平線(球面の前後境界)をまたぐ場合がある。背面半球の頂点はアイスペースで p.z < 0 となり画面に表示されないが、それでもファントライアングルに参加するため問題が生じる。

単純な対処:裏面頂点を NDC 原点に潰す

// ❌ 地平線付近に縮退トライアングルが発生する
if (p.z < 0.0) { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); return; }

ファンの軸点(NDC 原点)に重ねると、地平線をまたぐエッジで面積ゼロの縮退トライアングルが生じる:

解決策:裏面頂点を地平線円上に押し出す

潰すのではなく、各裏面頂点を地平線円(球面の投影外縁、半径 u_scale px)上へ、 球中心からの画面方向を保ったまま押し出す:

// ✅ 地平線円上に押し出し — 縮退トライアングルを排除
if (p.z < 0.0) {
	vec2 v = p.xy - u_viewport * 0.5;     // 球中心からの方向ベクトル
	float d = length(v);
	gl_Position = d < 1e-4                 // ガード:頂点が完全に中心と一致
		? vec4(0.0, 0.0, 0.0, 1.0)
		: toNDC(u_viewport * 0.5 + v * (u_scale / d));
	return;
}
❌ 原点に潰す — 縮退トライアングル
O V1 V2 V3(裏面) 地平線付近に穴
✅ 地平線円へ押し出し — 正常塗り
O V1 V2 H3 完全塗り
なぜこれが正しいか

各エッジ種別で有効な(面積のある)トライアングルが生成される。 EXITO, V可視, H裏面):可視弧から地平線までの扇形を埋める。 BACK-BACKO, Hi, Hi+1):連続する裏面頂点間の地平線円弧を埋める — CPU 側の closing-arc triangle の GPU 版に相当。 ENTRY:EXIT と対称。 可視弧と押し出された地平線円弧が合わさって閉じた単純ポリゴンを形成し、 ステンシル XOR が正しい巻き数をカウントする — CPU によるアーク位相計算は不要。

6. 性能比較

指標 従来タイル方式 Gint
ファイル数 O(4z) タイル 1 ファイル
初回ロード 現在の z に必要なタイルのみ 全 LOD を含む 1 ファイル
ズームイン時の追加転送 ズームごとに複数タイル +0 bytes
ビューポートクエリ tile x/y/z でサーバ問い合わせ Morton bit-shift O(log n)(ローカル)
LOD 切り替え 別タイルを再取得 メモリ内フィルタのみ(即座)
CPU 三角分割 フレームごとに earcut O(n²) 不要(GPU ステンシル)
穴・凹形ポリゴン earcut で特別処理が必要 even-odd ルールで自動解決
事前生成コスト 全ズームレベル × 全タイルを生成 1 回のエンコード(Morton sort + VW)

7. 設計思想

地理空間データの表現方法は、どの「詳細さ」を現実として採用するかという選択でもある。 NaturalEarth の physical/cultural という分類が地球上の現象を人間の視点から整理したものであるように、 gint の LOD 設計はスケールによって「見えるもの」を動的に変化させるという概念を実装している。

VW ウェイトは「ある頂点を削除したとき形状が失う情報量」を定量化する。 これは情報理論的には「重要度」の測定であり、 地形や境界線において何が「本質的な構造」で何が「観測スケール依存の詳細」かを数値化したものとも言える。

もうひとつ、正射影地球儀に特有の問題として:日付変更線(経度 ±180°)をまたぐポリゴンは、 描画前にその境界で分割しなければ球の逆側をぐるっと回って描かれてしまう。 球体カリングとこの反子午線(アンチメリジアン)処理、この 2 つが、 平面地図レンダラーと球体レンダラーを分ける、静かに非自明な関門である。


デモファイル

ファイル内容対応 Stage
GINT.html Morton オーダーのアニメーション、VW ウェイトの対話的可視化 Stage 1 エンコーディング
LOD.html 動的 LOD:vertex shader が毎フレーム VW 閾値で頂点を破棄 Stage 3 GPU 描画
ST.html GPU ステンシルテッセレーション:CPU 三角分割との比較 Stage 3 GPU 描画
gint / ortho-map · Kenji Yoshida · 2026