最近読んでいる「C/C++へのLua組み込み実践」はSDLとOpenGLで作った仕組みをLuaから叩こうという内容なのですが、昔触ってたはずのOpenGLを綺麗さっぱり忘れていたので、少しまとめておきます。手元の環境はSnow Leopardです。
最初にOpenGLとGLUとGLUTの関連性についてです。汎用性を優先した結果、OpenGLはプリミティブなAPIしか提供していません。少し触った感じ3D空間上に直線的な図形を書くので精一杯という感じです。曲線やカメラ視点を扱う事すら、複数のAPIを組み合わせて頑張って実現する必要があります。そこでGLU(OpenGL Utility Library)というやや高レベルなAPIを提供してくれている補助ライブラリがあります。たいていの場合、標準で添付されているようで実質的にはOpenGLとGLUをまとめて扱っているケースが多いようです。
さてGLUでやや高レベルなAPIが使用できると言っても実際のアプリケーションを書くには全然APIが足りません。プラットフォームに依存したウィンドウやイベントなどの処理はOpenGL, GLUでは行う事ができないわけです。冒頭の「C/C++へのLua組み込み実践」がわざわざSDLを利用しているのはこの為で、OpenGLでは処理できないプラットフォームに依存した処理をさせています。ウィンドウやらイベントやらを扱えないとなると、Hello Worldなコード書こうにも、OpenGL以外の部分で学習コストが発生して大変です。そこでGLUT(OpenGL Utility Toolkit)があります。大雑把にいうとOpenGL用の簡易ウィンドウシステムなのですが、X WindowやOSX等、大抵のプラットフォームに対応しているので、このライブラリを使えばウィンドウシステムも含めた、完全なアプリケーションとしてクロスプラットフォームなソースコード互換を保てる事になります。手元のSnow Leopardの場合は標準でインストールされていました。
なのでデスクトップ環境で動作するアプリケーションを書くのであれば、GLUTまで含めたライブラリを使用しても互換性にはほぼ影響しないと考えて良さそうです。ただし iPhoneやAndroidで採用されている、モバイル端末用のOpenGLのサブセットなOpenGL ESの場合は現在のところGLUやGLUTは使用できません。OpenGL ESまで含めてのソースコード互換を考慮する場合は、注意する必要があります。蛇足ですが、標準OpenGL, GLU, GLUTが提供しているAPIは、それぞれgl, glu、glutと関数名にプリフィックスがつきます。
前置きが長くなりました。本エントリでは特にOpenGL ESは意識せず、GLU、GLUTを含めてコードを書こうと思います。
とりあえず2次元で四角を書いてみます。コードの雰囲気はつかめるかと思います。要はglBeginとglEndの間で色々書くという事です。点を1つずつ置いていって、その点が何を表すかをGL_LINE_LOOP等で指定します。四角1つ書くにも大変ですね。
#include "glut /glut.h"
void display(void) {
glClear(GL_COLOR_BUFFER_BIT);
glColor3d(0.0, 0.0, 0.0);
glBegin(GL_LINE_LOOP);
glVertex2d(-0.5, -0.5);
glVertex2d(-0.5, 0.5);
glVertex2d(0.5, 0.5);
glVertex2d(0.5, -0.5);
glEnd();
glFlush();
}
int main(int argc, char *argv[]) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutCreateWindow("square");
glutDisplayFunc(display);
glClearColor(1.0, 1.0, 1.0, 1.0);
glutMainLoop();
return 0;
}
なおこのコードのコンパイルはosxの場合は以下のように行います。
$ g++ main.cpp -framework GLUT -framework OpenGL
実行結果です。無事に四角が描画されていますね。
さて、2次元の四角では面白く無いという事で、立方体を書いてみます。3次元な処理をする場合は、glMatrixModeにGL_PROJECTIONを指定して射影変換行列の設定をする必要があります。これを容易に行う為に使用するgluPerspectiveの各引数は正数しかとらないのですが、ここに0を指定してしまいハマりました。注意しましょう。
#include "glut/glut.h"
void display(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glLoadIdentity();
gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glColor3d(0.0, 0.0, 0.0);
static GLfloat cube_points[][4]={
{ 1.0, 1.0, 1.0},
{ 0.0, 1.0, 1.0},
{ 0.0, 0.0, 1.0},
{ 1.0, 0.0, 1.0},
{ 1.0, 1.0, 0.0},
{ 0.0, 1.0, 0.0},
{ 0.0, 0.0, 0.0},
{ 1.0, 0.0, 0.0},
};
glBegin(GL_LINE_LOOP);
glVertex3fv(cube_points[0]);
glVertex3fv(cube_points[1]);
glVertex3fv(cube_points[2]);
glVertex3fv(cube_points[3]);
glEnd();
glBegin(GL_LINE_LOOP);
glVertex3fv(cube_points[4]);
glVertex3fv(cube_points[5]);
glVertex3fv(cube_points[6]);
glVertex3fv(cube_points[7]);
glEnd();
glBegin(GL_LINE_LOOP);
glVertex3fv(cube_points[0]);
glVertex3fv(cube_points[1]);
glVertex3fv(cube_points[5]);
glVertex3fv(cube_points[4]);
glEnd();
glBegin(GL_LINE_LOOP);
glVertex3fv(cube_points[2]);
glVertex3fv(cube_points[3]);
glVertex3fv(cube_points[7]);
glVertex3fv(cube_points[6]);
glEnd();
glBegin(GL_LINE_LOOP);
glVertex3fv(cube_points[3]);
glVertex3fv(cube_points[0]);
glVertex3fv(cube_points[4]);
glVertex3fv(cube_points[7]);
glEnd();
glBegin(GL_LINE_LOOP);
glVertex3fv(cube_points[1]);
glVertex3fv(cube_points[2]);
glVertex3fv(cube_points[6]);
glVertex3fv(cube_points[5]);
glEnd();
glFlush();
}
void resize(int w, int h) {
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, double(w)/double(h), 0.1, 100.0);
glMatrixMode(GL_MODELVIEW);
}
int main(int argc, char *argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutCreateWindow("cube");
glutDisplayFunc(display);
glutReshapeFunc(resize);
glClearColor(1.0, 1.0, 1.0, 1.0);
glutMainLoop();
return 0;
}
次に立方体にテクスチャを張ってみます。理想を言えばファイル名渡したらテクスチャオブジェクトが返ってくる位のAPIがあると嬉しいですが、実際はそういったAPIは全くなく、画像ファイルからピクセルデータを抜き出して、それをテクスチャとして登録という処理をする必要があります。画像ファイルからピクセルデータ抜き出すなんて処理はただでさえ面倒なのですが、クロスプラットフォームを考えるとさらに厄介で、ちょっとテクスチャ試したいだけで、libpngインストールすんの?みたいな事になります。
何とかならんのかと思って調べていると、コチラのサイトにビンゴな記事がありまして、今まで知らなかったのですがraw形式の画像を使えば単にファイル読むだけでピクセルデータが得られます。またアルファチャンネルにも対応しているので、実際のアプリケーションでは使えないという事もありません。素晴らしいです。生の画像データなのでファイルサイズは大きくなりますが、今時デスクトップでファイルサイズなんて気にする事でもないでしょう。
#include "glut/glut.h"
#include "stdio.h"
const int TEXWIDTH = 64;
const int TEXHEIGHT = 64;
static GLuint textureID1;
static GLuint textureID2;
void draw_cube()
{
static GLfloat vert[][4]={
{ 1.0, 1.0, 1.0},
{ 0.0, 1.0, 1.0},
{ 0.0, 0.0, 1.0},
{ 1.0, 0.0, 1.0},
{ 1.0, 1.0, 0.0},
{ 0.0, 1.0, 0.0},
{ 0.0, 0.0, 0.0},
{ 1.0, 0.0, 0.0},
};
glColor3d(0.7, 0.7, 0.7);
glBindTexture(GL_TEXTURE_2D , textureID1);
glBegin(GL_QUADS);
{
glTexCoord2d(1, 0);
glVertex3fv(vert[0]);
glTexCoord2d(0, 0);
glVertex3fv(vert[1]);
glTexCoord2d(0, 1);
glVertex3fv(vert[2]);
glTexCoord2d(1, 1);
glVertex3fv(vert[3]);
}
glEnd();
glBindTexture(GL_TEXTURE_2D , textureID2);
glBegin(GL_QUADS);
{
glVertex3fv(vert[4]);
glVertex3fv(vert[5]);
glVertex3fv(vert[6]);
glVertex3fv(vert[7]);
glTexCoord2d(1, 0);
glVertex3fv(vert[0]);
glTexCoord2d(0, 0);
glVertex3fv(vert[1]);
glTexCoord2d(0, 1);
glVertex3fv(vert[5]);
glTexCoord2d(1, 1);
glVertex3fv(vert[4]);
glVertex3fv(vert[2]);
glVertex3fv(vert[3]);
glVertex3fv(vert[7]);
glVertex3fv(vert[6]);
glVertex3fv(vert[3]);
glVertex3fv(vert[0]);
glVertex3fv(vert[4]);
glVertex3fv(vert[7]);
glVertex3fv(vert[1]);
glVertex3fv(vert[2]);
glVertex3fv(vert[6]);
glVertex3fv(vert[5]);
}
glEnd();
}
void display() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
gluLookAt(1.0, 2.0, 3.0,
0.0, 0.0, 0.0,
0.0, 1.0, 0.0);
draw_cube();
glFlush();
}
void reshape(int w, int h) {
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, double(w)/double(h), 0.1, 100.0);
glMatrixMode(GL_MODELVIEW);
}
GLuint loadTexture(const char *filename) {
GLuint texID;
GLubyte texture[TEXHEIGHT][TEXWIDTH][4];
FILE *fp = NULL;
if ((fp = fopen(filename, "rb")) != NULL) {
fread(texture, sizeof texture, 1, fp);
fclose(fp);
}
else {
perror(filename);
}
glEnable(GL_TEXTURE_2D);
glGenTextures(1 , &texID);
glBindTexture(GL_TEXTURE_2D , texID);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
GL_RGBA, GL_UNSIGNED_BYTE, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
return texID;
}
void init() {
glClearColor(1.0, 1.0, 1.0, 1.0);
glEnable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
textureID1 = loadTexture("1.raw");
textureID2 = loadTexture("2.raw");
}
int main(int argc, char **argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
glutCreateWindow("Texture Test");
glutDisplayFunc(display);
glutReshapeFunc(reshape);
init();
glutMainLoop();
return 0;
}
なお有効なテクスチャを設定するglBindTextureは、glBeginとglEndの間で指定するとうまく動きません。僕はかなり長時間ハマりました。注意しましょう。
静止オブジェクトが作れるようになってきたので、次はカメラをいじりたいです。先ほど書いたオブジェクトの周りをグルグル歩く感じのコードが以下になります。実際はカメラオブジェクトみたいなものは存在せず、カメラの位置までモデルビュー座標系を移動させてから、各オブジェクトを描画しているだけです。慣れるまではX,Y,Zの各座標軸を描画しておくと何かと理解が進みやすい気がします。以下のコードでは矢印キーが押されて視点が移動するたびに、glutPostRedisplayで再描画要求を投げています。それによってdisplay関数が繰り返し呼ばれます。何となくそれっぽい感じになってきました。
#include "stdio.h"
#include "math.h"
#include "glut/glut.h"
const int TEXWIDTH = 64;
const int TEXHEIGHT = 64;
static double s_angle = 90;
static double s_posX = 0.0;
static double s_posZ = 0.0;
static GLuint textureID1;
static GLuint textureID2;
void specialKeyboard(int key, int x, int y) {
double radian = -s_angle * M_PI / 180.0; // 右手系
switch (key) {
case 101: // up
{
s_posX += sin(radian) * 0.1;
s_posZ += cos(radian) * 0.1;
break;
}
case 103: // down
{
s_posX -= sin(radian) * 0.1;
s_posZ -= cos(radian) * 0.1;
break;
}
case 100: // left
s_angle -= 2;
break;
case 102: // right
s_angle += 2;
break;
}
s_angle = int(s_angle) % 360;
glutPostRedisplay();
}
void draw_cube()
{
static GLfloat vert[][4]={
{ 1.0, 1.0, 1.0},
{ 0.0, 1.0, 1.0},
{ 0.0, 0.0, 1.0},
{ 1.0, 0.0, 1.0},
{ 1.0, 1.0, 0.0},
{ 0.0, 1.0, 0.0},
{ 0.0, 0.0, 0.0},
{ 1.0, 0.0, 0.0},
};
glColor3d(1.0, 0.0, 1.0);
glBindTexture(GL_TEXTURE_2D , textureID1);
glBegin(GL_QUADS);
{
glTexCoord2d(1, 0);
glVertex3fv(vert[0]);
glTexCoord2d(0, 0);
glVertex3fv(vert[1]);
glTexCoord2d(0, 1);
glVertex3fv(vert[2]);
glTexCoord2d(1, 1);
glVertex3fv(vert[3]);
}
glEnd();
glBindTexture(GL_TEXTURE_2D , textureID2);
glBegin(GL_QUADS);
{
glTexCoord2d(1, 0);
glVertex3fv(vert[4]);
glTexCoord2d(0, 0);
glVertex3fv(vert[5]);
glTexCoord2d(0, 1);
glVertex3fv(vert[6]);
glTexCoord2d(1, 1);
glVertex3fv(vert[7]);
glVertex3fv(vert[0]);
glVertex3fv(vert[1]);
glVertex3fv(vert[5]);
glVertex3fv(vert[4]);
glVertex3fv(vert[2]);
glVertex3fv(vert[3]);
glVertex3fv(vert[7]);
glVertex3fv(vert[6]);
glVertex3fv(vert[3]);
glVertex3fv(vert[0]);
glVertex3fv(vert[4]);
glVertex3fv(vert[7]);
glVertex3fv(vert[1]);
glVertex3fv(vert[2]);
glVertex3fv(vert[6]);
glVertex3fv(vert[5]);
}
glEnd();
}
void draw_axes() {
// draw axes
glBegin(GL_LINES);
{
// x-axis
glColor3d(1.0, 0.0, 0.0);
glVertex3d(0.0, 0.0, 0.0);
glVertex3d(10.0, 0.0, 0.0);
// y-axis
glColor3d(0.0, 1.0, 0.0);
glVertex3d(0.0, 0.0, 0.0);
glVertex3d(0.0, 10.0, 0.0);
// z-axis
glColor3d(0.0, 0.0, 1.0);
glVertex3d(0.0, 0.0, 0.0);
glVertex3d(0.0, 0.0, 10.0);
glEnd();
}
}
void draw_field() {
double size = 5.0;
glColor3d(0.5, 0.5, 0.5);
double panelNum = 10;
double len = size / panelNum;
for (int i = 0; i < panelNum; i++) {
for (int j = 0; j < panelNum; j++) {
double x = len * i - size / 2.0;
double z = len * j - size / 2.0;
int mode = (i + j) % 2 == 0 ? GL_LINES : GL_POLYGON;
glBegin(mode);
glVertex3d(x, 0, z);
glVertex3d(x + len, 0, z);
glVertex3d(x + len, 0, z + len);
glVertex3d(x, 0, z + len);
glEnd();
}
}
}
void display() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glRotated(s_angle, 0, 1, 0);
glTranslated(s_posX, -0.1, s_posZ);
draw_axes();
draw_cube();
draw_field();
glFlush();
}
void reshape(int w, int h) {
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, double(w)/double(h), 0.1, 100.0);
}
GLuint loadTexture(const char *filename) {
GLuint texID;
GLubyte texture[TEXHEIGHT][TEXWIDTH][4];
FILE *fp = NULL;
if ((fp = fopen(filename, "rb")) != NULL) {
fread(texture, sizeof texture, 1, fp);
fclose(fp);
}
else {
perror(filename);
}
glEnable(GL_TEXTURE_2D);
glGenTextures(1 , &texID);
glBindTexture(GL_TEXTURE_2D , texID);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
GL_RGBA, GL_UNSIGNED_BYTE, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
return texID;
}
void init() {
glClearColor(1.0, 1.0, 1.0, 1.0);
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_COLOR_MATERIAL);
glEnable(GL_TEXTURE_2D);
textureID1 = loadTexture("1.raw");
textureID2 = loadTexture("2.raw");
}
int main(int argc, char **argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
glutCreateWindow("walk through test");
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutSpecialFunc(specialKeyboard);
init();
glutMainLoop();
return 0;
}
とりあえずOpenGLのコードが書けないまでも、読めるようにはなったかなという感じです。あとあまりよく調べていませんが、日本語の文字列描画はプラットフォームに依存したコードを書く必要がありそうです。なのでGLUT使っとけば不自由しないという訳ではなさそうです。ちなみにSDLを使うと日本語フォントの周辺は面倒見てくれるので、作るものによって何と組み合わせて使うかを選んでいく必要がありそうです。
参考資料としてコチラの研究室のページが大変わかりやすくてオススメです。その他はヘッダとリファレンスが結局は早そうな雰囲気です。