概要
緯度経度+高度を指定して現実世界に3Dモデルを表示したい
既に類似の記事がいくつかありますが、
- 高度まで指定している記事が見当たらなかった
- Unityで「Unity AR Alignment Gravity and Heading」の設定がなくなってからの実装例が見当たらなかった
という理由から、本記事を残します。
環境
- Mac mini (2018年モデル / Catalina 10.15.4)
- Unity (2020.3.14f1)
- iPhone 12 Pro (iOS 14.2.1)
Location Based AR
ARの実現方法はいくつかありますが、その中でもGNSS情報を使って実世界にモデルを表示するものはLocation Based ARと呼ばれます。
Loaction Based ARの実現方法を調べたところ、以下3つが候補に上がりました。
1. ARKit単体
iOSでARといえばARKitだろうと最初に考えました。
ARKit4からはLocation Based AR(Location Anchor)に対応していますが、対象地域がアメリカの一部ということで、今回は除外となりました。
2. AR.js
こちらはWebARで、少ない行数で簡単にARアプリを作成できます。
(使い方やできることはこちらのサイトが詳しいです)
また、AR.js Studioを使うとコーディング不要で5分程でアプリを作成できます。
導入が容易なためとても魅力的でしたが、高度が反映できなさそうなので、今回の要件を満たせず除外となりました。
3. Unity+ARKit
Location Based ARで調べて良くヒットするのがこの方式でした。
先人がいたことと、高度を反映させるにはこの方式しかなさそうなので今回採用となりました。
よって、本記事では3のUnity+ARKitを使って実装していきます。
方針
実世界の座標をUnityの座標に変換するため、以下の処理での実現を考えます。
(前提: 現在位置、端末の方向(θ2)が取得できていること)
- 現在位置と目標位置の緯度経度から、2点の距離を計算(m単位)
- 現在位置から目標位置の方角(θ1)、端末の方向から目標位置の方角(θ1-θ2)を計算
- 1, 2からモデルを表示する位置を計算
1. 現在位置と目標位置の緯度経度から、2点の距離を計算(m単位)
Unityでのデフォルトの1辺が1mなので、算出した距離を用いて計算していけばUnity上の座標が計算できると考えます。後述しますが、距離の計算はHaversine式を使います。
2. 現在位置から目標位置の方角(θ1)、端末の方向から目標位置の方角(θ1-θ2)を計算
以前は「Unity AR Alignment Gravity and Heading」という設定を有効化すればUnity側でいい感じに方角を変換してくれたようですが、現在この設定値はなくなってしまったようです。そのため、自前で方角を考慮する必要があります。方角(θ1)の計算はこちらを使います。
3. 1, 2からモデルを表示する位置を計算
1,2で算出した値を使い、モデルを表示する位置を計算します。
- X(東(+) / 西(-)): Sin(θ1-θ2) * 目標までの距離
- Y(上(+) / 下(-)): 端末起動地点からの高度
- Z(南(+) / 北(-)): Cos(θ1-θ2) * 目標までの距離
実装
今回は手っ取り早く検証するため、こちらのSample ProjectのSimple AR sceneに追加する形で実装しました。
適当なObjectを作成し、下記のスクリプトを追加します。(スクリプトはこちらをベースにさせていただきました)
(2022/03/09追記)
System.Device.Locationのimportでエラーが出る場合、dllの手動追加をお試しください。(@azamiyaさん、補足ありがとうございます。)
- https://www.nuget.org/packages/System.Device.Location.Portable からpackageを入手
- 入手したpackage内のSystem.Device.Portable.dllをPluginsの下に配置
(packageが解凍できない場合、拡張子を.zipに変換→unzipコマンド等での解凍をお試しください)
using UnityEngine;
using System;
using System.Device.Location;
public class GPSController : MonoBehaviour
{
public double latitude = 34.000000; // 任意の緯度
public double longitude = 139.000000; // 任意の経度
public double altitude = 1; // 任意の高度(端末起動地点からの高度)
void Start()
{
// compassから値を取得するのに必要
Input.compass.enabled = true;
Input.location.Start();
Invoke("UpdateGPS", 1.0f);
}
public void UpdateGPS()
{
if (Input.location.isEnabledByUser)
{
if (Input.location.status == LocationServiceStatus.Running)
{
LocationInfo lastData = Input.location.lastData;
GeoCoordinate coordinate = new GeoCoordinate(lastData.latitude, lastData.longitude);
float heading = Input.compass.trueHeading;
// 表示するobjectの位置更新
transform.position = ConvertCoordinate(coordinate, heading);
}
}
}
private Vector3 ConvertCoordinate(GeoCoordinate currentCoordinate, float currentHeading)
{
GeoCoordinate targetCoordinate = new GeoCoordinate(latitude, longitude);
double distance = currentCoordinate.GetDistanceTo(targetCoordinate); // 現在位置と目標位置の距離
double bearing = CalculateBearing(currentCoordinate, targetCoordinate); // 現在位置から目標位置の方角
// tan(90)
if (Mathf.Approximately(currentHeading, (float)bearing))
{
return new Vector3(0, (float)altitude, (float)distance);
// tan(-90)
} else if (Mathf.Approximately(currentHeading, (float)-bearing))
{
return new Vector3(0, (float)altitude, (float)-distance);
} else
{
double angleInRadian = ToRadian(bearing - currentHeading); // 端末の方向から目標位置の方角
return new Vector3(
(float)(Math.Sin(angleInRadian) * distance),
(float)altitude,
(float)(Math.Cos(angleInRadian) * distance)
);
}
}
// https://www.movable-type.co.uk/scripts/latlong.html
private double CalculateBearing(GeoCoordinate origin, GeoCoordinate target)
{
double φ1 = ToRadian(origin.Latitude);
double φ2 = ToRadian(target.Latitude);
double λ1 = ToRadian(origin.Longitude);
double λ2 = ToRadian(target.Longitude);
double y = Math.Sin(λ2 - λ1) * Math.Cos(φ2);
double x = Math.Cos(φ1) * Math.Sin(φ2) - Math.Sin(φ1) * Math.Cos(φ2) * Math.Cos(λ2 - λ1);
double θ = Math.Atan2(y, x);
double bearing = (ToDegree(θ) + 360) % 360;
return bearing;
}
private double ToRadian(double degree)
{
return degree * Math.PI / 180;
}
private double ToDegree(double radian)
{
return radian * 180 / Math.PI;
}
}
2点の緯度経度から距離を出すのは、GeoCoordinateクラスのGetDistanceToメソッドをありがたく使わせていただきます。(ちなみにこちらのメソッドではHaversine式が使われているようです)
2点の緯度経度から方角を出すのは、残念ながらメソッドが用意されていなかったため、こちらのサイトのjsをC#に変換して使わせていただきました。
結果
赤羽駅に1、3、5mの高さで赤い四角を表示することができました。
高度まで反映できるとアパートでテトリスなんかも作れそうですね。