TOTAL:698, TODAY:74

GLSLでバンプマッピング

前回、法線補間によるシェーディング(いわゆるフォーンシェーディング)を紹介しましたので、今回は、バンプマッピングを紹介します。バンプマッピングは、法線マップと呼ばれるテクスチャを使用します。普通のテクスチャは、各画素にカラーRGBAが格納されていますが、法線マップでは、各画素に法線が格納されており、それを表面にマッピングすることで、凸凹感を表現するものです。

GIMPによる法線マップの作成

法線マップの作り方は色々ありますが、ここでは高さマップ(各画素に高さを格納したテクスチャ)から法線マップを作成する方法を紹介します。まず高さマップですが、例として次のような画像を用意しました(この画像自体もGIMPで作成しています)。白い部分が高く、黒い部分が低いことを表しています。下の画像では、淵の部分、4隅の丸、星型の部分が凹んでいる感じです。

GIMPには、高さマップから法線マップを生成するプラグインが下記サイトにあるので、それを使用します。

ダウンロードして展開すると、4種類のファイルができるので、各々を下記のディレクトリにコピーします。

normalmap.exe
glew32.dll
C:\Documents and Settings\ユーザ名\.gimp-2.2\plug-ins
GIMPのプラグインディレクトリです。GIMP2.2ならば上記のようなディレクトリがデフォルトです。
libgdkglext-win32-1.0-0.dll
libgtkglext-win32-1.0-0.dll
C:\Program Files\Common Files\GTK\2.0\bin

これで、GIMPで法線マップを作成する準備ができました。GIMPを起動し、上の高さマップ画像を読み込み、メニューの「フィルタ」→「マップ」→「normal map」を選択すれば、次のようなダイアログが現れます。

色々なオプションがありますが、Filterとしては、「4 sample」や「Sobel 3x3」が適当だと思います。「4 sample」を選んだ場合、Scaleも適宜調整した方がいいです。ここで最も注意すべき点は、Optionsの「Invert Y」にチェックを入れることです。GIMPは左上原点の座標系、OpenGLは左下原点の座標系なので、Y座標を反転させる必要があります。こうすることで、OpenGLの座標系に合った法線マップを作成できます。法線マップは、正規化された法線(x,y,z)(R,G,B)に置き換えて表現します。計算式は、R = 255* (0.5 * x + 0.5) であり、x=-1.0の場合には、R成分が0x=1.0の場合には、R成分が255となります。y, zも同様に、G, Bに変換されます。先ほどの高さマップの場合、次のような法線マップになります。

法線マップでは、平らのところは法線が(0.0, 0.0, 1.0)になるため、法線マップのRGB値は(127, 127, 255)となり、青っぽい色になります。また、上側の淵の部分では、法線は斜め上方向(OpenGL座標ではx=0, +y, +z方向)を向いているため、緑っぽい色になります。一方、下側の淵の部分は、斜め下方向(OpenGL座標ではx=0, -y, +z方向)を向いているため、紫っぽい色になります。
説明が長くなりましたが、上記のことに注意して法線マップを作成します。先のダイアログに戻れば、右下のOKボタンを押せば、法線マップに変換されるので、それを保存します。このサンプルではBMPファイルとして保存しています。

バンプマップの原理

バンプマップでは、モデルの法線を、法線マップの法線に置き換えるのですが、その際、モデルの法線を、法線マップの座標系に変換する必要があります。図にすると、次のような感じです。

この変換に必要なのが、上の図にある法線ベクトルN、接線ベクトルT、従法線ベクトルBです。シェーダプログラムでは、視点座標系で光源計算等を行うことが多くなりますが、視点座標系でのN, T, Bが分かれば、視点座標系から法線マップの座標系(接平面の座標系)への変換行列は次のようになります。

従法線ベクトルBは、法線ベクトルNと接線ベクトルTから外積で求めることができます。そのため、各頂点で接線ベクトルTを決める必要があります。接線ベクトルは法線と垂直なベクトルであるため、無数の方向が考えられますが、この接線ベクトルTは、マッピングする法線マップの+x方向と一致させる必要があります。即ち、上で示した図のように、貼り付ける法線マップの+x方向と一致するように、接線ベクトルTを決める必要があります。

バーテックスシェーダ

バーテックスシェーダでは、まず視点座標系における座標位置、光源ベクトル、法線ベクトル、接線ベクトルを求めます。接線ベクトルは、attribute変数としてOpenGLプログラムから頂点ごとのデータとして渡されます。attribute変数の詳しい説明は「GLSLのQualifier変数タイプ」に書きましたので、参考にしてください。そして、法線ベクトルnと接線ベクトルtの外積から従法線ベクトルbを求めています。外積を求めるビルトイン関数crossの引数の順番には注意してください。逆にすると、従法線ベクトルbも逆になります(※私はnからtに回す右ねじの法則で覚えています)。その後、前節で示した座標変換を内積関数dotにより行い、法線マップ座標系での視線ベクトルと光源ベクトルを求めています。そして、この二つのベクトルをフラグメントシェーダに渡します。フラグメントシェーダでは、法線マップ座標系で光源計算を行うようにします。

// vertex shader of bump mapping

/* 接線ベクトル */
attribute vec3 tangent;

varying vec3 viewVec;
varying vec3 lightVec;

void main(void)
{
    /* 位置を視点座標に変換 */
    vec3 pos = vec3(gl_ModelViewMatrix * gl_Vertex);
    vec3 lgt = gl_LightSource[0].position.xyz - pos;

    /* 法線と接線を正規化 */
    vec3 n = normalize(gl_NormalMatrix * gl_Normal);
    vec3 t = normalize(gl_NormalMatrix * tangent);
    
    /* 従法線 */
    vec3 b = cross(n, t);
    
    /* 法線マップ座標系(接空間)における視線ベクトル */
    viewVec.x = dot(t, pos);
    viewVec.y = dot(b, pos);
    viewVec.z = dot(n, pos);
    viewVec = normalize(viewVec);

    /* 法線マップ座標系(接空間)における光線ベクトル */
    lightVec.x = dot(t, lgt);
    lightVec.y = dot(b, lgt);
    lightVec.z = dot(n, lgt);
    lightVec = normalize(lightVec);

    /* 法線マップのテクスチャ座標 */
    gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;

    gl_Position = ftransform();
}

フラグメントシェーダ

フラグメントシェーダでは、uniform変数によりOpenGLプログラムから法線マップテクスチャを受け取ります。そして、テクスチャの画素値、即ちRGB化された法線ベクトルは、texture2DProjで取得します。RGB値は[0.0, 1.0]の範囲であるため、計算式 vec3(normal) * 2.0 - 1.0; により[-1.0, 1.0]の正規化された法線にしています。残りは、フォーンシェーディングで行った光源計算とほぼ同じです。今回はややこしくなるため、減衰係数の計算を省略しています。また、反射ベクトルではなく、ハーフベクトルを使用することにしました。

// fragment shader of bump mapping

uniform sampler2D nrmmap;
 
varying vec3 viewVec;
varying vec3 lightVec;

void main (void)
{
    /*
     * 法線マップ座標系(接平面)での光源計算
     */
    /* 法線マップ */
    vec4 normal = texture2DProj(nrmmap, gl_TexCoord[0]);
    vec3 fnormal = vec3(normal) * 2.0 - 1.0;
    
    /* 光源ベクトル */
    vec3 flight = normalize(lightVec);
    
    /* 法線と光源との内積 */
    float diffuse = dot(flight, fnormal);

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

    if (diffuse > 0.0)
    {
        /* ハーフベクトル */
        vec3 fview = normalize(viewVec);
        vec3 halfVec = normalize(flight - fview);
        float specular = pow(max(dot(fnormal, halfVec), 0.0), 
                                    gl_FrontMaterial.shininess);

        gl_FragColor += gl_FrontLightProduct[0].diffuse * diffuse
                     +  gl_FrontLightProduct[0].specular * specular;
    }
}

OpenGLプログラム

OpenGL側のプログラムで、これまでと大きく異なる点は、頂点ごとに異なる接線ベクトルをバーテックスシェーダに渡すことです。まず次のように、トーラスモデル作成時に接線ベクトルを頂点バッファとして登録しています。頂点バッファへの登録は、法線同様これまでと同じです。

int FUTL_MakeTorus(
        char *texFile
)
{
    :   : 途中略 :   :

    /* 法線の登録 */
    glGenBuffers(1, &nrmID);
    glBindBuffer(GL_ARRAY_BUFFER, nrmID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(torusNrm), torusNrm, 
        GL_STATIC_DRAW);

    /* 接線の登録 */
    glGenBuffers(1, &tgtID);
    glBindBuffer(GL_ARRAY_BUFFER, tgtID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(torusTgt), torusTgt, 
        GL_STATIC_DRAW);
    :   : 途中略 :   :
}

描画する際の接線データの設定ですが、まず、glGetAttribLocation関数により、シェーダプログラムからattribute変数tangentのインデックスを取得します。

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

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

    /* バーテックスシェーダのattribute変数tangentのインデックス取得 */
    tangentLoc = glGetAttribLocation(shdProg, "tangent");

    :   : 途中略 :   :
}

そして、次のように、glEnableVertexAttribArrayglVertexAttribPointerで頂点データの有効化とポインタの設定を行います。通常のglEnableClientStateやglNormalPointerと比較すれば、使用方法は分かると思います。詳しくはOpenGLのマニュアルを見てください。簡単な説明でよければ、「GLSLのQualifier変数タイプ」にも少し書きましたので、参考にしてください。

int FUTL_MakeTorus(
        char *texFile
)
{
    :   : 途中略 :   :

    /* 法線の設定 */
    glEnableClientState(GL_NORMAL_ARRAY);
    glBindBuffer(GL_ARRAY_BUFFER, nrmID);
    glNormalPointer(GL_FLOAT, 0, 0);

    /* 接線の設定 */
    glEnableVertexAttribArray(tangentLoc);
    glBindBuffer(GL_ARRAY_BUFFER, tgtID);
    glVertexAttribPointer(tangentLoc, 3, GL_FLOAT, GL_FALSE, 0, 0);

    :   : 途中略 :   :
}

長い説明でしたが、これで終わりです。サンプルプログラム(Linux版はこちらから)を動かせば、次のような絵が描画されます。バンプマッピングにより、淵の部分、星型の部分、4隅の丸い部分が窪んで見えると思います。

上で説明したように、貼り付ける法線マップの座標系と一致させるように、接線や従法線を求めないと、光が当たっているのに暗かったり、当たっていないのに明るかったりして、凸凹が逆に見えたりします。OpenGLのテクスチャ座標系が左下原点であることにも注意してください。

最新の7件

OpenGL

電子工作

玄箱HG

ホームページ

日記

Copyright (C) 2007 Arakin , All rights reserved.