Mapbox の新スタイルと GL JS v3 の新機能

## はじめに
Mapbox 社から、Mapbox GL JS の新機能と、Mapbox 社が提供する新スタイルの発表がありました。

https://www.mapbox.com/blog/standard-core-style

この機能強化で、Mapbox GL JS は v3 となり、3D 表示関係の機能がさらに充実するようです。

同時に、Mapbox 社が提供するスタイルとデータについても、3D 表示についてよりリッチなものになっているようです。今回は、この新スタイルを見ながら、網羅的ではありませんが、Mapbox GL JS の新機能にも触れていきたいと思います。

:::note
この記事の画像、コードには、Mapbox 社のプロダクトを利用しています。再現するには、Mapbox 社の[アクセストークン](https://docs.mapbox.com/help/getting-started/access-tokens/)が必要です。
:::

## 新しいスタイル

早速、新しいスタイルを眺めてみると、特に目立つのは、以下の２点です。
* ランドマークとなる建物の 3D モデルが表示されている
* 樹木の 3D モデルが表示されている

下の画像は東京ドームの周りですが、建物の 3D モデルと樹木の存在に、臨場感が増しています。個人的には、実写の画像ではなく、モデルでこのような表示がされるというのに、逆に未来を感じます。都市開発等で使われるような完成イメージ模型と似たような感覚を覚えるためでしょうか？

![TokyoDome.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/9b4458a6-c75f-c51a-aa6f-d678edcaedd3.png)

スタイル関係の機能拡充については、GL JS の GitHub に [MIGRATION_GUIDE_v3.md](https://github.com/mapbox/mapbox-gl-js/blob/f501fa4498b22f9193a6fe3e8852dc1aeafac812/MIGRATION_GUIDE_v3.md) が掲載されています。以下、簡単に解説します。

* 影を表示する機能なども実装されているようで、この影の効果を活用して、時間帯の異なる４つのスタイル（Dawn：夜明け、Day：日中、Dusk：夕暮れ、Night：夜）が提供されています。
* 上記の時間帯やラベルのオン・オフ等、各種設定を切り替えられるような `setConfigProperty()` が用意されています。
* スタイルに `slot` という概念が生まれました。以前は、スタイルレイヤを既存のスタイルレイヤの途中に挿入する際に、挿入する先のスタイルレイヤ ID を記載しなければいけませんでした。一方、この `slot` の概念を用いることで、頻繁に挿入されうる場所に `slot` を指定することで、スタイルレイヤ ID を特定せずとも簡単に挿入することができるようになります。
今までは、挿入したい箇所には、透明か非表示の `background` レイヤを予め入れておき、そのレイヤを指定するという方法をとっていた方も多いのではないかと思いますが、もっと簡便にできるようになるかもしれません。 
* その他、スタイルの設定が増えています。個人的にうれしいのは、以下の３つでしょうか。
    * 色について、`["hsl", h, s, l]` と HSL 形式で指定できるようになったようです。
    * ラスタタイルの色を取得したり、操作できそうな機能が増えています。
      →こちらについては、[Mapbox GL JS v3 で地理院の標高タイルを表示しようとして上手くいかなかった話](https://qiita.com/mg_kudo/items/978709ad67bce885bfe5)をご覧ください。
    * 地味に、ランダムな値を発生させる `random` もうれしいです。今までは、同じ種類の地物（建物など）を少しずつ違った色合いで見せたい、というような場合、データ段階で色のデータを仕込んでおかねばならなかったので。 →補足記事を書きました（[Mapbox GL JS v3 の "random" について](https://qiita.com/mg_kudo/items/081a0b02756e04525f75)）

## 3D モデルについて
新しい Mapbox のスタイル（standard）で表示される 3D モデルについても見てみましょう。

構成としては、建物の 3D モデル用のデータ、樹木の 3D モデル用のデータの２つが、通常の pbf ベクトルタイルに上乗せされている形のようです。ですので、スタイルの `source` としては、建物 3D 用、樹木 3D 用、通常の地物用の3種類存在します。

### 建物 3D モデルについて
建物の 3D データは、pbf 形式のベクトルタイル同様、**[Slippy map tilenames](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames) 形式に分割された b3dm 形式のデータ**となっています（これも、「ベクトルタイル」と言ってよいでしょう）。

スタイル設定ですが、まず、`source` は、新しい **`"type":"batched-model"`** が指定されているようです。また、これを表示するスタイルレイヤも、**`"type":"model"`** という新しい種類となっています。

以下、Mapbox 社のスタイルから引用した、建物の 3D モデルのみを表示するスタイル設定（`source`、`layers`）の例です。なお、b3dm 形式のベクトルタイルの場合、[アクセストークン](https://docs.mapbox.com/help/getting-started/access-tokens/)もURLに含めて設定することが求められるようです。
```JSON:source
"sources":{
   "3dbuildings":{
     "type":"batched-model",
     "attribution":"<a href=\"https://www.mapbox.com/about/maps/\" target=\"_blank\" title=\"Mapbox\" aria-label=\"Mapbox\" role=\"listitem\">&copy; Mapbox</a> <a class=\"mapbox-improve-map\" href=\"https://www.mapbox.com/contribute/\" target=\"_blank\" title=\"Improve this map\" aria-label=\"Improve this map\" role=\"listitem\">Improve this map</a>",
     "maxzoom":14,
     "minzoom":14,
     "tiles":[
       "https://a.tiles.mapbox.com/3dtiles/v1/mapbox.mapbox-3dbuildings-v1-beta/{z}/{x}/{y}.b3dm?access_token=<your-access-token>",
       "https://b.tiles.mapbox.com/3dtiles/v1/mapbox.mapbox-3dbuildings-v1-beta/{z}/{x}/{y}.b3dm?access_token=<your-access-token>"
     ]
   }
}
``` 
```JSON:layer
"layers": [
  {
    "id":"building-models",
    "minzoom":14,
    "paint":{
      "model-ambient-occlusion-intensity":0.75,
      "model-color":["match",["get","part"],"roof",["hsl",22,82,90],"wall",["hsl",0,0,100],"window",["interpolate",["linear"],["measure-light","brightness"],0,["hsl",["random",0,90,["id"]],["random",20,100,["id"]],87],0.15,["hsl",["random",200,215,["id"]],100,["random",70,80,["id"]]]],["interpolate",["linear"],["measure-light","brightness"],0.16,["hsla",["random",10,70,["id"]],55,["random",80,90,["id"]],1],0.4,"hsl(0, 100%, 100%)"]],
      "model-color-mix-intensity":["match",["get","part"],"logo",["interpolate",["linear"],["measure-light","brightness"],0.2,0,0.4,0.3],1],
      "model-emissive-strength":["match",["get","part"],"door",["interpolate",["linear"],["measure-light","brightness"],0.2,1.5,0.4,2.5],"logo",0.6,"window",["random",0.5,0.8,["id"]],0],
      "model-height-based-emissive-strength-multiplier":["match",["get","part"],"window",["literal",[0,0.9,0,1,0.5]],["literal",[1,1,1,1,1]]],
      "model-opacity":["interpolate",["linear"],["zoom"],14.2,0,14.5,1],
      "model-roughness":["match",["get","part"],"window",0,1],
      "model-scale":["interpolate",["linear"],["zoom"],14.2,[1,1,0],14.5,[1,1,1]],
      "model-type":"common-3d"
    },
    "source":"3dbuildings",
    "type":"model"
  }
]
```
以下は、東京駅周辺の 3D モデルのみ表示させた画像です。
![mapbox-v3-3dbuilding-models.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/c81933c9-a894-58f1-ace2-e03d2092711c.png)

### 樹木 3D モデルについて
樹木の 3D モデルについても見てみましょう。実は、こちらは建物 3D モデルとは異なり、位置情報や属性値については、ポイントデータとして、**pbf 形式のベクトルタイルに格納され配信されています**。そして、その**ポイントデータの属性値から、個別の 3D モデル（形式は `glb` のようです）を呼び出している**ようです。

確かに、見た目上、樹木の形状はそこまで多くなくても良いはずなので、１本１本別データで格納するのではなく、共通のモデルを使いまわすのが合理的です。

スタイル設定上は、`source` については、通常の pbf 形式のベクトルタイルの設定と違いはありませんが、別途モデルの読込先を指定する必要があり、**`models`** という設定が、`source` や `layers` と同じ階層に追加されています。また、スタイルレイヤについては、建物 3D モデルと同じく、**`"type":"model"`** を指定します。

以下、Mapbox 社のスタイルから引用した、樹木の 3D モデルのみを表示するスタイル設定（`source`、`models`、`layers`）の例です。新しいスタイル設定 `random` を用いて変化を出しているようです。

```JSON:source
"sources":{
  "trees":{
    "type":"vector",
    "maxzoom":16,
    "minzoom":13,
    "tiles":[
      "https://a.tiles.mapbox.com/v4/mapbox.mapbox-models-v1/{z}/{x}/{y}.vector.pbf",
      "https://b.tiles.mapbox.com/v4/mapbox.mapbox-models-v1/{z}/{x}/{y}.vector.pbf"
    ]
  }
}
```
```JSON:models
"models":{
  "maple1-lod1":"mapbox://models/mapbox/maple1-lod1.glb",
  "maple1-lod2":"mapbox://models/mapbox/maple1-lod2.glb",
  （省略）
}
```
```JSON:layers
"layers": [
  {
    "id":"trees",
    "layout":{
      "model-id":[
        "step", ["zoom"],
          [
          "match",["get","leaf_type"],
          "needleleaved",["case",["==",["%",["number",["id"]],2],0],"pine1-lod4","pine2-lod4"],
          "palm","palm1-lod4",["case",
            ["==",["%",["number",["id"]],4],0],"oak1-lod4",
            ["==",["%",["number",["id"]],4],1],"oak2-lod4",
            ["==",["%",["number",["id"]],4],2],"maple1-lod4",
            "maple2-lod4"
          ]
        ],
        15.5,["match",["get","leaf_type"],"needleleaved",["case",["==",["%",["number",["id"]],2],0],"pine1-lod3","pine2-lod3"],"palm","palm1-lod3",["case",["==",["%",["number",["id"]],4],0],"oak1-lod3",["==",["%",["number",["id"]],4],1],"oak2-lod3",["==",["%",["number",["id"]],4],2],"maple1-lod3","maple2-lod3"]],
        16.5,["match",["get","leaf_type"],"needleleaved",["case",["==",["%",["number",["id"]],2],0],"pine1-lod2","pine2-lod2"],"palm","palm1-lod2",["case",["==",["%",["number",["id"]],4],0],"oak1-lod2",["==",["%",["number",["id"]],4],1],"oak2-lod2",["==",["%",["number",["id"]],4],2],"maple1-lod2","maple2-lod2"]],
        17.5,["match",["get","leaf_type"],"needleleaved",["case",["==",["%",["number",["id"]],2],0],"pine1-lod1","pine2-lod1"],"palm","palm1-lod1",["case",["==",["%",["number",["id"]],4],0],"oak1-lod1",["==",["%",["number",["id"]],4],1],"oak2-lod1",["==",["%",["number",["id"]],4],2],"maple1-lod1","maple2-lod1"]]
      ]
    },
    "paint":{
      "model-color":["hsla",["random",50,200,["id"]],60,["random",70,90,["id"]],1],
      "model-color-mix-intensity":0.21,
      "model-opacity":["interpolate",["linear"],["zoom"],15,0,16,1],
      "model-rotation":[0,0,["random",0,360,["id"]]],
      "model-scale":["match",["%",["number",["id"]],5],0,["literal",[0.8,0.8,0.8]],1,["literal",[0.8,0.8,0.8]],2,["literal",[0.9,0.9,0.9]],["literal",[1,1,1]]]
    },
    "source":"trees",
    "source-layer":"tree",
    "minzoom":15,
    "type":"model"
  }
]
```

以下は、樹木の 3D モデルのみを表示した例です。なお、元が pbf のベクトルタイルなので、点の位置を青色の円（`circle`）、モデル名（`model-id`）をラベルとして表示しています。
![mapbox-v3-tree-models.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/5c1c2692-ada8-f200-1133-4583a2e26c6f.png)

### 3D モデルのあり・なし比較とメリデメ

3D モデルのあり・なしのスタイルを作成し、表示を比較してみました。

（あり）
![sample-with-buildings-and-trees.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/555e4f7d-58d3-5a52-bcca-4b7f4f1cb604.png)
（なし）
![sample-no-buildings-no-trees.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/43dc4dba-4aba-b2c7-33fe-2cc1dc5f82a3.png)

もともと、建物は `fill-extrusion` を用いて立体感が出されていましたが、やはり、詳細でテクスチャもある 3D モデルが存在するとにぎやかに見えますし、ランドマークによって、どの都市か分かりやすくなります。また、特に都心だと、樹木が与えるイメージが大きいと感じました（潤いを感じる、的な……）。

一方、b3dm のタイルは、東京駅周辺を観察する限り、データが存在する場所では１つのタイルが 100 kb 以上がざらで、時には 1 MB を超えるようなこともあります。これは、pbf 形式のベクトルタイルに比べてかなり重くなる印象です。一方、（今後どの程度整備していくのかは分かりませんが、）郊外や地方を見ると、このような建物 3D データがほとんどなく、逆に b3dm タイルのリクエストが無駄になっているという印象がありました。

**3D データのようなリッチなデータは、場所によって地物の密度が異なり、均一な Slippy map tilenames での分割だと、まだ厳しい**のかもしれません。局所的（特に地物が密集する大都市）にタイルのデータサイズが大きくなるというベクトルタイル特有の傾向に加え、整備状況の傾向（大都市の地物の方が 3D モデル化されやすい）も拍車をかけている印象です。

ベクトルタイルは、軽く・早くを目指してきたところがありますから、この 3D モデルのデータの大きさが許容されるものなのか、興味があるところです。

また、樹木については、公園や山など、樹木が本来たくさんある場所に全く存在しないのが物足りなく感じました。もちろん、大量の 3D モデルを表示すれば重くなるため、公園や山の植生は土地利用の塗りつぶしで表現する、という割り切りは合理的だと思いますが、人の欲は底知れずです。

## 感想
Mapbox GL JS v2 はほとんど触っていませんでしたが、今回は 3D モデルの衝撃が大きく、色々と遊んでみました。地図の 3D 化のトレンドはまだ衰えそうにないですね。

一方、ウェブ地図だから当たり前ですが、今回の機能拡充は「見た目」特化のものが多いです。個人的に、地図が 3D になって何が変わるのか？　という問いに悩んでいるところですが、まだ「何と言っても見た目が変わる」と答えるのが現状な気がします。むしろ、「見た目が変わると何が変わるのか？」を突き詰めていく必要性の方が高そうです。

また、何か気づきがあれば追記します。

## 参考文献
https://www.mapbox.com/blog/standard-core-style

https://github.com/mapbox/mapbox-gl-js