3Dグラフィックスwith GLUT

これまでも3次元グラフィックスの変換をしたり、バックフェースカリングをおこなったり、2次元である画面に3次元物体を描画(投影変 換)してきましたが、それらはお手製の関数や投影変換を用いており、ほぼ手動でおこなっていました。

しかし、OpenGLやGLUTには、それらを手動でおこなわなくても手軽に実現できる便利な関数群が用意されています。今回はその使 い方を演習していきます。OpenGL、GLUT固有の関数が沢山出てきますので、個々の関数の意味や挙動をしっかりと掴み取ってくださ い。


描画するウインドウサイズの決定

いきなり3DCGと直接関係はないのですが、GLUTには開くウインドウのサイズと位置を指定する関数があります。今までは画面の左隅に正方 形のウインドウが表示されていましたが、例えば、画面左上から(100, 100)の位置に、(320, 240)の大きさのウインドウを表示したければ以下のように書きます。
#include<GL/glut.h>

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	glFlush();

}


int main(int argc, char *argv[])
{
	glutInit(&argc, argv);

	glutInitWindowPosition(100, 100); //ウインドウを表示する画面上の左上から(100, 100)の位置
	glutInitWindowSize(320, 240); //ウインドウのサイズ(320,240)

	glutInitDisplayMode(GLUT_RGBA);
	glutCreateWindow(argv[0]);
	glutDisplayFunc(display);
   	glClearColor(0.0, 0.0, 0.0, 0.0);
        glutMainLoop();
	return 0;
}

glutInitWindowPositionを指定しない場合は、デフォルトで glutInitWindowPosition(0,0)が、glutInitWindowSizeを指定しない場合、 暗黙的にglutInitWindowSize(300,300) が設定されます。ですから、今まではずっと左隅に正方形のウインドウが 表示されていたのです。


ビューポート変換

では、3DCGの解説に入っていきましょう。

ビジュアル情報論で学んだビューイングパイプラインを 思い出してください。モデルは、モデル座標系から、[a]モデリング変換M(ワール ド座標系へ)→[b]視野変換V(カメラ座標系へ)→[c]投影変換P(投影座標系へ)→[d]ビューポート変換U(ビューポートへ)の順番で変 換され、最終的には画面表示まで行われているのでした。

パイプラインの最後にあるのはビューポート変換で す。これは、いろいろと変換を重ねてきた結果に対して、実際に画面 に切り取る部分や表示する位置を設定するものでした。

OpenGLでは、上の図のビューイングパイプラインと若干異なるアーキテクチャなのですが、基本的には大体同じです。このビュー ポート変換は、OpenGLでは関数「glViewport」で実現できます。ただし、上記の図とは異なり、OpengGLではある領域 を切り取る作業はビューポート変換の役割ではなく、画面内で表示する位置(左上を原点としてx,y)、幅(width)、高さ (height)だけを指定することができます。

glViewport (GLint x, GLint y, GLsizei width, GLsizei height);

まず、どの部分が切り取られているのか判るように、正規化デバイ ス座標系内で比較的大きめの四角形を表示してビューポート変換をしてみます。ビューポート変換では特に3Dの情報を使 いませんから、判りやすいように2Dの四角を描画しています。

#include<GL/glut.h>

double vertices[][2]={//大きな四角形
	{0.9, 0.9},
	{-0.9, 0.9},
	{-0.9, -0.9},
	{0.9, -0.9}
};

void display(void)
{
	glViewport(0, 0, 320, 240); //ビューポート変換

	glClear(GL_COLOR_BUFFER_BIT);

	glColor3f(0.0, 1.0, 0.0);
	glBegin(GL_LINE_LOOP);
	for(int i=0; i < 4; i++)
		glVertex2d(vertices[i][0], vertices[i][1]);
	glEnd();

	glFlush();

}


int main(int argc, char *argv[])
{
	glutInit(&argc, argv);

	glutInitWindowPosition(100, 100); //ウインドウを表示する画面上の左上の位置
	glutInitWindowSize(320, 240); //ウインドウのサイズ

	glutInitDisplayMode(GLUT_RGBA);
	glutCreateWindow(argv[0]);
	glutDisplayFunc(display);
   	glClearColor(0.0, 0.0, 0.0, 0.0);
        glutMainLoop();
	return 0;
}

この時、

glViewport(100, 100, 320, 240);//左下からの表示位置が変更される

glViewport(0, 0, 320, 120);//元のウインドウに対して歪んだ形で表示される

などと変更してみて下さい。前の二つの引数を変更すると、左下からの表示位置が変更され、後ろの二つの引数で高さと幅を指定すると、元 のウインドウに対して歪んだ形で表示されます。

多くの場合、画面に対して歪めて表示させることに意味はありませ んから、画面サイズに合わせて表示させます。上記のプログラムでは画面の幅と高さは決め打ちで与えていましたから、 glViewportでも同じ値を入力すれば歪まずに表示できましたが、例えばマウスドラッグでウインドウのサイズを変えてしまうと整合 性がとれなくなってしまいます。

そこで、GLUTでは、ウインドウを一番はじめに描画する前、及 びウインドウの大きさを変えた時に呼び出されるコールバック関数を設定することができます。普通、ビューポート変換はここで設定します。

#include <iostream>
#include<GL/glut.h>

double vertices[][2]={//大きな四角形
	{0.9, 0.9},
	{-0.9, 0.9},
	{-0.9, -0.9},
	{0.9, -0.9}
};

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	glColor3f(0.0, 1.0, 0.0);
	glBegin(GL_LINE_LOOP);
	for(int i=0; i < 4; i++)
		glVertex2d(vertices[i][0], vertices[i][1]);
	glEnd();

	glFlush();

}

void resize(int w, int h) //w,h は現在のウインドウの幅と高さが代入される
{
	std::cout << w << ":" << h << std::endl; //幅と高さの確認
	glViewport(0, 0, w, h); //ウインドウ全体に表示

}


int main(int argc, char *argv[])
{
	glutInit(&argc, argv);

	glutInitWindowPosition(100, 100); //ウインドウを表示する画面上の左上の位置
	glutInitWindowSize(320, 240); //ウインドウのサイズ

	glutInitDisplayMode(GLUT_RGBA);
	glutCreateWindow(argv[0]);
	glutDisplayFunc(display);
	glutReshapeFunc(resize); //関数resizeをコールバックに設定
   	glClearColor(0.0, 0.0, 0.0, 0.0);
        glutMainLoop();
	return 0;
}

なお、ビューポート変換をなにも指定しない場合も glViewport(0, 0, w, h);が暗黙的に設定されます。ですから、今まで何も設定していなくても画 面いっぱいに出ていたのですね。


投影変換(何もしないが視野変換、モデリング変換)

教科書p.41の図2.43におけるビューポート変換(この変換行列をUとする)は、投影座標系の絵をビューポート(画面)に変換する 処理でし た。とい うわけで、次に投影座標系に絵を作りたいのですが、そのためには、モデリング座標系の物体をまずモデリング変換(M)してワールド座標系 へ、次に視野変換(V)してカメラ座標系へ、最後に投影変換(P)をして投影座標系にもってこなくてはいけません。

このように変換を重ねるのは各変換行列を合成(行列の積計算)していって、できあがった行列(積計算の結果)と座標値とを掛け合わせればよかったのでし た。OpenGLでは、ビューポート 変換と投影変換とそれ以外で異なる行列として保持することになっています。

具体的には、投影変換をおこなうときには glMatrixMode(GL_PROJECTION);という宣言を行い、投影変換モードに入る必要があります。 これは通常、ビューポート変換の下に書きます。(な お、ビューポート変換の時は特にモードを設定する必要はありません。)

glMatrixMode (GLenum mode);
void resize(int w, int h) 
{
	glViewport(0, 0, w, h); //ウインドウ全体に表示
	glMatrixMode(GL_PROJECTION); //投影変換モードへ

この宣言がおこなわれると、投影変換用の変換行列を受け入れるモードに入っているので 、まずglLoadIdentity();と いう関するをコールして変換行列を単位行列で初期化します。

void resize(int w, int h) 
{
	glViewport(0, 0, w, h); //ウインドウ全体に表示
	glMatrixMode(GL_PROJECTION); //投影変換モードへ
	glLoadIdentity(); //投影変換の変換行列を単位行列で初期化
}

次に、投影変換を設定するのですが、投影変換には平行投影と透視投影がありました。ここでは簡単のために平行投影を設定してみます。平 行投影にはglOrthoという関数を使い、引数で投影 変換をおこなう範囲を指定します。

glOrtho (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);
void resize(int w, int h) 
{
	glViewport(0, 0, w, h); //ウインドウ全体に表示
	glMatrixMode(GL_PROJECTION); //投影変換モードへ
	glLoadIdentity(); //投影変換の変換行列を単位行列で初期化
	glOrtho(-1.0, 1.0, -1.0, 1.0, 1.0, -1.0); //各軸-1.0~1.0で囲まれる立方体の範囲を平行投影
}

次に、視野変換とモデリング変換を設定します。視野変換とモデリング変換が纏められているのは、視点の移動と物体の移動は表裏一体で、 プログラム的にはなんの違いもないからです。

では視野変換(モデリング変換)の変換行列を設定するためには、glMatrixMode(GL_MODELVIEW);という命令をコールして、モード をさきほどの投影変換のモードから切り替えます。 また、投影変換モードの時と同じく単位行列で初期化もしておきます。

void resize(int w, int h) 
{
	glViewport(0, 0, w, h); //ウインドウ全体に表示
	glMatrixMode(GL_PROJECTION); //投影変換モードへ
	glLoadIdentity(); //投影変換の変換行列を単位行列で初期化
	glOrtho(-1.0, 1.0, -1.0, 0.0,1 .0, -1.0); //各軸-1.0~1.0で囲まれる立方体の範囲を並行投影

 glMatrixMode(GL_MODELVIEW); //視野変換・モデリング変換モードへ glLoadIdentity(); //視野変換・モデリング変換の変換行列を単位行列で初期化 }

これで準備は整いました。モデリング座標系に以下の図のような物体(正方形の枠)を作って、一歩も動かずにワールド座標系にそのまま置 いてみましょう。これでは、ローカル座標系とワールド座標系が一致していることになりますが、それは立派な「何もしない」。つまり、単位 行列を変換行列として掛けるというモデリング変換と同じものになります。

local
#include<GL/glut.h>


double vertices[][3]={//四角形
	{0.0, 0.5, 0.0},
	{0.0, 0.0, 0.0},
	{0.5, 0.0, 0.0},
	{0.5, 0.5, 0.0}
};

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	glColor3f(0.0, 1.0, 0.0);
	glBegin(GL_LINE_LOOP); //プリミティブの指定は2Dの時と同じ
	for(int i=0; i < 4; i++)
		glVertex3dv(vertices[i]); //2dではなく3d
	glEnd();

	glFlush();

}

void resize(int w, int h) 
{
	glViewport(0, 0, w, h); //ウインドウ全体に表示
	glMatrixMode(GL_PROJECTION); //投影変換モードへ
	glLoadIdentity(); //投影変換の変換行列を単位行列で初期化
	glOrtho(-1.0, 1.0, -1.0, 1.0, 1.0, -1.0); //各軸-1.0~1.0で囲まれる立方体の範囲を並行投影

 glMatrixMode(GL_MODELVIEW); //視野変換・モデリング変換モードへ glLoadIdentity(); //視野変換・モデリング変換の変換行列を単位行列で初期化 } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitWindowPosition(100, 100); //ウインドウを表示する画面上の左上の位置 glutInitWindowSize(320, 240); //ウインドウのサイズ glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); //関数resizeをコールバックに設定 glClearColor(0.0, 0.0, 0.0, 0.0); glutMainLoop(); return 0; }

無事表示がされたところで、glOrthoの値を変更するなど して挙動を確かめてみましょう


モデリング変換

上記の例では、モデリング座標系とワールド座標系は一致していたので、モデリング変換は実質行われていませんでした。ここでは、モデル を平行移動や回転をさせることでモデリング変換をおこなってみます。

まずglTranslatedという関数を使い平行移動からやってみましょう。この関数は引数のx,y,zにそれぞれ移動量を渡すと変 換行列に平行移動を掛ける関数です。

void glTranslated(GLdouble x, GLdouble y, GLdouble z);

ここでは、X軸方向に0.3、Y軸方向に-0.4動かしてみます。(なお、OpenGLでは視点はZ軸上のマイナス方向を見ていること になっています。)

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	//ここからモデリング変換
        glLoadIdentity(); //視野変換・モデリング変換の変換行列を単位行列で初期化
	glTranslated(0.3, -0.4, 0.0); //X軸方向に0.3、Y軸方向に-0.4の平行移動

	//ここからモデリング座標系
	glColor3f(0.0, 1.0, 0.0);
	glBegin(GL_LINE_LOOP); //プリミティブの指定は2Dの時と同じ
	for(int i=0; i < 4; i++)
		glVertex3dv(vertices[i]); //2dではなく3d
	glEnd();

	glFlush();
}

次に、回転させます。回転はglRotatedという関数を使います。これは引数angleに角度を指定し、回転させたい軸を x,y,zそれぞれに0.0から1.0の間で指定します。

void glRotated (GLdouble angle, GLdouble x, GLdouble y, GLdouble z);

例えば、X軸を回転の軸として60度回転させたい場合は以下のようにします。

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	//ここからモデリング変換
        glLoadIdentity(); //視野変換・モデリング変換の変換行列を単位行列で初期化
	glTranslated(0.3, -0.4, 0.0); //X軸方向に0.3、Y軸方向に-0.4の平行移動
	glRotated(60, 1.0, 0.0, 0.0); //X軸周り60度の回転
    
	//ここからモデリング座標系
	glColor3f(0.0, 1.0, 0.0);
	glBegin(GL_LINE_LOOP); //プリミティブの指定は2Dの時と同じ
	for(int i=0; i < 4; i++)
		glVertex3dv(vertices[i]); //2dではなく3d
	glEnd();

	glFlush();
}

上記のようなサンプルではわかりにくいので、キーボードで動かす ようにしてみましょう。

#include<GL/glut.h>

double vertices[][3]={//四角形
	{0.0, 0.5, 0.0},
	{0.0, 0.0, 0.0},
	{0.5, 0.0, 0.0},
	{0.5, 0.5, 0.0}
};

double tx = 0.0;
double ty = 0.0;
double tz = 0.0;
double rx = 0.0;
double ry = 0.0;
double rz = 0.0;

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	//ここからモデリング変換
	glLoadIdentity(); //視野変換・モデリング変換の変換行列を単位行列で初期化
	glTranslated(tx, ty, tz); //平行移動
	glRotated(rx, 1.0, 0.0, 0.0); //X軸回転
	glRotated(ry, 0.0, 1.0, 0.0); //Y軸回転
	glRotated(rz, 0.0, 0.0, 1.0); //Z軸回転

	//ここからモデリング座標系
	glColor3f(0.0, 1.0, 0.0);
	glBegin(GL_LINE_LOOP); //プリミティブの指定は2Dの時と同じ
	for(int i=0; i < 4; i++)
		glVertex3dv(vertices[i]); //2dではなく3d
	glEnd();

	glFlush();
}

void resize(int w, int h) 
{
	glViewport(0, 0, w, h); //ウインドウ全体に表示
	glMatrixMode(GL_PROJECTION); //投影変換モードへ
	glLoadIdentity(); //投影変換の変換行列を単位行列で初期化
	glOrtho(-1.0, 1.0, -1.0, 1.0, 1.0, -1.0); //各軸-1.0~1.0で囲まれる立方体の範囲を並行投影

glMatrixMode(GL_MODELVIEW); //視野変換・モデリング変換モードへ glLoadIdentity(); //視野変換・モデリング変換の変換行列を単位行列で初期化 } void keyboard(unsigned char key, int x, int y) { switch (key) { case '4': tx -= 0.01; break; case '6': tx += 0.01; break; case '2': ty -= 0.01; break; case '8': ty += 0.01; break; case '9': tz -= 0.01; break; case '1': tz += 0.01; break; case 'x': rx += 2.0; break; case 'y': ry += 2.0; break; case 'z': rz += 2.0; break; default: break; } glutPostRedisplay(); } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitWindowPosition(100, 100); //ウインドウを表示する画面上の左上の位置 glutInitWindowSize(320, 240); //ウインドウのサイズ glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); //関数resizeをコールバックに設定 glutKeyboardFunc(keyboard); glClearColor(0.0, 0.0, 0.0, 0.0); glutMainLoop(); return 0; }

透視投影(と視野変換)

今のサンプルではZ軸方向の動きが判らないので、透視投影にしてみましょう。glOrthoの代わりに以下の関数をコールします。引数 は自由に決められるのですが、多少ややこしいので、今後は以下のものを使ってください。

void gluPerspective (GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
	gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0); //視点から1.0-100.0 Z軸方向離れた区間を画角30度でクリッピング

これを実行すると今まで見えていた四角形が見えなくなったと思います。平行投影では、視点と物体までの距離に何の意味もありませんでした が、透視投影では意味をなします。上記のような設定をした場合は、物体は視点から少なくとも1.0は離れなくてはいけません。よって、物 体をglTranslatedを用いて離しましょう。

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	glLoadIdentity(); //視野変換・モデリング変換の変換行列を単位行列で初期化

	//ここから視野変換
	glTranslated(0.0, 0.0, -2.0); //物体をZ向きに離す

	//ここからモデリング変換
	glTranslated(tx, ty, tz); //平行移動
	glRotated(rx, 1.0, 0.0, 0.0); //X軸回転
	glRotated(ry, 0.0, 1.0, 0.0); //Y軸回転
	glRotated(rz, 0.0, 0.0, 1.0); //Z軸回転

	//ここからモデリング座標系
	glColor3f(0.0, 1.0, 0.0);
	glBegin(GL_LINE_LOOP); //プリミティブの指定は2Dの時と同じ
	for(int i=0; i < 4; i++)
		glVertex3dv(vertices[i]); //2dではなく3d
	glEnd();

	glFlush();
}

このように物体を平行移動や回転で視点から移動させる作業が、本質的な視野変換がやる作業です。よって、上記のプログラムでは、 glTranslated(0.0, 0.0, -2.0);の行は視野変換を担っているといえます。

モデルが一つだとモデリング変換も一つであり、視野変換と区別がつかないですから、もう一つモデルを用意してみましょう。

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT);

	glLoadIdentity(); //視野変換・モデリング変換の変換行列を単位行列で初期化

	//ここから視野変換
	glTranslated(0.0, 0.0, -2.0); //物体全部をZ向きに離す

	//ここからモデル1のモデリング変換(動かず)
	//ここからモデル1のモデリング座標系
	glColor3f(0.0, 0.0, 1.0);
	glBegin(GL_LINE_LOOP); 
	for(int i=0; i < 4; i++)
		glVertex3dv(vertices[i]); 
	glEnd();

	//ここからモデル2のモデリング変換
	glTranslated(tx, ty, tz); //平行移動
	glRotated(rx, 1.0, 0.0, 0.0); //X軸回転
	glRotated(ry, 0.0, 1.0, 0.0); //Y軸回転
	glRotated(rz, 0.0, 0.0, 1.0); //Z軸回転

	//ここからモデル2のモデリング座標系
	glColor3f(0.0, 1.0, 0.0);
	glBegin(GL_LINE_LOOP); 
	for(int i=0; i < 4; i++)
		glVertex3dv(vertices[i]); 
	glEnd();


	glFlush();
}

視野変換はワールド座標系すべての物体に対して平等になされるのに対して、モ デリング変換は個々のモデルに対しておこなわれていること が判ると思います。 (教科書p.41の図2.43の一番右側参照)