Cesium for UnityとPLATEAUを連携して、GeospatialAPIで位置合わせを行う
概要
MESONではGeospatialAPIを利用したコンテンツ制作や、プロトタイプ制作を意欲的に行っています。最近はMESON内でプロトタイプ作ろうぜ会が発足して、そこで出た案を「EmoBalloon」というものを作りました。これは街中でバルーンを立てて待ち合わせがしやすくなる、みたいなコンセプトのプロトタイプです。
Twitterに動画も投稿しているのでぜひ見てみてください。
上の動画を見てもらうと分かりますがGeospatialAPIの精度は相当ですね。
このプロトタイプでも実はPLATEAUを使ってビルの遮蔽を行っています。しかし、ビルモデルの位置については事前に計算して手動で合わせ、さらにアプリ内に配置した状態でビルドを行っています。
上記のプロトタイプではこの方法を使っています。その様子が分かりやすい動画も投稿してみました↓
しかし、これではせっかくのGeospatialAPIの強みである「世界中どこでも位置合わせができる」という利点を殺してしまっています。
そこで今回は、タイトルの通り「Cesium」というサービスを利用して動的にビルモデルを読み込んで位置合わせする方法を調べて実装したので、その方法について書いていきたいと思います。
▼ Cesium for Unityのリポジトリ
これを実際に実装して動かしてみた動画が以下です↓
仕組みを概観する
位置合わせの詳細に入っていく前に、前提となる知識について整理しておきましょう。
3D Tiles
ビルモデルのデータ構造として3D Tilesという仕様があります。これはGoogle Mapなどの2Dの地図アプリで見られる、地図をタイル上に分割してユーザがマップを移動すると次々に周辺を読み込んでいく仕組みの3D版と見ることができます。
CesiumのGitHubリポジトリで以下のように説明されています。
3D Tiles is an open specification for sharing, visualizing, fusing, and interacting with massive heterogenous 3D geospatial content across desktop, web, and mobile applications.
3D Tilesは大規模な異なる3D地理空間コンテンツを、デスクトップ、Web、モバイルなどを横断してシェア、ビジュアライズ、融合そして対話するためのオープン仕様です。
オープン仕様が示す通り、Cesium独自の技術ではなくその他のサービスともコラボレーションできるようになっています。PLTEAUもこの仕様に対応しており、今回はそれを利用してコンテンツの配置を試しています。
3D Tilesについてはこちらも参考にしました。
上記から画像を引用させてもらうと以下のような形でデータが保存されています。
データは入れ子構造になっており、それぞれの区画にモデルを配置しています。大きな枠(tile)の場合は粗いモデルを、小さい枠の場合は詳細のモデルを配置しています。いわゆるLODを実現しているわけですね。これによって遠い場所にあるビルは粗く、近くのモデルは詳細に、ということが可能となります。
高度についての概念整理
今回、Cesium、PLATEAU、GeospatialAPIという3つのサービスを併用します。その関係で、それぞれの定義が若干異なるためその知識を整理しておきます。特に大きな問題になるのが高度の取り扱い方です。
これが一致していないとビルがとても下、あるいは上にある状態で描画されてしまい、位置合わせがうまくいきません。
手描きの絵で見づらいかもしれませんが、大まかに概観すると以下のような関係になっています。
これがなにを示しているか、の話をする前にまず前提知識から話していきます。
標高、ジオイド高、高度
まず前提となる知識として、一言に「高さ」と言ってもいくつかの表現があります。それが「標高」「ジオイド高」「高度」です。
標高とジオイド高についての関係は地理院の以下のサイトに詳しく書かれています。
このサイトから説明用の画像を引用させてもらうと以下のように説明されています。
この図が説明しているのは、標高は楕円体高からジオイド高を引いたものである、ということです。
楕円体と楕円体高
楕円体高は言い換えると「高度」です。英語では「Altitude」ですね。後ほど出てくるので覚えておいてください。
図を見ると分かるように、楕円体はなめからな曲線をしています。これは地球を楕円体として大まかに近似した形です。そして楕円体高はそこにジオイド高と標高を緯度経度に合わせてにマッピングし、最終的にそれを加算することで実際の高さを表現しています。
Wikipeidaによると以下のように説明されています。
地球楕円体(ちきゅうだえんたい、英: Earth ellipsoid)とは、測地学において地球のジオイド(平均海面)の形に近似した回転楕円体(扁球)を指す。その中心は地球の重心に、短軸は自転軸に一致させる。
標高とジオイド高を調べるサービス
標高とジオイド高を調べるサービスがあります。デバッグ時など、その場所(緯度経度)の高度が知りたい場合などに役に立つサービスです。
▼ ジオイド高を調べる
▼ 標高を調べる
このふたつのサービスで該当の緯度経度の標高とジオイド高を求め、それを加算することで高度を求めることができます。
PLATEAUモデルの底面は標高位置
Cesiumと組み合わせて使う場合は気にしなくて大丈夫ですが、補足として書いておきます。
PLATEAUモデルのfbxなどをダウンロードしてきて利用する場合、ひとつだけ注意点があります。それは、ビルの底面が標高の位置に合わせてあるということです。
これを把握していないと、例えばGeospatialAPIのTerrain Anchorなどで配置しようとすると標高分位置がずれることになってしまい、うまく位置が合いません。
今自分が操作している「高さ」が、「標高」なのか「ジオイド高」なのか「高度」なのかは常に意識する必要があります。
やや遠回りしましたが、冒頭の図の説明に戻りましょう。
この図が示していることは、Cesium APIに入力する「Height」の 0
位置が楕円体表面を示しているということです。
そしてGeospatial APIのTerrain Anchorに対して altitudeAboveTerrain: 0
の位置に生成される位置は「高度」、つまり「楕円体高」となります。
そして最後に、仮にこの位置にPLATEAUモデルを配置したときの位置を図示しています。前述したように、PLATEAUモデルのビルの底面は標高の高さを持っているので、そのまま配置してしまうとその分上に浮いてしまう、というわけなのですね。
なので適切に配置したい場合は「標高分引いてやる」必要があります。
上の図を再掲します。
なんとなくそれぞれの関係がイメージできるでしょうか。
座標系について整理
次に説明するのは座標系についてです。これまた色々あり、さらにUnity空間とも異なる座標系を持っているためしっかりと理解しておかないとすぐに混乱してしまいます。しかも都合の悪いことに、これらの位置合わせは実機にビルドし、実際に街中に出ていってチェックする必要があります。さらに、位置が合っていない理由がどこにあるのかが分かりづらいのも問題です。ここでしっかり座標系について把握しておきましょう。
EUN座標系とEUS座標系
EUN / EUSは、East-Up-North / East-Up-Southの頭文字を取ったものです。以下の図を見てもらうとイメージしやすいかと思います。
EUN座標系の図示
上記画像はWikipediaから引用させてもらいました。
この図が示しているのは、基準位置となる点(緯度経度)に接平面を設置し、X軸を東に、Y軸を空方向に、Z軸を北に取った座標系ということです。(EUSはその逆なのでZ軸が南を向きます)
GeospatialAPIには EunRotation
というクォータニオンを返してくれるAPIがありますが、これは対象となるオブジェクトのforwardを北に向ける回転を示すクォータニオンです。
これで、位置合わせの仕組みを実装していく上での必要な知識が整理できました。
さっそく実装を、と行きたいところですがその前に、Cesium自体のセットアップも解説しておきます。
Cesiumのセットアップ
セットアップについては以下の記事を参考にさせていただきました。
サンプルプロジェクトをダウンロード
GitHubのReleaseページにサンプルプロジェクトがあるのでまずはそれをダウンロードしてきます。
今回はv0.2.0をベースにしています。
3D都市モデル(3D Tiles)を設定する
サンプルプロジェクトを開いたら 01_CesiumWorld
シーンを開きます。
そして Cesium OSM Buildings
を選択、Tileset Source
を From Url
に変更し国土交通省が提供してくれているJSONのリストから設定したい箇所のJSONのURLを選んで設定します。
アクセストークンとアセットIDを利用する
上記の3D都市モデルの設定ではJSONのURLを設定しましたが、アクセストークンとアセットIDを利用することもできます。
アクセストークンとアセットIDは国土交通省が用意してくれているものを利用できるようです。
以下のページから取得します。
取得したら Cesium World Terrain
オブジェクトの Cesium 3D Tileset
の ion Asset ID
と ion Access Token
の欄に設定します。
カメラのセットアップ
サンプルプロジェクトはAR用に作られているわけではないため、通常のカメラが設置されています。そのカメラオブジェクトに、上記のコンポーネントなどがアタッチされて、自由に街中を動き回ることができる状態になっています。
しかし今回はARで利用するため、カメラコンポーネントは削除してください。
AR自体のカメラのセットアップについてはここでは割愛します。
Cesiumのセットアップは以上です。
次からGeospatialAPIを用いた位置合わせについて見ていきます。
GeospatialAPIを用いた位置合わせ
前提知識などについてまとめてきました。ここからは実際に位置合わせを行う方法について見ていきたいと思います。
Cesiumについて理解する
位置合わせに際して少しだけCesiumの知識を整理しましょう。ここではCesiumの仕様がどうなっているのかを見ていきます。
Cesiumのサンプルプロジェクトを開くとCesiumGeoreferenceというオブジェクトがシーン内に配置されています。
この Cesium Georeference
コンポーネントの意味は、これに設定された緯度経度の位置をUnity空間におけるこのオブジェクト位置に固定し、そこから3D都市モデルを読み込みます。
シーンビューで見たもの
上図は自分の家付近の緯度経度を設定した様子です。ちょっと分かりづらいですが、UnityのGizmoが表示されている位置がちょうど設定した緯度経度の位置になっています。
(実際に入力して試してみてください。Google Mapなどの位置と見比べてみると合っているのが分かるかと思います)
Cesium Globe Anchorで位置を移動させる
次に見るのは、上記の Cesium Georeference
オブジェクトの子要素に設定されている Cesium Globe Anchor
コンポーネントです。
図を見てもらうと分かりますが、このコンポーネントにも緯度経度(Latitude / Longitude / Height)の設定項目があります。また本オブジェクトに Cesium Origin Shift
コンポーネントもついています。
このコンポーネントの緯度経度を操作することで Georeference
の緯度経度が変更されます。Unity世界の Transform
は変化しませんが、緯度経度が移動することにより、実質的に空間が移動しているように見える、という仕組みです。
実はこのオブジェクトは、サンプルプロジェクトを開いた直後にはカメラコンポーネントも持っており、この緯度経度を操作すると街中を移動することができるようになっています。
緯度経度を操作することによって移動することができることが分かりました。GeospatialAPIにはカメラの位置の緯度経度を返してくれるAPI、 AREarthManager.CameraGeospatialPose
があり、この構造体が持つ緯度経度を利用することで位置合わせを行うことができます。
Georeferenceをカメラに追従させる
上で書いたように、移動には緯度経度を利用する必要があります。これは、モバイルゲームなどの環境では問題になりません。しかし、ARの環境では問題が出てしまいます。
というのも、ARKitやARCoreによってカメラが動かされてしまうからです。上で少しだけ触れた Transform
が動いていない、ということを思い出してください。カメラの移動は緯度経度を動かすことで実現し、自分自身の Transform
は変化していませんでした。
しかしARの場合はカメラが自動で動かされてしまいます。この問題を回避するために、 Georeference
の位置をカメラ位置に追従させることで対処します。
ただ、Y軸方向の移動は Height
の値で対応しているため、X,Z平面のみ追従させます。
これを踏まえて、コードは以下のようになります。
// Yの値だけ0にしてコピーする
Vector3 pos = _camera.transform.position;
pos.y = 0;
_georeference.transform.position = pos;
こうすることで、ARカメラが移動しても Georeference
は常にカメラ位置についてくるようになります。
Geofenceを北に向ける
位置自体は緯度経度によって定まります。しかしこれは「点」であり「方向」を持っていません。そこで最後はこの「方向」を設定します。
結論から言ってしまうと、GeospatialAPIには「北方向を向く回転」を得ることができるようになっており、北方向への回転を加えてやることで位置合わせが完了します。
具体的には、カメラの forward
ベクトル方向に向かせる回転に、 EunRotation
からY軸回転だけを取り出したものを掛けることで実現します。
ただここでひとつ注意点があります。というのも、GeospatialAPIのネイティブで実行されている座標系はEUN座標系ですが、実はUnityの座標系はZ軸が反転したものとなっています。そのため、APIのC#実装を紐解くと、Z軸だけ反転する回転を加えてからAPIに渡しています。
これを利用して、今回の実装では以下のように補正した上で Georeference
オブジェクトの回転を設定しました。
// カメラの向いている方向へ向かせる回転を計算(Vector3.forwardに掛けるとカメラの方向になる回転)
Vector3 forward = Vector3.ProjectOnPlane(_camera.transform.forward, Vector3.up).normalized;
Quaternion look = Quaternion.LookRotation(forward, Vector3.up);
// GeospatialAPIの回転をUnity座標系の回転へ変換する
float y = data.EunRotation.eulerAngles.y;
Quaternion rotation = _glWorldToUnityWorldRotation * Quaternion.AngleAxis(y, Vector3.up);
_georeference.transform.rotation = rotation * look;
なお、 _glWorldToUnityWorldRotation
クォータニオンは以下のように計算しています。
private static readonly Matrix4x4 _unityWorldToGLWorld = Matrix4x4.Scale(new Vector3(1, 1, -1));
private static readonly Matrix4x4 _unityWorldToGLWorldInverse = _unityWorldToGLWorld.inverse;
private static readonly Quaternion _unityWorldToGLWorldRotation = Quaternion.LookRotation(_unityWorldToGLWorld.GetColumn(2), _unityWorldToGLWorld.GetColumn(1));
private static readonly Quaternion _glWorldToUnityWorldRotation = Quaternion.Inverse(_unityWorldToGLWorldRotation);
以上で終了です。あとは実際にビルドして実機で見てみましょう。
冒頭にも載せましたが、改めて実際に動かしてみた動画を載せておきます。しっかりと周りの環境に建物モデルが重なっているのが確認できます。
最後に
CesiumとPLATEAUを組み合わせてビル群を配置し、GeospatialAPIによってリアルの街に重畳させることで色々と妄想の幅が広がりますね。
個人的には巨大オブジェクトなどを街中に登場させると面白いのではないかな、と思っています。ぜひみなさんも色々とチャレンジしてみてください。
エンジニア絶賛募集中!
MESONではUnityエンジニアを絶賛募集中です! XRのプロジェクトに関わってみたい! 開発したい! という方はぜひご応募ください!
MESONのメンバーページからご応募いただくか、TwitterのDMなどでご連絡ください。
書いた人
比留間 和也(あだな:えど)
カヤック時代にWEBエンジニアとしてリーダーを務め、その後VRに出会いコロプラに転職。 コロプラでは仮想現実チームにてXRコンテンツ開発に携わる。 DAYDREAM向けゲーム「NYORO THE SNAKE & SEVEN ISLANDS」をリリース。その後、ARに惹かれてMESONに入社。 MESONではARエンジニアとして活躍中。
またプライベートでもAR/VRの開発をしており、インディー部門でTGSに出展など公私関わらずAR/VRコンテンツ制作に精を出す。プライベートな時間でも開発しているように、新しいことを学ぶことが趣味で、最近は英語を学んでいる。
MESON Works
MESONの制作実績一覧もあります。ご興味ある方はぜひ見てみてください。
Discussion