モバイルアプリを開発していると、画面前方にある何かに向きを合わせて表示したくなることがあると思います。
例えば地図を表示したときは、自分の向きがわかるように表示したいですよね。
ただ携帯(Unity上のカメラ)、表示するもの、向きを合わせたいものなど複数出てきますし、
位置とか向きとか、local、world、ベクトル、内積、クォータニオンとかを考え出すと頭が混乱してきませんか?
Unity には三次元空間上でベクトルや角度を計算するためのメソッドが用意されています。
数学的には難しいと感じるものでも、用意されているメソッドを利用することで数行で実装することができます。
そのサンプルです。
TL;DR
以下二つのメソッドを利用しています。
- 三次元ベクトルをある平面上(二次元)に投影するメソッド、ProjectOnPlane
- 2つのベクトルから角度を求めるメソッド、SignedAngle
サンプル
想定
矢印が常に携帯の前に表示されていて、かつ北向きを表示してくれるようにします。
- あるタイミング(今回はRキーを押したとき)に
- 携帯の画面前方(今回は3m前方)に
- 進行方向(今回は仮想の北向き)を教えてくれる
結果
いきなり出てきて終わりという感じです。
全コード
using UnityEngine;
using Random = UnityEngine.Random;
public class Sample : MonoBehaviour
{
[SerializeField] private Transform north;
[SerializeField] private Transform came;
[SerializeField] private GameObject arrowPrefab;
void Start()
{
north.position = new Vector3(RandPos(), 1, RandPos());
north.rotation = Quaternion.Euler(0, RandAngle(), 0);
came.position = new Vector3(RandPos(), 1, RandPos());
came.rotation = Quaternion.Euler(RandAngle(), RandAngle(), RandAngle());
}
void Update()
{
if (Input.GetKeyDown(KeyCode.R))
{
// カメラから見て 5m 先にポジションする
var pos = came.position + came.forward * 5;
// インスタンス生成、角度はなし
var arrow = Instantiate(arrowPrefab, pos, Quaternion.identity);
// xz平面上における camera の local の前向き
var came2d = Vector3.ProjectOnPlane(came.forward, Vector3.up).normalized;
// コンパス分を戻す
var direction = Quaternion.Euler(new Vector3(0, -InputCompassTrueHeading(), 0)) * came2d;
// 向き先を local から world にするために自身の position を追加する
arrow.transform.LookAt(arrow.transform.position + direction);
}
}
/// <summary>
/// Input.compass.trueHeading の代わり
/// </summary>
private float InputCompassTrueHeading()
{
var north2d = Vector3.ProjectOnPlane(north.forward, Vector3.up);
var came2d = Vector3.ProjectOnPlane(came.forward, Vector3.up);
return Vector3.SignedAngle(north2d, came2d, Vector3.up);
}
private int RandAngle()
{
return Random.Range(0, 360);
}
private int RandPos()
{
return Random.Range(-3, 3);
}
}
コード紹介
登場人物
- north : 仮想北を向く青矢印。携帯では簡単に取得できるがサンプルなのでオレオレ実装。(以下仮想略)
- came : 仮想カメラで撮影用みたいなカメラを使用。見えないとわかりにくいので仮想。(以下仮想略)
- arrowPrefab : 今回の主人公。came の前に north に合わせて表示される。
[SerializeField] private Transform north;
[SerializeField] private Transform came;
[SerializeField] private GameObject arrowPrefab;
方位磁石に合わせて came を xz 平面の角度で考えたい
三次元ベクトルをある平面上に投影する ProjectOnPlane を使用します。
これだけでカメラの前方の向きを、xz平面上に投影することができます。
こうすることで方位磁石の北向きと比較できるようにします。
// xz平面上における camera の local の前向き
var came2d = Vector3.ProjectOnPlane(came.forward, Vector3.up).normalized;
北向きとカメラの向きの角度を知りたい
平面上のベクトル間の角度を求める SingledAngle を使用します。
先ほどの ProjectOnPlane で二つのオブジェクト(north2d = 仮想の北向き、came2D = カメラの前向き)の、
ベクトルに対する角度を取得することができます。
up と down の設定を逆にすると、角度の正負が逆になるので注意が必要です。
private float InputCompassTrueHeading()
{
// ここの up は down でもどちらでも大丈夫、面を指定するので同じ意味合いになります。
var north2d = Vector3.ProjectOnPlane(north.forward, Vector3.up);
var came2d = Vector3.ProjectOnPlane(came.forward, Vector3.up);
// ここの down を up にしてしまうと、角度が正負逆になってしまうので注意です。
return Vector3.SignedAngle(north2d, came2d, Vector3.up);
}
カメラの 5m 手前に矢印を表示する
forward に 5 をかけて、カメラの位置から和算してあげればいいだけですね。
// カメラから見て 5m 先にポジションする
var pos = came.position + came.forward * 5;
// インスタンス生成、角度はなし
var arrow = Instantiate(arrowPrefab, pos, Quaternion.identity);
矢印を常に北向きに表示する
arrow を direction に LookAt するときに、came2d をコンパス分回転させたものが local なので、
arrow の位置が原点にない場合はおかしくなってしまいます。
わかっていればすぐですが、わからないと何がなんだかのところなので注意が必要です。
// xz平面上における camera の local の前向き
var came2d = Vector3.ProjectOnPlane(came.forward, Vector3.up).normalized;
// コンパス分を戻す
var direction = Quaternion.Euler(new Vector3(0, -InputCompassTrueHeading(), 0)) * came2d;
// 向き先を local から world にするために自身の position を追加する
arrow.transform.LookAt(arrow.transform.position + direction);