MapLibre（v2.2.0-pre.2.）での3次元表示（Terrain 3D）を試してみる

# はじめに
最近、MapLibreからTerrain 3Dがリリースされましたね（Twitterだとpre-releaseとなっています）。おめでとうございます。そこで、どんな感じか試してみることにしました。

https://twitter.com/maplibre/status/1527966432973185026

https://www.maptiler.com/news/2022/05/maplibre-2/

## （参考）Terrain 3Dについて
Mapbox GL JSのversion 2ではこれまでにも標高の3D表示が出来ていたのですが、開発されたのはちょうどライセンス体系が切り替わる時期でもあったので、私自身はベクトルタイルと地図の3次元表示の分野については本格的に実装できてませんでした。今回のMapLibreさんの開発はいい機会になるので、少しどんなものか試してみて、使えそうなら使っていこうかなぁと思っています。

## 作業環境
今回は以下のような環境で試しました。

テキストエディタ（メモ帳）
Google Chrome　
PowerShell 

# 実験作業
どうやれば3D表示になるかの概略はmaptilerさんの[このページ](https://www.maptiler.com/news/2022/05/maplibre-2/)に書いてあります。標高データとしてはRGBAタイルを使っていて、そこから三角メッシュを作って地表を表現するようですね。TIN、つまりイレギュラーな三角なのかは以下の説明文だけではわかりません。3D表示をするためには、まず raster-demのソースを足すことが必要な様子です。

> Now you can visualize the elevation of the terrain from a DEM (RGBA tiles) including drawing the tracks, labels, and points.
> 
> To render a 3D terrain, MapLibre creates a triangle mesh, a set of 3D triangles representing the surface. This mesh is created from the terrain data coming from the DEM (RGBA tiles).
> 
> By default, the maps are 2D, but you now have the ability to view your maps in 3D. To activate the 3D option, you have to add a “raster-dem” source that contains the terrain elevations (RGBA tiles) to the map.

なお、詳細な作り方はこちら( https://maplibre.org/news/2022-05-20-terrain3d/ )をみればよいですが、作業に取りかかっていきます。

## Step1. raster-dem（標高タイル）の準備
### 1-1. 標高タイルについて調べる
MapLibreのTerrain DEMのリリースのニュースから、全く深入りしていないのですが、用意する標高タイルが**RGBAタイル**というところが気になっています。RGBタイルではないのでしょうか？ラスターの標高タイルは初心者なので、標高タイルについて、いくつか例を見ながら検討します。

#### 国土地理院のRGB標高タイル
例えば、国土地理院の標高タイルはRGBタイルということなので、一つのピクセルあたりの深度は8bits × 3で24bitsではないかと思います。高さ方向の解像度は1cmにしているようです。

> x = 2^16R + 2^8G + B
> x < 2^23の場合　h = xu
> x = 2^23の場合　h = NA
> x > 2^23の場合　h = (x-2^24)u
> uは標高分解能（0.01m）を表します。また、無効値（標高タイル（テキスト形式）の「e」に該当する箇所）は(R, G, B)=(128, 0, 0)です。
（https://maps.gsi.go.jp/development/demtile.html より）

国土地理院の標高タイルは、一つのbitを+/-の識別に割り当てているようなので（signed）、実際は2^23 = 8,388,608（+方向にはその半分で4,194,304）のステップに分けられると思いますし、これで標高-4,194～4,194メートルの範囲を表すとすれば最大で1mm位の解像度がでると思います。


### MapboxのRGB標高タイル
Mapbox GL JSで使っている標高タイルは@Kanahiro(MIERUNE) さんの記事（ https://qiita.com/Kanahiro/items/e22594b738655a189c1d ）を参考にさせていただきましたが、以下の様な定義だそうです。タイル自体は通常の24bit深度のデータのようで、これをライブラリが解釈するときに-10000しているのでしょうね。解像度は地理院タイルと~~同様に0.1の様です~~ちがって、0.1のようです。

> height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)

Mapboxさんのページも見てみましたが、以下のページに説明があります。（RGBAタイルというのは見つけられなかった）

* https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/
* https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-rgb-v1/


### ではMapLibreのタイルは？？
RGBではなくて、RGBAということは8 bit × 4 = 32 bitsの深度になっているのでしょうか？？？高さの解像度を考えてみて、ピクセルあたりもう1バイト増やす必要があるのかなぁ？？と初心者として疑問がいっぱいでした。RGBA,DEM,tile,maplibreあたりで検索しても技術情報を見つけられませんでした。
しかし、以下のサンプルをみると、maptilerから提供されているterrain-rgbのデータ形式を確認すればいいのではないかと思うわけです。

```
map.addSource("some id", {
  "type": "raster-dem",
  "url": "https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=YOUR_API_KEY",
});
```

maptilerさんのterrain-rgbタイルを調べて、たとえば、こんなページ（[RGB Terrain by MapTiler](https://documentation.maptiler.com/hc/en-us/articles/4405444055313-RGB-Terrain-by-MapTiler)）を読むと、RGBAタイルではなくて、MapboxのRGBタイルと同じなのではないか、、、と推測できました。

ですので、RGBAのAはとりあえず忘れて、既存のRGBタイルを使うことでいいのではないかと思うようになりました。（この作業のあと、将来的には確認したいポイントです）。

## 1-2. 使う標高タイルを決める
本来ならば、DEMデータから標高DEMタイルをサクッと作りたいところですが、今回の目標はMapLibreでのTerrain 3D表示なので、既存のDEMタイルでテストしようと思います。（省力化）

* 相対的な凹凸がチェックできればいいので、マイナス標高の処理に違いがあるものの、今回は国土地理院の標高タイルでテストする。
* [地理院タイル一覧](https://maps.gsi.go.jp/development/ichiran.html)で標高タイルをチェックする。
* まず表示ができるかを見るので、小縮尺（ZL0～ZL8）までで全球をカバーしている標高タイル（地球地図全球番標高第2版）を使うことにします。これは基本測量成果ではないので、利用ポリシーの観点からもハードルが低いと思います。主なソースデータはGMTED2010ですね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/a3beb262-a96c-362b-1ef2-23f3bc258781.png)

決まりました。 **https://cyberjapandata.gsi.go.jp/xyz/demgm_png/{z}/{x}/{y}.png** でトライしたいと思います。（個人的にも思い入れがあるデータなので嬉しいです。）

## Step 2 MapLibre の地図作成
以前の記事（[MapLibreで地理院地図Vector（仮称）を表示するページを作る（入門者向け）](https://qiita.com/T-ubu/items/59ce878b2b19d5bad141)）と同じような感じで地図を作っていきます。
なお、つかうMapLibreのバージョンはMapLibre GL JS v2.2.0-pre.2.にしましょう。

### 2-1 作業場所（GitHubレポジトリ）の作成
GitHubレポジトリを作って、そのなかにdocsディレクトリを作っておきます。レポジトリに中身をいれたらGitHubページの設定も忘れずにやるようにします。
今回の作業レポジトリ　https://github.com/ubukawa/maplibre-3d-test

### 2-2 ライブラリのダウンロード
docsディレクトリに移動して、maplibre v2.2.0-pre.2をダウンロードしてきます。私は、パワーシェルでやったのでcurl.exeを使っています。unpkg.comさんからダウンロードさせていただくと早いです。いつもありがとうございます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/989495d3-7e6a-fa2f-d804-0bbc1d136a3d.png)


### 2-3 参照するスタイルファイルの準備（失敗）
今回は国土地理院の標準地図風スタイルを使います。以下のURLからコピーしてきて加工します。
https://gsi-cyberjapan.github.io/gsivectortile-mapbox-gl-js/std.json

このJSONファイルに、以下の3カ所だけ手を入れます。

```json:sourcesにterrainSourceとhillshadeSourceを追加
    "sources":{
        "gsibv-vectortile-source-1-4-16":{"type":"vector","tiles":["https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap/{z}/{x}/{y}.pbf"],"minzoom":4,"maxzoom":16,"attribution":"<a href=\"https://maps.gsi.go.jp/vector/\" target=\"_blank\">地理院地図Vector（仮称）</a>"},
        "terrainSource":{"type":"raster-dem","tiles":["https://cyberjapandata.gsi.go.jp/xyz/demgm_png/{z}/{x}/{y}.png"],"attribution":"Global Map Elevation v2","tileSize": 256},
        "hillshadeSource":{"type":"raster-dem","tiles":["https://cyberjapandata.gsi.go.jp/xyz/demgm_png/{z}/{x}/{y}.png"],"attribution":"Global Map Elevation v2","tileSize":256 }
    },
```

```json:Layersにhillsを追加
{"id":"hills","type":"hillshade","source":"hillshadeSource","layout": { "visibility": "visible" },"paint": { "hillshade-shadow-color": "#473B24" }}
```

```json:terrainを追加
"terrain":{
    "source":"terrainSource",
    "exaggeration":1
},
```
これらの調整をしたスタイルはこちら https://ubukawa.github.io/maplibre-3d-test/std-3d.json


### 2-4 htmlファイルの準備（失敗）
あとはhtmlファイルを作ります。https://maplibre.org/news/2022-05-20-terrain3d/ のページに書いてあるHTMLを参考に作ります。ライブラリの場所、参照するスタイルを変えます。私はスタイルを別のファイルで記載しているので、html中にはstyleの中身を書き込みません。

```html:map.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MapLibre GM JS Terrain 3D with GSI Maps Vector (std)</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<script src="https://ubukawa.github.io/maplibre-3d-test/maplibre-gl.js"></script>
<link href="https://ubukawa.github.io/maplibre-3d-test/maplibre-gl.css" rel="stylesheet" />
<style>
	body { margin: 0; padding: 0; }
	#map { height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
var map = (window.map = new maplibregl.Map({
container: 'map', // container id
style: 'https://ubukawa.github.io/maplibre-3d-test/std-3d.json', // style URL
center: [140.084556, 36.104611], // starting position [lng, lat]
zoom: 7, // starting zoom
minZoom: 4, // min zoom of map
maxZoom: 17, // max zoom of map
pitch: 52,
hash: true,
maxPitch: 85
}));

//UI
map.addControl(
        new maplibregl.NavigationControl({
          visualizePitch: true,
          showZoom: true,
          showCompass: true,
        })
      );
map.addControl(new maplibregl.GeolocateControl({positionOptions: {enableHighAccuracy: true},trackUserLocation: true}), 'top-right');
map.addControl(new maplibregl.ScaleControl() );
map.addControl(
        new maplibregl.TerrainControl({
          source: "terrainSource",
          exaggeration: 1,
        })
      );
//map.addControl(new maplibregl.AttributionControl({customAttribution: "custom attribution" })); // you can add

//debug
//map.showTileBoundaries = true;
//map.showCollisionBoxes = true;


</script>
 
</body>
```

こうしてつくったHTMLファイルですが、まだ見られません。私にとってはよくあることです。
https://ubukawa.github.io/maplibre-3d-test/map.html#7/36.105/140.085/0/52
ベクトルタイルに対応していないのか、スタイルファイルを別途参照させたのが悪かったのか、理由は今後調べていく必要がありますね。もう少し簡単なやり方で再挑戦してみることにします。

### 2-5 参照するスタイルファイルの準備（2-3,4 再チャレンジ）
今回はとりあえず3D表示をみたいので、以下の点を変更して再チャレンジです。

* ベクトルタイルの代わりに関東近辺でLandsatのラスタタイル（ZL4-14）を使う。
    * タイルURL　https://ubukawa.github.io/Landsat9-Tokyo20211117/107035_20211117/{z}/{x}/{y}.png
    * 作り方はこちら https://qiita.com/T-ubu/items/d31e767e5d624b1fc715
* スタイルを別のJSONファイルにしないでhtmlの中に入れてしまう。

この方針でhtmlファイルを作り直します。スタイル情報も一緒にいれてしまいます。（MapLibreのサンプルではDEMのソースをurlとして指定していますが、私はtilesで指定しました。問題なさそうです。）
```html:map3.html
<!DOCTYPE html>
<html>
  <head>
    <title>MapLibre GL JS Terrain3D test -landsat map</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=no"
    />
    <script src="https://ubukawa.github.io/maplibre-3d-test/maplibre-gl.js"></script>
    <link href="https://ubukawa.github.io/maplibre-3d-test/maplibre-gl.css" rel="stylesheet" />

    <style>
      body {
        margin: 0;
        padding: 0;
      }

      html,
      body,
      #map {
        height: 100%;
      }
    </style>
  </head>

  <body>
    <div id="map"></div>

    <script>
      var map = (window.map = new maplibregl.Map({
        container: "map",
        zoom: 12,
        center: [140.084556, 36.104611],
        pitch: 52,
        hash: true,
        style: {
          version: 8,
          sources: {
            landsat: {
              type: "raster",
              tiles: ["https://ubukawa.github.io/Landsat9-Tokyo20211117/107035_20211117/{z}/{x}/{y}.png"],
              tileSize: 256,
              attribution: "Landsat-9 image courtesy of the U.S. Geological Survey",
              maxzoom: 14,
            },
            terrainSource: {
              type: "raster-dem",
              tiles:["https://cyberjapandata.gsi.go.jp/xyz/demgm_png/{z}/{x}/{y}.png"],
              attribution: "Global Map Global Elevation v2",
              tileSize: 256,
            },
            hillshadeSource: {
              type: "raster-dem",
              tiles:["https://cyberjapandata.gsi.go.jp/xyz/demgm_png/{z}/{x}/{y}.png"],
              attribution: "Global Map Global Elevation v2",
              tileSize: 256,
            },
          },
          layers: [
            {
              id: "landsat",
              type: "raster",
              source: "landsat",
            },
            {
              id: "hills",
              type: "hillshade",
              source: "hillshadeSource",
              layout: { visibility: "visible" },
              paint: { "hillshade-shadow-color": "#473B24" },
            },
          ],
          terrain: {
            source: "terrainSource",
            exaggeration: 1,
          },
        },
        maxZoom: 14,
        maxPitch: 85,
      }));

      map.addControl(
        new maplibregl.NavigationControl({
          visualizePitch: true,
          showZoom: true,
          showCompass: true,
        })
      );

      map.addControl(
        new maplibregl.TerrainControl({
          source: "terrainSource",
          exaggeration: 1,
        })
      );
    </script>
  </body>
</html>
```

出来たファイルをGitHubページにアップロードしました。

## Step 3. 結果の観察
https://ubukawa.github.io/maplibre-3d-test/map3.html から結果を観察してみます。

一応、Terrain 3Dは出来ている様子です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/23056299-260c-f133-0217-43ec2c2506db.png)

もともとの標高データの関係で、水部は標高値が高いように解釈されていますが、今回のテストでは気にしなくて良いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/f6574681-3842-1f2e-2ecd-80fe48917763.png)

どうも高さ方向に強調されすぎているような感じがするのですが、DEMデータの高さ方向解像度があっていないのだろうか？？今後調べてみます。（追記：地理院のタイルは解像度が0.01メートル、MapboxやMapLibreは0.1メートルなので、地理院の標高が10倍強くでています。多分exaggerationあたりを調整すれば大丈夫でしょう。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/4b11df26-130e-c97f-32d8-87fe0e2992ad.png)

# Step 4. 再挑戦（2022-05-23追記）
もう一度、ベクトルタイルを使ってトライしてみます。

（4-1）
NaturalEarthのベクトルタイルに地理院地図標高（地球地図）のhillshadeを重ねてみる。スタイルはJSONファイルで書いてみる。→できました。 Hillshadeだけ見ていても結構面白いです。 
https://ubukawa.github.io/maplibre-3d-test/map2-2.html
![2022-05-23-3d-001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/fd41b53f-5a61-aa2a-72be-a80a0783af7e.png)

（4-2）
4-1にterrainを加えてみる。→できました。標高値を処理していないので海のところが高く見えますが、terrain 3Dとベクトルタイルが一緒にできていることを確認しました。
https://ubukawa.github.io/maplibre-3d-test/map2-3.html#7/35.954/140.059/18.4/35
![2022-05-23-3d-002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/919bb4d5-2987-5ae1-c103-d3ef4c8f3fc5.png)!

（4-3）
背景を地理院地図Vector（仮称）にしてやってみる。スタイルはJSONファイルで記述する。→できました。昨日失敗したですが、やり直したらできましたね。
https://ubukawa.github.io/maplibre-3d-test/map0-2.html#9.08/36.2697/139.6994/18.4/35
![2022-05-23-3d-003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/8c64fd1d-2916-d396-bca3-a32a625fcd7f.png)!


# まとめ
今回のテストではMapLibreのTerrain 3D表示を調べてみました。地理院の小縮尺の標高タイル（地球地図）と、Landsat画像での三次元表示テストは一応できました。（追記5月23日：ベクトルタイルでも大丈夫でした。）

## 今後の課題
* ベクトルタイルを含めた地図での3次元表示ができるか確かめる。（**追記：確かめた5/23**）
* ベースとなる標高タイルについて検討を進める。（地理院のタイルはsignedのデータですから、mapboxやmaplibreなどと定義が違う。）
* RGBAタイルのAについて自分の理解を深める。
* 国土地理院の地球地図標高タイルをMapbox、MapLibre形式に変換できるといいなぁ。

## 将来に向けた検討
国土地理院のRGBタイルの説明によると、1つのタイルは平均100kbだそうです。
世界の30％が陸で、その陸の標高タイルの平均が100kbとして全体のサイズをざっくり見積もると、ZL11までで160GBになります。自分で標高タイルを使うとすると、グローバルにはZL11位までが限界でしょうし、あとは優先地域や関心がある地域だけ別途標高タイルを準備するのかなと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/396d89f6-dd80-95a0-323d-d9a284280859.png)



# 参考ページ
* [MapLibreのツイート](https://twitter.com/maplibre/status/1527966432973185026)
* [MapLibre V2: Add 3D terrain to your map](https://www.maptiler.com/news/2022/05/maplibre-2/)
* [国土地理院・標高タイルの詳細仕様](https://maps.gsi.go.jp/development/demtile.html)
* [地理院タイルとdeck.glで3D地図をつくろう(Kanahiro(MIERUNE),2020)](https://qiita.com/Kanahiro/items/e22594b738655a189c1))
* [Mapbox Terrain-DEM v1](https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/)
* [Mapbox Terrain-RGB v1](https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-rgb-v1/)
* [RGB Terrain by MapTiler(Petr Pokorný, 2022)](https://documentation.maptiler.com/hc/en-us/articles/4405444055313-RGB-Terrain-by-MapTiler)
* [国土地理院・地理院タイル一覧](https://maps.gsi.go.jp/development/ichiran.html)
* [MapLibreで地理院地図Vector（仮称）を表示するページを作る（入門者向け）（T-ubu, 2021）](https://qiita.com/T-ubu/items/59ce878b2b19d5bad141)
* [今回の作業レポジトリ](https://github.com/ubukawa/maplibre-3d-test)
* [地理院地図標準地図風スタイル（国土地理院）](https://gsi-cyberjapan.github.io/gsivectortile-mapbox-gl-js/std.json)
* [MapLibreで地理院地図Vector（仮称）を表示するページを作る（入門者向け）(T-Ubu, 2022)](https://qiita.com/T-ubu/items/d31e767e5d624b1fc715)