これまでは2次元の図形の表示を行ってきましたが、OpenGL の内部では実際には3次元空間の xy 平面への平行投影像を表示していました。
試しに「図形を塗りつぶす」で作ったプログラムにおいて、図形を y 軸中心に25度回転してみましょう。
【ConsoleApplication1.cpp】 2次元と3次元(不要行削除)
#include <iostream>
#include <GL/glut.h>
// この行から 削除
#define MAXPOINTS 100 // 記憶する点の数 削除
GLint point[MAXPOINTS][2]; // 座標を記憶する配列 削除
int pointnum = 0; // 記憶した座標の数 削除
int rubberband = 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 init()
{
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
void resize(int w, int h) // この行から 削除
{ // 削除 関数内は省略
} // 削除
// 削除
void mouse(int button, int state, int x, int y) // 削除
{ // 削除 関数内は省略
} // 削除
// 削除
void motion(int x, int y) // 削除
{ // 削除 関数内は省略
} // 削除
// 削除
void keyboard(unsigned char key, int x, int y) // 削除
{ // 削除 関数内は省略
} // 削除
// この行まで 削除
int main(int argc, char* argv[])
{
glutInitWindowPosition(100, 100); // 削除
glutInitWindowSize(320, 240); // 削除
glutInit(&argc, argv);
glutCreateWindow(argv[0]);
glutDisplayFunc(display);
glutReshapeFunc(resize); // この行から 削除
glutMouseFunc(mouse); // 削除
glutMotionFunc(motion); // 削除
glutKeyboardFunc(keyboard); // この行まで 削除
init();
glutMainLoop();
return 0;
}
【ConsoleApplication1.cpp】 2次元と3次元
#include <iostream>
#include <GL/glut.h>
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
glRotated(25.0, 0.0, 1.0, 0.0);
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 init()
{
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
int main(int argc, char* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutCreateWindow(argv[0]);
glutDisplayFunc(display);
init();
glutMainLoop();
return 0;
}
実行結果)
[解説]
void glRotated(GLdouble angle, GLdouble x, GLdouble y, GLdouble z)
変換行列に回転の行列を乗じます。引数はいずれも GLdoube 型で、1つ目の引数 angle は回転角、残りの3つの引数 x, y, z は回転軸の方向ベクトルです。
原点を通らない軸で回転させたい場合は、 glTranslated を使っていったん軸が原点を通るように図形を移動し、回転後に元の位置に戻します。
プログラムを実行して描かれている図形を見てみると、Y軸中心に回転したものを正面から見ているため、少し縦長になっていると思います。このウィンドウを最小化したり他のウィンドウを重ねたりしてみると、再描画するたびに図形の形が変わると思います。これは変換行列に glRotated による回転の行列が積算されるからです。
これを防ぐには描画のたびに変換マトリクスを glLoadIdentity で初期化するか、glPushMatrix, glPopMatrix を使って変換行列を保存します。
GLdouble vertex[][3] = {
{ 0.0, 0.0, 0.0 }, // A
{ 1.0, 0.0, 0.0 }, // B
{ 1.0, 1.0, 0.0 }, // C
{ 0.0, 1.0, 0.0 }, // D
{ 0.0, 0.0, 1.0 }, // E
{ 1.0, 0.0, 1.0 }, // F
{ 1.0, 1.0, 1.0 }, // G
{ 0.0, 1.0, 1.0 } // H
};
int edge[][2] = {
{ 0, 1 }, // ア(A-B)
{ 1, 2 }, // イ(B-C)
{ 2, 3 }, // ウ(C-D)
{ 3, 0 }, // エ(D-A)
{ 4, 5 }, // オ(E-F)
{ 5, 6 }, // カ(F-G)
{ 6, 7 }, // キ(G-H)
{ 7, 4 }, // ク(H-E)
{ 0, 4 }, // ケ(A-E)
{ 1, 5 }, // コ(B-F)
{ 2, 6 }, // サ(C-G)
{ 3, 7 } // シ(D-H)
};
この場合、たとえば「点C」(1, 1, 0) と「点D」(0 , 1, 0) を結ぶ線分「ウ」は、以下のようにして描画できます。
glVertex3dv は、引数に3つの要素を持つ GLdouble 型の配列のポインタを与えて、頂点を作成します。
glBegin(GL_LINES);
glVertex3dv(vertex[edge[2][0]]); // 線分ウの1つ目の端点C
glVertex3dv(vertex[edge[2][1]]); // 線分ウの2つ目の端点D
glEnd();
従って、立方体全部を描くプログラムは以下のようになります。立方体がウィンドウからはみ出さないように、glOrtho で表示する座標系を (-2, -2)~(2, 2) にしています。
【ConsoleApplication1.cpp】 線画を表示する(不要行削除)
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
glRotated(25.0, 0.0, 1.0, 0.0); // この行から 削除
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();
}
【ConsoleApplication1.cpp】 線画を表示する
#include <iostream>
#include <GL/glut.h>
GLdouble vertex[][3] = {
{ 0.0, 0.0, 0.0 }, // A
{ 1.0, 0.0, 0.0 }, // B
{ 1.0, 1.0, 0.0 }, // C
{ 0.0, 1.0, 0.0 }, // D
{ 0.0, 0.0, 1.0 }, // E
{ 1.0, 0.0, 1.0 }, // F
{ 1.0, 1.0, 1.0 }, // G
{ 0.0, 1.0, 1.0 } // H
};
int edge[][2] = {
{ 0, 1 }, // ア(A-B)
{ 1, 2 }, // イ(B-C)
{ 2, 3 }, // ウ(C-D)
{ 3, 0 }, // エ(D-A)
{ 4, 5 }, // オ(E-F)
{ 5, 6 }, // カ(F-G)
{ 6, 7 }, // キ(G-H)
{ 7, 4 }, // ク(H-E)
{ 0, 4 }, // ケ(A-E)
{ 1, 5 }, // コ(B-F)
{ 2, 6 }, // サ(C-G)
{ 3, 7 } // シ(D-H)
};
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
// 図形の描画
glColor3d(0.0, 0.0, 0.0);
glBegin(GL_LINES);
for (int i = 0; i < 12; ++i) {
glVertex3dv(vertex[edge[i][0]]);
glVertex3dv(vertex[edge[i][1]]);
}
glEnd();
glFlush();
}
void init()
{
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
void resize(int w, int h)
{
glViewport(0, 0, w, h);
glLoadIdentity();
glOrtho(-2.0, 2.0, -2.0, 2.0, -2.0, 2.0);
}
int main(int argc, char* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutCreateWindow(argv[0]);
glutDisplayFunc(display);
glutReshapeFunc(resize);
init();
glutMainLoop();
return 0;
}
実行結果)
[解説]
void glVertex3dv(const GLdouble *v)
3次元の座標値を指定します。引数 v は3個の要素を持つ GLdouble 型配列を指定します。v[0] には x 座標値、v[1] には y 座標値、v[2] には z 座標値を格納します。
先ほどのプログラムでは立方体が画面に平行投影されるため、正方形しか見えていませんでした。そこで、現実のカメラのように透視投影をしてみます。これには glOrtho の代わりに gluPerspecrive を使います。gluPerspecrive は、座標軸の代わりに、カメラの画角やスクリーンのアスペクト比(縦横比)を用いて表示領域を指定します。また、glOrtho と同様に、前方向や後方向の位置の指定も行います。
視点の位置の初期値は原点なので、このままでは立方体が視点に重なってしまいます。そこで、glTranslated を使って立体の位置を少し奥にずらしておきます。
【ConsoleApplication1.cpp】 透視投影する(不要行削除)
void resize(int w, int h)
{
glViewport(0, 0, w, h);
glLoadIdentity();
glOrtho(-2.0, 2.0, -2.0, 2.0, -2.0, 2.0); // 削除
}
【ConsoleApplication1.cpp】 透視投影する(変更点のみ)
void resize(int w, int h)
{
glViewport(0, 0, w, h);
glLoadIdentity();
gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0);
glTranslated(0.0, 0.0, -5.0);
}
実行結果)
[解説]
void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar)
変換行列に透視変換の行列を乗じます。最初の引数 fovy はカメラの画角である、度で表します。これが大きいほどワイドレンズ(透視が強くなり絵が小さくなります)になり、小さいほど望遠レンズになります。2つ目の引数 aspect はアスペクト比(縦横比)であり、1 であればビューポートに表示される図形の x 方向と y 方向のスケールが等しくなります。3つ目の引数 zNear と 4つ目の引数 zFar は表示する奥行き方向の範囲で、zNear は手前(前方面)、zFar は後方(後方面)の位置を示します。この範囲にある図形が描画されます。
void glTranslated(GLdouble x, GLdouble y, GLdouble z)
変換行列に平行移動の行列を乗じます。引数はいずれも GLdouble 型で、3つの引数 x, y, z には現在の位置からの相対的な移動量を指定します。
ウィンドウをリサイズしたときに表示図形がゆがまないようにするためには、 gluPerspective で設定するアスペクト比 aspect を、glVewPort で指定したビューポートの縦横比と一致させます。
先ほどのプログラムのように、リサイズ後のウィンドウのサイズをそのままビューポートに指定している場合には、仮に aspect が定数であれば、ウィンドウのリサイズに伴って表示図形が伸縮するようになります。したがって、ウィンドウをリサイズしても表示図形の縦横比が変わらないようにするため、ここでは aspect をビューポートの縦横比に設定しています。
先ほどのプログラムのように視点の位置を移動するには、図形の方を glTranslated や glRotated を用いて逆方向に移動することで実現します。しかし、視点を任意の位置に指定したいときは glLookAt を使うと便利です。
【ConsoleApplication1.cpp】 視点の位置を変更する(不要行削除)
void resize(int w, int h)
{
glViewport(0, 0, w, h);
glLoadIdentity();
gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0);
glTranslated(0.0, 0.0, -5.0); // 削除
}
【ConsoleApplication1.cpp】 視点の位置を変更する(変更点のみ)
void resize(int w, int h)
{
glViewport(0, 0, w, h);
glLoadIdentity();
gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0);
gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
}
実行結果)
[解説]
void gluLookAt( GLdouble eyex, GLdouble eyey, GLdouble eyez,
GLdouble centerx, GLdouble centery, GLdouble centerz,
GLdouble upx, GLdouble upy, GLdouble upz)
この最初の3つの引数 eyex, eyey, eyez は視点の位置、次の3つの引数 cx, cy, cz は目標の位置、最後の3つの引数 upx, upy, upz はウィンドウに表示される画像の上方向を示すベクトルです。
先ほどのプログラムでは (3, 4, 5) の位置から原点 (0, 0, 0) を眺めますから、立方体の A (0, 0, 0) の頂点がウィンドウの中心にくると思います。
3次元空間上で立方体を線画できるようになったので、今度は立方体を面を使って描画してみましょう。線画と同じ様に点の位置(幾何情報)と面(位相情報)を別々のデータにして、6面のデータを追加します。そして、面を作成した順に赤, 緑, 青, 赤, 緑, 青と色を付けてみます。
変更の前に edge は使用しないので削除しておきます。
【ConsoleApplication1.cpp】 面を描画する(不要変数削除)
int edge[][2] = { // この行から 削除
{ 0, 1 }, // ア(A-B) 削除
{ 1, 2 }, // イ(B-C) 削除
{ 2, 3 }, // ウ(C-D) 削除
{ 3, 0 }, // エ(D-A) 削除
{ 4, 5 }, // オ(E-F) 削除
{ 5, 6 }, // カ(F-G) 削除
{ 6, 7 }, // キ(G-H) 削除
{ 7, 4 }, // ク(H-E) 削除
{ 0, 4 }, // ケ(A-E) 削除
{ 1, 5 }, // コ(B-F) 削除
{ 2, 6 }, // サ(C-G) 削除
{ 3, 7 } // シ(D-H) この行まで 削除
};
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
// 図形の描画
glColor3d(0.0, 0.0, 0.0); // この行から 削除
glBegin(GL_LINES); // 削除
for (int i = 0; i < 12; ++i) { // 削除
glVertex3dv(vertex[edge[i][0]]); // 削除
glVertex3dv(vertex[edge[i][1]]); // 削除
} // この行まで 削除
glEnd();
glFlush();
}
【ConsoleApplication1.cpp】 面を描画する(変更点のみ)
GLdouble vertex[][3] = {
// 変更なしなので省略
};
int quad[][4] = {
{ 0, 4, 5, 1 }, // A, E, F, B
{ 3, 2, 6, 7 }, // D, C, G, H
{ 0, 1, 2, 3 }, // A, B, C, D
{ 4, 7, 6, 5 }, // E, H, G, F
{ 0, 3, 7, 4 }, // A, D, H, E
{ 1, 5, 6, 2 } // B, F, G, C
};
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
// 図形の描画
glBegin(GL_QUADS);
for (int i = 0; i < 6; ++i) {
double r = (i % 3 == 0) ? 1.0 : 0.0;
double g = (i % 3 == 1) ? 1.0 : 0.0;
double b = (i % 3 == 2) ? 1.0 : 0.0;
glColor3d(r, g, b);
for (int j = 0; j < 4; ++j) {
glVertex3dv(vertex[quad[i][j]]);
}
}
glEnd();
glFlush();
}
実行結果)
輪郭は立方体っぽいが、騙し絵のような表示となっています。
先ほどの立方体ですが、面は描画した順番通りに表示されています。視点からは遠くにあり近くの面の後ろに隠れて見えないはずなのに後から描画されたので見えてしまっている状態です。そこでデプスバッファを設定し、陰面消去を行うことで面の重なった部分では視点の遠くにある面は視点の近くにある面に隠れて表示されるようになります。
【ConsoleApplication1.cpp】 陰面消去する(変更点のみ)
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 図形の描画
// 変更なしなので省略
}
void init()
{
glEnable(GL_DEPTH_TEST);
glClearColor(1.0, 1.0, 1.0, 1.0);
}
実行結果)
[解説]
void glClear(GLbitfield mask)
GL_DEPTH_BUFFER_BIT は、オブジェクトを描画する前に深度バッファをクリアします。
void glEnable(GLenum cap)
GL_DEPTH_TEST を有効にした場合、深度比較を行い深度バッファを更新します。
glColor3d を使って面に色を設定すると面全体が均一に塗られます。簡単な形状であれば面ごとに異なる色を設定するなどすれば、そのモデルがどのような形状をしているか判断できますが、面数の多いモデルではそうはいきません。そういった場合は、光源と材質を設定すると陰影のついた表示ができるようになります。
【ConsoleApplication1.cpp】 光源と材質を設定する(変更点のみ)
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 図形の描画
glBegin(GL_QUADS);
for (int i = 0; i < 6; ++i) {
double r = (i % 3 == 0) ? 1.0 : 0.0; // この行から 削除
double g = (i % 3 == 1) ? 1.0 : 0.0; // 削除
double b = (i % 3 == 2) ? 1.0 : 0.0; // 削除
glColor3d(r, g, b); // この行まで 削除
for (int j = 0; j < 4; ++j) {
glVertex3dv(vertex[quad[i][j]]);
}
}
glEnd();
glFlush();
}
【ConsoleApplication1.cpp】 光源と材質を設定する(変更点のみ)
int quad[][4] = {
// 変更なしなので省略
};
GLdouble normal[][3] = {
{ 0.0, -1.0, 0.0 }, // A, E, F, B
{ 0.0, 1.0, 0.0 }, // D, C, G, H
{ 0.0, 0.0, -1.0 }, // A, B, C, D
{ 0.0, 0.0, 1.0 }, // E, H, G, F
{ -1.0, 0.0, 0.0 }, // A, D, H, E
{ 1.0, 0.0, 0.0 } // B, F, G, C
};
GLfloat position[][4] = {
{ 3.0f, 3.0f, 3.0f, 1.0f }, // GL_LIGHT0
{ 5.0f, 3.0f, 0.0f, 1.0f } // GL_LIGHT1
};
GLfloat light[][4] = {
{ 0.3f, 0.3f, 0.3f, 1.0f }, // GL_AMBIENT
{ 0.9f, 0.9f, 0.9f, 1.0f } // GL_DIFFUSE
};
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 光源設定
for (int i = 0; i < 2; ++i) {
glLightfv(GL_LIGHT0 + i, GL_POSITION, position[i]);
for (int j = 0; j < 2; ++j) {
glLightfv(GL_LIGHT0 + i, GL_AMBIENT + j, light[j]);
}
}
// 図形の描画
glBegin(GL_QUADS);
for (int i = 0; i < 6; ++i) {
// 材質設定
float r = 192.0f / 255;
float g = 192.0f / 255;
float b = 192.0f / 255;
GLfloat material[][4] = {
{ r * 0.3f, g * 0.3f, b * 0.3f, 1.0f }, // GL_AMBIENT
{ r * 0.6f, g * 0.6f, b * 0.6f, 1.0f } // GL_DIFFUSE
};
for (int j = 0; j < 2; ++j) {
glMaterialfv(GL_FRONT, GL_AMBIENT + j, material[j]);
}
// 四角形の法線方向と頂点設定
glNormal3dv(normal[i]);
for (int j = 0; j < 4; ++j) {
glVertex3dv(vertex[quad[i][j]]);
}
}
glEnd();
glFlush();
}
void init()
{
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_LIGHT1);
glClearColor(1.0, 1.0, 1.0, 1.0);
}
実行結果)
6面とも灰色になるように設定しましたが、光の当たる部分は明るく、当たらない部分は暗く表示されるようになります。
[解説]
void glLightfv(GLenum light, GLenum pname, const GLfloat *params)
光源パラメータを設定します。light にどの光源(GL_LIGHT0~7)のパラメータを設定するかを指定します。pname に環境光(GL_AMBIENT), 拡散光(GL_DIFFUSE), 位置(GL_POSITION)など、どのパラメータを設定するかを指定します。params に環境光, 拡散光なら光のRGBA強度、位置なら光の位置XYZWなど4つの浮動小数点値を渡します(params はパラメータの種類によって値の個数や範囲が変わります)。
void glMaterialfv(GLenum face, GLenum pname, const GLfloat *params)
マテリアルパラメータを指定します。face にパラメータを面の表側(GL_FRONT), 裏側(GL_BACK), 両側(GL_FRONT_AND_BACK)のどこに設定するかを指定します。pname
に環境光(GL_AMBIENT), 拡散光(GL_DIFFUSE), 位置(GL_POSITION)など、どのパラメータを設定するかを指定します。params に環境光, 拡散光なら光のRGBA強度、位置なら光の位置XYZWなど4つの浮動小数点値を渡します(params はパラメータの種類によって値の個数や範囲が変わります)。
void glNormal3dv(const GLdouble *v)
面の法線方向を設定します。面には裏表があり、法線方向から見える側が表になります。指定しない場合は3頂点が時計回りに並ぶように見える側が表になります。