エアホッケーのパック(玉)の動きを再現するプログラムを作ってみましょう。 下のプログラムはエアホッケー台の中心に、円盤状のパックを静止画で表示します。 マウスのクリックによってこのパックが弾かれたように動きだし、 壁に当たって跳ね返りながら動くようにしてください。 なお、パックは台の表面から受ける速度に比例した摩擦力によって次第に速度を下げ、 速度が一定値以下になれば静止するものとします。 一方、壁への衝突による速度低下は考慮しなくても構いません (もちろん考慮した方がいいことは言うまでもありません)。 このプログラムは結構長いので、 時間が無ければ 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 /* パックの質量 */ int wh; /* ウィンドウの高さ */ int frame = 0; /* 現在のフレーム数 */ double vx0, vz0; /* パックの初速度 */ double px0 = PX, pz0 = PZ; /* パックの初期位置 */ /* * 台を描く */ void myGround(double height) { static GLfloat ground[][4] = { /* 台の色 */ { 0.6, 0.6, 0.6, 1.0 }, { 0.3, 0.3, 0.3, 1.0 } }; static GLfloat wall[] = { /* 壁の色 */ 0.8, 0.8, 0.8, 1.0 }; 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(); } /* * 円柱を描く */ 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(); } /* * 画面表示 */ void display(void) { static GLfloat lightpos[] = { 3.0, 4.0, 5.0, 1.0 }; /* 光源の位置 */ 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(); } 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); } void keyboard(unsigned char key, int x, int y) { /* ESC か q をタイプしたら終了 */ if (key == '\033' || key == 'q') { exit(0); } } 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; } } } void init(void) { /* 初期設定 */ glClearColor(1.0, 1.0, 1.0, 0.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 の円盤です。 したがって、 パックの可動範囲は一点鎖線内の領域(鴬色の領域)に限られます。
パックが台の壁に衝突した時、パックは反対側に跳ね返るものとします。 衝突による速度低下は考慮しなくても構いません。
v = v0 e-(µ/m) t
p = p0 + v0 (m/µ) {1 - e-(µ/m) t}