ARやMRは座標系の扱いが難しい
今回は前提としてVuforiaを使用し、3つの座標系を扱う必要があって苦労したので忘れないようにメモ
ARで表示したいオブジェクトをImageTargetの子要素にするケースがほとんどだが、親子関係は操作できない制約があったので
**「親子関係を作らずに、ImageTargetの移動や回転に合わせてオブジェクト群の表示を更新する」**必要がある
ImageTarget
Vuforiaに登録した画像マーカー
ARオブジェクト用シーン
Vuforiaを動かすメインのシーンとは別に用意したオブジェクト配置用のシーン
その中でImageTargetの位置を設定
異なる3つの座標系
- カメラに映る座標系(ImageTarget含む)
- ARオブジェクト用シーンの座標系 (※Prefab化して配置)
- ARオブジェクト用シーンに含まれるImageTargetを中心とした座標系
カメラ映像上にARオブジェクト用シーンを配置する
実際にカメラに映るImageTargetの位置にARオブジェクト用シーンを配置する
以下の手順で配置を行う
- ImageTargetの位置にARオブジェクト用シーンを移動
- ARオブジェクト用シーンに含まれるImageTargetをImageTargetの位置に移動
- ARオブジェクト用シーンに含まれるImageTargetをImageTargetと一致するように回転
- 以降ImageTargetの回転や移動に合わせて、ARオブジェクト用シーンも更新する
完成したコード
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);
}
}