こんにちは、TechFULでアルバイトをしているSiiiecです!
はじめまして、もしくは以前のブログを読んでくださっていた方はお久しぶりです。
TechFULでは問題作成やTCB運営、新機能の検証をしています。
過去にはTechFULサイトエラー時のロゴを作成したりしていました。
私は研究でVRシミュレータを開発しているのですが、以前詰まってしまった
「VR空間に配置した物体と現実(実空間)の物体の位置合わせ(キャリブレーション)」について紹介したいと思います。
目次
動機と基礎知識
研究で使用している入力機器(キーボードなど)をVR空間で使いたい、ということがありました。
しかし、VR空間に入力機器の3Dモデルを配置しただけでは現実の入力機器とずれてしまうため、調整に苦労していました。
同じように悩んでいる方の解決や、3D・VRに興味があるけどどんなことをしているんだろうといった方に興味を持ってもらえると嬉しいです!
本記事では、ゲームエンジンであるUnityと、HMD(Head Mounted Display)のOculus Quest 2を使用しています。 上述の環境以外でも、基本的な考え方は同様です。
Unityについて
ここでは、本記事を読む上で必要となるUnityの基礎知識を説明します。
Unityは左手系と呼ばれる座標系を使用しています。
左手系では、回転軸に対して時計回りの回転が正の方向の回転となります。
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つの内蔵カメラが搭載されています。
このカメラからの映像により、自己位置の推定とコントローラのトラッキングを可能としています。
この方式はインサイドアウトと呼ばれており、外部のセンサ(カメラ)を必要としない特長があります。
一方、旧モデルであるOculus Rift CV1などは外部のセンサによって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$が表す回転をするという意味となり、回転の合成を表しています。
※積の順序が異なると、合成した回転の結果も変わることに注意が必要です。
Unityではp * q
と表すことで、回転$p$の後に回転$q$を合成する操作となります。
VR空間と実空間の位置合わせ
ここからは、実際にVR空間に配置した物体と現実の物体を位置合わせする方法について説明します。
本記事では位置合わせのために、コントローラの位置および姿勢を使用します。
例えば、以下の画像のように現実のキーボード(ターゲット)とコントローラの方向を合わせておき、
このときのコントローラの位置・姿勢をもとに、HMDから見えるキーボードの位置を調節します。
この操作は、以下の画像中で、青色のキーボードをVR空間に配置したキーボード、黒色のキーボードをターゲットとしたとき、
青色のキーボードを動かしてターゲットに重ねる操作に該当します。
次に、VR空間でのカメラ(HMD)とコントローラの関係について説明します。
VR空間でのカメラとコントローラの関係
Unityに限らず、VR空間ではカメラ(HMD)とコントローラは以下のような関係になっていることが多いです。
まず親オブジェクト(Parent)があり、その子オブジェクトとしてカメラやコントローラがあります。
また、カメラやコントローラの位置および姿勢は、センサで計測した値をもとに毎フレーム更新(上書き)されていきます。
このため、VR空間内でカメラを任意の位置および姿勢にしたい場合は親オブジェクトを動かす必要があります。
画像中の赤・青の直線は、オブジェクトの方向を表しています。
位置合わせの実装
大まかな流れとしては、
- ①コントローラの姿勢とコントローラの初期姿勢の差分を求める
- ②ターゲットの姿勢に合わせるため、差分をとる
- ③親オブジェクトの姿勢を調整する
- ④コントローラとターゲットの位置を合わせる
- ⑤コントローラの位置を微調整する
といったものになります。
また、ここから説明する処理には以下の変数を用います。
// コントローラの位置・姿勢(Unityエディタ上でコントローラを表すオブジェクトを紐づけます) public Transform controller; // 親オブジェクトの位置・姿勢(Unityエディタ上で親オブジェクトを紐づけます) public Transform parent; // ターゲットの位置・姿勢(Unityエディタ上でターゲットオブジェクトを紐づけます) 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
を求めます。
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.forward
をrot
によって回転(rotatedForward
)した後で、
rotatedForwad
のy成分(yComponent
)のみ引くことで、xz平面に投影します(projectionXZ
)。
続いて、Vector3.SignedAngle
関数により、Vector3.forward
とprojectionXZ
のなす角(diffAngleDeg
)を求めます。
最後にy軸周りにdiffAngleDeg
°回転させるクォータニオンdiffQuaternion
を定義します。
②ターゲットの姿勢に合わせるため、差分をとる
// ②ターゲットの姿勢との差分をとる diffQuaternion *= Quaternion.Inverse(target.rotation);
③親オブジェクトの姿勢を調整する
// ③親オブジェクトごと回転することで、コントローラの姿勢とターゲットの姿勢を合わせる parent.rotation = parent.rotation * Quaternion.Inverse(diffQuaternion);
カメラとコントローラを回転させるため、親オブジェクトをdiffQuaternion
の逆向きに回転させます。
④コントローラとターゲットの位置を合わせる
// ④コントローラの位置がターゲットの位置に合うように親オブジェクトごと移動する parent.position = parent.position + (target.position - controller.position);
親オブジェクトの座標に、コントローラからターゲットへ向かうベクトルを足します。
このままではコントローラがターゲットにめり込んでしまうため、次の手順で微調整します。
⑤コントローラの位置を微調整する
// ⑤コントローラがターゲットに重ならないよう、微調整する parent.position = parent.position + target.rotation * offset;
ターゲットが任意の姿勢でも固定のoffset
で対応できるように、offset
をターゲットの姿勢ぶん回転させています。
終わりに
今回は、TechFULからは少し離れた内容となる、3Dに関する話題を取り上げました。 本記事の
- 姿勢を合わせる
- 位置を合わせる
という処理は3Dでは基本的な操作のため、VRに限らずさまざまな位置合わせで応用できます。
私も以前詰まった部分なので、同じように詰まっている人の助けになれば幸いです。
また、少しでも3DやVRに興味を持っていただけると嬉しいです!
最後まで読んでくださりありがとうございました!