床井研究室

退職後どうしよう

まー自分もいつの間にか定年退職を否が応でも意識させられる年齢になってきたわけで、昨今の状況を鑑みるに、やはり働ける間は働かんといかんのだろうな、いや、自分の現状では定年まで居座らず早々に若い人に席を譲るべきなんじゃなかろうか、などと色々悩むところではあります。それなのに「にゃーん」とか恥ずかしいことをつぶやいてみたり、その後に「このツイート見てる学生さんもいるんだよな」と反省してみたり、そういえばネタ吐いたら寒いという仕草をしていた学生さんがいたなとか、話の脱線のせいでゼミが終了時間までに終わりそうにないと学生さんから「終わりですよ」と言われたなとか、いろいろ残念な気持ちにもなります。取り巻く状況は厳しさを増すばかりです(何が)。

C++ で Oculus Rift に表示するプログラムを書く

なんてことは単に時間と労力の無駄で、研究といえども Unity なり Unreal Engine なりのミドルウェアを使いこなした方が生産性は上がります。まあ、Unity を使って研究した人の卒論がどこか企画書っぽくて具体性に欠けた感じがしたり、Unity の機能を使って実現することが目的になっていたりするという話もあって、しょぼくてもスクラッチからプログラムを書いてもらった方が研究っぽくなるのかな、などと考えたりします。でも、しっかりサーベイをしてロジックを組んでから取り掛かった人の卒論は、Unity を使ったかどうかに関係なく読ませられる内容だったりするので、結局それも指導次第なのだな、という結論に至っています。

というわけで、HMD を使った研究を Unity 等を使わず C++ でやりたいという偏屈が現れることまで想定する必要はサラサラないとは思うのですが、実は授業の宿題で補助プログラムと一緒に使っている GLFW のラッパークラス GgApplication.h は、当初から Oculus Rift の DK2 と CV1 に対応していました。でも、そのことに気づいた学生さんいるのかな…いたら手を挙げるように。

実をいうと、これは自分自身もどうでもよくなっていて、ラッパーのこの部分との整合性を考えずに補助プログラムの方を更新していたり、Oculus の SDK の LibOVR 自体も更新されてちょっとだけ互換性がなくなっていたりしていて、いつの間にかそのままでは動かなくなっていました。でも先日、たまたま作っているプログラムの表示を HMD で見たいというニーズがあって、久しぶりに使ってみたらエラーになってしまったので、ちょっと修正しました。

このプログラムは Oculus SDK の 0.8 (Oculus Rift DK2) と 1.30 以降 (Oculus Rift CV1) に対応しています。これは #include する Oculus SDK のヘッダファイルのバージョンで判断しています。なお、このパッケージには 1.30 を同梱しています。Oculus Rift S でも動作することを確認しています。

サンプルプログラム

この発端になったプログラムをそのまま出してきても色々あってわけがわかんないと思うので、とりあえず箱を一つ描くプログラムを作りました。

このプログラムを実行すると、次のような図形を表示します。図形はマウスの左ドラッグで回転できます。

箱を描く

この箱は実は Alias OBJ 形式のデータなので、プロジェクトの中に含まれている box.obj (と box.mtl) を差し替えて他の形状も描くことができます (三角形分割されている必要があります・テクスチャは反映されません)。

このプログラムの本体 OculusRiftSample.cpp の描画ループは、次のようになっています。

  // ウィンドウが開いている間繰り返す
  while (window)
  {
    // 焦点距離 (cameraNear) が 1 のときのスクリーンの大きさ
    GLfloat screen[4] = { -window.getAspect(), window.getAspect(), -1.0f, 1.0f };

    // 投影変換行列を求める
    const GgMatrix mp(ggFrustum(screen[0] * zNear, screen[1] * zNear,
      screen[2] * zNear, screen[3] * zNear, zNear, zFar));

    // ビュー変換行列を求める
    const GgMatrix mv(ggTranslate(origin[0], origin[1], origin[2]));

    // ウィンドウを消去する
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 光源をワールド座標系に配置する
    lightBuffer.loadPosition(mv * light.position);

    // シェーダプログラムの使用開始
    simple.use(mp, mv * window.getTrackball(), lightBuffer);

    // 図形を描画する
    object.draw();

    // カラーバッファを入れ替える
    window.swapBuffers();
  }

めんどくさいので、このプログラムでは宿題用の補助プログラムを使っていますが、このループには普通に OpenGL による描画手続きを書くことができ (るはずだと思い) ます。またビュー変換行列や投影変換行列の算出に ggLookAt()ggPerspective() のようなものを使っていないのでちょっとややこしいですが、単に下図のような視錐台を ggFrustum() で設定しているだけです。

視錐台

まず、GgApplication.h で宣言している記号定数 USE_OCULUS_RIFT に 1 を設定します。

// Oculus Rift を使うなら 1
#define USE_OCULUS_RIFT 1

次に、上記のループの内部を次のように書き換えます。まず window.begin() を呼び出して、その戻り値が真のとき、すべての「目」について (window.eyeCount には HMD のディスプレイの数、Oculus Rift の場合は ovrEye_Count すなわち 2 が格納されています) 描画を行います。

  // ウィンドウが開いている間繰り返す
  while (window)
  {
    // 描画開始
    if (window.begin())
    {
      // すべての目について
      for (int eye = 0; eye < window.eyeCount; ++eye)
      {

描画する「目」を選択し、その目におけるスクリーンのサイズ (screen) や HMD の姿勢 (position, orientation) を取り出します。screen は 4 要素の配列で、それぞれの要素には視点とスクリーンの距離を 1 としたときのスクリーンの左端、右端、上端、下端の位置が格納されています。position は 3 要素の配列で、HMD の視点の位置が格納されています。また orientation は HMD の回転を表す四元数です。

        // スクリーンの大きさ, HMD の位置, HMD の方向
        GLfloat screen[4], position[3], orientation[4];

        // 描画する目を選択してトラッキング情報を得る
        window.select(eye, screen, position, orientation);

投影変換行列はスクリーンのサイズに zNear を掛けたものを ggFrustum() の引数に指定して求めます。ビュー変換行列は、物体の方を視点の位置や方向とは逆方向に移動することによって求めます。getConjugateMatrix() メソッドは、共役四元数による回転の変換行列を求めます。

        // 投影変換行列を求める
        const GgMatrix mp(ggFrustum(screen[0] * zNear, screen[1] * zNear,
          screen[2] * zNear, screen[3] * zNear, zNear, zFar));

        // ビュー変換行列を求める
        const GgMatrix mv(GgQuaternion(orientation).getConjugateMatrix()
          * ggTranslate(origin[0] - position[0], origin[1] - position[1], origin[2] - position[2]));

図形の描画を行います。

        // ウィンドウを消去する
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // 光源をワールド座標系に配置する
        lightBuffer.loadPosition(mv * light.position);

        // シェーダプログラムの使用開始
        simple.use(mp, mv * window.getTrackball(), lightBuffer);

        // 図形を描画する
        object.draw();

一つ目の目に対する図形の描画が完了したら、描画した目を指定して window.commit() を呼び出します。

        // 片目の描画を完了する
        window.commit(eye);
      }

すべての目の描画が完了したら、window.submit() により描画した画像を Oculus Rift に転送します。window.swapBuffers() は削除してください。

      // フレームを転送する
      window.submit();
    }
  }

window.submit(false) とするとミラー表示を行わなくなるので、その後に glBindFramebuffer(GL_FRAMEBUFFER, 0) で通常のフレームバッファへの書き込みに戻してから描画を行うこともできます。ビューポートを元に戻すには window.resetViewport() が使えます。この場合は描画の最後に window.swapBuffers() を呼んでください。

これで Oculus Rift に次のように表示されます。

Oculus Rift への表示