アンカーに対するオブジェクト相対位置の共有、ARCore Cloud Anchor の下調べ

## 時代はAR!
最近スマートフォンを新しいやつに買い替えたので、ついにおうちで[ARCore](https://developers.google.com/ar/)ができるようになりました。
サンプルを一通り触ってみたのですが、目玉機能である[Cloud Anchor](https://developers.google.com/ar/develop/unity/cloud-anchors/overview-unity)のサンプルがあまりにもわからないすぎる[^WTF]ので自分で勉強がてら作り直してみようかと思っています。この記事はその下調べ。
Unityです。

## 参考サイト
話を始める前に参考になったサイトを貼ります。これを元に話をしています。
### 前提とか
- [Working with anchors](https://developers.google.com/ar/develop/developer-guides/anchors)  
anchorとはなんぞや、という説明。
- [Using Instant Preview](https://developers.google.com/ar/develop/unity/instant-preview)  
開発効率がぜんぜん違うので、できるようにしておきましょう[^dose]。あと2mくらいの長くて頑丈なUSBケーブル。
- [はじめようArcore (修正版)](https://www.slideshare.net/ssuserc0d7fb/arcore-108717431)   
まず最初にこれをこなすと公式サンプルへの理解が捗ります。
- [Supported Devices](https://developers.google.com/ar/discover/supported-devices)    
どのデバイスで動くか。このページいつも探してる気がするので、ついでにメモとして貼ります。

### やりたいこと
[ARCore Cloud Anchor APIをマスターする（前編）](https://www.jyuko49.com/entry/2018/06/06/092513)
[ARCore Cloud Anchor APIをマスターする（後編）](https://www.jyuko49.com/entry/2018/06/18/082003)
[どうして空間共有で先に座標系の逆変換（inverse）を行うのか？](https://www.jyuko49.com/entry/2018/06/22/073949)

Cloud Anchorは実空間上の一点をアンカーと定め、それを共有する仕組みです。
つまり起点であるアンカーとの相対距離・相対角度を求めることができればオブジェクトの位置と向きを共有できるはずです。
CloudAnchorのサンプル内でそれを行っているのは以下のメソッドです。

```c#
/// <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を便利に拡張する](http://q7z.hatenablog.com/entry/2016/07/06/223422)
元記事のコードにちょっと足して`Position`と`LocalPosition`を同時に表示できるようにします。

<img width="369" alt="スクリーンショット 2019-06-27 2.19.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/fb7c33fe-a830-db5d-8d0c-bb6381a36c03.png">

#### Positionを相対に変換
共有したいオブジェクトをアンカーの子オブジェクトにして`LocalPosition`だけ引っこ抜けばいいような気もします。
しますが、`Position`と`LocalPosition`を混用すると不思議の森に迷い込むので、わたしは基本的に`Position`だけを使うようにしています。なので`Position`だけで相対座標を導出します。

```c#:送信側
// 送信側のアンカー
Transform SenderAnchor;
// 共有したい座標
var worldPosition = ts.position;

// アンカーからの相対座標に変換します
var relativePosition = SenderAnchor.transform.worldToLocalMatrix.MultiplyPoint3x4(worldPosition);
```

```c#:受信側
// 受信側のアンカー
Transform ReceiverAnchor;
// 受け取り側のPositionに復元します
var local = ReceiverAnchor.transform.localToWorldMatrix.MultiplyPoint3x4(relativePosition);
```

変換の仕組みは標準APIの中にありました。メインスレッドからしか実行できません。たぶん。たしか。
別スレッドで行いたい場合は行列を勉強してなんか頑張ってください。

### `Rotation`
**わからんことはわかっている**[^Socrates]ので考えないで検索します。

[【Unity道場 博多スペシャル 2017】クォータニオン完全マスター](https://www.youtube.com/watch?v=uKWLPU8gfIY)
[【Unity道場スペシャル 2017博多】クォータニオン完全マスター](https://www.slideshare.net/UnityTechnologiesJapan/unity-2017)

相対の`Rotation`の出し方は最後のほうにあります。でもすごい勉強になるので最初から見たほうがよいと思います。[^bear]

```c#:送信側
// 共有したい姿勢
var worldRotation = ts.rotation;
// アンカーからの相対角度に変換します
var relativeRotation = Quaternion.Inverse(SenderAnchor.transform.rotation) * worldRotation;
```
```c#:受信側
// 受け取り側のRotationに復元します
var local = ReceiverAnchor.transform.rotation * relativeRotation;
```

### `Position`と`Rotation`
完成品はこれ。

```c#
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);
}
```

### 実験

必要なコードはできたので適当なコードでテストしてみます。

```c#
// 送信側のアンカー
[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;
    }
}
```

![ezgif-2-85609692c59e.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/9929c2ea-cf65-a67d-8216-e529850c5414.gif)

このgifでは`Capsule`をアンカー、`Cylinder`を共有オブジェクトに設定しています。
目視での確認、および先程拡張したInspectorの`Transform`で確認します。ちゃんと`Position`も`Rotation`も一致しているようです。

## まとめ
正直`Rotation`の共有部分は2×2の4パターンを試しただけです。まあなんかあってるし。いいかなって。
……あってると書いておいてなんなのですが、CloudAnchorのサンプルの座標変換では`Rotation`を`Matrix`を使って変換しているので、もしかしたらこの記事のやり方ではだめなのかも。
というかサンプル内で送られてきた座標を復元しているところも今の所発見できていません。そんなに量はないはずなのに、あのサンプル、なんかほんとよみづらい……。

次は Unity + CloudAnchor + PUN2 を組み合わせて**わたしのかんがえたさいこうのCloudAnchorさんぷる**を出す予定です。

おしまい。


[^WTF]: わからん！　Google様のサンプルはわからん！
[^dose]: Instant Preview？　AndroidStudioのアレと同レベルでしょ？　とか言っててすいませんでした。
[^Socrates]: 脱線ですが、ソクラテスってあれだけ煽り散らしてたらそら処刑されますよね。
[^bear]: じつはわたしは熊なのでこのような勉強会に行くには差し障りがあるのですが、一度は行ってみたいものです。