3Dモデルを3DTilesに変換してdeck.glで表示する

# やりたいこと
Mapbox GL JS、deck.glのような2.5次元表現ができるWebGISや、Cesium、iTownsといった3次元WebGIS上への3Dモデル配置の要望をよく聞くようになりました。
2020年4月に公開された[国土交通データプラットフォーム](https://www.mlit-data.jp/platform/)でも、Mapbox GL JS/deck.glを使用した点群データの表示が行われています。今後さらに事例は増えてくると思われます。
![国土交通データプラットフォーム](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/620a03dc-6442-7ec3-3f4b-7e9c52721a39.png)

点群データの場合は、計測時の地理座標が付いてさえいれば、deck.glに単体のデータとして読み込むことや、3DTilesに変換した上で読み込むことができます。
一方、構造物の3DモデルをglTF、objで受け取った際は、地理座標はついていないし、方向はわからないし、苦労して単体のモデルを配置することになります。ジオリファレンスが必要なのですが、どういう手順が一番いいのか試行錯誤です。
特に迷うのが、3DTilesで扱いたい際にどうやって変換しようという点でした。

この記事は、適当な3Dモデルを強引に3DTilesに変換していい感じにdeck.glに配置してみたものです。たぶん、こんな方法じゃなく楽にできる方法があると思います。

# BlenderGIS
## インストール
まずは位置合わせを行いたいのですが、ここではBlenderのaddon「[BlenderGIS](https://github.com/domlysz/BlenderGIS)」を使用します。
Blenderに各種GISフォーマットをインポートできたり、DEMを読み込み3D表示ができるのですが、ここでの目的はWEBメルカトル座標を定義して、GoogleMap等のWeb地図を読み込むことです。WEBメルカトル座標上で3Dモデルを配置します。

[Install and usage](https://github.com/domlysz/BlenderGIS/wiki/Install-and-usage)を参考に、githubからソースのダウンロードを行い、addonへインストールします。

[Quick start](https://github.com/domlysz/BlenderGIS/wiki/Quick-start)の2.に書かれていますが、「cache folder」を指定しておきます。また、「spatial reference systems」が「Web Mercator」になっていることも確認しておきましょう。

## GoogleMap表示
インストールが完了すると、3Dビューウィンドウ上に「GIS」が追加されますので、「GIS」から「Web Geodata」→「Basemap」の順に選択を行います。
![GIS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/820ddbe0-1c68-2258-a3ff-d44fd38abdd6.png)

Source:Google
Layer:Map
を選択して「OK」をクリックします。
![googlemap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/4c203a61-bc89-4794-da76-da8e88b74354.png)

GoogleMapが表示されますので、ドラッグで移動、マウスホイールで拡大/縮小を行い、モデルを配置したい位置まで地図を移動します。移動が終わったら、「E」キーを押すことでtextured plane meshが作成されます。

この時に地図の中心とした位置の緯度経度は調べておきましょう。後で3DTilesに変換する際に、原点座標として使います。
![clip](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/429bb6f1-47b3-b15c-d362-8a56fdba92c4.png)

## objの位置合わせ
適当にfree素材のobjを配置してみます。CloudCompareで表示した例ですが、寸法も実寸にはなっていない建物の3Dモデルです。
<img width="1069" alt="model.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/4371d6ce-e72a-ce1b-3d67-344f7ebb0709.png">

Blenderへインポートすると、ちっさいです。
![Blenderへインストール](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/c9d4b102-8c9f-a3f1-8d50-74ac3e625852.png)

このモデルは拡大したり、回転したり、移動したりして、なんとなく山頭火本店に合わせて配置してみます。
![rotate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/0b1e0766-5cd8-9c6a-0808-d61600a41253.png)

![move.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/cdc9564c-3de0-358f-cbf4-8e4db2a7a6ef.png)

![scale.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/f13a6f61-794a-215e-e17b-2b0220002236.png)

大体いいかなという位置に配置できたら、取り込んだtextured plane meshは削除した上で、建物モデルのみobjにexportしておきます。

# obj23dtiles
## インストール
objから3DTilesへの変換には、[obj23dtiles](https://github.com/PrincessGod/objTo3d-tiles)を使用しました。メンテナンスされてないので注意が必要ですが、tileset.jsonまで作ってくれるので便利です。

インストール自体は、
`npm install -g obj23dtiles`
で完了です。

## 変換準備
BlenderGISで地図を切り出した際の中心緯度経度を、customTilesetOptions.jsonに記載しておきます。obj23dtilesで変換する際に、3Dモデルの原点座標(0,0)が緯度経度(ラジアン単位)でいくつかを与えます。
先ほど切り出した地図の場合、緯度：43.764783、経度:142.359914でしたので、これをラジアンに変換して記載します。

```customTilesetOptions.json
{
    "longitude":      2.48464922215,
    "latitude":       0.763839559769,
    "transHeight":    0.0,
    "region":         true,
    "box":            false,
    "sphere":         false
}
```

## 変換
"Create a single tileset with .b3dm tile and custom tileset options"になるようにオプションを指定して、building.objを入力に3DTilesを作成します。

`obj23dtiles -i ./building.obj --tileset -p ./customTilesetOptions.json`

変換がうまくいくと、Batchedbuildingフォルダに、tileset.jsonとbuilding.b3dmが作成されます。

# deck.glで表示
## deck.glの3d-tilesサンプルを変更して表示
deck.glに[3d-tiles表示のサンプル](https://github.com/visgl/deck.gl/tree/master/examples/website/3d-tiles)があります。これを少しだけ変更します。
app.js内の_renderTile3DLayer()において、TILESET_URLは作成したtileset.jsonへのパスに、loaderとしてTiles3DLoaderを指定とします。
その他、実行まえに、

`export MapboxAccessToken=<mapbox_access_token>`

でmapboxのキーを設定することも忘れずに。

```app.js
  _renderTile3DLayer() {
    return new Tile3DLayer({
      id: 'tile-3d-layer',
      pointSize: 2,
      data: TILESET_URL,
      loader: Tiles3DLoader,
      loadOptions: {"_lighting": "pbr"},
      onTilesetLoad: this._onTilesetLoad.bind(this)
    });
  }
```

描画してみると、大体いい位置にモデルを表示できています。
![deckgl3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/903e16bd-32b8-ecfe-f00c-a7465817c25a.png)

<img width="1287" alt="declgl2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58748/4fcdc433-c53d-509c-6eb0-aa0533240ca5.png">