ゲームのマップに適した地図投影法は | 春採製作所 忍者ブログ
ヴォストク(Vostok Saporovskii)の技術メモ・アイデア帳・近況など
NEW
最近ゲーム作りに熱が入ってます、ヴォストクです。

さて、Twitterでは何度か話を出しているのですが、私は現在『実物大で再現された北海道をドライブするゲーム』をUnityで作っているところです。DLCでサハリン州にも行けたらいいなとか思っています。

実物大で北海道(とサハリン州)を再現するとなれば、一番の問題はその大きさです。
作業量が㌧でもないことになるのは明白ですが、それとは別の問題もあります。
これだけ広い地域だと、地球を平面に投影したことによるワールドの歪みが無視できなくなるのです。
一般人が立ち入ることができる北海道(とサハリン州)の範囲はおおむね以下のような感じです。
北海道とサハリン州の範囲
by Google マップ
北端: 45.5°N (稚内市)
南端: 41.4°N (松前町)
西端: 139.4°E (奥尻町)
東端: 145.8°E (根室市)
東西に6.4°、南北に4.1°もあります。
地球を半径6371kmの真球として考えた場合、経線にそった緯度1°あたりの距離は111.319kmほど。北緯43°線にそった経度1°あたりの距離は81.413kmほどとなります。
つまり、東西521km南北456kmもの広大なワールドです。
ここからさらにサハリン州本土の樺太島を含めると、北端は54.4°N、南北に13.0°すなわち1443kmにまで広がります。
この場合、南端の41.4°Nにおける経度1°あたりの距離は83.502km、北端の54.4°におけるそれは64.801kmと、同じ経度差でなんと1.289倍も実距離が違ってしまいます。さすがに無視できる歪みではありません。

もちろんUnityでは当たり前のように3Dを扱えるので、原理的には地球を模した球面上に歪みねぇワールドを作ってしまうことも可能です。でも重力をはじめとした物理演算や座標系を考えると、それはそれで不都合が多そうです。その上に置かれるオブジェクトも面倒なRotationを何度もいじらなきゃいけないし!

というわけで、一般的なXZ平面上にマップを展開していくことを考え、この目的に適した投影法を探っていきます。

まず元にする地形データの形式を確認しておきましょう。北海道全域の地形をUnityのTerrainで作ってたらしんでしまうので、各機関が提供する数値標高モデル(DEM; Digital Elevation Model)を使って半自動的に地形を生成します。北方地域を除く北海道内については、国土地理院が配布している『基盤地図情報 数値標高モデル 10mメッシュ』を活用したいと思います。なんと全国が網羅された標高データが無料でもらえます
数値標高モデルはXML形式で記述されており、ここからゲームのマップを生成するには一工夫要りますが、ゲームを創る程度の能力があれば問題なく扱えるはずです。

さて国土地理院のFAQによると、数値標高モデルの『10mメッシュ』とは以下のような意味合いのようです。
3-5 基盤地図情報(数値標高モデル)のうち10mメッシュ(標高)のデータの水平位置の精度と高さの精度はどれくらいですか?
10mメッシュ(標高)は、地形図、火山基本図の等高線データを基に、経度差、緯度差0.4秒(約10m)間隔のメッシュの中心点の標高を算出しています。水平位置はメッシュの規格で指定されるため、座標の誤差はありませんが、数値標高モデルの水平位置の精度は基となる等高線データの精度に依存します。
つまり、0.4秒がおおよそ10mに相当する(緯度35.964°くらいの値)ということで、どの緯度でも東西南北ともに10m間隔のデータというわけではありません。というかそのようなデータは作りようがありません。
仮に記録されたデータをそのまま等間隔に展開(正距円筒図法)したら、極に近いほど形は横長に、そして基準緯線が赤道でない場合、赤道に近いほど縦長に引き伸ばされることになります。
SRTM30 緯度経度座標系
北緯35.964°を基準にカシミール3Dで描画した東アジア
インドシナ半島と北極海周辺を見比べると、それぞれ縦横に引き伸ばされている。
もっとも一定の角度で経緯線に平行に羅列されたデータはコンピュータで非常に扱いやすく、わたしの北斗計画の世界地図でもそうしています(ステマ)
それはおいといて、数値標高モデルが経緯度を基準に記録されていることがわかりましたので、本題の投影法に入りたいと思います。
まず下の画像はわたしが今年の頭くらいに作りかけたゲームの様子です。
サンソン図法
スペースシャトルが記録した全世界の30秒きざみの数値標高モデル『SRTM30』を組み込んでおり、世界のどこでも地形ポリゴンを出力できるというものでした。

このマップはサンソン図法でプロットされています。
サインカーブ型に高緯度の幅を縮めることで、面積が正しく描画されます。緯線は等間隔で東西は cos(緯度) をかけて縮めるだけなので、これもまたコンピュータで扱いやすい投影法です。
いっぽうで中央経線から外れた特に高緯度では、本来直交すべき経緯線が斜めに交わることになるのが画像の世界地図の歪み方からわかるかと思います。
サンソン図法
by Strebe / Wikimedia Commons
つまりこの投影法では形や角度が正しく描画できないのです。もし中央経線から外れた場所をプレイヤーが訪れれば、十字路であるべきものがメ字路に見えてしまいます。建物も平行四辺形に歪めなければなりません。これはゲーム的に大問題です。
では同じく面積が正しくなるランベルト正積円筒図法はどうでしょうか?
ランベルト正積円筒図法
by Strebe / Wikimedia Commons
こちらは緯度によらず経線の間隔を一定とし、代わりに緯線の間隔を詰めることで正積性を保っています。
これなら経緯線はどこでも直交しており、四角く展開されるのでUnityの座標系で扱いやすそう……ですが、緯線を詰めるということは極に近いほどZ軸方向につぶれます。やはりあらゆるオブジェクトのScaleをいじることになりますし、ビジュアルも良くありません。
さてそれでは、おそらく最もよく知られている投影法、メルカトル図法はどうでしょう。
メルカトル図法
by Strebe / Wikimedia Commons
これは極に近づくにつれ経度方向を引き伸ばしたのと同じ比率で緯度方向も引き伸ばす投影法です。
この投影法の特徴は正角性にあります。局所的にはどこでも形状が正しくなります襟裳岬で正方形のものは、エリザベート岬でも正方形のままです。これはゲームのマップを作るにあたって大きな利点です。
いっぽうで長さや面積は正しく描画できません。この点については今後どう表現するか検討中です……
そして経度方向を引き伸ばした比率ということは、高緯度ほど倍率が限りなく無限大に近づくので、極の描画ができません。が、ゲームで北極点・南極点を出すわけではないので今回は特に問題ないでしょう。

経緯度からメルカトル図法の地図上の位置、あるいはその逆といった計算は、上の2つの投影法に比べてかなり複雑です。数式が気になる方は上のリンクからWikipediaをご覧になってください。
人に見せられるコードではなさそうですが、座標変換部分を以下に置いておきます。
アタッチされた対象GameObjectの中心座標から、ワールドがメルカトル図法であるとした経緯度を出力します。ワールド原点の経緯度を変更することができます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Position2Coords : MonoBehaviour {
	public float targetLatitude; // 対象の緯度
	public float targetLongitude; // 対象の経度

	public float baseLatitude = 43.061103f; // Z=0 の緯度(deg)
	public float baseLongitude = 141.356426f; // X=0 の経度(deg)

	public float realOffsetNS; // 基準緯度からの実距離(m)
	public float realOffsetEW; // 基準経度からの実距離(m)

	float earthRadius = 6371010.0f; // 真球で近似した地球の半径(m)
	float offsetFromEquator; // 赤道から基準緯度までのマップ上距離(unit)

	void Start() {
		offsetFromEquator = InvGd(baseLatitude * Mathf.Deg2Rad) * earthRadius;
	}

	void Update() {
		Vector3 position = GetComponent<Transform>().position;
		float offsetLongitude;

		// 座標から緯度を求める
		targetLatitude = Gd((position.z + offsetFromEquator) / earthRadius) * Mathf.Rad2Deg;
		realOffsetNS = ((targetLatitude - baseLatitude) * Mathf.PI * earthRadius) / 180.0f;

		// 座標から経度を求める
		offsetLongitude = (position.x / earthRadius) * Mathf.Rad2Deg;
		targetLongitude = baseLongitude + offsetLongitude;
		realOffsetEW = position.x * Mathf.Cos(targetLatitude * Mathf.Deg2Rad);
	}

	float Gd(float x) {
		// グーデルマン関数 (位置→緯度 に使用)
		float gd = (2.0f * Mathf.Atan(Mathf.Exp(x))) - (Mathf.PI / 2.0f);
		return gd;
	}

	float InvGd(float x) {
		// 逆グーデルマン関数 (緯度→位置 に使用)
		float gd = Mathf.Log(Mathf.Abs(Mathf.Tan((Mathf.PI / 4.0f) + (x / 2.0f))));
		return gd;
	}
}

まとめ

ゲームの平面マップにはメルカトル図法がよさそう!
PR
© 2018 Vostok Saporovskii
忍者ブログ [PR]