TOTAL:720, TODAY:96

GLSLによるグーローとフォーンシェーディング

今日は、GLSLによるグーローシェーディングとフォーンシェーディングを紹介します。グーローは頂点ごとに光源計算を行い、頂点の色を計算し、三角形の内部を色補間します。フォーンは三角形内部を法線補間し、画素毎に色を計算します。フォーンの方が重いのですが、スペキュラー等は綺麗になります。

OpenGLの光源計算

プログラムを紹介する前に、OpenGLの光源計算方法を簡単に説明します。OpenGL(DirectXでも同じですが)では、光源による物体表面の色は、主にdiffuse(拡散反射)specular(鏡面反射)から計算します。下の図に示すように、diffuseは光源ベクトルと法線ベクトルとの内積で計算されます。

一方、specularは光源ベクトル、法線ベクトル、視線ベクトルから計算されます。OpenGLの固定パイプラインによる光源計算(GLSLを使用しない光源計算)では、左下の図のように、光源ベクトルと視線ベクトルの足し算であるハーフベクトル法線ベクトルとの内積でspecularを計算します。これは、かなり簡略化したやり方で、計算負荷も低いのですが、今回はせっかくなので、光源の反射ベクトルを計算して、視線ベクトルとの内積を計算する方法でやることにしました。GLSLにはreflectというビルトイン関数があるので、プログラム自体は簡単です。

グーローシェーディング

グーローシェーディングは頂点ごとに光源計算をするので、バーテックスシェーダで主な処理を行います。今回は、光源は点光源であることを前提に作成しました。上では説明しませんでしたが、光源のambient減衰係数も計算するようにしました。プログラムを見ると、光源の様々なパラメータは、gl_LightSource[i]で取得できます。ここで注意してほしいのは、光源の位置を示すgl_LightSource[0].positionは、gl_Vertexやgl_Normalと違い、既にModeViewマトリックスが掛けられた視点座標系での位置になっています。そのため、光源ベクトルは、ModelViewで座標変換された位置座標positionとgl_LightSource[0].position.xyzの差で計算できます。また、プログラムにgl_FrontLightProduct[0]という変数がありますが、これはGLSLのビルトインUniform変数です。光源とマテリアルのそれぞれの係数(ambient, diffuse, specular)の積が格納されています。

// vertex shader of gouraud shading

#define USE_REFLECTION (1)  /* スペキュラー計算に反射ベクトルを使用 */

void main(void)
{
    /* 視点座標で光源計算 */
    vec3 position = vec3(gl_ModelViewMatrix * gl_Vertex);
    vec3 normal = gl_NormalMatrix * gl_Normal;
    
    /* 光源ベクトル */
    vec3 lightVec = gl_LightSource[0].position.xyz - position;
    
    /* 光源までの距離 */
    float dis = length(lightVec);
    
    lightVec = normalize(lightVec);
    
    /* 減衰係数 */
    float attenuation = 1.0 / (gl_LightSource[0].constantAttenuation +
       gl_LightSource[0].linearAttenuation * dis +
       gl_LightSource[0].quadraticAttenuation * dis * dis);

    /* ディフューズ */
    float diffuse = dot(lightVec, normal);

    /* アンビエント */
    gl_FrontColor = gl_FrontLightProduct[0].ambient;

    if (diffuse > 0.0)
    {
#if USE_REFLECTION
        /* 反射ベクトルによるスペキュラー */
        vec3 viewVec = normalize(-position);
        vec3 reflectVec = reflect(-lightVec, normal);
        float specular = pow(max(dot(viewVec, reflectVec), 0.0), 
                             gl_FrontMaterial.shininess);
#else
        /* ハーフベクトルによるスペキュラー */
        vec3 viewVec = normalize(-position);
        vec3 halfVec = normalize(lightVec + viewVec);
        float specular = pow(max(dot(normal, halfVec), 0.0), 
                             gl_FrontMaterial.shininess);
#endif
        gl_FrontColor += 
            gl_FrontLightProduct[0].diffuse * diffuse * attenuation;
          + gl_FrontLightProduct[0].specular * specular * attenuation;
    }
  
    gl_Position = ftransform();
}

ビルトイン関数が多用されていますが、dotは内積計算を行う関数であり、reflectは反射ベクトルを計算します。reflectにおいて、第1引数は入射ベクトルI、第2引数は法線ベクトルNです。法線は正規化されている必要があり、反射ベクトルの計算は次のようになります。これをプログラムしてもいいのですが、ビルトイン関数の方が高速らしいので、そちらを使います。

一方、フラグメントシェーダは、ほとんど空っぽです。バーテックスシェーダで計算した頂点色を使って色補間するだけです。

// fragment shader of gouraud shading

void main (void)
{
    gl_FragColor = gl_Color;
}

フォーンシェーディング

フォーンシェーディングでは、バーテックスシェーダで行った光源計算をフラグメントシェーダが行います。そのため、バーテックスシェーダは、頂点の位置座標や法線をフラグメントシェーダに渡す必要があります。フラグメントシェーダに渡すためには、変数のQualifierとしてvaringを宣言します。Quailiferタイプの詳しい説明は「GLSLのQualifier変数タイプ」に書きましたので、参考にしてください。

// vertex shader of phong shader

varying vec3 position;
varying vec3 normal;

void main(void)
{
    /* 視点座標で光源計算 */
    position = vec3(gl_ModelViewMatrix * gl_Vertex);
    normal = gl_NormalMatrix * gl_Normal;
  
    gl_Position = ftransform();
}

フラグメントシェーダの処理内容は、グーローで行ったバーテックスシェーダの処理がほとんどそのままです。フォーンでは、法線が画素ごとに補間されるので、シェーダー関数内部で、毎回正規化する必要があることに注意してください。

// fragment shader of phong shader

#define USE_REFLECTION (1)  /* スペキュラー計算に反射ベクトルを使用 */

varying vec3 position;
varying vec3 normal;

void main (void)
{
    vec3 fnormal = normalize(normal);
    vec3 lightVec = gl_LightSource[0].position.xyz - position;
    
    /* 光源までの距離 */
    float dis = length(lightVec);
    
    lightVec = normalize(lightVec);
    
    /* 減衰係数 */
    float attenuation = 1.0 / (gl_LightSource[0].constantAttenuation +
                 gl_LightSource[0].linearAttenuation * dis +
                 gl_LightSource[0].quadraticAttenuation * dis * dis);

    /* ディフューズ */
    float diffuse = dot(lightVec, fnormal);

    /* アンビエント */
    gl_FragColor = gl_FrontLightProduct[0].ambient;

    if (diffuse > 0.0)
    {
#if USE_REFLECTION
        /* 反射ベクトル */
        vec3 viewVec = normalize(-position);
        vec3 reflectVec = reflect(-lightVec, fnormal);
        float specular = pow(max(dot(viewVec, reflectVec), 0.0),
                             gl_FrontMaterial.shininess);
#else
        /* ハーフベクトル */
        vec3 viewVec = normalize(-position);
        vec3 halfVec = normalize(lightVec + viewVec);
        float specular = pow(max(dot(fnormal, halfVec), 0.0), 
                             gl_FrontMaterial.shininess);
#endif
        gl_FragColor += 
               gl_FrontLightProduct[0].diffuse * diffuse * attenuation
             + gl_FrontLightProduct[0].specular * specular * attenuation;
    }
}

OpenGLプログラム

今回は、フォーンシェーディングの効果を分かりやすくするために、立方体モデルcubemodel.cを作成しました。頂点バッファを使用し、独立三角形(GL_TRIANGLES)プリミティブを用いることで、1回のglDrawElements呼び出しで立方体が描画できるようにしています。トーラスモデル同様、初期化時に、FUTL_MakeCubeで立方体モデルを作成し、GLUTの描画コールバック関数でFUTL_DrawCubeVBOを呼び出しています。また、初期化時にグーローシェーディングとフォーンシェーディングを読み込み、「s」キーで切り替えることができるようにしました。

int main(int argc, char *argv[])
{
    :   : 途中略 :   :

    /* グーローシェダープログラムの読み込み */
    if (FUTL_LoadShader("gouraud.vs", "gouraud.fs", &grdProg) < 0)
    {
        return -1;
    }

    /* フォンシェダープログラムの読み込み */
    if (FUTL_LoadShader("phong.vs", "phong.fs", &phgProg) < 0)
    {
        return -1;
    }

    /* 立方体モデルの作成 */
    if (FUTL_MakeCube(NULL) < 0)
    {
        return -1;
    }

    :   : 途中略 :   :
}

/* 描画のコールバック関数 */
void display(void)
{
    :   : 途中略 :   :

    /* シェーダプログラムの適用 */
    if (psh != 0)
        glUseProgram(phgProg);
    else
        glUseProgram(grdProg);

    /* 立方体の描画 */
    triangles = FUTL_DrawCubeVBO(count);

    :   : 途中略 :   :
}

サンプルプログラムを動かせば、次のような絵が描画されると思います。フォーンシェーディングにより、面の内側に綺麗なスペキュラーを描画することができます。もちろん、この正方形の面は、4つの頂点しかありません。

一方、「s」キーを押せば、グーローシェーディングとなり、次のような絵が描画されます。頂点部分ではスペキュラーが弱く、頂点の色で正方形内部が補間されるため、スペキュラーの特徴である白い光が描画されません。もちろん、頂点を増やせば、それなりのスペキュラーが描画できますが、フォーンのような丸いスペキュラーにするには、かなりの頂点数が必要です。また、光の角度によっては、正方形の対角線部分(正方形を構成している三角形の境界部分)に色の境界がでることがあります。

最初に説明したハーフベクトルによるスペキュラー計算の場合、フォーンシェーディングでは次のような絵になります。反射ベクトルによる計算と比べると、スペキュラー部分がより大きくなりやすいです。まあ、好みの問題といえば、それまですが。。。。

シェーダとは関係ないですが、キー操作が増えたきたので、「h」キーによるヘルプ表示を追加しました。英語なので間違っている可能性が大ですが、押せば分かると思います。

最新の7件

OpenGL

電子工作

玄箱HG

ホームページ

日記

Copyright (C) 2007 Arakin , All rights reserved.