TechFULの中の人

TechFULスタッフ・エンジニアによる技術ブログ。IT関連のことやTechFULコーディングバトルの超難問の深掘り・解説などを毎週更新

VR空間と現実の位置合わせがしたい【3D】【Unity】

こんにちは、TechFULでアルバイトをしているSiiiecです!
はじめまして、もしくは以前のブログを読んでくださっていた方はお久しぶりです。

TechFULでは問題作成やTCB運営、新機能の検証をしています。
過去にはTechFULサイトエラー時のロゴを作成したりしていました。

TechFULエラー時のロゴ。ロゴのうち崩れている部分は後述するUnityで作成しました。

私は研究でVRシミュレータを開発しているのですが、以前詰まってしまった
VR空間に配置した物体と現実(実空間)の物体の位置合わせ(キャリブレーション)」について紹介したいと思います。

目次

動機と基礎知識

研究で使用している入力機器(キーボードなど)をVR空間で使いたい、ということがありました。
しかし、VR空間に入力機器の3Dモデルを配置しただけでは現実の入力機器とずれてしまうため、調整に苦労していました。

同じように悩んでいる方の解決や、3D・VRに興味があるけどどんなことをしているんだろうといった方に興味を持ってもらえると嬉しいです!


本記事では、ゲームエンジンであるUnityと、HMD(Head Mounted Display)のOculus Quest 2を使用しています。 上述の環境以外でも、基本的な考え方は同様です。

Unityについて

ここでは、本記事を読む上で必要となるUnityの基礎知識を説明します。

Unityは左手系と呼ばれる座標系を使用しています。
左手系では、回転軸に対して時計回りの回転が正の方向の回転となります。

f:id:techful-444:20210405153213p:plain


Unityでは、Transformクラスによって、オブジェクトの位置・姿勢・スケールを管理しています。

Transformクラスの変数tに対して、

  • t.positoinで位置を表す3次元ベクトル(Vector3型)
  • t.rotationで姿勢を表すクォータニオンQuaternion型)(後述)
  • t.scaleでスケールを表す次元ベクトル(Vector3型)

が取得できます。


Vector3クラスではよく使う値として、

  • Vector3.right: $(1, 0, 0)$
  • Vector3.up: $(0, 1, 0)$
  • Vector3.forward: $(0, 0, 1)$

が定義されています。

Oculus Quest 2について

Oculus Quest2はHMDに4つの内蔵カメラが搭載されています。
このカメラからの映像により、自己位置の推定とコントローラのトラッキングを可能としています。

この方式はインサイドアウトと呼ばれており、外部のセンサ(カメラ)を必要としない特長があります。

HMDとコントローラ

一方、旧モデルであるOculus Rift CV1などは外部のセンサによってHMDやコントローラの位置および姿勢を計測します。 この方式はアウトサイドインと呼ばれます。

HMDとセンサ

クォータニオンについて

本記事ではクォータニオンの実際の計算については解説せず、Unityでの使用方法を中心に解説します。

Unityでは、クォータニオン(Quaternion)を用いて回転と姿勢を表しています。
クォータニオン複素数を拡張したもので、4つの実数から構築されます。
クォータニオン$q$は、以下のように4次元ベクトルとして表記することができます。

$$q = (q_x, q_y, q_z, q_w)$$

Unityでクォータニオンを作成する場合は、以下のようになります。

Quaternion q = new Quaternion(qx, qy, qz, qw);

クォータニオンによる回転

クォータニオンで回転を表す場合は、回転軸の方向$v=(v_x, v_y, v_z)$と回転量$\theta$で表すことができ、以下のような形で表します。

$$(v_x \sin⁡{\frac{\theta}{2}}, v_y \sin⁡{\frac{\theta}{2}}, v_z \sin⁡{\frac{\theta}{2}}, \cos{\frac{\theta}{2}})$$

Unityである軸周りの回転を表す場合は、以下のようになります。

// vは方向を表すベクトル、thetaは回転角度(°)
Quaternion q = Quaternion.AngleAxis(theta, v);

もしくは、定義通りに作成すると以下のようになります。

Quaternion q = new Quaternion(v_x * Mathf.Sin(theta / 2), v_y * Mathf.Sin(theta / 2), v_z * Mathf.Sin(theta / 2), Mathf.Cos(theta / 2));

Mathf.Sin()Mathf.Cos()はそれぞれ$\sin$関数、$\cos$関数を表します。

クォータニオンによるベクトルの回転

ベクトル$v$をクォータニオン$q$によって回転させる場合、$qv\bar{q}$で表します。
ここで、$\bar{q}$は$q$の共役クォータニオンと呼ばれます。
$\bar{q}$は以下のように表します。

$$\bar{q} = (−q_x, −q_y, −q_z, q_w)$$

Unityでは$\bar{q}$はQuaternion.Inverse(q);となります。

クォータニオン同士の積(回転の合成)

クォータニオン$p$とクォータニオン$q$の積$qp$は、
$p$が表す回転をした後に$q$が表す回転をするという意味となり、回転の合成を表しています。

積の順序が異なると、合成した回転の結果も変わることに注意が必要です。

f:id:techful-444:20210405161026p:plain:w397:h236
x軸周りに30°回転させるクォータニオン$q_x$とy軸周りに60°回転させるクォータニオン$q_y$を合成した例  左が回転前の立方体  中央が$q_x$→$q_y$の順に適用した場合  右が$q_y$→$q_x$の順に適用した場合

Unityではp * qと表すことで、回転$p$の後に回転$q$を合成する操作となります。

VR空間と実空間の位置合わせ

ここからは、実際にVR空間に配置した物体と現実の物体を位置合わせする方法について説明します。
本記事では位置合わせのために、コントローラの位置および姿勢を使用します。

例えば、以下の画像のように現実のキーボード(ターゲット)とコントローラの方向を合わせておき、
このときのコントローラの位置・姿勢をもとに、HMDから見えるキーボードの位置を調節します。

f:id:techful-444:20210405162103j:plain:w336:h222
キーボードとコントローラの切れ目を合わせる

この操作は、以下の画像中で、青色のキーボードをVR空間に配置したキーボード、黒色のキーボードをターゲットとしたとき、
青色のキーボードを動かしてターゲットに重ねる操作に該当します。

f:id:techful-444:20210405162148p:plain:w328:h128
VR空間に配置したキーボード

次に、VR空間でのカメラ(HMD)とコントローラの関係について説明します。

VR空間でのカメラとコントローラの関係

Unityに限らず、VR空間ではカメラ(HMD)とコントローラは以下のような関係になっていることが多いです。

f:id:techful-444:20210405164152p:plain
Parentの子として、Controller、Cameraが配置される

まず親オブジェクト(Parent)があり、その子オブジェクトとしてカメラやコントローラがあります。
また、カメラやコントローラの位置および姿勢は、センサで計測した値をもとに毎フレーム更新(上書き)されていきます。
このため、VR空間内でカメラを任意の位置および姿勢にしたい場合は親オブジェクトを動かす必要があります。

f:id:techful-444:20210405165835j:plain:h250f:id:techful-444:20210405165842j:plain:h250
親オブジェクトごと移動・回転する

画像中の赤・青の直線は、オブジェクトの方向を表しています。

位置合わせの実装

大まかな流れとしては、

  • ①コントローラの姿勢とコントローラの初期姿勢の差分を求める
  • ②ターゲットの姿勢に合わせるため、差分をとる
  • ③親オブジェクトの姿勢を調整する
  • ④コントローラとターゲットの位置を合わせる
  • ⑤コントローラの位置を微調整する

といったものになります。

また、ここから説明する処理には以下の変数を用います。

// コントローラの位置・姿勢
public Transform controller;
// 親オブジェクトの位置・姿勢
public Transform parent;
// ターゲットの位置・姿勢
public Transform target;

// コントローラの初期姿勢(計測したもの)
Quaternion initialControllerRotation = new Quaternion(-0.912928f, -0.09355207f, -0.03671198f,-0.3955535f);

// 微調整用のベクトル(コントローラがターゲットに重ならないようにする)
Vector3 offset = Vector3.back * 0.16f;

①コントローラの姿勢とコントローラの初期姿勢の差分を求める

// ①コントローラの姿勢とコントローラの初期姿勢の差分を求める

Quaternion rot = controller.rotation * Quaternion.Inverse(initialControllerRotation);

// y軸周りの回転のみ抽出する

Vector3 rotatedForward = rot * Vector3.forward;
// y軸方向の成分を引くことで、xz平面に投影する
Vector3 yComponent = Vector3.Dot(Vector3.up, rotatedForward) * Vector3.up;
Vector3 projectionXZ = (rotatedForward - yComponent).normalized;
// y軸周りの回転角度(°)を算出する
float diffAngleDeg = Vector3.SignedAngle(Vector3.forward, projectionXZ, Vector3.up);
// y軸周りにdiffAngleDeg°回転させるクォータニオン
Quaternion diffQuaternion = Quaternion.AngleAxis(diffAngleDeg, Vector3.up);

順にコードを解説していきます。

Quaternion rot = controller.rotation * Quaternion.Inverse(initialControllerRotation);

まずは現在のコントローラの姿勢と図の初期姿勢との差分rotを求めます。

f:id:techful-444:20210405170129p:plain:h200
コントローラを机に置いた時を初期姿勢としたとき、現在の姿勢との差分をとる


Vector3 rotatedForward = rot * Vector3.forward;
// y軸方向の成分を引くことで、xz平面に投影する
Vector3 yComponent = Vector3.Dot(Vector3.up, rotatedForward) * Vector3.up;
Vector3 projectionXZ = (rotatedForward - yComponent).normalized;
// y軸周りの回転角度(°)を算出する
float diffAngleDeg = Vector3.SignedAngle(Vector3.forward, projectionXZ, Vector3.up);
// y軸周りにdiffAngleDeg°回転させるクォータニオン
Quaternion diffQuaternion = Quaternion.AngleAxis(diffAngleDeg, Vector3.up);

続いて、計測した初期姿勢に誤差があった場合を考え、rotからy軸周りの回転のみ抽出します。

まずはVector3.forwardrotによって回転(rotatedForward)した後で、
rotatedForwadのy成分(yComponent)のみ引くことで、xz平面に投影します(projectionXZ)。

f:id:techful-444:20210405170313p:plain:w350
ベクトルをxz平面へ投影する

続いて、Vector3.SignedAngle関数により、Vector3.forwardprojectionXZのなす角(diffAngleDeg)を求めます。

f:id:techful-444:20210405170410p:plain:w300
Vector3.forwardとprojectionXZのなす角を求める

最後にy軸周りにdiffAngleDeg°回転させるクォータニオンdiffQuaternionを定義します。

②ターゲットの姿勢に合わせるため、差分をとる

// ②ターゲットの姿勢との差分をとる
diffQuaternion *= Quaternion.Inverse(target.rotation);

③親オブジェクトの姿勢を調整する

// ③親オブジェクトごと回転することで、コントローラの姿勢とターゲットの姿勢を合わせる
parent.rotation = parent.rotation * Quaternion.Inverse(diffQuaternion);

カメラとコントローラを回転させるため、親オブジェクトをdiffQuaternionの逆向きに回転させます。

f:id:techful-444:20210405170531p:plain:w300f:id:techful-444:20210405170538p:plain:w300
親オブジェクトごとカメラ、コントローラを回転する

④コントローラとターゲットの位置を合わせる

// ④コントローラの位置がターゲットの位置に合うように親オブジェクトごと移動する
parent.position = parent.position + (target.position - controller.position);

親オブジェクトの座標に、コントローラからターゲットへ向かうベクトルを足します。

f:id:techful-444:20210405171122p:plain:w400

このままではコントローラがターゲットにめり込んでしまうため、次の手順で微調整します。

⑤コントローラの位置を微調整する

// ⑤コントローラがターゲットに重ならないよう、微調整する
parent.position = parent.position + target.rotation * offset;

ターゲットが任意の姿勢でも固定のoffsetで対応できるように、offsetをターゲットの姿勢ぶん回転させています。

f:id:techful-444:20210405171210p:plain:w400


終わりに

今回は、TechFULからは少し離れた内容となる、3Dに関する話題を取り上げました。 本記事の

  1. 姿勢を合わせる
  2. 位置を合わせる

という処理は3Dでは基本的な操作のため、VRに限らずさまざまな位置合わせで応用できます。
私も以前詰まった部分なので、同じように詰まっている人の助けになれば幸いです。

また、少しでも3DやVRに興味を持っていただけると嬉しいです!

最後まで読んでくださりありがとうございました!