この記事は 3D都市モデル Project PLATEAU Advent Calendar 2024 の3日目の記事です。
2年前に Mini Tokyo 3D × PLATEAU という記事を書きました。その後、2023年4月に PLATEAU VIEW 2.0 のリリースとそれに合わせた配信データの更新があり、おそらくそのタイミングで 3D Tiles の仕様変更があったため、上記の記事の内容は新しいデータでは動かなくなっていました。さらに、2024年5月には PLATEAU VIEW 3.0 のリリースと配信データ更新、データカタログ API の公開もあり、「ここらで Mini Tokyo 3D の PLATEAU プラグインも最新のデータに合わせて刷新するか」と思い立ち色々試してみたのが本記事の内容です。2024年11月6日の PLATEAU LT 07 でのトークの詳細でもあります。

ちなみに前回の記事をご覧いただいてない方向けに簡単に解説すると、Mini Tokyo 3D は東京の公共交通のリアルタイム 3D マップで、Mapbox を地図ライブラリとして利用しているウェブアプリケーションです。PLATEAU plugin for Mini Tokyo 3D は Mini Tokyo 3D 上で動作するプラグインで、PLATEAU配信サービス(試験運用) チュートリアルで配信されているデータを利用して、テクスチャ付き建築物のリアルな 3D 風景の中を列車や旅客機が移動する様子を楽しめます。
データ仕様変更への対応
まず、以前は README に列挙されていた 3D Tiles の URL ですが、現在はデータカタログ API を使って取得する形になっています。https://api.plateauview.mlit.go.jp/datacatalog/plateau-datasets をコールすると 3D Tiles/MVT のデータ一覧が JSON のレスポンスとして返ってくるので、その中の url フィールドを使います。

次に、更新されたデータでは建築物の表示ができなくなっていましたので、それに対応する必要があります。Tile3DLayer の onTileLoad に引数として渡ってくるデータと onTileLoad での処理内容を比べると、前回は content.batchTableJson._gml_id の長さから項目数を取得していましたが、_gml_id が gml_id に名称変更されていたためにエラーになっていることがわかりました。gml_id の長さを取得するように変更してもいいのですが、content.featureTableJson.BATCH_LENGTH を使う方がより良いので、そのように変更します。また、Tileset によっては content.batchTableJson._zmin が存在しないこともあるので、そのチェックを入れています。
onTileLoad: ({content}) => {
const zmin = content.batchTableJson._zmin;
const cartographicOrigin = content.cartographicOrigin;
if (zmin) {
const buffer = content.batchTableBinary.buffer;
const len = content.featureTableJson.BATCH_LENGTH;
const zMinView = new DataView(buffer, zmin.byteOffset, len * 8);
const zMins = [];
for (let i = 0; i < len; i++) {
zMins.push(zMinView.getFloat64(i * 8, true));
}
zMins.sort((a, b) => a - b);
cartographicOrigin.z -= zMins[Math.floor(len / 2)];
}
cartographicOrigin.z -= 36.6641;
}
とりあえずこれでエラーは解消し、選択した 3D Tiles のモデルを Tile3DLayer で表示できるようになりました。が、相変わらずブラウザで表示させるには重たいです。
動的なデータのロード/アンロード
前回はロードしておく 3D Tiles タイルセットは都内6つの区に限定していて、それでもメモリは不足気味でした。そこで、見えている部分の必要なモデルだけ動的にロードしてメモリを使うようにすればよいだろうという考えのもと、次の方針を立てました。
- 事前に 3D Tiles の URL と領域を表す GeoJSON ポリゴンを用意
- 地図が移動されたら、ポリゴンを使ってビューポート内の領域を特定
- 新たに表示領域になった 3D Tiles をロードし、非表示になった領域の 3D Tiles をアンロード
もう表示対象は全国の都市の 3D Tiles にしてしまえということで、上述のデータカタログ API で取得してきた JSON から、データタイプが建築物(type_en が 'bldg')かつ LOD が 2(lod が 2)かつテクスチャ付き(texture が存在)のデータセットを抜き出します。そして市区町村コード(ward_code または city_code)と URL(url)を取得します。2024年12月現在、これは213市区町村の分があります。
次に、国土数値情報ダウンロードサイトの行政区域データのページから、2023年版全国データ N03-20230101_GML.zip をダウンロードします。最新の2024年版ではダメです。というのも、PLATEAU の 3D Tiles の最新データは2023年度に整備されており、その後浜松市で行政区の再編があって市区町村コードが一致しなくなるからです。

N03-20230101_GML.zip の中には N03-23_230101.geojson というGeoJSON ポリゴンデータが入っており、各 Feature のプロパティに含まれる行政区域コード(N03_007)を参照すれば、3D Tiles とポリゴンデータを対応づけることができます。
さて、このポリゴンデータを使えば、見えている地図の範囲に対象の行政区域が含まれるかを判定できそうですが、例えば地図の中心の座標を判定に使ったとすると、ポリゴンの領域が少しでも中心からずれれば範囲外と判定されてしまいます。つまり、地図上の半分くらいモデルが表示されない状態ができてしまう可能性があります。これに対処するために、ポリゴン領域を少し拡大して、隣り合うポリゴンをある程度オーバーラップさせることで、地図の中心から多少離れても領域内と判定させることができます。
このポリゴン領域の拡張には Python のライブラリ Shapely の buffer 関数を使います。また、正確な領域の判定は不要なので、simplify 関数を使って頂点数を削減します。最後に拡張したポリゴンに、市区町村コードと 3D Tiles の URL をプロパティとして付けて、GeoJSON 形式で出力します。ここまでの一連の処理を行う Python スクリプトは次の通りです。
import json
import requests
from shapely import from_geojson, to_geojson, from_wkt, to_wkt, MultiPolygon, get_coordinates
datasets = requests.get('https://api.plateauview.mlit.go.jp/datacatalog/plateau-datasets').json()['datasets']
models = [{
'name': dataset['name'],
'code': dataset['ward_code'] or dataset['city_code'],
'url': dataset['url']
} for dataset in datasets if dataset['type_en'] == 'bldg' and dataset['lod'] == '2' and dataset['texture']]
print('Processing {} models...'.format(len(models)))
with open('N03-23_230101.geojson') as res:
data = json.load(res)
with open('plateau-models.geojson', 'w') as f:
f.write('{"type":"FeatureCollection","features":[\n')
for model in models:
geometries = []
for feature in data['features']:
if feature['properties']['N03_007'] == model['code']:
geometries.append(from_geojson(json.dumps(feature)))
if len(geometries) == 0:
print('{} ({}) is empty'.format(model['name'], model['code']))
continue
geometry = MultiPolygon(geometries).buffer(0.01).simplify(0.001)
f.write('{')
f.write('"type":"Feature",')
f.write('"geometry":{},'.format(to_geojson(from_wkt(to_wkt(geometry, rounding_precision=7)))))
f.write('"properties":{')
f.write('"code":"{}",'.format(model['code']))
f.write('"url":"{}"'.format(model['url']))
f.write('}')
f.write('}')
if model != models[-1]:
f.write(',')
f.write('\n')
f.write(']}\n')
生成された全国213市区町村の領域データはこんな感じです。オーバーラップしていますよね。


次はフロントエンド側です。Mapbox の Map を作成したら、事前に先ほどの GeoJson ファイルをデータソースとして、透明(fill-opacity が 0)な fill レイヤーを追加しておきます。
map.addLayer({
id: 'plateau-model',
type: 'fill',
source: {
type: 'geojson',
data: MODEL_URL
},
paint: {
'fill-opacity': 0
}
});
透明なので地図上には何も表示されませんが、queryRenderedFeatures を使って画面内の任意の座標に存在する Feature を取得することが可能です。このメソッドは CPU の幾何計算ではなく、GPU ピッキングを行う手法のため、パフォーマンス上も有利です。次のようにすれば、move イベントが発生するたびに画面中央に存在する(複数の)Feature を取得して、市区町村コードと 3D Tiles URL を使った処理を進めることができます。
map.on('move', () => {
const {width, height} = map.getContainer().getBoundingClientRect();
const features = map.queryRenderedFeatures([width / 2, height / 2], {layers: ['plateau-model']});
for (const feature of features) {
const {code, url} = feature.properties;
/* ... */
}
});
あとは新たに領域が検出されたら新しい Tile3DLayer を追加し、画面外に外れた領域の Tile3DLayer を削除するようにするだけで、動的なデータのロード/アンロードは実装完了です。
テクスチャの軽量化
これで全国の都市モデルが表示できるようになったものの、場所によってはまだ非常に動作が重たいですし、フリーズしてしまうこともありました(私の MacBook Pro のメモリ 16GB なので・・)。
ブラウザの開発者ツールのプロファイルを見ても、大して JavaScript ヒープは消費しておらず、一方でシステムのメモリはいっぱいいっぱい。macOS のユニバーサルメモリのことを考えると、おそらく GPU がテクスチャデータ用に確保しているメモリだろうと想像がつきます。実際、テクスチャなしであれば PLATEAU のモデルは軽快に動作します。
そこで、テクスチャデータを圧縮できる場所はないだろうかと探していたところ、テクスチャ画像は 3D Tiles に含まれる GLTF データの一部として存在しており、Tile3DLayer の引数として渡ってくる Tile オブジェクトからアクセスできること、そしてロード直後は画像データの差し替えが可能なことがわかりました。
Mini Tokyo 3D の利用ケースであれば、そこまで精細なテクスチャ画像は不要なため、画像の高さ、幅ともに 1/4 にして、データサイズとしては約 1/16 になることを目論みました。
これに加えて featureTableBinary、featureTableJson、batchTableBinary、batchTableJson の各データもロード後は不要なので、参照を削除しておきます。
onTileLoad: ({content}) => {
/* ... */
content.featureTableBinary = null;
content.featureTableJson = null;
content.batchTableBinary = null;
content.batchTableJson = null;
for (const item of content.gltf.images || []) {
const image = item.image;
const resizeWidth = image.width / 4;
const resizeHeight = image.height / 4;
createImageBitmap(image, {resizeWidth, resizeHeight}).then(resizedImage => {
item.image = resizedImage;
});
}
}
この最後のテクスチャの軽量化がかなり有効だったようです。全国どこに移動しても、スムーズに建築物モデルが表示できるようになりました!
ぜひお試しを
そこそこ軽くなった PLATEAU プラグインをお楽しみいただくには、https://minitokyo3d.com にアクセスし、画面右側の下から3つ目の「レイヤー設定」ボタンを押して、「PLATEAU」レイヤーを選択して有効にしてください(デフォルトでは表示オフになっています)。
Mini Tokyo 3D、PLATEAU Plugin for Mini Tokyo 3D は GitHub で公開されているので、チェックしてね!