OGRE + LeapMotion

ちょいまえに,LeapMotionのOculusRift用マウンタがリリースされました. LeapMotionは正直積みデバイスと化していたのですが,面白そうなので注文してみました.

マウンタが届く前にLeapMotionの使い方を勉強しておこうと思って,OGREでLeapMotionのキャプチャデータを 可視化するデモを作ってみました.

  • バイナリ(64bit)
  • ソースリポジトリ
    • LeapSDKは2.1.4+22333を使用しています.
    • ソースには依存ライブラリ(boost/OGRE/OIS/LeapSDK)を含めていないので,自分でビルドしてみたい場合にはプロジェクトのインクルードディレクトリ,ライブラリパス,デバッグ時の環境変数,plugin(_d).cfgの記述などを,自分の環境に合わせて書き換えてください.

操作方法

Alt+左マウス
カメラ回転
Alt+Shift+左マウス
カメラ平行移動
Alt+Ctrl+左マウス or スクロール
ズームイン/アウト

キャプチャしている手のプロポーションを無視して,相対的なボーンの姿勢だけを適用してスキニングしています. なので,ちゃんとウェイトペインティングしたモデルを使うことも可能だと思います.

以下技術的な備忘録です.

ポイント

LeapMotionを弄っていてちょっと戸惑ったのは,これはキャプチャデバイスであるからして,指や手のひらの位置や向きを ワールド空間の座標系でアプリケーションに報告する点です.

各ボーンの位置や姿勢をそのままワールド座標にプロットするならそれらのデータはそのまま使えますが, このデモのようにボーンの姿勢のみを既存のモデルに適用する場合には少しだけ工夫が必要です.
つまり,親ボーンの姿勢と相対的な子ボーンの姿勢を作る必要があると言うことですね.

“姿勢”の記述

LeapSDKから取得することができるボーンの姿勢を示すデータは,実質的にLeap::Bone::direction()のみです. ただ,SDKのバグなのか何なのか分かりませんがこのメソッドはライブラリドキュメントの記述とは逆の向き―指先から手首に近い方向のベクトルを返すようです.

上述のメソッドはそのまま使うことができませんが,ボーンの始点と終点位置はそれぞれLeap::Bone::prevJoint(),Leap::Bone::nextJoint()を使って得ることができます.それを元に方向ベクトルを作り,さらにその方向ベクトルを使って 直交基底行列(回転行列)を作ります. 直交基底行列,つまり三個一組の互いに直行するベクトルを列成分に持つ行列は回転行列として使用できます.

デモの中では以下のように,姿勢の基準となるベクトルからベクトルのクロス積を2回使って 直交基底ベクトルを得て,回転行列を作っています.カメラ制御なんかの時にもよくこの手の方法は使いますね.

const Ogre::Matrix4 CreateCurrentPoseMatrix(Leap::Bone& b, Leap::Hand& h)
{
    using namespace Ogre;
    Matrix3 rot;
    // 基底ベクトル1
    Vector3 yBasis = (b.nextJoint() - b.prevJoint()).toVector3<Vector3>();
    yBasis.normalise();
    Vector3 palmNml = h.palmNormal().toVector3<Vector3>();
    Vector3 palmFwd = h.direction().toVector3<Vector3>();
    Vector3 pinkyDir = palmNml.crossProduct(palmFwd);// 小指方向
    Vector3 zBasis = pinkyDir.crossProduct(yBasis);// 基底ベクトル2
    zBasis.normalise();
    Vector3 xBasis = yBasis.crossProduct(zBasis);// 基底ベクトル3
    xBasis.normalise();
    rot = Matrix3(
        xBasis.x, yBasis.x, zBasis.x,
        xBasis.y, yBasis.y, zBasis.y,
        xBasis.z, yBasis.z, zBasis.z
        );
    if (h.isLeft())
    {
        Matrix3 flip;
        flip.FromAngleAxis(Vector3::UNIT_Y, Degree(180));
        rot = rot * flip;
    }
    Ogre::Matrix4 res(rot);

    return res;
}

数式的に書くと以下のようになります.

\( \begin{eqnarray} \begin{array}{l} \vec{\bf D_{forward}} \\ \vec{\bf U}\left( \vec{\bf D_{forward}}\cdot\vec{\bf U} \neq 1 \right) \\ \vec{\bf D_{right}} = \vec{\bf D_{forward}}\times\vec{\bf U} \\ \vec{\bf D_{up}} = \vec{\bf D_{right}}\times\vec{\bf D_{forward}} \\ \end{array} \end{eqnarray} \)


\( {\bf M} = \left(\begin{array}{lll} \vec{\bf D_{right}} \vec{\bf D_{forward}} \vec{\bf D_{up}} \\ \end{array}\right) \)


このクロス積を2回使う方法だと,ジンバルロックが発生し得ます.
方向ベクトル$$\vec{\bf D_{forward}}$$からクロス積を使って二つ目の基底ベクトル $$\vec{\bf D_{right}}$$を作るときに,$$\vec{\bf U}$$というベクトルを使っています(よくY方向の単位ベクトルを使います). この$$\vec{\bf U}$$はどんなベクトルでもよいのですが,例外として$$\vec{\bf D_{forward}}$$と 同じ向き(あるいは逆向き)の場合にクロス積の結果がゼロベクトルとなってしまうのです.

コードでは小指方向を割り出して先に$$\vec{\bf D_{up}}$$(zBasis)を作っています. 指が手のひらに対して真横を向くようなことは,(特殊な人を除いて)たぶん無いと思うのでこれでジンバルロックを回避しています.

実は,これと同様のことをやってくれるメソッドしてLeap::Bone::basis()というのもあるのですが,これはそのままでは使えません. ボーンの方向ベクトルがZの基底ベクトルとして使われているためです(ボーンがZ-upではない場合は使えない). また,このZの基底ベクトルも内部的にLeap::Bone::direction()に依存しているのか,逆を向いています.

ちなみに,Leap::Matrix::toMatrix4x4()でOgre::Matrix4などに変換すると,転置行列が返ってきます. DirectXなど列優先な行列を使う環境を想定しているためのようですが,OGREは基本的に行優先なのでこの 関数テンプレートも使用には注意する必要がありそうです.

相対的な姿勢

上記までで,各ボーンのワールド空間上の姿勢をとることはできました.
親ボーンに対する子ボーンの姿勢を得るには以下のようにします.

あるボーンの親ボーンに対するローカルな向き(回転行列)を$${\bf R_i}$$と 表すことにしましょう.インデックスが小さくなるほどスケルトンの ルートに近くなっていくものとします. 上記までの方法でLeapMotionから得ることができるのは, そのボーンからスケルトンのルートに至るまでの途中にある全ての 祖先ボーンの回転行列がはじめから掛け合わされた回転行列です.

LeapMotionから得られるボーンのワールド空間における回転行列を$${\bf W_i}$$, とするならば,

\( \begin{array}{lll} {\bf W_i} & = & {\bf R_0} {\bf R_1} {\bf R_2} \cdots {\bf R_i} \\ & = & {\bf W_{i-1}} {\bf R_i} \\ \end{array} \)

となります.あるボーンの回転行列は$$i-1$$番目の回転行列,つまり親ボーンの ワールド空間における回転行列に,自身のローカル回転を掛けたものになるわけですね.

いま得ることができているのは$${\bf W_i}$$, $${\bf W_{i-1}}$$などの ワールド空間における回転行列のみです.これらから$$R_i$$を特定するには 上記の両辺に$${\bf W_{i-1}}$$の逆行列を左から掛けてあげます.

\(\begin{array}{lll} {\bf W_i} & = & {\bf W_{i-1}} {\bf R_i} \\ {\bf W_{i-1}^{-1}}{\bf W_i} & = & {\bf W_{i-1}^{-1}}{\bf W_{i-1}} {\bf R_i} \\ {\bf W_{i-1}^{-1}}{\bf W_i} & = & {\bf I}{\bf R_i} \\ {\bf W_{i-1}^{-1}}{\bf W_i} & = & {\bf R_i} \\ \end{array} \)

※ $${\bf I}$$は単位行列.

$${\bf W_i}$$などは全て回転行列であることが分かっているので, 実装上は転置行列を使えば良いことになります.

実際にローカルポーズを計算する場合は,更にこの回転行列にバインドポーズに おけるボーンの回転行列を掛ける必要があるようです.

OGREにおけるスケルタルアニメーション

OGREにはスケルタルアニメーションを読み込んで再生する機能があります. しかし今回の場合アニメーションのフレームがリアルタイムに入ってくるわけで この仕組みは使えません.

なので,カレントポーズを格納する行列パレットを自分で作って,手動で シェーダにバインドする必要があります.こうなってくると, もう殆どOGRE関係ないですね…

ただし,OGREでこれをやるためには以下のようにシェーダ宣言でinclude_skeletal_animationtrueに設定する必要があります. このフラグが立っていないとOGREは頂点に関連付いている ブレンドインデックスとブレンドウェイトをシェーダに渡してくれません.

vertex_program SimpleHandVP cg
{
  source SimpleHand.cg
  entry_point SimpleHandVP
  profiles vs_4_0 vs_3_0 arbvp1
  includes_skeletal_animation true // REQUIRED!

  default_params{ ... }
}

プログラム内でシェーダ定数に値をバインドするには以下のようにします.


auto mat = Ogre::MaterialManager::getSingleton().getByName("Hoge");
auto pass = mat->getTechnique(0)->getPass(0);
auto params = pass->getVertexProgramParameters();
params->setNamedConstant("piyo", ...);

上記のようにOgre::GpuProgramParameters::setNamedConstant()を使います.上の例だとマテリアル”Hoge”が存在していて,バーテックスシェーダが関連づけられていることが前提です.

行列の配列をバインドする場合は以下のオーバーロードを使います.

const int N = ...;
Ogre::Matrix4 mtx[N] = {...};

params->setNamedConstant("piyo", mtx, N);

前節までの方法で行列パレットを構築することは可能です.フレーム毎にLeapMotionからボーンの姿勢情報を得て,行列パレットを更新して,シェーダ定数を更新する,といった手順です.

行列パレットの構築方法はどちらかというとソースを見てもらった方が早いのですが, 最初に逆バイドポーズ行列を掛けて,子ボーンから親ボーンに向かって回転,平行移動,回転,平行移動…と変換を掛け合わせていく感じですね.
今回の場合,ボーンのローカルの平行移動はLeapMotionからとっておらず バインドポーズのオフセットをそのまま使っています.

ただ一つだけ注意する点があります,行列パレットのインデックスです.

てっきり,.skeletonファイルに定義されているボーンのIDそのものが インデックスとして使えるものだと思っていたのですが, どの頂点にもウェイトを持っていないボーンのインデックスは省略されて詰められます

デモでスケルトンのルート(手首)から各指の始点をずらすために,頂点に重みを持っていないボーンがある(下図)のですが,これがあったために少しハマリました.

bone

※ Blender的には親ボーンと非接続なボーンを作れますが,これはOGREが対応していません.

今回はモデルが単純なのでこのようなことが起きましたが,ちゃんとしたモデルを使う場合にも気をつける必要がありそうです.

*  *  *

結構苦労したものの,この方法,実用性あるのかとちょっと疑問だったり.

手のプロポーションが現実の座標でなく,モデルの形状に依存するということは, たとえば,このモデルの指先で3D空間内の何かを触りたい場合に LeapMotionの報告してくる指先位置が全くアテにならないことを示しているわけで.

ただ,毎フレームの行列パレットの更新時に一緒に,このモデルにおける カレントポーズの指先位置の計算もできると思うので全く使えないこともないとは思いますが.

Leave a Reply