OpenTKの導入から始まり、お勉強のために久々にEmbarcadero C++コンパイラーとGLUT(freeglutですが...)でアンマネージドプログラムサンプルを通してOpenGLを学習してきましたが、今回の照光(Lighting)と質感(Material)でひとまず初級を終了したいとおもいす。(というか、私の美術能力の限界です。)
前回のワイアー、ソリッドの直方体と正三角錐を交代で表示するプログラムに、今回は「ソリッド表示の際に照光効果と質感表現を確認できるようにする」というのが今回のお題です。いつものことですが、末尾に追加変更部分を彩色したプログラム(【TestGLUT004.cpp】)を掲示します。(なお、前回はソリッド表示で直接頂点データを関数に与えていましたが、ワイアーフレーム表示のデータがあるので、それを使ったループによる処理に変えています。g_Qface、g_Tface配列を参照願います。)
今回の照光(Lighting)を使う場合のポイントは、
(1)照光効果を有効にする設定(glEnable(GL_LIGHTING);)
(2)(OpenGLの使用によれば最低8つあるべき)光源を有効にする設定(glEnable(GL_LIGHT0);)
(3)glLight(~fは単精度引数型、~fvは単精度配列引数型)関数を使った光源の位置設)
(4)glLight(f | fv)関数を使った環境光(GL_AMBIENT)、拡散光(GL_DIFFUSE)、鏡面光(GL_SPECULAR)のRGB強度設定
(5)(glColorによる色指定ではなく)glNormal3(~dは倍精度引数型、~dvは倍精度配列引数型)関数による法線ベクトルの設定(g_QNormal、g_TNormalを参照願います。またg_TNormalについては二つのGetNormal関数も参照して下さい。)
です。正直に言えば、私は法線ベクトルも、どういう光の種類とRGB強度設定をすればよいのか、よく分かりませんでした。しかし回転させるプリミティブの反射がおかしいことから法線ベクトルをglColorの代わりに使わないとならないことを知り、更に照光効果を使う際はglColorによる色指定はできず、単色の濃淡で表現することも知りました。(注)
注:最初、色指定して照光効果が出ないので可也混乱して色々と調べまくった初心者でした。(汗;)
いずれにせよ、今回のプログラムでは、「ソリッド表示」時に、'l'または'L'キーを押すことで照光効果を体験することが出来るようにしました。(ワイアー表示の際には'l'または'L'キーを押しても無視するようにしました。)
私の無智は更に質感設定で露見します。照光効果を得るためには法線設定だけの単色表示、と思い込んでいたのですが、質感を設定することで色も表現できることを知りました。但し、そのような高度な技術は私にはなく、今回のプログラムでは'm'または'M'キーを押すことで質感が変化することが環意見できれば良しとしました。(データは既定値を使っています。)
いかがでしたでしょうか?私自身がOpenGLというか3Dグラフィックスの超初心者なので説明の行き届かなかった感がありますが、多くの皆さんと同じ目線でOpenGL、GLUTそして3Dグラフィックスと接することが出来たのではないでしょうか?
冒頭で書きましたように、これ以上進む(次はテキスチャー表示、でしょうが)のは私の能力を超えており、単に人のプログラムの紹介になってしまうこと、また当初予定した3Dグラフィックス初心者による犯しがちな過ちや悩みの共有という目的は達せられたと思われることから、「3DグラフィックとGLUT」シリーズは今回でめでたくお開きにさせていただきます。なお、次回以降は、またC#とOpenTKを深堀することを予定しています(が、明日から旅行に行くのでその道中で考えるとにしましょう。)
So long!
と書いたところで、前にBCCSkeltonで作ったGLUTのデモプログラム、Glut_BCC
の紹介をしていないことを思い出しました。GLUT学習終了記念作品ということでGLUTの特徴の一つであるソリッド、ワイアーフレーム計18の基本図形を使って、今までやってきた視点、視野(投影)、回転、照光をメインウインドウから操作するプログラムをお楽しみください。
【TestGLUT004.cpp】
#include <stdio.h> //C言語ベースなのでprintf等を使う場合必要
#include <GL\freeglut.h> //Embarcadero C++ コンパイラーの"BCC102\include\windows\sdk\GL"に入れる
#include <math.h> //powとsqrt関数を使う為
//外部変数
int g_w, g_h; //クライアント領域幅、高さ
GLdouble eyex = 0.0, eyey = 0.0, eyez = 0.9; //視点座標
bool g_Ortho_Persp = TRUE; //平行投影か否かのフラグ
bool g_WorS = TRUE; //Wire図形かSolid図形か
bool g_Light = FALSE; //ライトを使うか否かのフラグ
bool g_Material = FALSE; //質感を使うか否かのフラグ
bool g_Stop = FALSE; //中止するか否かのグフラグ
GLfloat g_Lightpos[4] = {500.0, 50.0, 50.0, 1.0}; //光源0の座標
//光源0のRGB強度(以下はGL_LIGHT0の規定値)
//0.0, 0.0, 0.0, 1.0 //RGBA強度(環境光)
//1.0, 1.0, 1.0, 1.0 //RGBA強度(拡散光)
//1.0, 1.0, 1.0, 1.0 //RGBA強度(鏡面光)
GLfloat g_LightColor[4][4] = {{0.0, 0.0, 0.0, 1.0}, //解説:光源0のRGB強度です。
{0.2, 0.2, 0.2, 1.0},
{0.5, 0.5, 0.5, 1.0},
{1.0, 1.0, 1.0, 1.0}
};
GLdouble g_Color[6][3] = { //6色 //解説:使用する色を配列化しました。
{1.0, 0.0, 0.0}, //Red
{0.0, 1.0, 0.0}, //Green
{0.0, 0.0, 1.0}, //Blue
{1.0, 1.0, 0.0}, //Yellow
{1.0, 0.0, 1.0}, //Magenda
{0.0, 1.0, 1.0} //Cyan
};
GLdouble g_Qvertex[8][3] = {//四角形の8頂点(奥左下から反時計回り4点、手前左下から反時計回り4点)
{-0.5, -0.5, -0.5}, //A
{0.5, -0.5, -0.5}, //B
{0.5, 0.5, -0.5}, //C
{-0.5, 0.5, -0.5}, //D
{-0.5, -0.5, 0.5}, //E
{0.5, -0.5, 0.5}, //F
{0.5, 0.5, 0.5}, //G
{-0.5, 0.5, 0.5} //H
};
int g_Qedge[12][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)
};
int g_Qface[6][4] = { //四角形6面(解説:今回は四角面描画データを配列にして使用します。)
{0, 1, 2, 3}, //ABCD(奥面)
{1, 5, 6, 2}, //BFGC(右面)
{5, 4, 7, 6}, //FEHG(手前面)
{4, 0, 3, 7}, //EADH(左面)
{4, 5, 1, 0}, //EFBA(底面)
{3, 2, 6, 7} //DCGH(天面)
};
GLdouble g_QNormal[6][3] = {//立方体法線(解説:glColorの代わりに使います。)
{0.0, 0.0, -1.0},
{1.0, 0.0, 0.0},
{0.0, 0.0, 1.0},
{-1.0, 0.0, 0.0},
{0.0, -1.0, 0.0},
{0.0, 1.0, 0.0}
};
GLdouble g_Tvertex[4][3] = {//三角形の4頂点(上から下反時計回り4点)
{0.000f, 0.544f, 0.000f}, //A
{0.500f, -0.272f, -0.289f}, //B
{-0.500f, -0.272f, -0.289f},//C
{0.000f, -0.272f, 0.577f}, //D
}; //2024年6月4日修正
int g_Tedge[6][2] = {
{0, 1}, //(A-B)
{1, 2}, //(B-C)
{2, 0}, //(C-A)
{0, 3}, //(A-D)
{3, 2}, //(D-B)
{1, 3} //(C-D)
};
int g_Tface[4][3] = { //三角形の4面(解説:今回は三角面描画データを配列にしました。)
{0, 1, 2}, //奥の面
{0, 2, 3}, //左手前面
{0, 3, 1}, //右手前面
{1, 2, 3} //底面
};
GLdouble g_TNormal[4][3] = {//三角錐法線(初期値はGetNormalの結果)
{0.00, 0.28, -0.96},
{-0.82, 0.34, 0.47},
{0.82, 0.34, 0.47},
{0.00, 1.00, 0.00} //解説:これは-1.00が正しい?
};
//面の法線の計算(解説:正三角錐の法線の計算を関数にしてみました。)
GLdouble* GetNormal(GLdouble* vertex1, GLdouble* vertex2, GLdouble* vertex3) { //引数は隣接する3頂点の座標
static GLdouble Normal[3]; //戻り値用静的変数
GLdouble ax, ay, az, bx, by, bz, nx, ny, nz, len;
//3-2間のベクトル(ax, ay, az)
ax = vertex3[0] - vertex2[0]; //x
ay = vertex3[1] - vertex2[1]; //y
az = vertex3[2] - vertex2[2]; //z
//1-2間のベクトル(bx, by, bz)
bx = vertex1[0] - vertex2[0]; //x
by = vertex1[1] - vertex2[1]; //y
bz = vertex1[2] - vertex2[2]; //z
//a、bベクトルの外積を計算
nx = ay * bz - az * by;
ny = az * bx - ax * bz;
nz = ax * by - ay * bx;
//法線を正規化
len = sqrt(pow(nx, 2.0) + pow(ny, 2.0) + pow(nz, 2.0)); //解説:nx, ny, nzの二乗の和の平方根です。
Normal[0] = nx / len;
Normal[1] = ny / len;
Normal[2] = nz / len;
return Normal;
}
//投影設定
void SetProjection(void) {
//変換行列の初期化(座標変換行列に単位行列を設定)
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
//透視投射範囲の設定
if(g_Ortho_Persp)
glOrtho(-1.4, 1.4, -1.4, 1.4, -1.4, 1.4); //平行投射範囲の設定
else {
if(!g_h) g_h = 1; //高さが0の場合は1に設定
gluPerspective(60.0, (double)g_w /(double)g_h, 1.0, 100.0); //透視投射範囲の設定
}
//モデルビュー変換行列を指定
glMatrixMode(GL_MODELVIEW);
}
void DrawAxes(void) {
glPushMatrix();
glBegin(GL_LINES);
//X軸(青)
glColor3d(0.0, 0.0, 1.0); //青
glVertex3d(0.0, 0.0, 0.0); //直方体の視野空間x軸
glVertex3d(0.95, 0.0, 0.0);
glVertex3d(0.95, 0.0, 0.0); //x軸の矢印
glVertex3d(0.9, 0.03, 0.0);
glVertex3d(0.95, 0.0, 0.0);
glVertex3d(0.9, -0.03, 0.0);
//Y軸(緑)
glColor3d(0.0, 1.0, 0.0); //緑
glVertex3d(0.0, 0.0, 0.0); //直方体の視野空間y軸
glVertex3d(0.0, 0.95, 0.0);
glVertex3d(0.0, 0.95, 0.0); //y軸の矢印
glVertex3d(0.03, 0.9, 0.0);
glVertex3d(0.0, 0.95, 0.0);
glVertex3d(-0.03, 0.9, 0.0);
//Z軸(赤)
glColor3d(1.0, 0.0, 0.0); //赤
glVertex3d(0.0, 0.0, 0.0); //直方体の視野空間z軸
glVertex3d(0.0, 0.0, 0.95);
glVertex3d(0.0, 0.0, 0.95); //z軸の矢印
glVertex3d(0.0, 0.03, 0.9);
glVertex3d(0.0, 0.0, 0.95);
glVertex3d(0.0, -0.03, 0.9);
glEnd();
glPopMatrix();
}
void DrawPolygon() {
static float theta = 0.0f; //シータ値
//ポリゴン描画
glPushMatrix();
glTranslated(0.5, 0.0, 0.0);
glScaled(0.5, 0.5, 0.5);
glRotated(theta, 0.0, 1.0, 0.0);
theta += 0.05;
if(g_WorS) {
glBegin(GL_LINES);
glColor3dv(g_Color[5]); //シアン
for(int i = 0; i < 6; i++) {
glVertex3dv(g_Tvertex[g_Tedge[i][0]]);
glVertex3dv(g_Tvertex[g_Tedge[i][1]]);
}
glEnd();
}
else {
glBegin(GL_TRIANGLES); //解説:三角形面の描画を配列データで行いました。
for(int j = 0; j < 4; ++j) {
if(g_Light)
glNormal3dv(g_TNormal[j]);
else
glColor3dv(g_Color[j]);
for(int i = 0; i < 3; ++i) {
glVertex3dv(g_Tvertex[g_Tface[j][i]]);
}
}
glEnd();
}
glPopMatrix();
glPushMatrix();
glTranslated(-0.5, 0.0, 0.0);
glScaled(0.5, 0.5, 0.5);
glRotated(theta, 0.0, 1.0, 0.0);
glRotated(theta, 1.0, 0.0, 0.0);
//描画処理
if(g_WorS) {
glBegin(GL_LINES);
glColor3dv(g_Color[3]); //黄
for(int i = 0; i < 12; i++) {
glVertex3dv(g_Qvertex[g_Qedge[i][0]]);
glVertex3dv(g_Qvertex[g_Qedge[i][1]]);
}
glEnd();
}
else {
glBegin(GL_QUADS); //解説:四角形面の描画を配列データで行いました。
for (int j = 0; j < 6; ++j) {
if(g_Light)
glNormal3dv(g_QNormal[j]);
else
glColor3dv(g_Color[j]);
for (int i = 0; i < 4; ++i) {
glVertex3dv(g_Qvertex[g_Qface[j][i]]);
}
}
glEnd();
}
theta += 0.1;
glPopMatrix();
}
//アイドル時処理関数
void idle(void) {
glutPostRedisplay(); //再描画関数
}
//描画関数
void display(void) {
if(g_Stop) return; //描画中止
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //色とZ(深度)バッファを初期化
glLoadIdentity(); //変換行列を初期化
gluLookAt( eyex, eyey, eyez, //eyex, eyey, eyez
0.0, 0.0, 0.0, //targetx, targety, targetz
0.0, 1.0, 0.0); //アップベクター(上方向)
if(g_Light) { //光源の位置設定は視点の位置を設定した後に行う必要があります。
glLightfv(GL_LIGHT0, //ライト識別子( GL_LIGHT1, GL_LIGHT2...)
GL_POSITION, //ライト位置を指定
g_Lightpos); //光源の座標を表すGLfloatの配列のポインタ
glLightfv(GL_LIGHT0, //ライト識別子( GL_LIGHT1, GL_LIGHT2...)
GL_AMBIENT, //光属性(自然光)
g_LightColor[0]); //RGBA強度設定
glLightfv(GL_LIGHT0, //ライト識別子( GL_LIGHT1, GL_LIGHT2...)
GL_DIFFUSE, //光属性(拡散光)
g_LightColor[3]); //RGBA強度設定
glLightfv(GL_LIGHT0, //ライト識別子( GL_LIGHT1, GL_LIGHT2...)
GL_SPECULAR, //光属性(鏡面光)
g_LightColor[3]); //RGBA強度設定
} //解説:ここら辺和私もよく分からないのですが(汗;)、光源の光はこのように重ねて定義することで混合できるようです。
const static GLfloat material_ambient[4] = {0.2, 0.2, 0.2, 1.0}; //質感設定(初期値-0.2, 0.2, 0.2, 1)
const static GLfloat material_diffuse[4] = {0.2, 0.2, 0.2, 1.0}; //質感設定(初期値-0.2, 0.2, 0.2, 1)
const static GLfloat material_specular[4] = {0.0, 0.0, 0.0, 1.0}; //質感設定(初期値-0 , 0 , 0, 1)
const static GLfloat material_shininess = 48.0;
if(g_Material) {
glMaterialfv(GL_FRONT, GL_AMBIENT, material_ambient);
glMaterialfv(GL_FRONT, GL_SPECULAR, material_specular);
glMaterialfv(GL_FRONT, GL_DIFFUSE, material_diffuse);
glMaterialf(GL_FRONT, GL_SHININESS, material_shininess);
} //解説:ここでも反射の相違によるRGB強度の数値を変えることで様々な色や質感を表現できるようです、知らんけど。
DrawAxes(); //x, y, z軸を描画
DrawPolygon(); //三角錐と直方体を描画
glFlush(); //glutSwapBuffersで行われるので不要かも
glutSwapBuffers(); //glutInitDisplayMode(GLUT_DOUBLE)でダブルバッファリングを利用可
}
//ウィンドウサイズ変更時関数
static void resize(int width, int height)
{
//クライアントエリアサイズを記録
g_w = width; g_h = height;
//クライアントエリア全体に表示
glViewport(0, 0, g_w, g_h);
//投影設定
SetProjection();
gluLookAt( eyex, eyey, eyez, //eyex, eyey, eyez
0.0, 0.0, 0.0, //targetx, targety, targetz
0.0, 1.0, 0.0); //アップベクター(上方向)
}
//マウス入力処理関数
void mouse(int button, int state, int x, int y) {
switch(button) {
case GLUT_LEFT_BUTTON:
//視点位置と視線方向の設定
eyex = 1.0; eyey = 1.0; eyez = 1.0;
g_Ortho_Persp = FALSE;
break;
case GLUT_MIDDLE_BUTTON:
//変換行列の初期化(座標変換行列に単位行列を設定)
glLoadIdentity();
//視点位置と視線方向の設定
eyex = 0.0; eyey = 0.0; eyez = 0.9;
g_Ortho_Persp = TRUE;
break;
case GLUT_RIGHT_BUTTON:
//視点位置と視線方向の設定
eyex = -1.0; eyey = 1.0; eyez = 1.0;
g_Ortho_Persp = FALSE;
break;
default:
break;
}
//投影設定
SetProjection();
display();
}
//キーボード入力処理関数
void keyboard(unsigned char key, int x, int y) {
switch(key) {
//ESC、Qまたはqで終了
case '\033': //ESC
case 'Q':
case 'q':
exit(0);
break;
case 'L':
case 'l':
if(g_WorS) { //ワイアーモードの場合はメッセージを出して戻る。
printf("While primitives are in wire mode, you cannot get light on.\r\n");
return;
}
g_Light = !g_Light;
if(g_Light) {
glEnable(GL_LIGHTING); //照光を有効化する
glEnable(GL_LIGHT0); //光源0を有効化する
glDisable(GL_COLOR_MATERIAL); //物体の質感を有効にする設定
}
else {
glDisable(GL_LIGHTING); //照光を無効にする設定
glDisable(GL_LIGHT0); //光源0を無効にする設定
glEnable(GL_COLOR_MATERIAL); //物体の質感を有効にする設定
}
break;
case 'M':
case 'm':
if(g_WorS || !g_Light) { //ワイアーフレームやライト不使用の場合強制的にオフ
printf("You can set surface material features only in the light on.\r\n");
g_Material = FALSE;
return;
}
g_Material = !g_Material;
break;
case 'S':
case 's':
g_Stop = !g_Stop;
break;
case 'W':
case 'w':
g_WorS = !g_WorS;
if(g_WorS) { //ワイアーフレームの時はライトと質感は外す
g_Light = FALSE;
g_Material = FALSE;
glDisable(GL_LIGHTING); //照光を無効にする設定
glDisable(GL_LIGHT0); //光源0を無効にする設定
glEnable(GL_COLOR_MATERIAL); //物体の質感を有効にする設定
}
break;
default:
break;
}
}
void GetNormal() { //解説:↑にある同名の関数(但し引数が異なるので識別される)を呼び出して、正三角錐の各面の法線を求めています。
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 3; j++)
g_TNormal[i][j] = (GetNormal(g_Tvertex[g_Tface[i][0]], g_Tvertex[g_Tface[i][1]], g_Tvertex[g_Tface[i][2]]))[j];
//printf("g_TNormal[%d] = {%.2f, %.2f, %.2f}\r\n", i, g_TNormal[i][0], g_TNormal[i][1], g_TNormal[i][2]); //法線テスト用
}
}
//メイン(エントリーポイント)関数
int main(int argc, char *argv[]) {
glutInit(&argc, argv); //GLUTの初期化
glutInitDisplayMode(GLUT_RGBA | //ディスプレーの初期化
GLUT_DOUBLE |
GLUT_DEPTH);
glutInitWindowPosition(100, 100); //ウィンドウ位置指定
glutInitWindowSize(640, 640); //ウィンドウサイズ指定
//ファイル名だけを表示する
char* fn;
for(char* pt = argv[0]; *pt; pt++) {
if(*pt == '\\')
fn = ++pt;
}
glutCreateWindow(fn); //タイトル付ウィンドウ生成
glShadeModel(GL_SMOOTH); //既定値の滑らかな網かけ
glEnable(GL_DEPTH_TEST); //深度テストを有効にする
glClearColor(0.0, 0.0, 0.0, 1.0); //画面消去色(黒)
glutDisplayFunc(display); //描画関数の指定
glutReshapeFunc(resize); //ウィンドウサイズ変更時関数の指定
glutIdleFunc(idle); //アイドル時処理関数の指定
glutMouseFunc(mouse); //マウス入力処理関数
glutKeyboardFunc(keyboard); //キーボード入力処理関数
GetNormal(); //解説:正三角錐の法線データを取得する。
glutMainLoop(); //メインループ関数
return 0;
}