この記事は MapLibre Advent Calendar 2025 21日目の記事です。
はじめに
MapLibre GL JS は、オープンソースの Web 地図ライブラリーですが、
glTF / GLB のような 3D モデルを直接表示する仕組みは持っていません。
一方で、MapLibre GL JS などのベースマップとの統合が容易で、
さまざまな 3D レイヤーが用意されている deck.gl を組み合わせることで、
- 3D モデル(glTF / GLB)
- 点群(3D Tiles)
を MapLibre GL JS の地図上に重ねて表示できます。
この記事では、
- MapLibre GL JS × deck.gl の基本構成
- deck.gl の
ScenegraphLayer
を使った 3D モデル表示 - 地形(Terrain)と高さを合わせる方法
- 応用として河川点群データ(3D Tiles)や空間IDの考え方
について、最小構成のサンプルコードを使って紹介します。
完成イメージ
- 地形上に 3D モデル(ドローン)を表示
- カメラ操作に追従して位置が更新される
- 河川点群(3D Tiles)と重ね合わせ可能
デモサイト
使用技術
- MapLibre GL JS
- deck.gl
- loaders.gl
使用データ
ディレクトリ構成(最小)
.
├── index.html
├── main.js
├── style.css
├── std.json
└── assets
└── dji_tello_1.glb ※3Dモデル(ドローン)
この記事では、3D モデルの表示を中心とした最小構成として、
上記の構成で動作するサンプルを使用しています。
ソースコードは、GitHubレポジトリに格納しています。
MapLibre GL JS の初期化
MapLibre 側では通常の地図初期化に加えて、
ピッチを効かせた 3D 表示と 地形(Terrain) を有効にします。
const map = new maplibregl.Map({
container: "map",
style: "std.json",
center: [139.114998, 36.08429],
zoom: 18.31,
pitch: 67,
bearing: -137.1,
});
標高タイル(Terrain)の設定
本記事では、MapLibre GL JS の Terrain 機能を使うために、
産業技術総合研究所(産総研)が提供しているシームレス標高タイル(Terrain-RGB形式)を使用しています。
// 標高タイルソース
map.addSource("gsidem-terrain-rgb", {
type: "raster-dem",
tiles: [
"https://gbank.gsj.jp/seamless/elev/terrainRGB/land/{z}/{y}/{x}.png",
],
attribution:
'<a href="https://tiles.gsj.jp/tiles/elev/tiles.html" target="_blank">産総研シームレス標高タイル</a>',
tileSize: 256,
});
// 標高タイルを地形として設定
map.setTerrain({
source: "gsidem-terrain-rgb",
exaggeration: 1,
});
この標高タイルを使うことで、
- MapLibre の Terrain 表示
- 3D モデルの高さ合わせ
- 点群(3D Tiles)との自然な重ね合わせ
が可能になります。
deck.gl のレイヤー を重ねる理由
MapLibre GL JS 単体では、
- glTF / GLB
- 点群(3D Tiles)
といった WebGL ベースの 3D 表現を直接扱えません。
deck.gl の MapboxOverlay を使うことで、
MapLibre GL JSのベースマップに deck.gl のレイヤーをそのまま重ねることができます。
MapboxOverlayの名前にMapboxとありますが、MapLibre GL JS でも問題なく使用できます。
https://deck.gl/docs/api-reference/mapbox/mapbox-overlay
ライティング設定
ScenegraphLayer を使う場合、
ライティングを設定しないとモデルが暗く(または真っ黒に)見えることがあります。
const ambientLight = new deck.AmbientLight({
color: [255, 255, 255],
intensity: 3.0,
});
const lightingEffect = new deck.LightingEffect({ ambientLight });
ScenegraphLayer で 3D モデルを表示
glTF / GLB 形式の 3D モデルは ScenegraphLayer で扱います。
const elevation = parseFloat(map.transform.elevation) || 0; // 標高が取得できない場合は0mとする
const scenegraphLayer = new deck.ScenegraphLayer({
id: "scenegraphLayer",
data: [{}],
scenegraph: "assets/dji_tello_1.glb",
getPosition: () => [lng, lat, elevation + 30],
getOrientation: [0, -93, 90],
sizeScale: 1,
_lighting: "pbr",
pickable: true,
});
- 座標は
[lng, lat, altitude] map.transform.elevationは MapLibre GL JS の内部プロパティであり、公式に公開・保証された API ではありません。本記事では、Terrain 有効時に標高の目安を取得するための簡易的な手法として使用しています。- orientation はモデルに応じて調整が必要
3Dモデル(ドローン)の高さはどう設定しているか
このサンプルでは、ドローンの高さ(altitude)を「標高 + 30m」として設定しています。
- 地表の標高:
map.transform.elevation(Terrain 有効時に参照できる標高の目安) - ドローンの高度:
elevation + 30(標高から 30m 上に固定)
const center = map.getCenter().toArray();
const elevation = parseFloat(map.transform.elevation) || 0;
// ドローンを「標高 + 30m」に配置
getPosition: () => [center[0], center[1], elevation + 30],
ここで +30 は「このデモではそれっぽく浮かせたい」ための固定値です。
実際の用途では、GNSS/気圧高度/レーザ計測など、取得した高度に置き換えることで、
より現実に近い表示にできます。
MapLibre GL JSのベースマップに deck.glの レイヤーを統合(MapboxOverlay)
const overlay = new deck.MapboxOverlay({
interleaved: true,
layers: [scenegraphLayer],
effects: [lightingEffect],
});
map.addControl(overlay);
カメラ移動に追従させる
deck.gl のレイヤーは、一度作ったものを変更するのではなく、状態が変わるたびに新しいレイヤーを作成して更新します。
MapLibre GL JSの move イベントに合わせて、レイヤーを更新します。
map.on("move", () => {
// (中略)
overlay.setProps({
layers: [tile3dLayer, newScenegraphLayer, newTextLayer],
});
});
応用:河川点群データ(3D Tiles)を重ねて表示する
本記事のサンプルコードでは、
応用例として河川点群データの 3D Tiles を deck.glのTile3DLayerを overlay に重ねています。
tile3dLayer = new deck.Tile3DLayer({
id: "tile3dlayer",
pointSize: 1,
data: "https://shiworks2.xsrv.jp/3dtiles/pref-saitama/river-pointcloud/chichibu-railway-spot/tileset.json",
loader: Tiles3DLoader,
onTileLoad: (d) => {
const { content } = d;
content.cartographicOrigin.z -= 0;
},
_lighting: "pbr",
opacity: 1,
});
これにより、
- 地形(Terrain)
- 河川点群(3D Tiles)
- 3D モデル(glTF)
を 同じ Web地図上で同時に可視化できます。
補足:3D 空間上の位置を空間IDで表現する
3D モデルを地図上に配置するだけであれば、
緯度・経度・高さ([lng, lat, altitude])を指定するだけで十分です。
しかし、実務や運用を考えると、
- ドローンや移動体の現在位置
- センサーや設備の設置位置
- 点検対象や災害対応時の対象エリア
といった情報を、
「どの 3D 空間ボクセルに属しているか」という形で
一意に表現したくなる場面があります。
そこで本記事では、
3Dモデルと位置情報を結びつける一例として、
簡易的な「空間ID」の考え方を紹介します。
今回使っている空間IDの考え方
今回のサンプルでは、以下のような考え方で空間IDを定義しています。
-
XY方向
Webメルカトル タイル座標を利用し、
地図タイルと同じ考え方でz / x / yを計算 -
Z方向(高さ)
高さ方向を一定の解像度で分割し、
ボクセル化したインデックスfとして扱う
「2D 地図タイル(z/x/y)+ 高さインデックス(f)」
を組み合わせた、シンプルな 3 次元空間インデックスです。
なお、空間IDの定義や算出方法は、4次元時空間情報利活用のための空間IDガイドライン(1.1 版)に記載の空間IDの各インデックスの算出方法に基づいています。
空間IDの計算例
以下は、サンプルコードで使用している空間ID計算関数です。
function calculateSpatialID(lng, lat, altitude, zoom) {
// 緯度を度からラジアンに変換
const lat_rad = (lat * Math.PI) / 180;
// n, Z, H を計算
const n = Math.pow(2, zoom);
const Z = 25; // ボクセルの高さが1mとなるズームレベル
const H = Math.pow(2, Z);
const z = zoom;
// 標高インデックス(f)を計算
const f = Math.floor((n * altitude) / H);
// x座標インデックス(x)を計算
const x = Math.floor(n * ((lng + 180) / 360));
// y座標インデックス(y)を計算
const y = Math.floor(
(n / 2) *
(1 - Math.log(Math.tan(lat_rad) + 1 / Math.cos(lat_rad)) / Math.PI)
);
// 空間IDをオブジェクトとして返す
return { z, f, x, y };
}
この関数に、
- 経度(lng)
- 緯度(lat)
- 高さ(altitude)
- 現在のズームレベル
を渡すことで、
現在位置が属している空間セルを
{ z, f, x, y } の形式で取得できます。
空間IDを可視化する例
サンプルコードでは、
計算した空間IDを deck.glのTextLayer を使って地図上に表示しています。
const label = `${spatialID.z}/${spatialID.f}/${spatialID.x}/${spatialID.y}`;
これにより、
- カメラ位置
- 地形の標高
- 3D モデルの配置高さ
に応じて、
現在の 3D 空間上の位置がどの空間ボクセルに属しているかを視覚的に確認できます。
まとめ
deck.gl のレイヤーを MapLibre GL JS に重ねることで、
Web 地図は 単なる 2D 表示ではなく、3D 表現を扱える可視化基盤として利用できるようになります。
本記事では、
- MapLibre GL JS をベースマップとして利用し
- deck.gl の
ScenegraphLayerを使って glTF / GLB 形式の 3D モデルを表示し - Terrain(標高タイル)と組み合わせて高さを合わせ
- 応用例として河川点群データ(3D Tiles)を重ねる
という構成を、最小限のサンプルコードで紹介しました。
この構成のポイントは、
「MapLibre だけでは難しい 3D 表現を、deck.gl を重ねることで無理なく拡張している」点にあります。
さらに補足として、
3D 空間上の位置を「空間ID」という形で表現する例も紹介しました。
これは厳密な規格実装を目的としたものではありませんが、
- ドローンや移動体の位置管理
- センサーや設備の配置管理
- 災害対応や点検業務での空間把握
といった場面で、
「どの 3D 空間ボクセルに属しているか」という視点を持つための
一つの考え方を示しています。
MapLibre GL JS × deck.gl の組み合わせは、
- 3D モデル(glTF / GLB)
- 点群(3D Tiles)
- 地形(Terrain)
を柔軟に組み合わせられるため、
Web地図 上での 3Dモデルの可視化において、
実務でも十分に使える選択肢だと感じています。
今回の記事が、
MapLibre GL JS や deck.gl を使った 3Dモデルの表示に取り組む際の
参考になれば幸いです。