■ 2005年03月01日 [OpenGL][テクスチャ] 第15回 キューブマッピングで Phong シェーディング
マスク
最近,花粉症対策のマスクをしています.ほこりでアレルギー性鼻炎を起こすことはあるのですが,花粉症にはまだ縁はありません.でも今年の花粉の飛散は悲惨だというので,予防のつもりマスクをかけてみました.そうしたら,妙に気分が落ち着くのです.素顔を見られない安心感からでしょうか.部屋の中でマスクをしていたら,学生さんに不審がられました.
ちなみに,あんまり吐き気・胸のつかえ・胸の痛み,あるいは気分の落ち込みのようなものが続くので,先日ついに病院に行きました.今風に言えば「メンタルクリニック」とかいうところです.「気分の悪さが吐き気なのか不安感なのか区別がつかない」とお医者さんに話したら,「そういうものをかつて心身症と呼んでいた」と言われました.なるほど.メイラックスとドグマチールという薬を処方してもらいました.効いているようです.
キューブマッピングで Phong シェーディング
さてさて今回は,前回の途中でくじけたキューブマッピングによる Phong シェーディングの実装を行います.雛型にはキューブマッピングでテクスチャを回転したときに作成したものを用います.
鏡面反射光強度の算出
今回はまず,鏡面反射光強度の算出手続きを関数 specular() としてあらかじめ定義しておくことにします.これは初期化の関数 init() の前に置いてください.
・・・ /* ** 鏡面反射光強度 */ static void specular(float fx, float fy, float fz, const float l[], GLubyte col[]) { /* 光線ベクトルと反射ベクトルの内積を求める */ float lf = l[0] * fx + l[1] * fy + l[2] * fz; if (lf > 0.0) { /* 鏡面反射率×255を求める */ float rs = pow(lf, kshi) * 255.0; /* 鏡面反射光強度を求める */ col[0] = (GLubyte)(kspec[0] * rs * lightcol[0]); col[1] = (GLubyte)(kspec[1] * rs * lightcol[1]); col[2] = (GLubyte)(kspec[2] * rs * lightcol[2]); } else { col[0] = col[1] = col[2] = 0; } }
鏡面反射光強度分布の画像を作る
この関数 specular() を使って鏡面反射光強度分布の画像を作成する関数 makeTexture() を,この直後(つまり関数 init() との間)で定義します.キューブマッピングなので6枚分の画像を作ります.
/* ** テクスチャの作成 */ static void makeTexture(GLubyte *tex[], int width, int height) { int i = 0; /* 反射ベクトルと光線ベクトルとの内積値でテクスチャを作る */ for (int v = 0; v < height; ++v) { float y = (float)(v + v - height) / (float)height; float y2 = y * y; for (int u = 0; u < width; ++u) { float x = (float)(u + u - width) / (float)width; float x2 = x * x; /* 反射ベクトル */ float r = 1.0 / sqrt(x2 + y2 + 1.0); float s = x * r; float t = y * r; /* 6面のテクスチャについてそれぞれ鏡面反射光強度を求める */ specular(-r, -t, s, lightpos, tex[0] + i); /* negative x */ specular( s, -r, -t, lightpos, tex[1] + i); /* negative y */ specular(-s, -t, -r, lightpos, tex[2] + i); /* negative z */ specular( r, -t, -s, lightpos, tex[3] + i); /* positive x */ specular( s, r, t, lightpos, tex[4] + i); /* positive y */ specular( s, -t, r, lightpos, tex[5] + i); /* positive z */ i += 3; } } }
テクスチャを割り当てる
テクスチャに使う画像を格納する配列を6枚分用意して,それぞれのポインタをポインタの配列変数 texture に格納します.次に,作成した関数 makeTexture() を使って texture に画像を作成します.太字の部分を追加・変更してください.
/* ** 初期化 */ static void init(void) { /* テクスチャの読み込みに使う配列 */ static GLubyte t[6][TEXHEIGHT * TEXWIDTH * 3]; static GLubyte *texture[] = { t[0], t[1], t[2], t[3], t[4], t[5] }; /* テクスチャの作成 */ makeTexture(texture, TEXWIDTH, TEXHEIGHT); /* テクスチャ画像はバイト単位に詰め込まれている */ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
テクスチャに使う画像を自分で作成するので,この後に続く画像ファイルの読み込み部分は不要になります.#if 0 〜 #endif ではさむなどして,無効にしておいてください.
あと,今回テクスチャに割り当てる画像にはアルファチャンネルを付けていないので,テクスチャを割り当てる際には画像の書式に GL_RGB を指定してください.また,6枚の画像が配列変数 texture の一つ一つの要素に格納されているので,texture に添え字 [i] を付けておいてください.
for (int i = 0; i < 6; ++i) { #if 0 /* テクスチャの読み込みに使う配列 */ GLubyte texture[TEXHEIGHT][TEXWIDTH][4]; FILE *fp; /* テクスチャ画像の読み込み */ if ((fp = fopen(textures[i], "rb")) != NULL) { fread(texture, sizeof texture, 1, fp); fclose(fp); } else { perror(textures[i]); } #endif /* テクスチャの割り当て */ glTexImage2D(target[i], 0, GL_RGB, TEXWIDTH, TEXHEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, texture[i]); } ・・・
材質の設定
あとはシーンの描画の際に,描画する物体の材質やテクスチャの合成方法を変更します.もともとの材質ははっきりとした移り込みを表現するために白にしてありました.ここではスフィアマッピングで Phong シェーディングをやったときと同様,青色にしてみます.材質のパラメータは,プログラムの最初の方で定義しておくことにします.
・・・ /* ** マテリアル */ static const GLfloat kdiff[] = { 0.0, 0.1, 0.3, 1.0 }; /* 拡散反射係数 */ static const GLfloat kspec[] = { 0.6, 0.6, 0.6, 1.0 }; /* 鏡面反射係数 */ static const GLfloat knone[] = { 0.0, 0.0, 0.0, 1.0 }; /* 鏡面反射無効 */ static const GLfloat kshi = 20.0; /* 輝き係数 */ ・・・
関数 scene() の中で定義している白色の材質 color は使わないので,#f 0 〜 #endif ではさむなどして,無効にしておきます(これは放置しておいてもかまいませんが…).あとは拡散反射係数に青色の材質 kdiff を設定し,鏡面反射係数に knone を設定して OpenGL による鏡面反射光成分の生成を行わないようにします.その代わり,テクスチャとしてマッピングする鏡面反射光成分を,OpenGL による陰影付けで得た拡散反射光成分と合成するために,glTexEnvi() を使って GL_TEXTURE_ENV_MODE に GL_ADD を指定します.
・・・ /* ** シーンの描画 */ static void scene(void) { #if 0 static const GLfloat color[] = { 1.0, 1.0, 1.0, 1.0 }; /* 材質 (色) */ #endif /* 材質の設定 */ glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, kdiff); glMaterialfv(GL_FRONT, GL_SPECULAR, knone); /* テクスチャ環境 */ glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD); /* テクスチャマッピング開始 */ glEnable(GL_TEXTURE_CUBE_MAP); ・・・
ここまでできたら一度コンパイルして実行してみてください.
拡散反射光も回そう
マウスをドラッグしてみてください.鏡面反射光成分(ハイライト)がマウスの移動方向と反対に動くのが少し気色悪いですけど,ここはこのままいきましょう. 一方,拡散反射光成分は今回も動いていません.そこで,今回はこの拡散反射光成分も,光源の位置にあわせて動くようにしてみます.拡散反射光成分をマウスの動きにあわせて回転させるには,光源の位置を設定しているところで,物体の回転と同じように回転の行列を乗じます.
・・・ static void display(void) { ・・・ /* 光源の位置を設定 */ glPushMatrix(); glMultMatrixd(trackballRotation()); glLightfv(GL_LIGHT0, GL_POSITION, lightpos); glPopMatrix(); ・・・
これでコンパイルして実行すると,多分,面白いことが起こります.
鏡面反射光成分と拡散反射光成分が逆回転している
これは鏡面反射光成分と拡散反射光成分が反対方向に回転している状態です.正しく回転させるには,どちらかを逆回転させる必要があります.回転行列の場合,逆回転,すなわち逆変換行列は,元の変換行列を転置するだけで求められます.しかし,OpenGL の Version 1.3 では,転置行列を扱う拡張機能 (GL_ARB_transpose_matrix) が標準機能になっていますから,ここではこれを使うことにします.
Windows の場合
なお Windows でこの機能を使う場合には,少しおまじないが必要になります.先にその部分を説明します.まず,プログラムの最初の部分で glext.h を #include している部分の後で,glMultTransposeMatrixd() という関数の関数ポインタ変数を宣言しておきます.
#include <stdio.h> #include <stdlib.h> #include <math.h> #if defined(WIN32) //# pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"") # include "glut.h" # include "glext.h" PFNGLMULTTRANSPOSEMATRIXDPROC glMultTransposeMatrixd; #elif defined(__APPLE__) || defined(MACOSX) # include <GLUT/glut.h> #else # include <GL/glut.h> #endif ・・・
そして,初期化の関数 init() の最後あたりで,この glMultTransposeMatrixd に関数のエントリポイント(プログラムの実体が格納されている場所)を代入しておきます.
・・・ /* ** 初期化 */ static void init(void) { ・・・ /* キューブマッピングする */ glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP); glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP); #if defined(WIN32) glMultTransposeMatrixd = (PFNGLMULTTRANSPOSEMATRIXDPROC)wglGetProcAddress("glMultTransposeMatrixd"); #endif } ・・・
実際に関数 glMultTransposeMatrixd() が使えるかどうかは,glGetString(GL_VERSION) で OpenGL のバージョンを調べたり,glGetString(GL_EXTENSIONS) で返される文字列の中に GL_ARB_transpose_matrix が含まれることを確認したりする必要があります.少なくとも,上記の手続きで得られた関数ポインタ glMultTransposeMatrixd が NULL でないことを確認しておかないと,この機能がサポートされていなかった場合にプログラムが異常終了してしまいます.ですが,ここでは手を抜いて「使えるもの」として話を進めます.
鏡面反射光成分のテクスチャを逆回転する
最後に,テクスチャを回転している部分の glMultMatrixd() を glMultTransposeMatrixd() に置き換えて,鏡面反光射成分が逆方向に回転するようにします.
・・・ static void display(void) { /* テクスチャ変換行列の設定 */ glMatrixMode(GL_TEXTURE); glLoadIdentity(); /* トラックボール処理による回転 */ glMultTransposeMatrixd(trackballRotation()); /* モデルビュー変換行列の設定 */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); ・・・
LoadObjectとやらのDXFとかobjとか3Dファイル形式を読み込んで<br>回転させる技は登場しないの?
それらのファイルフォーマット自体簡単だから別に論議すべきではないと思いますよ。読み込む方法はファイル操作と文字列操作だけです。
コメントありがとうございます.<br>確かにティーポットばかり回してていても面白くないなあとは思ってるんですが,とりあえずテクスチャマッピングの話でまとめさせて頂きたい(まだ続ける気でいるらしい>自分)ので,とりあえずご容赦ください.<br>手元には LightWave で三角形分割してから出力したものしか扱えない手抜きの Alias OBJ 形式のローダくらいしかないんですけど,また気が向いた時にティーポット以外のものも回してみたいと思います(実は学生さんが使っていた DXF ローダとかもあるんですけど,これは出所が謎なのでここでは使えないなぁ).
関数static void makeTexture(...)中の<br><br> /* 反射ベクトル */<br> float r = 1.0 / sqrt(x2 + y2 + 1.0);<br> float s = x * r;<br> float t = y * r;<br><br>はどのように導出されたのでしょうか?ご教示頂ければ幸いです。よろしくお願い致します。
コメントありがとうございます.<br> ここでいう反射ベクトルは,立方体の中心(=原点)からその表面上の1点に向かう方向ベクトルです.仮にこの点が立方体の6つの面のうち z = 1 の平面上にあれば,その点の位置ベクトルは (x, y, 1) となります.これを正規化してこの点に至る方向(単位)ベクトル (s, t, r) を求めています.
床井先生<br><br>ご丁寧にご回答して頂き、ありがとうございました。ということは、それぞれ6面に対して、<br>z=1, positive z: (x, y, 1)<br>z=-1, negative z: (x, y, -1)<br>y=1, positive y: (x, 1, z)<br>y=-1, negative y: (x, -1, z)<br>x=1, positive x: ( 1, y, z)<br>x=-1, negative x: (-1, y, z)<br><br>として、これらのベクトルを正規化すると反射ベクトルが求められるという理解でよろしいでしょうか?それからもう1点ご質問があるのですが、この下の部分で、実際に鏡面反射光強度を求めるところで、符号が反転しているところがありますが、これはどのように決定しているのでしょうか?<br><br>---<br>例えばz=1のpositive zで<br><br>specular( s, -t, r, lightpos, tex[5] + i); /* positive z */<br><br>のように、tに負の符号がついています。<br>---<br><br>たびたび申し訳ございませんが、これらの点に関しても、ご教示頂ければ幸いです。
はい,そういうつもりで組んでいます.ただ,面によって座標値の増加方向が変わります(例えば z = 1 の面を外から見たとき,x 軸の正の方向は右側になりますが,z = -1 の面を外から見たとき,x 軸の正の方向は左側になります)ので,<br> z = 1, positive z: ( x, -y, 1)<br> z = -1, negative z: (-x, -y, -1)<br> y = 1, positive y: ( x, 1, y)<br> y = -1, negative y: ( x, -1, -y)<br> x = 1, positive x: ( 1, -y, -x)<br> x = -1, negative x: (-1, -y, x)<br>というようにしています.<br> ここで,例えば z = 1 の時に t の符号を反転させているのは,テクスチャの画素の位置とテクスチャ座標の上下が反転している(テクスチャ画像では原点が画像の左上にあり下方向が正なのに対して,テクスチャ座標は上方向が正になります)からです.<br> もっともこれは,y を求める際に,<br> float y = (float)(v + v - height) / (float)height;<br>とする代わりに,<br> float y = (float)(height - v - v) / (float)height;<br>とすべきでしたが,最初にプログラムを動かした時にテクスチャの上下が反転していることに気づいて,後から t にマイナスを付けたんだと思います.
床井先生<br><br>度々の質問にご丁寧にご回答して頂き、ありがとうございます。<br>先生のご説明で、座標系の関係を理解することができました。<br>お忙しい中、ありがとうございました。