■ 2005年10月07日 [OpenGL][GLSL] 第2回 Gouraud シェーディングと Phong シェーディング
一度に一つのことしかできない
やらなければならない仕事が二つ以上あると,とてもストレスを感じます.だいたい落ち込んでいるときは物事が決められなくなっているので,仕事が複数あっても優先順位を付けることができません.それに,タスク切り替えにすごく時間がかかる(数時間から1日)ので,効率もよくありません.それで余計に落ち込んでしまいます.周りの人はみんなうまくやってるなぁと思えるので,それでまた落ち込みます.どうしたもんでしょうか.
GLSL の変数
GLSL は C や C++ 言語によく似ていますが,レンダリングのプロセスを反映した仕組みを備えています.特に変数については,アプリケーションプログラムとシェーダプログラムとの間や,シェーダプログラム同士でのデータのやり取りを行うために,以下に示す独特の型修飾子を備えています.また GLSL は,これらにより修飾された多くの組み込み変数を用意しています.
- attribute
- アプリケーションプログラムからバーテックスシェーダに渡す,頂点に関係するデータを格納した変数を表します.位置 (gl_Vertex),色 (gl_Color),法線ベクトル (gl_Normal),テクスチャ座標 (gl_MultiTexCoord0 ほか) など,glVertex*() およびそれと一緒に設定するデータがこれに相当します.この変数はバーテックスシェーダにおいて読み出しのみ可能です.
- uniform
- アプリケーションプログラムからバーテックスシェーダおよびフラグメントシェーダに渡す,光源情報などのあまり変化しないデータを格納した変数を表します.モデルビュー変換行列 (gl_ModelViewMatrix),透視変換行列 (gl_ProjectionMatrix),モデルビュー変換行列と透視変換行列の積 (gl_ModelViewProjectionMatrix),法線ベクトルの変換行列 (gl_NormalMatrix),テクスチャ変換行列 (gl_TextureMatrix[0] ほか),光源情報 (gl_LightSource[0] ほか),材質 (gl_FrontMaterial, gl_BackMaterial) など,図形の描画の際にあらかじめ設定しておくデータがこれに相当します.これはバーテックスシェーダとフラグメントシェーダのどちらからでもアクセス可能なグローバルな変数で,いずれも読み出しのみ可能です.
- varying
- OpenGL の陰影付けに用いられる Gouraud シェーディングでは,頂点ごとに陰影計算を行って頂点における色を求め,ポリゴンの内部の画素の色は頂点の色を補間して決定します.このような処理を実現するために,バーテックスシェーダで varying 変数に値を格納すると,フラグメントシェーダでは格納された値そのままではなく,その値の補間値を取り出すことができるようになっています.バーテックスシェーダで値を格納する varying 変数には,表面色 (gl_FrontColor),背面色 (gl_BackColor),テクスチャ座標 (gl_TexCoord[0] ほか) などがあります.またフラグメントシェーダで補間値を取り出す varying 変数には,フラグメントの色 (gl_Color),テクスチャ座標 (gl_TexCoord[0] ほか) などがあります.gl_BackColor はポリゴンの両面に異なる色を設定する場合(glEnable(GL_VERTEX_PROGRAM_TWO_SIDE) 実行時)に使用し,この場合 gl_Color からは,視点に対するポリゴンの向きによって,gl_FrontColor あるいは gl_BackColor のどちらかの補間値を取り出すことができます.
- const
- 値が変化しない定数を表します.使用できる光源の数 (gl_MaxLights) やテクスチャユニットの数 (gl_MaxTextureUnits) のように,システムに依存した定数を取り出すことができます.
組み込み変数にはシェーダプログラムの計算結果の出力先として用いられるものがあります.このような組み込み変数には型修飾子が与えられていません.
- 出力変数
- バーテックスシェーダやフラグメントシェーダの計算結果は,それぞれ頂点の座標値と画素の色として出力します.バーテックスシェーダでは変数 gl_Position に頂点の位置を出力します.フラグメントシェーダでは変数 gl_FragColor に画素の色を出力します.フラグメントシェーダで値を出力しない(フラグメントを書き込まない)ときは,この変数に書き込む代わりに discard 命令を呼び出します.このほか,バーテックスシェーダでは点の大きさ (gl_PointSize) やクリッピング座標 (gl_ClipVertex),フラグメントシェーダではフラグメントの奥行き値 (gl_FragDepth) を出力することもできます.
- 入力変数
- フラグメントシェーダでは,そのフラグメントの画素位置を gl_FragCoord で調べることができます.またそのフラグメントがポリゴンの表なのか裏なのかを gl_FrontFacing で調べることができます.
なお,GLSL の組み込み変数は,上記の限りではありません.詳しくは GLSL の仕様書を参照してください.また,このような変数をユーザが自分で定義して使用することもできます.シェーダプログラム側で定義した attribute 変数や uniform 変数にアプリケーションソフトウェア側から値を設定したり,バーテックスシェーダで独自の varying 変数を設定してフラグメントシェーダで使用したりすることができます.
拡散反射を実装してみる
前回のプログラムではポリゴンの陰影が失われてしまっていますから,バーテックスシェーダで陰影を計算するようにしてみましょう.現在はフラグメントシェーダで色を設定していますから,これにバーテックスシェーダから得た色の補間値である varying 変数の gl_Color を用いるようにします.
// simple.frag void main (void) { gl_FragColor = gl_Color; }
この状態でプログラムを実行すると,黒い四角が表示されます.
バーテックスシェーダにおいて gl_FrontColor に何も設定していないと,gl_Color には黒が入っています.そこで,バーテックスシェーダで varying 変数の gl_FrontColor に色を設定してみます.vec4() は () 内の値を4要素の実数からなるベクトルに直します.ついでに,gl_Position の値を ftransform() という GLSL の組み込み関数の値にします.この関数は OpenGL の固定機能による座標変換を忠実に再現するものです.
// simple.vert void main(void) { gl_FrontColor = vec4(1.0, 0.0, 0.0, 1.0); gl_Position = ftransform(); }
これを実行すると,先ほどと同じ陰影の付かない赤い四角形が表示されるはずです.これにより,色のデータが varying 変数を介してバーテックスシェーダからフラグメントシェーダに送られていることがわかります.
それではここで,拡散反射光の算出を行ってみましょう.0番目の光源位置は uniform 変数 gl_LightSource[0].position で得られます.また物体表面上の点の視点座標系における位置は,gl_ModelViewMatrix * gl_Vertex で求めることができます.したがって光線ベクトルは,これらの差から求めることができます.またこの点における法線ベクトルは,gl_NormalMatrix * gl_Normal で求めることができます.
vec3, vec4 はそれぞれ3要素,4要素の実数型のベクトルを表します.gl_LightSource[0].position.xyz の .xyz は,ベクトル gl_LightSource[0].position の4つの要素のうち,xyz の3つの成分を(この順で)使用することを示します.normalize() はベクトルを正規化する GLSL の組み込み関数です.
// simple.vert void main(void) { vec4 position = gl_ModelViewMatrix * gl_Vertex; vec3 normal = normalize(gl_NormalMatrix * gl_Normal); vec3 light = normalize((gl_LightSource[0].position * position.w - gl_LightSource[0].position.w * position).xyz);
拡散反射率 diffuse は光線ベクトル light と法線ベクトル normal の内積により求めます.これに光源強度の拡散反射光成分 gl_LightSource[0].diffuse と拡散反射係数 gl_FrontMaterial.diffuse を乗じて,拡散反射光強度を求めます.dot() は内積を求める GLSL の組み込み関数で,その結果が負の時は 0 になるよう GLSL の組み込み関数 max() を用いて dot() と 0 の大きい方を求めます.
float diffuse = max(dot(light, normal), 0.0); gl_FrontColor = gl_LightSource[0].diffuse * gl_FrontMaterial.diffuse * diffuse; gl_Position = ftransform(); }
これでポリゴンに暗めの赤の陰影をつけることができます.
鏡面反射を実装してみる
それでは,これにさらに鏡面反射成分を追加してみましょう.鏡面反射光強度の計算には,視線ベクトル u と光線ベクトル l の中間ベクトル h と,法線ベクトルn' との内積を用いることにします.
視点座標系では視点の位置は原点にあるので,視線ベクトル view は物体表面上の点の位置ベクトルの逆ベクトルになります.まず,これを正規化します.次に正規化した光線ベクトル light と正規化した視線ベクトル view の逆ベクトルとの和から中間ベクトル halfway を求めます.鏡面反射率 specular には,この中間ベクトル halfway と法線ベクトル fnormal の内積を求め,max() 関数を使って負の値が 0 になるようにした後,指数関数 pow() を使って輝き係数 gl_FrontMaterial.shininess によるべき乗したものを用います.
そして光源強度の鏡面反射光成分 gl_LightSource[0].specular と鏡面反射係数 gl_FrontMaterial.specular の積にこの specular を乗じて鏡面反射光強度を求め,これと環境光の反射光強度を先ほど求めた拡散反射光強度に加えて gl_FrontColor に代入します.
// simple.vert void main(void) { vec4 position = gl_ModelViewMatrix * gl_Vertex; vec3 normal = normalize(gl_NormalMatrix * gl_Normal); vec3 light = normalize((gl_LightSource[0].position * position.w - gl_LightSource[0].position.w * position).xyz); float diffuse = max(dot(light, normal), 0.0); vec3 view = -normalize(position.xyz); vec3 halfway = normalize(light + view); float specular = pow(max(dot(normal, halfway), 0.0), gl_FrontMaterial.shininess); gl_FrontColor = gl_LightSource[0].diffuse * gl_FrontMaterial.diffuse * diffuse + gl_LightSource[0].specular * gl_FrontMaterial.specular * specular + gl_LightSource[0].ambient * gl_FrontMaterial.ambient; gl_Position = ftransform(); }
なお,光源強度と反射係数の積は,あらかじめ gl_FrontLightProduct という uniform 変数に格納されています.この部分を置き換えると,最終的なプログラムは次のようになります.
// simple.vert void main(void) { vec4 position = gl_ModelViewMatrix * gl_Vertex; vec3 normal = normalize(gl_NormalMatrix * gl_Normal); vec3 light = normalize((gl_LightSource[0].position * position.w - gl_LightSource[0].position.w * position).xyz); float diffuse = max(dot(light, normal), 0.0); vec3 view = -normalize(position.xyz); vec3 halfway = normalize(light + view); float specular = pow(max(dot(normal, halfway), 0.0), gl_FrontMaterial.shininess); gl_FrontColor = gl_FrontLightProduct[0].diffuse * diffuse + gl_FrontLightProduct[0].specular * specular + gl_FrontLightProduct[0].ambient; gl_Position = ftransform(); }
これで通常の OpenGL による陰影付けと同じ,Gouraud シェーディングが実装できました.
ティーポットのような曲面では,ポリゴンの境界がわずかながら見えてしまいます.
Phong シェーディングを実装してみる
Phong シェーディングは,前述の Gouraud シェーディングで実装したバーテックスシェーダの中の陰影計算を,フラグメントシェーダに移すことにより実現できます.その際,陰影計算を画素単位に行うために,オブジェクト表面上の点の視点座標系での位置と,その点における法線ベクトルが必要になります.そこでローカル変数 position と normal を,ともに varying 変数として宣言し直すことにします.そしてバーテックスシェーダでこれらの値を計算し,フラグメントシェーダでこれらの値の補間値を参照します.position と normal を vec3 型の varying として宣言するので,main の中の position と normal の型宣言の vec3 は削除してください.また,その次の light の計算から gl_Position の計算の前までを削除してください.
// simple.vert varying vec4 position; varying vec3 normal; void main(void) { position = gl_ModelViewMatrix * gl_Vertex; normal = normalize(gl_NormalMatrix * gl_Normal); // 途中削除 gl_Position = ftransform(); }
バーテックスシェーダで計算した position と normal の補間値を得るために,フラグメントシェーダでもこれらを varying 変数として宣言します.また,バーテックスシェーダから削除した部分は,そっくりそのままフラグメントシェーダの main() の中に移します.ただし,この状態ではまだフラグメントシェーダは動作しません(コンパイルできません).
// simple.frag varying vec4 position; varying vec3 normal; void main (void) { vec3 light = normalize((gl_LightSource[0].position * position.w - gl_LightSource[0].position.w * position).xyz); float diffuse = max(dot(light, normal), 0.0); vec3 view = -normalize(position.xyz); vec3 halfway = normalize(light + view); float specular = pow(max(dot(normal, halfway), 0.0), gl_FrontMaterial.shininess); gl_FrontColor = gl_FrontLightProduct[0].diffuse * diffuse + gl_FrontLightProduct[0].specular * specular + gl_FrontLightProduct[0].ambient; }
この計算結果の格納先を,gl_FrontColor から gl_FragColor に書き換えます.また補間によって得られた法線ベクトル normal は正規化されていませんから,ここで改めて正規化します.
// simple.frag varying vec4 position; varying vec3 normal; void main (void) { vec3 light = normalize((gl_LightSource[0].position * position.w - gl_LightSource[0].position.w * position).xyz); vec3 fnormal = normalize(normal); float diffuse = max(dot(light, fnormal), 0.0); vec3 view = -normalize(position.xyz); vec3 halfway = normalize(light + view); float specular = pow(max(dot(fnormal, halfway), 0.0), gl_FrontMaterial.shininess); gl_FragColor = gl_FrontLightProduct[0].diffuse * diffuse + gl_FrontLightProduct[0].specular * specular + gl_FrontLightProduct[0].ambient; }
これで Phong シェーディングが実装できました.正面から光を当てたときに,ハイライトが消失していないことを確認してください.
またティーポットにおいても,ポリゴンの境界が現れるようなことはありません.
(nVIDIA のビデオカードだと,ティーポットの底の中央部が黒くなるのはどうして?)
はじめまして。<br>ここのホームページの内容は私の勉強にとても役立っています。<br>私は最近、GLSLの勉強をはじめたのですが少し質問があります。<br>通常のOpenGLではglViewport()とglOrtho()を使用することで表示領域変更ができますが、GLSLでこのような表示領域変更ができないのでしょうか?<br>OpenGLのmain側でglViewport()とglOrtho()を使用して表示領域を変更しているのですがシェーダーを使用しまうと何も変更していないままに戻ってしまいます。初心的な質問で申し訳ありませんがおしえていただけないでしょうか…
アキカゲさま,コメントありがとうございます.<br>glOrtho() はプロジェクションマトリクスを設定する関数なので,シェーダ内でこの変換を反映するには,バーテックスシェーダで gl_Vertex に gl_ModelViewProjectionMatrix を掛けてやる必要があります.<br><br>gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br><br>あるいは,<br><br>gl_Position = ftransform();
これほど早く返答していただき、ありがとうございます。<br>記述されている通り、バーテックスシェーダのプログラムを書き直すとうまくいきました。<br>これでまたGLSLの勉強を進めていくことができます。<br>このような簡単な質問にお答えいただいて本当にありがとうございました。
はじめまして。<br>学校のい授業でマルチメディア論を履修しました。課題がGouroudシェーディングとPhongシェーディングの違いについて具体的な例を挙げて説明せよと出たのですが、どう答えたらよいでしょうか?教えて下さい。
多面体に対して陰影付けを行う際、まず頂点における法線ベクトルを求めます。Gouroudシェーディングの場合はここで陰影計算を行い、頂点における反射光の色を求めます。そしてポリゴンの内部の色は頂点における反射光の色を補間して求めます。<br> 一方Phongシェーディングの場合はポリゴン内部における法線ベクトルを、頂点の法線ベクトルから補間して求めます。求めた法線ベクトルを用いて、画素単位に陰影計算を行い、反射光の色を求めます。したがって、色を補間するだけで済むGouroudシェーディングに比べてコストがかかる半面、Gouroudシェーディングのもついくつかの問題が発生せず高品質な陰影付けを行うことができます。
お世話になっています。<br>ここのホームページの内容は勉強にとても役立っています。<br><br>「Phong シェーディングを実装してみる」では、頂点シェーダ<br>で法線が計算された後、正規化され、それがvarying変数で<br>フラグメントシェーダに渡されています。<br><br>varying vec3 normal;<br>void main(void)<br>{<br> position = vec3(gl_ModelViewMatrix * gl_Vertex);<br> normal = normalize(gl_NormalMatrix * gl_Normal);<br><br>この法線が、フラグメントシェーダで再び正規化されています。<br>この場合、フラグメントシェーダで正規化する必要はあると<br>思うのですが、頂点シェーダで正規化する必要はないのでは<br>ないでしょうか?頂点シェーダでは法線は使いませんし ...<br><br>// simple.frag<br> <br>varying vec3 position;<br>varying vec3 normal;<br> <br>void main (void)<br>{<br> vec3 fnormal = normalize(normal)<br><br>よろしくお願いします。
まりさま,コメントありがとうございます.<br> ここでバーテックスシェーダで正規化しているのは Gouraud の方法を引きずっているだけに過ぎないので,不要と言えば不要だと思います.<br> でも,二つのベクトルを補間するときに,一方がもう一方よりもずっと大きかったりすると,補間値が大きい方に引きずられてしまう(その結果,例えば面の中央部における補間値が面の法線方向からずれる)ので,補間する前に正規化しておくのもありかな,と思っています.<br> 頂点の法線ベクトルの正規化の有無で粗いポリゴンのスムーズシェーディングではハイライトの形状が結構変わってくるのですが,そのこと自体が無理をしていると言える(からハイライトの形にこだわっても仕方ない)ので,そこそこ細かくポリゴンを分割していれば,頂点の法線ベクトルを正規化する必要性は余りないと思います.
確かに、モデルの法線の長さが異なる場合には有効そうですね。ありがとうございます。
いつも拝見させて頂いてます。<br>GLSLでのライティングについての質問なのですが、<br>ライトの位置(gl_LightSource[0].position.xyz)と補間された頂点座標(varying vec3 position)のベクトルと補間された法線(fnormal)で内積を取ってライトが当たるかを判定していますが、<br>頂点座標にカメラ行列をかけている場合、そのかけた頂点座標がフラグメントシェーダにきますが、<br>このライト位置にもカメラ行列をかける必要がやはりあるのでしょうか?<br>ライト位置は内部的になんの行列もかかっていない元のデータのままでした。
JUN1さま,コメントありがとうございます.この辺,いい加減に書いているので,突っ込みどころ満載だと思いますw<br><br>おっしゃる通り,頂点位置に gl_ModelViewMatrix をかけた場合は陰影計算を視点座標系で行う必要がありますから,光源位置をワールド座標系で設定している場合は,それにビュー(視野)変換行列をかけたものを用いる必要があります.もし光源位置をそのまま使う場合は陰影計算をワールド座標系で行う必要がありますから,頂点位置の方にモデル変換行列をかけたものを用いる必要があります.<br><br>しかし,モデルビュー変換行列 gl_ModelViewMatrix はモデル変換とビュー変換の積なので,これをモデル変換行列とビュー変換行列に分けて使うことはできません.シェーダで光源位置を視点座標系に移すには,gl_ModelViewMatrix とは別に,ビュー変換行列を uniform で渡す必要があります.<br><br>ですが,光源位置 gl_LightSource[0].position も uniform なので,描画単位で変化することはありません.したがって,シェーダでこれにビュー変換行列を乗じる処理を書いても,同じ計算が繰り返されるだけになります.この辺はシェーダコンパイラがよしなに最適化してくれるのではないかと思いますが,やはり gl_LightSource[0].position には CPU 側で光源位置にビュー変換行列をかけて,その結果(視点座標系に置ける光源位置)を格納すべきではないかと思います.シェーダで uniform である gl_LightSource[0].position に uniform である変換行列をかけるのは,なんかもったいない気がしますので.<br><br>なお,このサンプルは光源は常に視点側から照射しているので(ヘッドライト),光源位置は視点座標系で設定した方が簡単だと思い,CPU 側ではあえて何もしておりません.いい加減ですみません (^_^;)
色々参考になります!<br>分かりました!ありがとうございます。
こんにちわ。質問があります。4つ目のコードから四角が赤にならなくて、<br>float diffuse = max(dot(light, normal), 0.0);<br> <br> gl_FrontColor = gl_LightSource[0].diffuse * gl_FrontMaterial.diffuse * diffuse * (1.0,0.0,0.0,1.0);//ここを変更しました。<br> <br> gl_Position = ftransform();<br>}<br>とすることで赤くなったのですが、鏡面反射のところがわかりません。影は付いているのですが、鏡面反射しません。
なるさま、コメントありがとうございます。<br>赤い色は gl_FrontMaterial.diffuse に格納されています。<br>これには C のプログラム側の glMaterialfv() で設定した値が入っています。<br>ここでどういう値が設定されているか、ご確認頂けますでしょうか。<br>よろしくお願いします。
遅レスすみません。レスありがとうございます。<br>不精して、QuartzComposerのGLSLシェーダでやってしまっていました。<br>当方モーショングラフィックをやっている身で、今はOpenFrameWorksでGLSLをやっています。<br>しかし始めたばかりで、いつも気がつくとこのページを見ています。大変貴重な文献です。ありがとうございます。
はじめまして。質問があります。<br>光線ベクトル light の計算に w 座標が出現するのはなぜでしょうか?<br>ほかのページで説明されていたら申し訳有りません。<br><br>普通は位置ベクトルの w値 は 1 なので<br>gl_LightSource[0].position * position.w - gl_LightSource[0].position.w * position<br>は<br>gl_LightSource[0].position - position<br>と同じかと思います。<br>この場では眠っている活用テクニックがあるのでしょうか。
やまさま、コメントありがとうございます。<br>位置は同次座標で表していますので、w で割って実座標に戻してから引き算する必要があります。<br>しかし、それだと w = 0 の場合(無限遠、光源の場合は平行光線)は計算できません。<br>その場合は場合分けすればいいのですが、シェーダではできるだけ分岐を使いたくありません。<br>そこで通分してから分子だけで引き算を行います。割り算は行いません。<br>分母は捨ててしまいますが、求めるのはベクトルなので、正規化すれば問題ありません。
ありがとうございます。<br>おかげでどんな同次座標値が来ても大丈夫なのは理解できました。<br>それでは物体表面相当の同次座標が position.w ≠ 1 になる事もあるのでしょうか?<br>単に通分計算の意図を強調するために「 *position.w」を残しているのでしょうか?
やまさま、<br>モデリング時に w = 1 として、それを変更しなければ、もちろん w ≠ 1 となることはありません。<br>この場合は陰影付けなので光源が無限遠にある場合を想定していますが、<br>例えばシャドウボリュームのポリゴンでは意図的に w = 0 にして、<br>影の範囲を光源の反対側の無限遠に伸ばしたりしますし、<br>Vertex Blending による変形では頂点位置を同時座標で合計したりするので、<br>w には合計した頂点の数が入ります(だから w で割れば頂点位置の「平均」が出ます)。<br>モデリング変換で w を変化させない決めておけば w = 1 であることを仮定して問題ないと思いますが、<br>陰影付けの段階で前段階のモデリング変換でどのような処理を行うのかわからなければ、<br>私は心配性なので、もしかしたら w ≠ 1 になってるかも知れないと考えてしまいます。
ありがとうございます。勉強になります。<br>それと別件ですが、今のMac (私の場合 OSX 10.9/Xcode 6.2) では、<br> glutInitWindowSize(〜,〜);<br>を挟まないとウィンドウを開いてくれませんでした。(glsl1〜glsl5.zip)<br>本来ならなんらかの標準サイズが適用されるはずだと思うのですが、<br>既にGLUT自体が非推奨なので仕方のない事かもしれません。