はじめに
「リアルタイム画像処理によるエフェクトをかけた全天球映像をHMDで見るシステム」を爆速で組みたい等の理由で,UnityではなくTouchDesignerでVRコンテンツ開発を行いたくなる場合が存在します.晴れて映像を見ることができたら,次は頭の先にカーソルを出してみたいですよね1.
Unityであれば,Cameraの子オブジェクトにカーソルとなるオブジェクトを入れるなり,CameraからRayを飛ばすなりすれば実装できそうですが,TouchDesignerでこれを実装しようとしたときに手数がかかったので,本記事ではこの実装方法を紹介します.
環境
- Windows 10
- TouchDesigner 2019.18360 Educational版(Non-Commercial版でも球に貼り付けるequirectangular形式画像のサイズに気を付ければ問題ありません.)
- HTC Vive Pro
準備
まずは全天球映像をHMDで見るシステムを組みましょう.なおこの節に似た内容は,TouchDesigner使いなら一度は通る道:Visual Thinking with TouchDesigner - プロが選ぶリアルタイムレンダリング&プロトタイピングの極意の3章11節「VR端末をTouchDesignerで使う」にも記載されています.
まずPaletteを開いて,Derivative/Vive
からviveSimple
をproject1
オペレータ内に配置します.これだけでトーラスが浮いた空間の映像をHMDで見れるはずですが,今回はトーラスの表示やコントローラ周りの実装は不要なので,これを消してしまいます.viveSimple
オペレータの中に入り,openVRRender
オペレータ・parexec1
オペレータ・text1
オペレータ・window1
オペレータ以外を全て消してください.
次に,全天球映像を球に貼り付けて表示します.ここでは全天球映像はequirectangular(正距円筒図法)形式で渡されているものとします.Geometry COMP
をopenVRRender
オペレータと同じ階層に置き,その中で以下の図のようにオペレータを組んでください.
equirectangular形式画像にはTouchDesignerインストール時にデフォルトで入る画像を用いました2.これをConstant MAT
のColor Mapにして,Sphere SOP
から繋いだMaterial SOP
のMaterialに指定します.この際,Sphereの半径は10にしました.オペレータ右下の紫色のフラグをONにしないと,HMDに映像が映らないので注意しましょう.Constant MAT
のColor Mapによしなにエフェクトをかければ,いい感じの映像が出来上がります.
カーソルの実装
方針
映像を無事に見ることができたので,いよいよカーソル表示を実装します.
カーソルとして,今回は小さな赤い球を使うことにします.これを頭の先から一定距離 $r$ だけ離れた位置に配置することで,カーソルとしての役割を果たせそうです.そこで頭部の位置 $\mathbf{a} \in \mathbb{R}^3$ と頭部の向きを表すベクトル $\mathbf{h} \in \mathbb{R}^3$ から,カーソルの位置 $\mathbf{c} \in \mathbb{R}^3$ を
\mathbf{c} = \mathbf{a} + r\mathbf{h}
と求めることにします.
この $\mathbf{a}$ や $\mathbf{h}$ を求めるために,OpenVR CHOP
を利用します.openVRRender
オペレータ内のsensor
オペレータを見てみると,head:tx
, head:ty
, head:tz
, head:rx
, head:ry
, head:rz
というチャンネルがあることが分かります.これはどうやらトラッキングされたHMDの位置・向きに対応しているようです.特にhead:rx
はHMDの上下方向の回転(ピッチ),head:ry
はHMDの左右方向の回転(ヨー)に対応していそうです.したがってこの6つの値から $\mathbf{c}$ を計算し,カーソルを配置すれば良さそうです.
罠
……そう思っていた時代もありました.この方針だと,泥沼にはまる可能性があります.
この罠はHMDをグルグル回すと観察できます.グルグル回すと,head:rx
, head:ry
, head:rz
の値が左右を向いたときに不連続に飛ぶことが分かると思います.これは,これらの値が定められた範囲に収まるようにオフセットが加減されるからです.例えばhead:ry
はHMDの左右の回転に対し,常に区間 [-270, 90] 内に収まるように調整されます.
これ自体は理解できる仕様なのですが,問題はこの値の飛び方です.HMDを正面からゆっくり左に回していくと,head:ry
が90に届く前に(80くらい),突然負の値,それも-270ではない値(-260くらい)に飛んでしまうのです.したがってhead:ry
をそのまま左右の回転の大きさとして採用すると,左を向くたびにカーソルが謎の方向に飛んでしまうバグが生じます.これは,ロール軸の回転を表すhead:rz
の値の変動が原因だと考えられます.
ではどうすれば,この影響を取り除いた正しい $\mathbf{c}$ を求められるでしょうか.そのヒントは,HMDの映像は見回しても飛びが無い状態に既になっていることにあります.つまりopenVRRender
オペレータは,先述の6つのパラメータを使わずにHMDの位置や向きを計算し,頭の先の映像を正しく描画しているのです.したがってこの方法を真似すれば,正しく $\mathbf{c}$ を求めることができそうです.
この頭の先の映像を描画する計算は,eyeLXform
オペレータ・eyeRXform
オペレータが持つ16個のチャンネル情報が,eyeL
オペレータ・eyeR
オペレータのPre-Xformページ内のXform Matrix/CHOP/DATに渡されることでなされます.この16個のチャンネルは全体で1つの4×4行列を表現しています.この処理を理解して真似するためには,同次座標系について知る必要があります.
同次座標系
同次座標系とは,簡単に言えば3次元座標系での点 $(x,y,z)^{\top}$ を, $(x,y,z,1)^{\top}$ と同一視した座標系のことです.何故わざわざ第4成分に1を付け加えるかというと,上手く4次元行列を考えれば回転移動と平行移動を1つの行列にまとめて表現することが可能だからです.
今,点 $\mathbf{p} = (x,y,z)^{\top}$ を回転行列 $\mathbf{R} \in \mathbb{R}^{3 \times 3}$ で回転移動させた後, $\mathbf{d} \in \mathbb{R}^3$ を加えて平行移動させ,点 $\mathbf{p'} = (x',y',z')^{\top}$ にする操作を考えます.つまり
\mathbf{R} =
\begin{pmatrix}
m_{00} & m_{01} & m_{02}\\
m_{10} & m_{11} & m_{12}\\
m_{20} & m_{21} & m_{22}
\end{pmatrix}
,
\mathbf{d} =
\begin{pmatrix}
m_{03}\\
m_{13}\\
m_{23}
\end{pmatrix}
として,
\mathbf{p'} = \mathbf{R} \mathbf{p} + \mathbf{d}
です.このとき次が成立します:
\begin{pmatrix}
\mathbf{p'} \\
1
\end{pmatrix}
=
\begin{pmatrix}
\mathbf{R} & \mathbf{d} \\
\mathbf{0} & 1
\end{pmatrix}
\begin{pmatrix}
\mathbf{p} \\
1
\end{pmatrix}.
実際この式の1行目(3成分まとめていることに注意)は,先ほどの回転移動の後に平行移動する操作を表現しています.また2行目は恒等的に成立します.これを各成分書き下すと,
\begin{pmatrix}
x' \\
y' \\
z' \\
1
\end{pmatrix}
=
\begin{pmatrix}
m_{00} & m_{01} & m_{02} & m_{03} \\
m_{10} & m_{11} & m_{12} & m_{13} \\
m_{20} & m_{21} & m_{22} & m_{23} \\
0 & 0 & 0 & 1
\end{pmatrix}
\begin{pmatrix}
x \\
y \\
z \\
1
\end{pmatrix}
となります.つまりあるベクトルに第4成分1を加え,このような4×4行列を左からかければ,その結果の第1-3成分が移動先を表すことになります.
計算
この同次座標系が理解できると,eyeLXform
オペレータ・eyeRXform
オペレータが持つ16個のチャンネル情報の意味も分かります.このチャンネルの名前のインデックスm_00, m_01, ..., m_33
は,4×4の変換行列の各成分 $m_{00}, m_{01}, \dots, m_{33}$ を表しています3.
ここでは簡単のためHMDの位置は原点に固定し,そこから頭部の向き $\mathbf{h}$ 方向に10だけ離れた位置(つまり,映像が貼られた球面上の点) $\mathbf{c}$ にカーソルを出すことにします.すなわち
\mathbf{c} = 10 \mathbf{h}
です.
まずHMDの位置を原点に固定します.これはeyeLXform
オペレータ・eyeRXform
オペレータで, $m_{03}, m_{13}, m_{23}$の値を0とすることに相当します.Replace CHOP
を用いて3つの値を0にして,その結果をeyeL
オペレータ・eyeR
オペレータのPre-Xformページ内のXform Matrix/CHOP/DATに渡してやりましょう.
次に頭部の向き $\mathbf{h} = (h_x, h_y, h_z)^{\top}$ を求めます.eyeL
オペレータ・eyeR
オペレータ内のカメラは,回転行列をかけていない状態では $(0,0,-1)$ 方向を向いています.したがって先に紹介した同次座標を用いれば,回転後の頭部の向きは
\begin{pmatrix}
h_x \\
h_y \\
h_z \\
1
\end{pmatrix}
=
\begin{pmatrix}
m_{00} & m_{01} & m_{02} & 0 \\
m_{10} & m_{11} & m_{12} & 0 \\
m_{20} & m_{21} & m_{22} & 0 \\
0 & 0 & 0 & 1
\end{pmatrix}
\begin{pmatrix}
0 \\
0 \\
-1 \\
1
\end{pmatrix}
=
\begin{pmatrix}
-m_{02} \\
-m_{12} \\
-m_{22} \\
1
\end{pmatrix}
と求められます. HMDを原点に固定するため, $m_{03} = 0, m_{13} = 0, m_{23} = 0$としていたことに注意してください.ここではSelect SHOP
で3成分を抜き出した後,Math CHOP
で $\mathbf{h}$ を求めました4.
最後に $\mathbf{c} = 10 \mathbf{h}$ にカーソルを配置します.全天球映像が貼られたGeometry COMP
と同じ階層に半径を0.2くらいにしたSphere SOP
を置き,centerの各成分を,$\mathbf{h}$の各成分を10倍した値にしてください.
この状態でHMDの映像を見ると次のようになります.
まとめ
TouchDesignerでのVR開発で頭部に追従するカーソルを実装するためには,viveSimple/eyeRXMatrix
オペレータ・viveSimple/eyeLXMatrix
オペレータの情報を利用すれば良いことがわかりました.
この方法はカーソルの表示に限らず,例えばHMDの向きに応じて何らかのイベントをトリガーしたい場合にも用いることができそうです.是非この実装方法を元にカスタマイズしてみてください.