エアホッケーの実験

エアホッケーのパック(玉)の動きを再現するプログラムを作ってみましょう. 下のプログラムはエアホッケー台の中心に,円盤状のパックを静止画で表示します. これをマウスのクリックによってこのパックが弾かれたように動きだし, 壁に当たって跳ね返りながら動くようにしてください. なお,パックは速度に比例した空気抵抗 (恥ずかしい間違いをしていた) によって次第に速度を下げ, 速度が一定値以下になれば静止するものとします. 一方,壁への衝突による速度低下は考慮しなくても構いません (もちろん考慮した方がいいことは言うまでもありません). このプログラムは結構長いので, 時間が無ければ Web ページからコピーアンドペーストして構いません. ソースファイル名は prog8.c としてください.

#include <stdlib.h>
#include <math.h>
#include <GL/glut.h>

#define PX 0.0                     /* 初期位置      */
#define PZ 0.0                     /* 初期位置      */
#define W 4                        /* 台の幅の2分の1  */
#define D 6                        /* 台の長さの2分の1 */
#define H 0.5                      /* 壁の高さ      */
#define R 0.3                      /* パックの半径    */
#define TIMESCALE 0.01             /* フレームごとの時間 */
#define SPEED 30.0                 /* パックの初速度   */
#define MU 0.5                     /* 台との摩擦係数   */
#define WEIGHT 1.0                 /* パックの質量    */

static int wh;                     /* ウィンドウの高さ  */
static int frame = 0;              /* 現在のフレーム数  */
static double vx0, vz0;            /* パックの初速度   */
static double px0 = PX, pz0 = PZ;  /* パックの初期位置  */

/*
 * 台を描く
 */
static void myGround(double height)
{
  const static GLfloat ground[][4] = {   /* 台の色    */
    { 0.6, 0.6, 0.6, 1.0 },
    { 0.3, 0.3, 0.3, 1.0 }
  };
  const static GLfloat wall[] = {        /* 壁の色    */
    0.8, 0.8, 0.8, 1.0
  };
  const static GLdouble panel[][9] = { /* 壁の形状データ */
    {  0.0,  0.0,  1.0, -W, 0.0, -D, -W, H, -D },
    { -1.0,  0.0,  0.0,  W, 0.0, -D,  W, H, -D },
    {  0.0,  0.0, -1.0,  W, 0.0,  D,  W, H,  D },
    {  1.0,  0.0,  0.0, -W, 0.0,  D, -W, H,  D },
    {  0.0,  0.0,  1.0, -W, 0.0, -D, -W, H, -D },
  };
  int i, j;

  glBegin(GL_QUADS);

  /* 床を描く */
  glNormal3d(0.0, 1.0, 0.0);
  for (j = -D; j < D; ++j) {
    for (i = -W; i < W; ++i) {
      glMaterialfv(GL_FRONT, GL_DIFFUSE, ground[(i + j) & 1]);
      glVertex3d((GLdouble)i, height, (GLdouble)j);
      glVertex3d((GLdouble)i, height, (GLdouble)(j + 1));
      glVertex3d((GLdouble)(i + 1), height, (GLdouble)(j + 1));
      glVertex3d((GLdouble)(i + 1), height, (GLdouble)j);
    }
  }

  /* 壁を描く */
  glMaterialfv(GL_FRONT, GL_DIFFUSE, wall);
  for (i = 0; i < 4; ++i) {
    glNormal3dv(panel[  i  ]);
    glVertex3dv(panel[  i  ] + 3);
    glVertex3dv(panel[i + 1] + 3);
    glVertex3dv(panel[i + 1] + 6);
    glVertex3dv(panel[  i  ] + 6);
  }

  glEnd();
}

/*
 * 円柱を描く
 */
static void myCylinder(double radius, double height, int sides)
{
  double step = 6.2831853072 / (double)sides;
  int i = 0;

  /* 上面 */
  glNormal3d(0.0, 1.0, 0.0);
  glBegin(GL_TRIANGLE_FAN);
  while (i < sides) {
    double t = step * (double)i++;
    glVertex3d(radius * sin(t), height, radius * cos(t));
  }
  glEnd();

  /* 底面 */
  glNormal3d(0.0, -1.0, 0.0);
  glBegin(GL_TRIANGLE_FAN);
  while (--i >= 0) {
    double t = step * (double)i;
    glVertex3d(radius * sin(t), -height, radius * cos(t));
  }
  glEnd();

  /* 側面 */
  glBegin(GL_QUAD_STRIP);
  while (i <= sides) {
    double t = step * (double)i++;
    double x = sin(t);
    double z = cos(t);

    glNormal3d(x, 0.0, z);
    glVertex3f(radius * x, height, radius * z);
    glVertex3f(radius * x, -height, radius * z);
  }
  glEnd();
}

/*
 * 画面表示
 */
static void display(void)
{
  const static GLfloat lightpos[] = { 3.0, 4.0, 5.0, 1.0 }; /* 光源の位置 */
  const static GLfloat yellow[] = { 0.9, 0.9, 0.2, 1.0 };   /* パックの色 */

  double t = TIMESCALE * frame;         /* フレーム数から現在時刻を求める */

  double v = exp(-MU * t / WEIGHT);                   /* パックの速度比  */
  double p = WEIGHT * (1.0 - v) / MU;                 /* パックの相対位置 */

  double px = vx0 * p + px0;                          /* パックの現在位置 */
  double pz = vz0 * p + pz0;                          /* パックの現在位置 */

  /*
  ** パックが台の壁に当たったら初期位置と初速度を変更する
  ** 速度(比)が一定以下になったら現在位置を初期位置にして
  ** アニメーションを止める
  ** (ここは自分で考えてください)
  */

  /* フレーム数(画面表示を行った回数)をカウントする */
  ++frame;

  /* 画面クリア */
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  /* モデルビュー変換行列の初期化 */
  glLoadIdentity();

  /* 光源の位置を設定 */
  glLightfv(GL_LIGHT0, GL_POSITION, lightpos);

  /* 視点の移動(物体の方を奥に移す)*/
  glTranslated(0.0, 0.0, -20.0);
  glRotated(45.0, 1.0, 0.0, 0.0);

  /* シーンの描画 */
  myGround(0.0);
  glPushMatrix();
  glTranslated(px, 0.0, pz);
  glMaterialfv(GL_FRONT, GL_DIFFUSE, yellow);
  myCylinder(0.3, 0.1, 8);
  glPopMatrix();

  glFlush();
}

static void resize(int w, int h)
{
  /* ウィンドウ全体をビューポートにする */
  glViewport(0, 0, w, wh = h);

  /* 透視変換行列の指定 */
  glMatrixMode(GL_PROJECTION);

  /* 透視変換行列の初期化 */
  glLoadIdentity();
  gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0);

  /* モデルビュー変換行列の指定 */
  glMatrixMode(GL_MODELVIEW);
}

static void keyboard(unsigned char key, int x, int y)
{
  /* ESC か q をタイプしたら終了 */
  if (key == '\033' || key == 'q') {
    exit(0);
  }
}

static void mouse(int button, int state, int x, int y)
{
  switch (button) {
  case GLUT_LEFT_BUTTON:

  /*
  ** マウスをクリックしたウィンドウ上の座標から
  ** 表示されている台の表面の空間位置を求める
  */

    if (state == GLUT_DOWN) {
      GLdouble model[16], proj[16];
      GLint view[4];
      GLfloat z;
      GLdouble ox, oy, oz;

      /* クリックした時にフレーム数を 0(すなわち時間を 0)にする */
      frame = 0;

      /*
      ** モデルビュー変換行列・透視変換行列・ビューポート変換行列を取り出す
      */
      glGetDoublev(GL_MODELVIEW_MATRIX, model);
      glGetDoublev(GL_PROJECTION_MATRIX, proj);
      glGetIntegerv(GL_VIEWPORT, view);

      /*
      ** クリックした位置の奥行きを z に取り出す
      ** (台の外をクリックするとまずいけどもういいや)
      */
      glReadPixels(x, wh - y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z);

      /* クリックしたところの台の上の位置を求める */
      gluUnProject(x, wh - y, z, model, proj, view, &ox, &oy, &oz);

      /*
      ** (px0, pz0) と (ox, oz) を使ってパックを打ち出す方向を決め,
      ** パックの初速度 (vx0, vz0) を決めて,アニメーションを開始する.
      ** (ここは自分で考えて下さい)
      */

      break;
    case GLUT_MIDDLE_BUTTON:
      break;
    case GLUT_RIGHT_BUTTON:
      break;
    default:
      break;
    }
  }
}

static void init(void)
{
  /* 初期設定 */
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_CULL_FACE);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
}

int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow(argv[0]);
  glutDisplayFunc(display);
  glutReshapeFunc(resize);
  glutKeyboardFunc(keyboard);
  glutMouseFunc(mouse);
  init();
  glutMainLoop();
  return 0;
}

上のプログラム内の関数 mouse() はマウスをクリックしたときに呼び出され, そのウィンドウ上のマウスの座標から, クリックした位置に表示されている物体表面の空間位置を求めます. この座標 (ox, oy, oz) とパックの初期位置 (px0, pz0) を使ってパックの打ち出し方向を決め,速度を決定してください. また,このとき同時にアニメーションを開始する必要があります.

エアホッケーの台 歩く箱男

台は幅が 2W,奥行きが 2D の大きさで,中心に原点があります. またパックは (px, pz) に中心をもち,半径が R の円盤です. したがって, パックの可動範囲は一点鎖線内の領域(鴬色の領域)に限られます.

パックが台の壁に衝突した時,パックは反対側に跳ね返るものとします. 衝突による速度低下は考慮しなくても構いません.

  1. パックの現在位置は関数 display() 内の px および pz の二つの変数で設定しています. この値を変更すれば,パックの位置を変えることができます.
  2. パックの質量を m とし,それが速度 v に比例した摩擦力 -µv を受けて運動するとき,初期位置を p0,初速度を v0 とすれば,時刻 t における速度 v と位置 p は次式で求めることができます.
    v = v0 e-(µ/m) t
    p = p0 + v0 (m/µ) {1 - e-(µ/m) t}
  3. 時刻 t におけるパックの位置 p が台上の稼働範囲外に出れば,p を衝突した壁に対して反対側の位置 p' に移動させ,その p' を新しい初期位置 p0 とします.
  4. そして,その時刻 t における速度 v の符号を反転したもの -v をその位置における新しい初速度 v0 とします.ただし,上記のプログラムでは速度 v を台上の座標軸に分解したもの (vx, vz) として取り扱っているので, 壁の向きによって vx あるいは vz のいずれかの符号を反転します.
  5. 時間 t を 0 にします.
  6. この 3 〜 5 の処理を4つの壁について行います.
  7. もちろん,パックは滑らかに動くようにしてください. また画面のちらつきも抑制するようにしてください.