[Work/Class/OpenGL/2_OpenGL2toX]

フラグメントシェーダとライティング(Phongシェーディング)

フラグメントシェーダ

前項でやったように,頂点シェーダ(Vertex Shader)は,頂点情報を,Model行列,View行列,Projection行列を合算して(もしくはそれぞれ個別に)受け取り,頂点の位置を画面に描画する機能を持っている.

それに対して,フラグメントシェーダ(Fragment Shader)は2次元に投影された状態から,2次元のピクセルレベルで色などを弄るための機能を持つシェーダプログラムである.

頂点シェーダプログラムの中でvaryingキーワードをつけて宣言された行列や配列は,頂点シェーダからフラグメントシェーダへ行列や配列を送り込むために使われる.本項で扱うような通常のPhongシェーディング等では,もともとはメインプログラムから送り込まれている頂点の情報が格納されている値(主に法線情報,頂点の色情報)をコピーしてそのまま引き渡すことが多いが,中身を弄ることも可能である.

ライティング

ライティングとは,「ライト」を設定するだけではなく,それによって色をつけることも含まれる.

古典的なグローシェーディング(Gouraud Shading)という手法は,頂点シェーダの方でライティングの計算を行うが,あまり実用的ではなく,現在では基礎的なライティングの手法として,本項で取り上げるフラグメントシェーダでライティング処理を行う「フォンシェーデイング(Phong Shading)」が一般的に用いられる.

法線ベクトル

法線ベクトル(Vector Normal, Vector Unit Normal)とは,平面に対して垂直に立ち上がるベクトル(直行ベクトル)を単位ベクトルにしたもののことである.3Dモデルは全て複数枚の平面(ポリゴン)からできているので,法線ベクトルを考えることができる.Phoneシェーディング(やGouraudシェーディング)ではこの法線を使って色付け(平行光源,環境光,反射光)等を行う.

頂点ABCからなる三角形のポリゴンの法線を求めるには以下の方法を取れば良い.

  • 直行ベクトル = ベクトルABとベクトルACの外積(つまり「頂点B-頂点A x 頂点C-頂点A」)
  • 法線ベクトル = 直行ベクトルの単位ベクトル

ただし,頂点ABCの順番に注意する必要がある,

OpenGLは「右手座標系」(親指がX, 人差し指がY,中指がZ,指の付け根が原点[0,0,0])と呼ばれる座標系を持っていて,三角形ポリゴンの頂点ABCは反時計回りに記述された面が「表」となり,そちらから法線ベクトルが飛び出ることになる.(Windows固有の3D環境であるDirect3Dでは逆で左手座標系であるので,法線ベクトルの計算の際に注意する必要がある)

幸いこの授業で扱うC++のライブラリであるglmには,直行ベクトル(外積)を求める関数crossとそれを単位ベクトル化する関数normalizeが用意されているので,これをそのまま使う.

//3点の座標vec3型(x, y, z)のコンストラクタを呼び出して初期化して保持
glm::vec3 pointA = glm::vec3(0.5, 0.2, 0.6);
glm::vec3 pointB = glm::vec3(0.4, 1.1, 0.7);
glm::vec3 pointC = glm::vec3(0.8, 1.2, 0.9);

// glm::crossで外積を求め,glm::normalizeで単位ベクトル化する
glm::vec3 vectorNormal = glm::normalize(glm::cross(pointB - pointA, pointC - pointA));

法線ベクトルは,3頂点から構成される1つの面に対して生成されるものであるが,今まで述べてきた通り3DCGでは頂点単位で計算を行うので,頂点シェーダプログラムとフラグメントシェーダプログラムに渡す際に,3つの頂点全てが同じ法線ベクトルを持っているようにデータを作っておく.つまり,上のコードで3次元のvectorNormalが求められるが,

glm::vec3 vectorNormalPointA = vectorNormal;
glm::vec3 vectorNormalPointB = vectorNormal;
glm::vec3 vectorNormalPointC = vectorNormal;

例えばこのようにして3つの変数に代入しておく必要がある.(実際のプログラム中では,頂点がそうであるように,配列に代入して取り扱うことが多い.)

平行光源(Directional Lighting)

地球の人間レベルで見た「太陽からの光」をイメージするとわかりやすいが,「無限遠からの強い光」を表す.太陽は全体から光を発する「巨大な球」,つまり円放射的に全体に光を発しているのだが,太陽と地球の距離と人間の身長スケールでは「平行に光の線が降り注いでいる」と考えることができる.

コード例中では,lightDirectionという座標指定が「平行光源の向き」を表す.指定した座標から原点(0, 0, 0)に向かって光が降り注いでいるというイメージである.実際の光源は「原点から座標に向かって進んだ直線の無限遠」となる.

環境光(Ambient Lighting)

環境光とは,自然界で光が乱反射していて,例えば直接光が当たっていないところもなんとなく見える,という状態をシミュレートするものである.

簡単に言えば「(場所や座標に依らず)見える範囲全体が薄明るい」だと思って良い.この環境光の存在によって,特に並行光源などのライトを設定しなくても「色」が存在しうる世界になる.

環境光は1.0がRGBそれぞれの最大値(つまり画面が真っ白)だとすると,0.05〜0.2ぐらいの範囲に収めるのが妥当と言われている.今回のコード例ではRGB各色を0.1に指定している.

環境光では「diffuse(拡散)」と呼ばれる色パラメータが重要となる.diffuseとは光(例えば白の光)が当たった時に,どのような色の光を反射するか,というイメージである.

光の三原色に対して色材の三原色を思い出して欲しい.「物体が赤い色をしている」の定義は,「白い光(通常の光)が当たった時に赤く見える物体は,光のRGBの成分のうちR成分のみを反射して,G成分とB成分は吸収してしまう表面材料を持つ物体」である.つまりdiffuseの中身が(R:1, G:0, B:0)であれば,赤い色の物体,ということである.

通常,環境光は白(つまりRGBが同じ値)なので,このdiffuseのRGB成分によって「物体の色」が決まることになる.

反射光(Specular Lighting)

簡単に言えば,光沢の反射率のことである.並行光源の光線のポリゴン平面への入射角度とカメラの位置によっては,ポリゴンが光って見える,ということであり,法線ベクトルと視点(と見ている先)が決まれば,あとはspecularの値によって,反射するの強さを決定することができる.

このspecularの各要素の値が小さいということは,あまり反射しないマットな質感,ということであり,逆にspeclarの各要素の値が大きいということは,金属のように光り輝く質感,ということである.

一般的にspecularはRGBを同値,すなわちRGBが同じ反射率を持つ,と設定することが多い.

コード例

フラグメントシェーダ側のプログラム,すなわちPhoneシェーディング本体の処理は「こんなもんか」レベルの理解で良い.

メインプログラムのソースコード

頂点シェーダのソースコード

// 頂点処理
attribute vec3 vertexPositionFromMain;
uniform mat4 finalMVP;

// 新しくメインプログラムから送られる
attribute vec4 vertexColorFromMain;
attribute vec3 vectorNormalFromMain; // 法線情報

// フラグメントシェーダへ送る変数
varying vec4 vertexColorToFragment;
varying vec3 vectorNormalToFragment; 

void main(void){
    vertexColorToFShaderFromVShader = vertexColorFromMain;
    vectorNormalToFShaderFromVShader = vectorNormalFromMain;

    // vec4 v = vec4(vertexPositionFromMain, 1);
    // もしくは以下のように書く
    vec4v = vec4(vertexPositionFromMain.x,
                 vertexPositionFromMain.y,
		 vertexPositionFromMain.z,
		 1.0);
    gl_Position = finalMVP * v;
}

フラグメントシェーダのソースコード.

// ピクセル処理
precision mediump float; 
// フラグメントシェーダの浮動小数点(float)の計算制度を表す.
// 「mediump」で十分
// 重いときにはlowpにする.

uniform mat4 inverseMatrixFromMain;
uniform vec3 lightdirectionFromMain;
uniform vec3 eyeDirectionFromMain;
uniform vec4 ambientColorFromMain;

varying vec4 vertexColorToFShaderFromVShader;
varying vec3 vectorNormalToFShaderFromVShader;

void main(void){
    // 逆行列を生成する
    vec3 inverseLight = 
        normalize(inverseMatrixFromMain * vec4(lightDirectionFromMain, 0.0)).xyz;
    vec3 inverseEye = 
        normalize(inverseMatrixFromMain * vec4(eyeDirectionFromMain, 0.0)).xyz;

    vec3 halfLE = 
        normalize(inverseLight + inverseEye);

    // clamp関数は,scaleみたいなもんで,
    // 第1引数で与えたvecやmat型の変数の各要素を,第2引数を最低値,第3引数を最高値として
    // スケーリングしてくれるGLSLの組み込み関数
    float diffuse = 
        clamp(dot(vectorNormalToFShaderFromVShader, inverseLight), 0.0, 1.0);

    float specular = 
        pow(clamp(dot(vectorNormalToFShaderFromVShader, halfLE), 0.0, 1.0), 50.0);
    vec4 destColor = 
        vertexColorToFShaderFromVShader * 
	  vec4(vec3(diffuse), 1.0) + 
	  vec4(vec3(specular), 1.0) + 
          ambientColorFromMain;

    gl_FragColor = destColor;
}