Experiment


10. ヘッドトラッキング

アプリケーションプログラムの中で3次元位置計測センサで得られたトラッキングデータを利用する方法について学習します。
仮想空間の中でのインタラクティブな映像体験を実現するためには、利用者の視点に応じた映像の更新を行う必要があります。ここでは3次元位置計測センサによるヘッドトラッキングデータに従った映像の更新方法を学びます。


10.1 センサデータの受渡し

3次元位置計測センサのデータをVRのプログラム中で使用するためには、磁気センサのデータ取得プログラムfobdで取得したデータをアプリケーションプログラムに受け渡すことが必要です。ここでは、共有メモリを介したプロセス間通信によってセンサデータの受け渡しを行う方法を使用します。
共有メモリとは、複数のプロセスが読み書き可能なメモリ領域であり、fobd はセンサのデータを取得し、画面に出力すると同時に共有メモリへの書き込みを行っています。そのため、アプリケーションプログラムにおいても、同一の共有メモリにアクセスすることで、センサデータを利用することができます。
以下の処理をアプリケーションプログラムの myinit() 関数の中に追加し、fob_data を使用できるようにしましょう。

  #include <sys/shm.h>
  #define SHMKEY 61

  int shmid;

  struct FOB
  {
  float pos[3];
  float ang[3];
  };
  struct FOB *fob_data;


  myinit()
  {
  ..........
  /* Set Shared Memory for Tracker Data */
  shmid=shmget(SHMKEY, sizeof(struct FOB), 0777 | IPC_CREAT);
  fob_data = (struct FOB *)shmat(shmid, 0, 0);

  ..........
  }

ここでは、shmget() のシステムコールによって共有メモリの識別子を獲得し、shmat() のシステムコールで自分のプロセスのアドレス空間に付加します。ここでは、は fobd が使用している共有メモリのキーにあわせて SHMKEY に 61 を指定しています。
この共有メモリの設定を行うこで、アプリケーションプログラムから fob_data->pos、fob_data->ang によって、センサの位置と角度のデータを参照することができるようになります。

また、fobd を用いるにはFlock of Birdsを動かす必要がありますが、ここではFlock of Birdsと同じ形式でデータを出力する fob_dummy を次の要領でダウンロードして使用して下さい。Set value: に対して入力した値がセンサの出力の代わりにセットされます。

  ・http://green.cc.tsukuba.ac.jp/tetsu/lecture/exp にアクセスし、fob_dummy.tarをダウンロード
  ・% tar xvf fob_dummy.tar によりファイルを展開
  ・% make
  ・% ./fob_dummy   を実行

  <fob data>
  Sensor: 0.000000 -300.000000 -100.000000 0.000000 0.000000 180.000000

  Set value: 10 -300 -100 0 0 180
  Sensor: 10.000000 -300.000000 -100.000000 0.000000 0.000000 180.000000

  Set value:


10.2 視点位置の計算

上記の方法でFlock of Birdsから取得されたデータ fob_data->pos は、あくまでレシーバの位置データであるため、これから左右の視点位置を求める必要があります。下図はメガネに取り付けたレシーバの位置関係を示したものです。 ここでは、このレシーバと目の位置関係から、左右の視点位置を求めます。


       図12.1:センサと視点位置

レシーバの位置から両眼位置へ向かう単位ベクトル eyevec が求められれば、顔の幅 face_dis、眼間距離 eye_dis の値から左右の目の位置を計算することができます。レシーバから両眼への単位ベクトルは、レシーバ座標系における単位ベクトル(0.0, 0.0, 1.0)をセンサ座標系に変換する座標変換によって計算できます。以下は、センサデータからワールド座標系における両眼位置を求める関数を示したものです。

  float face_dis = 16.0;   /* 顔の幅 (cm) */
  float eye_dis = 7.0;   /* 眼間距離 (cm) */
  float viewpos[2][3];    /* ワールド座標系での視点位置 (m) */

  void getEyepos(void)  
  {
  int i;
  float inivec[3] = {0.0, 0.0, 1.0}; /* レシーバ座標系の両眼方向の単位ベクトル */
  float eyevec[3];          /* センサ座標系の両眼方向の単位ベクトル */
  float s_viewpos[2][3];

  /* レシーバから両眼位置への単位ベクトル */
  rvec_to_svec(inivec, fob_data->ang, eyevec);

  /* センサ座標系での視点位置 */
  for (i=0;i<3;i++){
  /* 左目 */
  s_viewpos[0][i] = fob_data->pos[i]+(face_dis/2.0 - eye_dis/2.0)*eyevec[i]; 
  /* 右目 */
  s_viewpos[1][i] = fob_data->pos[i]+(face_dis/2.0 + eye_dis/2.0)*eyevec[i]; 
  }
  /* ワールド座標系での視点位置 */
  spos_to_wpos(s_viewpos[0], viewpos[0]);   /* 左目の位置 */
  spos_to_wpos(s_viewpos[1], viewpos[1]);   /* 右目の位置 */
  return;
  }

まず、前回定義した rvec_to_svec() の関数によって、レシーバの角度データ fob_data->ang を用いて単位ベクトル(0.0, 0.0, 1.0)がセンサ座標系で表されるベクトル eyevec を求めます。これはレシーバから両眼位置へ向かう単位ベクトルを表し、これを用いてセンサ座標系の上で左目の位置 s_viewpos[0]、右目の位置 s_viewpos[1] をそれぞれ求めます。ここで、face_dis は顔の幅、eye_dis は眼間距離を示しています。最終的には前回定義された、spos_to_wpos() の関数を用いて、s_viewpos を仮想空間のワールド座標系の視点位置の値 viewpos に変換します。


10.3 視点位置による映像更新

各自のプログラムに上記の関数を導入し、glFrustum()、gluLookAT() の視点位置に viewpos[3] を使用するように改良することで、ヘッドトラッキングデータを使用したインタラクティブなプログラムに変更して下さい。

  void display(void)
  {
  float screen_w = 2.8;
  float screen_h = 2.1;
  float near = 0.05;
  float far = 100.0;
  float ns_left, ns_right, ns_bottom, ns_top;
  float distance;

  getEyepos();
  .........
  /*** Left Eye ***/
  /* viewport transform */
  glViewport(0, 0, 512, 384);

  /* projection transform */
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  distance = screen_h/2.0-viewpos[0][1];
  ns_left = (-screen_w/2.0-viewpos[0][0])*near/distance;
  ns_right = (screen_w/2.0-viewpos[0][0])*near/distance;
  ns_bottom = (0.0-viewpos[0][2])*near/distance;
  ns_top = (screen_h-viewpos[0][2])*near/distance;
  glFrustum(ns_left, ns_right, ns_bottom, ns_top, near, far);

  /* viewing transform */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(viewpos[0][0], viewpos[0][1], viewpos[0][2],
         viewpos[0][0], viewpos[0][1]+1.0, viewpos[0][2], 0.0, 0.0, 1.0);


  /* modering transform */
  draw_model();

  /*** Right Eye ***/
  /* viewport transform */
  glViewport(512, 0, 512, 384);

  /* projection transform */
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  distance = screen_h/2.0-viewpos[1][1];
  ns_left = (-screen_w/2.0-viewpos[1][0])*near/distance;
  ns_right = (screen_w/2.0-viewpos[1][0])*near/distance;
  ns_bottom = (0.0-viewpos[1][2])*near/distance;
  ns_top = (screen_h-viewpos[1][2])*near/distance;
  glFrustum(ns_left, ns_right, ns_bottom, ns_top, near, far);

  /* viewing transform */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(viewpos[1][0], viewpos[1][1], viewpos[1][2],
         viewpos[1][0], viewpos[1][1]+1.0, viewpos[1][2], 0.0, 0.0, 1.0);


  /* modering transform */
  draw_model();

  glutSwapBuffers();    /* Buffer Swap */
  }

視点のトラッキングデータを利用して映像を連続的に更新するには、プログラムのアイドル時にウインドウを連続的に再描画するため、glutIdleFunc() の中でglutPostRedisplay() を指定します。またディスプレイモードとして、ダブルバッファを指定し、レンダリングの終わりに glFlush() の代わりに glutSwapBuffers() を使用するようにします。

・ void glutIdleFunc(void (*func)(void))
未処理のイベントがないアイドル時に実行する関数 func を指定します。

・void glutPostRedisplay(void)
現在のウインドウを再描画が必要なものとし、glutDisplayFunc() で指定した関数が呼び出されます。

・void glutSwapBuffers(void)
ダブルバッファを使用している際に、レンダリングの終了時にバックバッファとフロントバッファの入れ替えを行います。


  void idle(void)
  {
  glutPostRedisplay();
  }

  int main(int argc, char **argv)
  {
  .........
  /* open window */
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
  .........
  glutDisplayFunc(display);
  glutIdleFunc(idle);
  .........
  }


※演習:

トランスミッタの設置位置の都合から、CS Galleryではメガネの左側にセンサのレシーバを装着して使用しています。上記の説明では、レシーバをメガネの左側に装着していましたが、これをメガネの右側に付けると上のプログラムがどう変わるか書き換えてみましょう。