時代はAR!
最近スマートフォンを新しいやつに買い替えたので、ついにおうちでARCoreができるようになりました。
サンプルを一通り触ってみたのですが、目玉機能であるCloud Anchorのサンプルがあまりにもわからないすぎる1ので自分で勉強がてら作り直してみようかと思っています。この記事はその下調べ。
Unityです。
参考サイト
話を始める前に参考になったサイトを貼ります。これを元に話をしています。
前提とか
-
Working with anchors
anchorとはなんぞや、という説明。 -
Using Instant Preview
開発効率がぜんぜん違うので、できるようにしておきましょう2。あと2mくらいの長くて頑丈なUSBケーブル。 -
はじめようArcore (修正版)
まず最初にこれをこなすと公式サンプルへの理解が捗ります。 -
Supported Devices
どのデバイスで動くか。このページいつも探してる気がするので、ついでにメモとして貼ります。
やりたいこと
ARCore Cloud Anchor APIをマスターする(前編)
ARCore Cloud Anchor APIをマスターする(後編)
どうして空間共有で先に座標系の逆変換(inverse)を行うのか?
Cloud Anchorは実空間上の一点をアンカーと定め、それを共有する仕組みです。
つまり起点であるアンカーとの相対距離・相対角度を求めることができればオブジェクトの位置と向きを共有できるはずです。
CloudAnchorのサンプル内でそれを行っているのは以下のメソッドです。
/// <summary>
/// Converts a pose from Unity world space to Anchor-relative space.
/// </summary>
/// <returns>A pose in Unity world space.</returns>
/// <param name="pose">A pose in Anchor-relative space.</param>
private Pose _WorldToAnchorPose(Pose pose)
{
if (!m_IsOriginPlaced)
{
return pose;
}
Matrix4x4 anchorTWorld = Matrix4x4.TRS(
m_AnchorTransform.position, m_AnchorTransform.rotation, Vector3.one).inverse;
Vector3 position = anchorTWorld.MultiplyPoint(pose.position);
Quaternion rotation = pose.rotation * Quaternion.LookRotation(
anchorTWorld.GetColumn(2), anchorTWorld.GetColumn(1));
return new Pose(position, rotation);
}
わからん。
Position
とRotation
Position
Position
とLocalPosition
を両方表示する
UnityのInspectorのTransform
は親がないときはPosition
を表示、親オブジェクトがいる場合はLocalPosition
を表示するというなかなか殺意に満ちた仕様をしています。
デバッグ表示にしてすら表示してくれません。なんでそんなひどいことするんでしょうか。謎。
これが見えないと話が始まらないのでInspectorを拡張します。
NGUIのようにTransformを便利に拡張する
元記事のコードにちょっと足してPosition
とLocalPosition
を同時に表示できるようにします。
Positionを相対に変換
共有したいオブジェクトをアンカーの子オブジェクトにしてLocalPosition
だけ引っこ抜けばいいような気もします。
しますが、Position
とLocalPosition
を混用すると不思議の森に迷い込むので、わたしは基本的にPosition
だけを使うようにしています。なのでPosition
だけで相対座標を導出します。
// 送信側のアンカー
Transform SenderAnchor;
// 共有したい座標
var worldPosition = ts.position;
// アンカーからの相対座標に変換します
var relativePosition = SenderAnchor.transform.worldToLocalMatrix.MultiplyPoint3x4(worldPosition);
// 受信側のアンカー
Transform ReceiverAnchor;
// 受け取り側のPositionに復元します
var local = ReceiverAnchor.transform.localToWorldMatrix.MultiplyPoint3x4(relativePosition);
変換の仕組みは標準APIの中にありました。メインスレッドからしか実行できません。たぶん。たしか。
別スレッドで行いたい場合は行列を勉強してなんか頑張ってください。
Rotation
わからんことはわかっている3ので考えないで検索します。
【Unity道場 博多スペシャル 2017】クォータニオン完全マスター
【Unity道場スペシャル 2017博多】クォータニオン完全マスター
相対のRotation
の出し方は最後のほうにあります。でもすごい勉強になるので最初から見たほうがよいと思います。4
// 共有したい姿勢
var worldRotation = ts.rotation;
// アンカーからの相対角度に変換します
var relativeRotation = Quaternion.Inverse(SenderAnchor.transform.rotation) * worldRotation;
// 受け取り側のRotationに復元します
var local = ReceiverAnchor.transform.rotation * relativeRotation;
Position
とRotation
完成品はこれ。
public static ValueTuple<Vector3, Quaternion> WorldToRelative(Transform anchor, Vector3 worldPosition, Quaternion worldRotation)
{
return (anchor.transform.worldToLocalMatrix.MultiplyPoint3x4(worldPosition), Quaternion.Inverse(anchor.rotation) * worldRotation);
}
public static ValueTuple<Vector3, Quaternion> RelativeToWorld(Transform anchor, Vector3 relativePosition, Quaternion relativeRotation)
{
return (anchor.transform.localToWorldMatrix.MultiplyPoint3x4(relativePosition), anchor.rotation * relativeRotation);
}
実験
必要なコードはできたので適当なコードでテストしてみます。
// 送信側のアンカー
[SerializeField] private GameObject SenderAnchor;
// 送信側の子
[SerializeField] private GameObject SenderChild;
// 受信側のアンカー
[SerializeField] private GameObject ReceiverAnchor;
// 受信側の子
[SerializeField] private GameObject ReceiverChild;
void Start()
{
var objects = new[] {SenderAnchor, SenderChild, ReceiverAnchor, ReceiverChild};
foreach (var o in objects)
{
// てきとうにカラーリング
o.GetComponent<Renderer>().material.color = UnityEngine.Random.ColorHSV();
}
StartCoroutine(Test());
}
private IEnumerator Test()
{
var wait = new WaitForSeconds(2);
while (true)
{
// アンカーをランダムな位置に配置
SenderAnchor.transform.SetPositionAndRotation(UnityEngine.Random.onUnitSphere, UnityEngine.Random.rotation);
ReceiverAnchor.transform.SetPositionAndRotation(UnityEngine.Random.onUnitSphere + new Vector3(7, 0, 0), UnityEngine.Random.rotation);
// 送信側の子オブジェクトをランダムな位置に配置
SenderChild.transform.position = UnityEngine.Random.onUnitSphere * 2;
SenderChild.transform.rotation = UnityEngine.Random.rotation;
// 送信側のワールド座標からアンカーからの相対座標に変換
var ts = SenderChild.transform;
var (sendP, sendQ) = WorldToRelative(SenderAnchor.transform, ts.position, ts.rotation);
// アンカーからの相対座標を受信側のアンカーに依るワールド座標に変換
var (receiveP, receiveQ) = RelativeToWorld(ReceiverAnchor.transform, sendP, sendQ);
ReceiverChild.transform.SetPositionAndRotation(receiveP, receiveQ);
yield return wait;
// アンカー同士の位置を揃えることで子オブジェクトの位置が揃っていることを確認する
SenderAnchor.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
ReceiverAnchor.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
yield return wait;
}
}
このgifではCapsule
をアンカー、Cylinder
を共有オブジェクトに設定しています。
目視での確認、および先程拡張したInspectorのTransform
で確認します。ちゃんとPosition
もRotation
も一致しているようです。
まとめ
正直Rotation
の共有部分は2×2の4パターンを試しただけです。まあなんかあってるし。いいかなって。
……あってると書いておいてなんなのですが、CloudAnchorのサンプルの座標変換ではRotation
をMatrix
を使って変換しているので、もしかしたらこの記事のやり方ではだめなのかも。
というかサンプル内で送られてきた座標を復元しているところも今の所発見できていません。そんなに量はないはずなのに、あのサンプル、なんかほんとよみづらい……。
次は Unity + CloudAnchor + PUN2 を組み合わせてわたしのかんがえたさいこうのCloudAnchorさんぷるを出す予定です。
おしまい。