AR/MRで異なる座標系を合わせる

# ARやMRは座標系の扱いが難しい

今回は前提としてVuforiaを使用し、3つの座標系を扱う必要があって苦労したので忘れないようにメモ

ARで表示したいオブジェクトをImageTargetの子要素にするケースがほとんどだが、親子関係は操作できない制約があったので
**「親子関係を作らずに、ImageTargetの移動や回転に合わせてオブジェクト群の表示を更新する」**必要がある

![Example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82250/dd5fac8d-0a30-e3d8-4942-4d3d27f00841.png)


**ImageTarget**

Vuforiaに登録した画像マーカー

**ARオブジェクト用シーン**

Vuforiaを動かすメインのシーンとは別に用意したオブジェクト配置用のシーン
その中でImageTargetの位置を設定

**異なる3つの座標系**

- カメラに映る座標系(ImageTarget含む)
- ARオブジェクト用シーンの座標系 (※Prefab化して配置)
- ARオブジェクト用シーンに含まれるImageTargetを中心とした座標系

# カメラ映像上にARオブジェクト用シーンを配置する

実際にカメラに映るImageTargetの位置にARオブジェクト用シーンを配置する
以下の手順で配置を行う

1. ImageTargetの位置にARオブジェクト用シーンを移動
2. ARオブジェクト用シーンに含まれるImageTargetをImageTargetの位置に移動
3. ARオブジェクト用シーンに含まれるImageTargetをImageTargetと一致するように回転
4. 以降ImageTargetの回転や移動に合わせて、ARオブジェクト用シーンも更新する

![Success.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82250/c36f5337-7d57-0e0b-c5f4-be6e3b4bb1d5.gif)

# 完成したコード

```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ARObjectsSetter : MonoBehaviour
{
    [SerializeField]
    private Transform _actualImageTarget;
    [SerializeField]
    private Transform _arObjectsScene;

    [SerializeField]
    private Vector3 _virtualImageTargetPosition;
    [SerializeField]
    private Vector3 _virtualImageTargetRotation;

    private GameObject dummyImageTarget;
    private Vector3 relativePosition;
    private Vector3 parentLocalAxis;
    private float rotationAngle;
    private bool _enableUpdate = false;

    [ContextMenu("Initialize")]
    private void Initialize()
    {
        // ARオブジェクト用シーンをImageTargetに合わせる
        _arObjectsScene.position = _actualImageTarget.position;
        _arObjectsScene.rotation = _actualImageTarget.rotation;

        // ARオブジェクト用シーンのローカル座標からワールド座標でのVirtualImageTargetのPositionを求める
        var offset = _arObjectsScene.TransformPoint(-_virtualImageTargetPosition);
        _arObjectsScene.position = offset;

        // 仮想の親オブジェクトを生成し、ARObjectsSceneを子要素のように扱う
        dummyImageTarget = new GameObject("DummyImageTarget");

        // DummyImageTargetの位置と回転をVirtualImageTargetに合わせる
        var rot = _actualImageTarget.rotation * Quaternion.Euler(_virtualImageTargetRotation);
        dummyImageTarget.transform.SetPositionAndRotation(_actualImageTarget.position, rot);

        // ワールド座標からDummyImageTargetのローカル座標でのPositionを求める
        relativePosition = dummyImageTarget.transform.InverseTransformPoint(_arObjectsScene.position);

        // DummyImageTarget(親)のRotationと回転行列Sの積からARObjectsScene(子)のRotationが求まる
        // ARObjectsScene.rotaion = DummyImageTarget.tranform.rotation * S
        // 両辺にDummyImageTarget.transfrom.rotaionの逆行列をかける
        // ※ 回転の合成はQuaternionの積
        var rotationMatrix = _arObjectsScene.rotation * Quaternion.Inverse(dummyImageTarget.transform.rotation);

        // rotationMatrixがワールド座標での値なので、回転量(rotaitonAngle)と回転軸(globalAxis)を抽出する
        rotationMatrix.ToAngleAxis(out rotationAngle, out Vector3 globalAxis);
        // globalAxisをワールド座標からDummyImageTargetのローカル座標に変換する
        parentLocalAxis = dummyImageTarget.transform.InverseTransformVector(globalAxis);

        // ここまでで親子階層を操作せずに、DummyImageTargetの回転と移動がARObjectsSceneにも影響されるようになったので、
        // DummyImageTargetとImageTargetの回転を一致させる
        dummyImageTarget.transform.rotation = Quaternion.LookRotation(_actualImageTarget.forward);

        _enableUpdate = true;
    }

    private void LateUpdate()
    {
        if (!_enableUpdate) return;

        dummyImageTarget.transform.SetPositionAndRotation(_actualImageTarget.transform.position, _actualImageTarget.transform.rotation);

        // あらかじめ計算しておいたDummyImageTargetのローカル座標でのARObjectsSceneのPositionをワールド座標に変換
        var position = dummyImageTarget.transform.TransformPoint(relativePosition);
        // あらかじめ計算しておいたDummyImageTargetのローカル座標での回転軸をワールド座標に変換
        var globalAxis = dummyImageTarget.transform.TransformVector(parentLocalAxis);
        // あらかじめ計算しておいた回転をDummyImageTargetのRotationに合成する
        var rotation = Quaternion.AngleAxis(rotationAngle, globalAxis) * dummyImageTarget.transform.rotation;

        _arObjectsScene.SetPositionAndRotation(position, rotation);
    }
}

```