🌏

MapLibre GL JS 5.x の描画の仕組みについてのいくつかのメモ

に公開

MapLibre GL JS 5.x の描画フローのうち、いくつかのトピックについてのメモ書きです。

https://maplibre.org/

以下は省略します:

  • 3Dリアルタイムレンダリングの基本的な知識
  • ベクトルタイルの仕組み

ほかにも思いついたことがあれば追記します。

深度テスト

src/render/painter.ts

MapLibre GL JS は深度バッファを、深度テストのためだけでなく、Opaque pass (後述) でのレイヤの順序を制御する目的でも使う:

  • 2Dレイヤ群の順序制御は、深度を微小値 (depthEpsilon = 1/65536) ずつずらして描画することによって行われる
  • 余った残りの深度範囲を、3Dレイヤ (fill-extrusion, terrain, 3d custom layer) の深度バッファとして利用する

タイルごとに描画 (draw call) する

GPUへの描画要求 (draw call) はタイルごとに行われる。頂点シェーダーにはタイル内座標 [0..8192] が入力され、シェーダーはそれを mercator projection または globe projection に基いてクリップ空間に変換する。

各頂点シェーダーの先頭には、MapLibreが用意する projectTile() などの投影のためのユーティリティ関数が挿入される。挿入される関数が投影法 (mercator projection or globe projection) によって切り替わるため、1つのシェーダーコードで両方の投影法に対応できる。

頂点シェーダーに挿入されるコードは以下などを参照:

projectTile() (およびその亜種) は、タイル内座標 [0..8192] を入力として、クリップ空間座標を返す関数である。(ただし custom layer は特別であり、タイル内座標ではなくメルカトル座標を入力するように、MapLibreによって投影行列が特別に調整される。)

projectTile() には2つの引数を受け取るバージョンもあり、これは一部のシェーダで極地を扱うために使われる。src/render/subdivision.ts:

export const NORTH_POLE_Y = -32768;
export const SOUTH_POLE_Y = 32767;

Terrain

Render to Texture (RTT)

src/render/render_to_texture.ts

Terrain が有効な場合、MapLibre は2D系レイヤの多くをいったんテクスチャ上に描いて (RTT = Render To Texture)、それを地形上に貼りつけて描画する。

RTT描画先のテクスチャをプールしたりキャッシュしたりする仕組みが実装されている。

メッシュの生成

メッシュの頂点データ自体は真っ平らなグリッドとして生成しておき (src/render/terrain.ts)、頂点シェーダーにてDEMテクスチャをサンプリングして高さ方向を変位させる (terrain.vertex.glsl, _prelude.vertex.glsl)。

描画の流れ

src/render/painter.ts

おおまかには以下の流れで描画される:

  1. Offscreen pass
    • 一部のレイヤ種 (Heatmap, Hillshade, ColorRelief および Custom Layer(必要なら)) が要求する事前準備のためのオフスクリーン描画を行う
  2. Sky の描画
  3. Opaque pass (不透明な Fill と Background を効率的に描く)
    • 描画順: 上のレイヤから下のレイヤへ (深度テストが効きやすくなり余計なピクセル描画を省ける)
    • 不透明な Fill layer と Background Layer のみがこの Opaque pass で描画されうる。ただし 3Dレイヤ (fill-extrusion, 3d custom layer) より上のレイヤはすべて Translucent pass で描画される(上→下レイヤ順の描画による最適化ができなくなるため)
  4. Trnaslucent pass
    • ほとんどのレイヤーおよび Fill layerのstroke は、常にこの Translucent pass で描画される
    • 描画順: 下のレイヤから上のレイヤへ
    • 3Dレイヤより上のレイヤでは深度テストは無効化される
  5. 大気 (atmosphere) の描画
  6. デバッグ用タイル境界、デバッグ用パディングの描画 (有効なら)

投影のための uniform 群

src/geo/projection/projection_data.ts

投影のための以下の uniforms が MapLibre によって与えられ、頂点シェーダーで利用される。

🗺️ = mercator projection で使われるもの。🌏 = globe projection で使われるもの。

  • 🗺️ 🌏 ProjectionData.mainMatrix (uniform名: u_projection_matrix)
    • mercator時: タイル内座標 [0-8192] をクリップ座標に変換する行列
    • globe時: 地心座標系の地球をクリップ座標に変換する行列
      • globe view では、この行列を適用する前にタイル座標をメルカトル座標を経由して直交座標系(地心座標系)に変換する
  • 🌏 ProjectionData.tileMercatorCoords (uniform名: u_projection_tile_mercator_coords)
    • 現在のタイルがメルカトル空間上に占める領域。[X offset, Y offset, X scale, Y scale]
  • 🌏 ProjectionData.clippingPlane (uniform名: u_projection_clipping_plane)
    • 地球(=単位球)の地平線を表わす平面方程式。地球の裏側のクリッピングに使う。
  • 🌏 ProjectionData.projectionTransition (uniform名: u_projection_transition)
    • globe projection において高ズームレベル時に mercator に推移させていくための補間パラメータ [0..1]
  • 🌏 ProjectionData.fallbackMatrix (uniform名: u_projection_fallback_matrix)
    • globe projection において、現在のタイルをメルカトルで投影するための行列。globe→mercatorの推移用

projectTile() での計算

Mercator projection の場合

src/shaders/_projection_mercator.vertex.glsl

  1. 入力: タイルローカル座標 (0-8192), 標高
  2. タイルローカル座標 → クリップ空間座標の変換
    • u_projection_matrix * vec4(posInTile, elevation, 1.0)
  3. 出力: クリップ空間座標

Globe projection の場合

src/shaders/_projection_globe.vertex.glsl

  1. 入力: タイルローカル座標 (0-8192), 標高
  2. 単位球面上の直交座標に変換: projectToSphere(posInTile)
    • タイル座標 → メルカトル座標:
      • mercator_pos = u_projection_tile_mercator_coords.xy + u_projection_tile_mercator_coords.zw * posInTile
    • メルカトル座標 → 極座標:
      • spherical.x = mercator_pos.x * PI * 2.0 + PI
      • spherical.y = 2.0 * atan(exp(PI - (mercator_pos.y * PI * 2.0))) - PI * 0.5
    • 極座標 → 直交座標:
      • pos = vec3(sin(spherical.x) * len, sin(spherical.y), cos(spherical.x) * len)
    • 戻り値: 単位球面上の直交座標
  3. Mercator projection とブレンドしつつ投影: interpolateProjection(posInTile, spherePos, elevation)
    • 標高を適用:
      • elevatedPos = spherePos * (1.0 + elevation / GLOBE_RADIUS)
    • Globe投影法でのクリップ空間座標を計算:
      • globePosition = u_projection_matrix * vec4(elevatedPos, 1.0)
      • 地球の反対側をクリップするためのZ値を計算:
        • globePosition.z = globeComputeClippingZ(elevatedPos) * globePosition.w
    • Mercator投影法でのクリップ空間座標を計算 (註: mercator projection での計算と同じ):
      • flatPosition = u_projection_fallback_matrix * vec4(posInTile, elevation, 1.0)
    • 遷移ブレンド:
      • result.xyw = mix(flatPosition.xyw, globePosition.xyw, u_projection_transition)
      • result.z = mix(0.0, globePosition.z, clamp(...))
  4. 出力: クリップ空間座標

3Dレイヤ用 projectTileFor3D() などでは処理が少し変わる。

視錐台カリング

TODO

MIERUNEのZennブログ

Discussion