■ 2006年06月01日 [OpenGL][GLSL] 第9回 GLSL によるシャドウマッピング
就職活動
現在,うちの研究室の学部4年生が就職活動にいそしんでいます.すでに活動の「第1波」が過ぎたようで,よその研究室からは内定を取ったという話がちらほらと入ってきます.でもうちの4年生は,なんだかのんきに構えているように見えます.大丈夫かなぁ(内心,焦っているのかもしれませんが).
それにしても就職活動というのは,本当にお見合いとよく似ていて,もちろん本人の能力や資質も問われますけど,それ以上に「縁」とか「相性」とか「運」みたいなものが成否を左右します.ところが学生さんは,偏差値という物差しを使って大学までやってきたせいか,同じような物差しが企業の間にもあるような錯覚をしていることがあって,就職活動の成否によって自分が「ランク付け」されているように感じることがあるみたいです.そのため,うまくいかないとどんどん落ち込んでいったりします.そこで,「うまくいかなかったのは相性が合わなかっただけだから,また自分も相手(就職先)もハッピーになれるような縁を探しに行こう」などとお仲人さんのようなことを言ったりします.あ,そういや先方が不採用を通知してくるとき,「ご縁が無かったということで…」って言うよな.
シャドウマッピング
今回は以前にテクスチャマッピングのテクニックとして紹介したシャドウマッピングを,GLSL を使って実装してみたいと思います.雛形のプログラムはこのときに用意したものを用います.このテクニックの肝となるテクスチャ変換行列の設定方法については,シャドウマッピングのところを参照してください.
このアーカイブには main() を含んだソースプログラムが三つ含まれていますが,ここではその中の main1.cpp を使います.これは次のような真っ黒い影を作ります.
テクスチャ座標の自動生成は不要
シャドウマッピングでは物体表面上の三次元位置を求めるために,テクスチャ座標の自動生成機能を使っていました.シェーダを使う場合は,バーテックスシェーダで頂点の座標値を得ることができるので,この設定は不要になります(というか,シェーダを使うとテクスチャ座標の自動生成が機能しない?).まず main1.cpp からテクスチャ座標の自動生成の設定を削除し,シェーダプログラムの読み込み処理を追加します.glsl.h と glsl.cpp は,前回のサンプルプログラムものなどを用いてください.
・・・ /* ** 初期化 */ static void init(void) { /* シェーダプログラムのコンパイル/リンク結果を得る変数 */ GLint compiled, linked; /* テクスチャの割り当て */ glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, TEXWIDTH, TEXHEIGHT, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0); /* テクスチャを拡大・縮小する方法の指定 */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); /* テクスチャの繰り返し方法の指定 */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); /* 書き込むポリゴンのテクスチャ座標値のRとテクスチャとの比較を行うようにする */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); /* もしRの値がテクスチャの値以下なら真(つまり日向) */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); /* 比較の結果を輝度値として得る */ glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE); #if 0 /* テクスチャ座標に視点座標系における物体の座標値を用いる */ glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); /* 生成したテクスチャ座標をそのまま (S, T, R, Q) に使う */ static const GLdouble genfunc[][4] = { { 1.0, 0.0, 0.0, 0.0 }, { 0.0, 1.0, 0.0, 0.0 }, { 0.0, 0.0, 1.0, 0.0 }, { 0.0, 0.0, 0.0, 1.0 }, }; glTexGendv(GL_S, GL_EYE_PLANE, genfunc[0]); glTexGendv(GL_T, GL_EYE_PLANE, genfunc[1]); glTexGendv(GL_R, GL_EYE_PLANE, genfunc[2]); glTexGendv(GL_Q, GL_EYE_PLANE, genfunc[3]); #endif /* 初期設定 */ glClearColor(0.3, 0.3, 1.0, 1.0); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); /* 光源の初期設定 */ glEnable(GL_LIGHT0); glLightfv(GL_LIGHT0, GL_AMBIENT, lightamb); /* GLSL の初期化 */ if (glslInit()) exit(1); /* シェーダオブジェクトの作成 */ vertShader = glCreateShader(GL_VERTEX_SHADER); fragShader = glCreateShader(GL_FRAGMENT_SHADER); /* シェーダのソースプログラムの読み込み */ if (readShaderSource(vertShader, "shadow.vert")) exit(1); if (readShaderSource(fragShader, "shadow.frag")) exit(1); /* バーテックスシェーダのソースプログラムのコンパイル */ glCompileShader(vertShader); glGetShaderiv(vertShader, GL_COMPILE_STATUS, &compiled); printShaderInfoLog(vertShader); if (compiled == GL_FALSE) { fprintf(stderr, "Compile error in vertex shader.\n"); exit(1); } /* フラグメントシェーダのソースプログラムのコンパイル */ glCompileShader(fragShader); glGetShaderiv(fragShader, GL_COMPILE_STATUS, &compiled); printShaderInfoLog(fragShader); if (compiled == GL_FALSE) { fprintf(stderr, "Compile error in fragment shader.\n"); exit(1); } /* プログラムオブジェクトの作成 */ gl2Program = glCreateProgram(); /* シェーダオブジェクトのシェーダプログラムへの登録 */ glAttachShader(gl2Program, vertShader); glAttachShader(gl2Program, fragShader); /* シェーダオブジェクトの削除 */ glDeleteShader(vertShader); glDeleteShader(fragShader); /* シェーダプログラムのリンク */ glLinkProgram(gl2Program); glGetProgramiv(gl2Program, GL_LINK_STATUS, &linked); printProgramInfoLog(gl2Program); if (linked == GL_FALSE) { fprintf(stderr, "Link error.\n"); exit(1); } /* シェーダプログラムの適用 */ glUseProgram(gl2Program); /* テクスチャユニット0を指定する */ glUniform1i(glGetUniformLocation(gl2Program, "texture"), 0); #if defined(WIN32) glMultTransposeMatrixd = (PFNGLMULTTRANSPOSEMATRIXDPROC)wglGetProcAddress("glMultTransposeMatrixd"); #endif } ・・・
この #if 0 〜 #endif の間は削除しても構いません.次に,描画時にテクスチャ座標の自動生成を有効にしないようにしておきます.
・・・ static void display(void) { ・・・ /* テクスチャマッピングとテクスチャ座標の自動生成を有効にする */ glEnable(GL_TEXTURE_2D); #if 0 glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); glEnable(GL_TEXTURE_GEN_R); glEnable(GL_TEXTURE_GEN_Q); #endif /* 光源の明るさを日向の部分での明るさに設定 */ glLightfv(GL_LIGHT0, GL_DIFFUSE, lightcol); glLightfv(GL_LIGHT0, GL_SPECULAR, lightcol); /* シーンを描画する */ scene(t); /* テクスチャマッピングとテクスチャ座標の自動生成を無効にする */ #if 0 glDisable(GL_TEXTURE_GEN_S); glDisable(GL_TEXTURE_GEN_T); glDisable(GL_TEXTURE_GEN_R); glDisable(GL_TEXTURE_GEN_Q); #endif glDisable(GL_TEXTURE_2D); /* ダブルバッファリング */ glutSwapBuffers(); } ・・・
glMaterialfv() の問題
本題とは直接関係ないのですが,このサンプルプログラムで表示されるチェッカーボード状の板は,シェーダを使うと色がうまく設定されなくなります.この板はポリゴン単位に glMaterialfv() を使って材質(色)を設定しているのですが,その値を得るために用いる gl_FrontMaterial や gl_BackMaterial は頻繁に値が変更されることが無い(ということが期待されている)uniform 変数であるために,ポリゴン単位の材質の変更には追従できないようです.
そこで scene.cpp において glMaterialfv() を使っている部分をすべて glColor4fv() に置き換えます.glColor4fv() で設定した色は attribulte 変数 gl_Color で参照できるため,ポリゴン単位や頂点単位に色を設定した場合でも,値を取り出すことができます.
・・・ /* ** タイルの描画 */ static void tile(double w, double d, int nw, int nd) { ・・・ glNormal3d(0.0, 1.0, 0.0); glBegin(GL_QUADS); for (j = 0; j < nd; ++j) { GLdouble dj = d * j, djd = dj + d; for (i = 0; i < nw; ++i) { GLdouble wi = w * i, wiw = wi + w; glColor4fv(color[(i + j) & 1]); glVertex3d(wi, 0.0, dj); glVertex3d(wi, 0.0, djd); glVertex3d(wiw, 0.0, djd); glVertex3d(wiw, 0.0, dj); } } glEnd(); } /* ** 箱の描画 */ static void box(double x, double y, double z) { ・・・ /* 箱の色 */ static const GLfloat color[] = { 0.8, 0.8, 0.2, 1.0 }; int i, j; glColor4fv(color); glBegin(GL_QUADS); for (j = 0; j < 6; j++) { glNormal3dv(normal[j]); for (i = 4; --i >= 0;) { glVertex3dv(face[j][i]); } } glEnd(); } /* ** シーンの描画 */ void scene(double t) { ・・・ /* 球を描く */ glPushMatrix(); glTranslated(r * cos(wt), 1.0, r * sin(wt)); glColor4fv(red); glutSolidSphere(0.9, 32, 16); glPopMatrix(); }
バーテックスシェーダ
バーテックスシェーダでは,以前にやった Gouraud シェーディングとほぼ同じ処理を行います.以前と異なるのは,材質(色)を gl_Color から得ることと,日向の部分の陰影のほかに影の部分の陰影も求めておくこと,それに頂点座標にテクスチャ変換行列を掛けてテクスチャ座標を求めておくこと,の3点です.
影の部分の陰影は,日向の部分の環境光成分+拡散反射光強度×0.2 とします.本当は環境光成分だけのはずですが,そうすると陰影がつぶれて平板な絵になってしまうので,多少光源の光の影響を与えます.一方,影の部分にハイライトが現れては不自然なので,鏡面反射光強度は加算しません.これをフラグメントシェーダに伝えるために,shadow という varying 変数に格納しておきます.
// shadow.vert varying vec4 shadow; void main(void) { vec3 position = vec3(gl_ModelViewMatrix * gl_Vertex); vec3 normal = normalize(gl_NormalMatrix * gl_Normal); vec3 light = normalize(gl_LightSource[0].position.xyz - position); float diffuse = dot(light, normal); // 環境光強度を求めておく gl_FrontColor = gl_LightSource[0].ambient * gl_Color; shadow = gl_FrontColor; if (diffuse > 0.0) { vec3 view = normalize(position); vec3 halfway = normalize(light - view); float specular = pow(max(dot(normal, halfway), 0.0), gl_FrontMaterial.shininess); // 拡散反射光強度を求める vec4 temp = gl_LightSource[0].diffuse * gl_Color * diffuse; // 影の部分の拡散反射光強度は日向の 0.2 倍にする shadow += temp * 0.2; // 日向の部分の陰影を求める gl_FrontColor += temp + gl_FrontLightProduct[0].specular * specular; } // 頂点のワールド座標値にテクスチャ変換行列を掛ける gl_TexCoord[0] = gl_TextureMatrix[0] * gl_ModelViewMatrix * gl_Vertex; gl_Position = ftransform(); }
フラグメントシェーダ
フラグメントシェーダでは GLSL の組み込み関数 shadow2DProj() を使ってテクスチャをサンプリングします.そして,その r 値(赤)が 0 でなければ,フラグメントに日向の陰影を設定し,0 なら影の陰影を背呈します.
// shadow.frag uniform sampler2DShadow texture; varying vec4 shadow; void main (void) { if (shadow2DProj(texture, gl_TexCoord[0]).r != 0.0) gl_FragColor = gl_Color; else gl_FragColor = shadow; }
if が使いたくなければ,これは次のように書くこともできます.(gl_Color - shadow) はバーテックスシェーダであらかじめ計算しておくこともできますね.
// shadow.frag uniform sampler2DShadow texture; varying vec4 shadow; void main (void) { gl_FragColor = shadow + (gl_Color - shadow) * shadow2DProj(texture, gl_TexCoord[0]); }
日向と影の描き分けが不要
以前に解説したシャドウマッピングでは,影の部分に陰影をつけるために,日向と影の部分を別々にレンダリングしていました.シェーダを使った場合は日向と影の陰影を同時に計算できるため,このような描き分けが不要になり,レンダリング回数を1回減らすことができます.しかし,GeForce FX 5200 だとフラグメントシェーダが遅いためか,かえって処理が遅くなってしまいました.
余談
GLSL の組み込み関数 shadow2DProj() の実装が ATI と nVIDIA で異なっていて,最初これらの間で同じ結果が得られずにえらく悩みました.ATI の方はオレンジブックに則したもので,私の期待したとおり 0 か 1 の値を返してくれたのですが,nVIDIA の方はなぜか中間的な値を返してくるのです.
いろいろ調べた結果,nVIDIA の GLSL のリリースノートに「glTexParameteri() で GL_TEXTURE_COMPARE_MODE に GL_COMPARE_R_TO_TEXTURE を指定していれば,テクスチャの値として比較の結果をサンプリングするので,shadow2DProj() は texture2DProj() と同じである」なんてことが書いてありました.私は最初この glTexParameteri() をはずしていたので,nVIDIA の shadow2DProj() の動作が期待と異なるものになってました.
なお,GL_TEXTURE_COMPARE_FUNC や GL_DEPTH_TEXTURE_MODE の設定も,shadow2DProj() が返す値に影響を与えます.GL_TEXTURE_COMPARE_FUNC に GL_LEQUAL を設定したときは,参照点がテクスチャの値以下(すなわち日向の)時に shadow2DProj() は 1 を返します.また GL_DEPTH_TEXTURE_MODE に GL_LUMINANCE を設定すれば,shadow2DProj() が返す値の RGB に結果が格納されます.これは ATI,nVIDIA とも同じでした.
ほんとそうです就職活動。(先生の難しいCGのところは読んでないですすみません。)<br>あたしも今の会社は小規模で、入るまでは正直ちょっとイヤだったんですけれど、今はものすごくいい会社に出会えてよかったなぁ〜と思えます。<br>やっぱり相性ですね!ほんとに。入ってみないとわかんないこととかたくさんあります。<br>そーゆーに出会えるのは難しいかもしれませんが、面接とかで頑張って自分らしさを認めてもらえて、こんな自分でもいいよって言ってくれるところが、自分に合っている会社なんだと思います。<br>その節はいろいろお世話になりましたw
そうかあ,もっさんはいいところに入ったんだね.よかったね.「たらこ」もかわいいし :-)<br>昨日はRさんの会社の人事の方がおいでになりました.私信でRさんもいいところだと言っていましたけど,確かに良識のあるいい会社なんだろうと感じました.その方がおっしゃってましたけど,大手のブランドなどがついてなくたって,いい会社はいっぱいあるんですね.<br>その一方で,社員の幸せってことをあんまり考えてくれない会社も中にはあるので,そのあたりをどう見極めるかってことも大切ですね.難しいんですけど,最近はそういう会社の評判はちゃんと伝わってくるから,情報収集は怠り無く,ってところですかね.<br>あと,自分自身も会社にかわいがってもらえる存在になるってことも,居心地を良くするために重要でしょうね.もっさんもいっぱい勉強して,今の会社にかわいがってもらってください.
内心、かなり焦っています…。僕もK君も。<br>ご心配おかけしてすいません。がんばります。
Rさんが私信の中で,この件に関して「なんとかなるんじゃないでしょうか。・・・去年のように。 」って書いてました.うん,去年も確かになんとかなった.だから,何とかなるとは思ってます.
いつもお世話になっています。<br><br>これコードをみると<br>static const GLfloat lightpos[] = { 4.0, 9.0, 5.0, 1.0 }; /* 位置 */<br>となっており、また、shadow.vertをみると<br> vec3 light = normalize(gl_LightSource[0].position.xyz - position);<br>となっていることから点光源(どこかに書いてあったらごめんなさい)の<br>影になっていますが、平行光源の影って同じように計算できるのでしょうか?
のりおさま,気付くのが遅くなり,申し訳ありません.<br>平行光線の影を作るには,シャドウマップを作る際の変換行列を平行投影変換にする必要があります.<br>http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20050926<br>この gluPerspective() を使っているところを glOrtho() に書き換えてください.<br>シェーダの lightpos や light は点光源による陰影付けに使っているだけですが,これも平行光線用に変更する必要があります.<br>ただ,lightpos の w 要素が 0 か非 0 に関わらず light を求める方法もあります.このページで使っている方法はダサいです.
ああ、なるほどありがとうございます。
床井先生、<br><br>いつもお世話になっております。<br>shadow2Dprojは注目しているピクセルが影かどうかを判定しているのだと思うのですが、具体的に、何の値と何の値を比較しているのでしょうか?<br>シャドウマッピングの原理はわかるのですが、プログラム的にどうなっているのかまだいまいち把握できていません。<br>教えていただけると幸いです。よろしくお願いします。