マウスのボタンを押したことを知るには glutMouseFunc という関数でマウスのボタンを操作したときに呼び出す関数を指定します。
【ConsoleApplication1.cpp】 マウスのボタンをクリックする(不要行削除)
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3d(1.0, 0.0, 0.0); // この行から 削除
glBegin(GL_POLYGON); // 削除
glVertex2d(-0.9, -0.9); // 削除
glVertex2d(0.9, -0.9); // 削除
glVertex2d(0.9, 0.9); // 削除
glVertex2d(-0.9, 0.9); // 削除
glEnd(); // この行まで 削除
glFlush();
}
void resize(int w, int h)
{
// ウィンドウ全体をビューポートにする
glViewport(0, 0, w, h);
// 変換行列の初期化
glLoadIdentity();
// この行から 削除
// スクリーン上の表示領域をビューポートの大きさに比例させる 削除
glOrtho(-w / 200.0, w / 200.0, -h / 200.0, h / 200.0, -1.0, 1.0); // この行まで 削除
}
【ConsoleApplication1.cpp】 マウスのボタンをクリックする(変更点のみ)
void mouse(int button, int state, int x, int y)
{
switch (button) {
case GLUT_LEFT_BUTTON:
std::cout << "left";
break;
case GLUT_MIDDLE_BUTTON:
std::cout << "middle";
break;
case GLUT_RIGHT_BUTTON:
std::cout << "right";
break;
default:
break;
}
std::cout << " at (" << x << ", " << y << ")" << std::endl;
}
int main(int argc, char* argv[])
{
glutInitWindowPosition(100, 100);
glutInitWindowSize(320, 240);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutCreateWindow(argv[0]);
glutDisplayFunc(display);
glutReshapeFunc(resize);
glutMouseFunc(mouse);
init();
glutMainLoop();
return 0;
}
実行例)
left at (35, 29)
left at (35, 29)
middle at (67, 91)
middle at (67, 91)
right at (65, 135)
right at (65, 135)
left at (153, 61)
left at (153, 61)
left at (153, 61)
left at (153, 61)
[解説]
void glutMouseFunc(void (*callback)(int button, int state, int x, int y))
引数の callback には、マウスボタンが押されたときに実行する関数のポインタを与えます。
この関数の引数 button には、押されたボタン(GLUT_LEFT_BUTTON, GLUT_MIDDLE_BUTTON, GLUT_RIGHT_BUTTON) が渡されます。引数 state には押した(GLUT_DOWN)のか離した(GLUT_UP)のかが渡されます。また、x と y にはその位置が渡されます。
プログラムを実行して開いたウィンドウ上でマウスのボタンをクリックしてみると、x と y に渡される座標は、ウィンドウの左上隅を原点 (0, 0) とした画面上の画素の位置になり、デバイス座標系とは上下が反転しています。
マウスの位置をもとに図形を描く場合は、マウスの位置からウィンドウ上の座標値を求めなければなりません。ここではちょっと手を抜いて、ワールド座標系がこのマウスの座標系に一致するように glOrtho を設定します(上図)。また、ウィンドウの上下も反転します。
【ConsoleApplication1.cpp】 ボタンを押した位置から離した位置まで線を描く(不要行削除)
void mouse(int button, int state, int x, int y)
{
switch (button)
{
case GLUT_LEFT_BUTTON:
std::cout << "left"; // 削除
break;
case GLUT_MIDDLE_BUTTON:
std::cout << "middle"; // 削除
break;
case GLUT_RIGHT_BUTTON:
std::cout << "right"; // 削除
break;
default:
break;
}
std::cout << " at (" << x << ", " << y << ")" << std::endl; // 削除
}
【ConsoleApplication1.cpp】 ボタンを押した位置から離した位置まで線を描く(変更点のみ)
void resize(int w, int h)
{
// ウィンドウ全体をビューポートにする
glViewport(0, 0, w, h);
// 変換行列の初期化
glLoadIdentity();
// スクリーン上の座標系とマウスの座標系に一致させる
glOrtho(-0.5, (GLdouble)w - 0.5, (GLdouble)h - 0.5, -0.5, -1.0, 1.0);
}
void mouse(int button, int state, int x, int y)
{
static int x0 = 0, y0 = 0;
switch (button) {
case GLUT_LEFT_BUTTON:
if (state == GLUT_UP) {
// ボタンを押した位置から離した位置まで線を描く
glColor3d(0.0, 0.0, 0.0);
glBegin(GL_LINES);
glVertex2i(x0, y0);
glVertex2i(x, y);
glEnd();
glFlush();
}
else {
// ボタンを押した位置を保持する
x0 = x;
y0 = y;
}
break;
case GLUT_MIDDLE_BUTTON:
break;
case GLUT_RIGHT_BUTTON:
break;
default:
break;
}
}
[解説]
void glVertex2i(GLint x, GLint y)
この関数は glVertex2d と同様に2次元の座標値を設定しますが、引数の型が GLint 型です。
先ほどのプログラムでは、ウィンドウのサイズを変えたり、ウィンドウが他のウィンドウに隠されたあと再び表示される度に、ウィンドウの中身が消えてしまいます。やはり、この場合もきちんと描き直してやる必要があるわけですが、そのためにはそれまで表示した内容を記憶しておかなければなりません。
mouse が実行されたときに、配列に現在の位置を記憶しておき、display が実行されたときに、それをまとめて描画するようにします。
【ConsoleApplication1.cpp】 ボタンを押した位置から離した位置まで線をまとめて描く(不要行削除)
void mouse(int button, int state, int x, int y)
{
static int x0 = 0, y0 = 0; // 削除
switch (button)
{
case GLUT_LEFT_BUTTON:
if (state == GLUT_UP) {
// ボタンを押した位置から離した位置まで線を描く
glColor3d(0.0, 0.0, 0.0);
glBegin(GL_LINES);
glVertex2i(x0, y0); // 削除
glVertex2i(x, y); // 削除
glEnd();
glFlush();
}
else { // この行から 削除
// ボタンを離した位置を保持する 削除
x0 = x; // 削除
y0 = y; // 削除
} // この行まで 削除
break;
case GLUT_MIDDLE_BUTTON:
break;
case GLUT_RIGHT_BUTTON:
break;
default:
break;
}
}
【ConsoleApplication1.cpp】 ボタンを押した位置から離した位置まで線をまとめて描く(変更点のみ)
#include <iostream>
#include <GL/glut.h>
#define MAXPOINTS 100 // 記憶する点の数
GLint point[MAXPOINTS][2]; // 座標を記憶する配列
int pointnum = 0; // 記憶した座標の数
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
// 記憶したデータで線を描く
if (pointnum > 1) {
glColor3d(0.0, 0.0, 0.0);
glBegin(GL_LINES);
for (int i = 0; i < pointnum; ++i) {
glVertex2iv(point[i]);
}
glEnd();
}
glFlush();
}
void mouse(int button, int state, int x, int y)
{
switch (button) {
case GLUT_LEFT_BUTTON:
// ボタンを操作した位置を記憶する
point[pointnum][0] = x;
point[pointnum][1] = y;
if (state == GLUT_UP) {
// ボタンを押した位置から離した位置まで線を描く
glColor3d(0.0, 0.0, 0.0);
glBegin(GL_LINES);
glVertex2iv(point[pointnum - 1]); // ボタンを押した位置
glVertex2iv(point[pointnum]); // ボタンを離した位置
glEnd();
glFlush();
}
if (pointnum < MAXPOINTS - 1) {
++pointnum;
}
break;
case GLUT_MIDDLE_BUTTON:
break;
case GLUT_RIGHT_BUTTON:
break;
default:
break;
}
}
[解説]
void glVertex2iv(const GLint *v)
この関数は glVertex2i と同様に2次元の座標値を設定しますが、引数 v には2個の要素を持つ GLint 型の配列を指定します。v[0] には x 座標値、v[1] には y 座標値を格納します。この例の様に、複数の点の座標を指定する場合に便利です。
マウスのボタンを押しながらマウスを動かす操作を、ドラッグといいます。ドラッグ中はマウスの位置を継続的に取得する必要があります。glutMouseFunc で指定したハンドラはボタンを押したときにしか実行されないので、この目的には使えません。
マウスを動かしたときに実行する関数を指定するには glutMotionFunc または glPassiveMotionFunc を使います。
glutMotionFunc で指定した関数は、マウスのボタンを押しながらマウスを動かしたときに実行されます。一方、glPassiveMotionFunc で指定した関数は、マウスのボタンを押さずにマウスを動かしたときに実行されます。
先ほどのプログラムでは、マウスの左ボタンを押してから離すまでウィンドウには何も表示されませんでした。これを、マウスのドラッグ中は線分をマウスに追従して描くようにします。このような効果をラバーバンド(輪ゴム)といいます。ここでは glutMotionFunc を使って、マウスのドラッグ中にラバーバンドを表示するようにします。
【ConsoleApplication1.cpp】 ラバーバンドを描く(変更点のみ)
#define MAXPOINTS 100 // 記憶する点の数
GLint point[MAXPOINTS][2]; // 座標を記憶する配列
int pointnum = 0; // 記憶した座標の数
int rubberband = 0; // ラバーバンドの消去
void mouse(int button, int state, int x, int y)
{
switch (button) {
case GLUT_LEFT_BUTTON:
// ボタンを操作した位置を記憶する
point[pointnum][0] = x;
point[pointnum][1] = y;
if (state == GLUT_UP) {
// ボタンを押した位置から離した位置まで線を描く
glColor3d(0.0, 0.0, 0.0);
glBegin(GL_LINES);
glVertex2iv(point[pointnum - 1]); // ボタンを押した位置
glVertex2iv(point[pointnum]); // ボタンを離した位置
glEnd();
glFlush();
// ボタンを離したのでラバーバンドを消す
rubberband = 0;
}
if (pointnum < MAXPOINTS - 1) {
++pointnum;
}
break;
case GLUT_MIDDLE_BUTTON:
break;
case GLUT_RIGHT_BUTTON:
break;
default:
break;
}
}
void motion(int x, int y)
{
static GLint savepoint[2]; // 以前のラバーバンドの端点
// 論理演算機能オン
glEnable(GL_COLOR_LOGIC_OP);
glLogicOp(GL_INVERT);
glBegin(GL_LINES);
if (rubberband) {
// 以前のラバーバンドを消す
glVertex2iv(point[pointnum - 1]);
glVertex2iv(savepoint);
}
// 新しいラバーバンドを描く
glVertex2iv(point[pointnum - 1]);
glVertex2i(x, y);
glEnd();
glFlush();
// 論理演算機能オフ
glLogicOp(GL_COPY);
glDisable(GL_COLOR_LOGIC_OP);
// 今描いたラバーバンドの端点を保存
savepoint[0] = x;
savepoint[1] = y;
// 今描いたラバーバンドは次のタイミングで消す
rubberband = 1;
}
int main(int argc, char* argv[])
{
glutInitWindowPosition(100, 100);
glutInitWindowSize(320, 240);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutCreateWindow(argv[0]);
glutDisplayFunc(display);
glutReshapeFunc(resize);
glutMouseFunc(mouse);
glutMotionFunc(motion);
init();
glutMainLoop();
return 0;
}
[解説]
void glEnable(GLenum cap)
GLenum 型(unsigned int と等価)の引数 cap に指定した機能を使用可能にします。GL_LOGIC_OP もしくは GL_COLOR_LOGIC_OP は、図形の描画の際にウィンドウに既に描かれている内容と、これから描こうとする内容の間で論理演算ができるようにします。
void glDisable(GLenum cap)
引数 cap に指定した機能を使用不可にします。
void glLogicOp(GLenum opcode)
引数 opcode には、ウィンドウに描かれている内容と、これから描こうとする内容の間で行う論理演算のタイプを指定します。GL_COPY は、これから描こうとする内容をそのままウィンドウ内に描きます。GL_INVERT はウィンドウに描かれている内容のこれから描こうとする図形の領域を反転します。
void glutMotionFunc(void (*callback)(int x, int y))
引数 callback には、マウスのいずれかのボタンを押しながらマウスを動かしたときに実行する関数のポインタを与えます。この関数の引数 x と y には、現在のマウスの位置が渡されます。この設定を解除するには、引数に NULL (ヌルポインタ)を指定します。
ラバーバンドを実現する場合、マウスを動かしたときに直前に描いたラバーバンドを消す必要があります。このとき、ラバーバンドを描いたことによってウィンドウに既に描かれていた内容が壊されてしまうので、その部分をもう一度描き直す必要があります。しかし、そのために画面全体を書き換えるのは、ちょっともったいない気がします。
そこで、ラバーバンドを描く際には、線を背景と異なる色で描く代わりに、描こうとする線の部分の画面の画素の色を反転するようにします。こうすれば、もう一度同じ方法で線を描いて画面のこの線上の画素の色を反転して、そこに描かれていた線を消すことができます。
画面上の画素のこのような論理演算には、glLogicOp を使います。glLogicOp で指定した論理演算は、白黒の場合は、glEnable(GL_LOGIC_OP) で有効になり、カラーの場合は、glEnable(GL_COLOR_LOGIC_OP) で有効になります。
ただし、マウスのボタンを押した直後はまだラバーバンドは描かれていないので、そのときだけラバーバンドの消去はしないようにしなければなりません。このため rubberband などという変数を使ったちょっと泥臭いプログラムになっています。
glutMotionFunc, glutPassiveMotionFunc で指定した関数は、マウスの移動に伴って頻繁に実行されるので、この関数の中で時間のかかる処理を行うと、マウスの応答が悪くなってしまいます。
OpenGL のアプリケーションプログラムが開いたウィンドウには、ターミナルウィンドウのようにキーボード入力をすることはできません。その代わりマウスのボタンと同様に、キーをタイプする毎に実行する関数を指定できます。それには glutKeybordFunc を使います。
これまで作ったプログラムは、プログラムを終了する方法を組み込んでいませんでした。そこで「q」キーや「ESC」キーをタイプしたときに exit を呼び出して、プログラムが終了するようにします。
【ConsoleApplication1.cpp】 キーボードから読み込む(変更点のみ)
void keyboard(unsigned char key, int x, int y)
{
switch (key) {
case 'q':
case 'Q':
case '\033': // '\033'はESCのASCIIコード
exit(0);
default:
break;
}
}
int main(int argc, char* argv[])
{
glutInitWindowPosition(100, 100);
glutInitWindowSize(320, 240);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutCreateWindow(argv[0]);
glutDisplayFunc(display);
glutReshapeFunc(resize);
glutMouseFunc(mouse);
glutMotionFunc(motion);
glutKeyboardFunc(keyboard);
init();
glutMainLoop();
return 0;
}
[解説]
void glutKeyboardFunc(void (*callback)(unsigned char key, int x, int y));
引数 callback には、キーがタイプされたときに実行する関数のポインタを与えます。この関数の引数 key には、タイプされたキーの ASCIIコード が渡されます。また、x と y にはキーがタイプされたときのマウスの位置が渡されます。
ファンクションキーのような文字キー以外のタイプを検出するときは glutSpecialFunc を使い、Shift や Ctrl のようなモディファイア(修飾)キーを検出するには glutGetModifiers を使います。