TOTAL:689, TODAY:65

GLSLでキューブ環境マッピング

久しぶりのOpenGLプログラムですが、今回はキューブ環境マッピング(写りこみ)を紹介します。環境マッピングにはスフィアマッピング(球状マッピング)やシリンダーマッピング(円筒マッピング)等もありますが、ここでは、GPUでサポートされているキューブ環境マッピングを使います。キューブ環境はテクスチャを6枚用意する必要がありますが、ハードウェアでの実装が簡単なので、GLSLでも標準のマッピング手法となっています。

キューブ環境マッピングの原理

キューブ環境マッピングは、次のような6枚の正方形テクスチャを必要とします。これらのテクスチャを作成するのは大変ですが、Terragenという景観作成ソフトを使用すれば、割と簡単に作成することができます。また、OpenGLでは、下のようなテクスチャではなく、上下左右が反転したものを用意する必要があります。その理由は「Terragenによるキューブ環境マップ作成」のページに書きました。良ければ参考にしてください。

下図のように、上記のようなテクスチャを立方体の内側の6面に貼り付け、視線ベクトルから反射されるベクトル(反射ベクトル)で決定されるテクスチャ画素の色を、物体に写りこませたものがキューブ環境マッピングです。

反射ベクトルが立方体とぶつかる位置は、反射ベクトルの開始位置(即ち、物体の反射位置)と反射方向によって決まりますが、キューブ環境マッピングでは、無限遠に立方体があると考え、反射方向(即ち、反射ベクトル)だけで決定します。
上の図のように、環境マッピングは視線の反射ベクトルで決まりますが、その反射ベクトルはワールド座標系で計算しなければなりません。なぜなら、上記の仮想的な立方体はワールド座標に配置されているからです。しかし、OpenGLではワールド座標変換と視点座標変換を区別していないため、ワールド座標系で色々な計算をするのは面倒です。そのため、下記のプログラムでは、ほとんどの計算を視点座標系で行いますが、キューブ環境テクスチャをサンプリングする場合だけ、ワールド座標に変換して求めています。

バーテックスシェーダ

バーテックスシェーダでは、視点座標系における座標位置、光源ベクトル、法線ベクトルを求めます。プログラムでは、前回紹介したバンプマッピングも同時に使用しているため、接線ベクトルも視点座標系に変換しています。また、光源パラメータのposition.wの値により平行光源と点光源を区別するようにしました。だからと言って、固定パイプラインの光源計算をきちんとしている訳ではないので、注意してください。

// vertex shader of bump & cube environment mapping

attribute vec3 tangent;

/* 視点座標系 */
varying vec3 view;      /* 視線ベクトル */
varying vec3 light;     /* 光源ベクトル */
varying vec3 nrm;       /* 法線ベクトル */
varying vec3 tgt;       /* 接線ベクトル */

void main(void)
{
    /* 位置を視点座標に変換 */
        view = vec3(gl_ModelViewMatrix * gl_Vertex);

    /* 光源ベクトル */
    if (gl_LightSource[0].position.w == 0.0)
        light = gl_LightSource[0].position.xyz;
    else
        light = (gl_LightSource[0].position.xyz / 
                 gl_LightSource[0].position.w) - view;

    /* 視点座標系での法線と接線 */
    nrm = gl_NormalMatrix * gl_Normal;
    tgt = gl_NormalMatrix * tangent;
    
    /* テクスチャ座標の変換 */
    gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;

    gl_Position = ftransform();
}

フラグメントシェーダ

フラグメントシェーダでは、uniform変数によりOpenGLプログラムから法線マップテクスチャ、環境マップテクスチャ、及び視点座標変換の回転成分(3x3)の逆行列を受け取ります。上でも述べたように、キューブ環境マッピングをサンプリングする場合、視点座標系での反射ベクトルでは正しい値が得られないため、ワールド座標系での反射ベクトルを計算するためです。
まず、バンプマッピングの計算ですが、前回紹介した方法と原理的には同じです。しかし、計算を視点座標系で行っているところが異なります。法線マップの座標系(接平面)から視点座標に変換するマトリックスtoViewを求め、法線マップから得られた法線を視点座標に変換しています。
キューブ環境マッピングでは、まず、視点座標系での反射ベクトルをreflect関数で求め、それををワールド座標系に変換しています。その後、textureCube関数により、環境マッピングの色を取得しています。textureCubeに渡す座標は2Dではなく、3D(もしくは4D)座標でなければなりません。
最後に、光源計算により得られた色と、環境マッピングにより得られた色を反射率REFLECTIVITYで合成しています。REFLECTIVITYが1.0の場合、クロムメッキのような完全反射の外観になり、0.0の場合、プラスチックのような外観になります。但し、光源計算で得られた色と、反射で得られた色を合成するのは正しい方法ではないですが、それらしく見えるため、こうしています。

/// fragment shader of bump & cube environment mapping

#define REFLECTIVITY    (0.5)

uniform mat4 invEyeRot;         /* 視点座標変換の回転成分の逆行列 */         
uniform sampler2D nrmmap;       /* 法線マップ */
uniform samplerCube envmap;     /* 環境マップ */
 
/* 視点座標系 */
varying vec3 view;      /* 視線ベクトル */
varying vec3 light;     /* 光源ベクトル */
varying vec3 nrm;       /* 法線ベクトル */
varying vec3 tgt;       /* 接線ベクトル */

void main (void)
{
    float offsetEnv;
    vec4 matColor;
    
    /* 法線マップ座標から視点座標への変換マトリックス */
    vec3 fn = normalize(nrm);
    vec3 ft = normalize(tgt);
    vec3 fb = cross(fn, ft);
    mat3 toView = mat3(ft, fb, fn);
  
    /* 法線マップ */
    vec4 color = texture2DProj(nrmmap, gl_TexCoord[0]);
    vec3 normal = vec3(color) * 2.0 - 1.0;
    
    /* 法線を視点座標系に変換 */
    vec3 fnormal = toView * normal;
    vec3 flight = normalize(light);
    
    /* 視線ベクトル */
    vec3 fview = normalize(view);
    
    /* 法線と光源との内積 */
    float diffuse = dot(flight, fnormal);

    /* アンビエント */
    matColor = gl_FrontLightProduct[0].ambient;
    
    /* 通常の光源計算 */
    if (diffuse > 0.0)
    {
        vec3 halfway = normalize(flight - fview);
        float specular = pow(max(dot(fnormal, halfway), 0.0), 
                                    gl_FrontMaterial.shininess);
    
        matColor += gl_FrontLightProduct[0].diffuse * diffuse
                 +  gl_FrontLightProduct[0].specular * specular;
    }

    /* 
     * 視点座標系の反射ベクトルをワールド座標に変換(視点座標の逆変換)し、
     * 変換された反射ベクトルでキューブマップの色を求める
     */
    vec3 texCoord = mat3(invEyeRot) * reflect(fview, fnormal);
    vec4 envColor = textureCube(envmap, texCoord);
    
    /* 反射率に応じた色の混合 */
    gl_FragColor = mix(matColor, envColor, REFLECTIVITY);
}

NVIDIAドライバー162.18からOpenGL2.1がサポートされたようですが、このバージョンでは、上記シェーダのように4x4行列mat4をmat3でキャストすると、次のようなワーニングが出ます。

Shader Info Log
(58) : warning C7536: OpenGL does not allow matrix casts without 
#version 120 or later

そのため、サンプルプログラムでは、OpenGLプログラムから渡す行列invEyeRotを3x3にして、シェーダプログラムはmat3で受け取るように変更しました。

OpenGLプログラム

キューブ環境マッピングを行うには、6枚のテクスチャをキューブ環境テクスチャとして登録する必要があります。テクスチャターゲット名は、GL_TEXTURE_2Dではなく、GL_TEXTURE_CUBE_MAPを使用します。これはプログラムを見てもらった方が分かりやすいと思います。

/*
 * キューブマップテクスチャの読み込み
 */
int FUTL_LoadCubeTexture(
    char *name[],
    unsigned int *lpTexID
)
{
    ImageData img;
    unsigned int texID;
    int i;
    GLenum target[] = {       /* テクスチャのターゲット名 */
        GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
        GL_TEXTURE_CUBE_MAP_POSITIVE_X,
        GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
        GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
    };

    glGenTextures(1, &texID);
    glBindTexture(GL_TEXTURE_CUBE_MAP, texID);

    /* テクスチャ画像は4バイトアライメント */
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

    for(i = 0; i < 6; i++)
    {
        memset(&img, 0, sizeof(img));
        if (loadBMP(&img, name[i]) < 0)
        {
            fprintf(stderr, "%s is not found!!\n", name[i]);
            return -1;
        }

        glTexImage2D(target[i], 0, GL_RGBA, 
            img.width, img.height, 0, GL_RGBA, 
            GL_UNSIGNED_INT_8_8_8_8_REV,
            img.data);

        releaseBMP(&img);
    }

    glTexParameteri(GL_TEXTURE_CUBE_MAP, 
                        GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, 
                        GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, 
                        GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, 
                        GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, 
                        GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

    *lpTexID = texID;

    return 0;
}

視線の反射ベクトルをワールド座標に変換するために、視点変換の逆行列を求めます。まじめに4x4行列の逆行列計算を行ってもよいのですが、実際に必要なのは回転成分の3x3の部分であり、今回扱う視点変換には拡大縮小がないことから転置行列で求めることにしました。この二つの条件が成り立つ場合は、転置行列は逆行列になります。プログラムでは、マウス操作による視点の回転行列eyeRotから転置行列invEyeRotを求めています。FUTL_MatTransposeは転置行列を求める関数です。

void display(void)
{
    :   : 途中略 :   :

    /* 視点 */
    gluLookAt(0.0f, 0.0f, 120.0f, 0.0f, 0.0f, 0.0f, 
            0.0f, 1.0f, 0.0f);

    /* マウス操作による視点の回転 */
    glMultMatrixf((float *)(&eyeRot));

    /* 視点回転の逆行列(転置行列) */
    FUTL_MatTranspose(&invEyeRot, &eyeRot);

    :   : 途中略 :  
}

上で述べた、キューブマップテクスチャや逆行列、及び法線マップや接線をシェーダプログラムに渡すために、次のように、glGetAttribLocationglGetUniformLocation関数で変数のインデックスを取得します。

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

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

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

    /* フラグメントシェーダのuniform変数invEyeRotのインデックス取得 */
    invEyeMatLoc = glGetUniformLocation(shdProg, "invEyeRot");

    /* フラグメントシェーダのuniform変数nrmmapのインデックス取得 */
    nrmmapLoc = glGetUniformLocation(shdProg, "nrmmap");

    /* フラグメントシェーダのuniform変数envmapのインデックス取得 */
    cubeEnvLoc = glGetUniformLocation(shdProg, "envmap");

    :   : 途中略 :   :
}

今回は、法線マップとキューブ環境マップの二つのテクスチャマッピングを同時に行うため、環境マップはテクスチャユニット1に指定します。テクスチャはglUniform1iでシェーダプログラムに渡します。

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

    /* シェーダプログラムの適用 */
    glUseProgram(shdProg);
    /* 法線マップをテクスチャユニット0に指定 */
    glUniform1i(nrmmapLoc, 0);
    /* 環境マップをテクスチャユニット1に指定 */
    glUniform1i(cubeEnvLoc, 1);

    :   : 途中略 :   :
}

一方、逆行列については、glUnifromMatrix4fvでシェーダに渡します。glUnifromMatrix4fvは次のような関数プロトタイプです。

void glUniformMatrix4fv(
    GLint   index,          /* glGetUniformLocationで得られる変数番号 */
    GLsizei count,          /* 渡す行列の個数 */
    GLboolean  transpose,   /* 転置行列に変換して渡すかどうか */
    const GLfloat *value    /* 行列へのポインタ */
); 

3番目の引数transposeは、渡す行列を転置行列に変換するかどうかを指定するフラグです。プログラムでの使用方法は次のようになります。既に逆行列として転置しているので、第3引数transposeにはGL_FALSEを与えています。また、キューブ環境テクスチャをテクスチャユニット1に設定するため、glActiveTextureでテクスチャユニット1を指定しています。

int FUTL_DrawTorusVBO(
    int count
)
{
    :   : 途中略 :   :

    /* テクスチャ(バンプマップ)の設定 */
    if (torusTexID != 0)
    {
        glActiveTexture(GL_TEXTURE0);
        glEnable(GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, torusTexID);
    }
    else
    {
        glActiveTexture(GL_TEXTURE0);
        glDisable(GL_TEXTURE_2D);
    }

    /* キューブ環境テクスチャの設定 */
    if (cubeEnvTexID != 0)
    {
        glActiveTexture(GL_TEXTURE1);
        glEnable(GL_TEXTURE_CUBE_MAP);
        glBindTexture(GL_TEXTURE_CUBE_MAP, cubeEnvTexID);

        /* 視点座標変換の逆行列の設定 */
        glUniformMatrix4fv(invEyeMatLoc, 1, 
                        GL_FALSE, (GLfloat *)(&invEyeRot));
    }
    else
    {
        glActiveTexture(GL_TEXTURE1);
        glDisable(GL_TEXTURE_CUBE_MAP);
    }

    :   : 途中略 :   :
}

キューブ環境テクスチャによる背景描画

今回は、背景にもキューブ環境テクスチャを使用しています。これは一辺が4096の大きな立方体を配置し、その内部にキューブ環境テクスチャを貼り付けることで実現しています。そのシェーダプログラムは次のようになります。キューブ環境テクスチャをサンプリングするためのテクスチャ座標(s, t, r)は、頂点の位置座標(x, y, z)と同じになるため、バーテックスシェーダで位置座標をテクスチャ座標として設定し、フラグメントシェーダでtextureCube関数に渡すだけです。

// cubetex.vs バーテックスシェーダ
void main(void)
{
    /* 座標変換していない頂点位置がテクスチャ座標 */
    gl_TexCoord[0] = gl_Vertex;

    gl_Position = ftransform();
}


// cubetex.fs フラグメントシェーダ
uniform samplerCube envmap;

void main (void)
{
    /* テクスチャ */
    gl_FragColor = textureCube(envmap, vec3(gl_TexCoord[0]));
}

上記、シェーダを使用した立方体を描画するOpenGLプログラムは、次のようなになっています。立方体の描画プログラム自体は説明しませんが、FUTL_DrawCubeEnvModel関数を見れば、分かると思います。

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

    /* 環境マップ背景シェーダの読み込み */
    if (FUTL_LoadShader("cubetex.vs", "cubetex.fs", &cubeTexProg) < 0)
    {
        return -1;
    }

    /* フラグメントシェーダのuniform変数envmapのインデックス取得 */
    cubeTexLoc = glGetUniformLocation(cubeTexProg, "envmap");

    :   : 途中略 :   :
}


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

    /* シェーダプログラムの適用 */
    glUseProgram(cubeTexProg);
    /* 環境マップをテクスチャユニット0に指定 */
    glUniform1i(cubeTexLoc, 0);
    /* キューブテクスチャの描画 */
    FUTL_DrawCubeEnvModel();

    :   : 途中略 :   :
) 

大変長い説明でしたが、これでほぼ終わりです。サンプルプログラムを動かせば、次のような絵が描画されます。この背景にドーナツが浮かんでいるのは不自然ですが、環境マッピングにより視点の後ろにある白く明るい部分(太陽)が写りこんでいるのが分かると思います。

今回、モデルだけでなく、視点もマウスで動かせるようにしました。マウスの右ボタンを押しながら動かせば、視点が回転します。以前説明した「四元数でモデルを回転」を応用しただけですが、一つだけ注意することがあります。これまでのプログラムでは、視点は常に(0, 0, 120)にあり、注視点は原点となっていました。しかし、このプログラムでは、注視点は常に原点ですが、視点が回転するため、動かした視点でモデル(ドーナツ)を動かす場合、ドーナツの回転は、視点から見た回転軸で考える必要があります。

/*
 * マウス移動時のコールバック関数
 * マウスが押されている時だけ、コールされる
 */
static void motion(int x, int y)
{
    if (drag != 0)  /* マウスで回転中 */
    {
        int dx, dy;
        FVector3D axis;
        FQuat dqtn;
        float rot;

        /* マウス位置は左上原点 */
        dx = x - startX;
        dy = y - startY;

        /* ドラッグ開始位置からの変位(ラジアン換算) */
        /* 横方向はウィンドウ幅が一周分、縦方向はウィンドウ高さが一周分 */
        /* 下方向のマウス移動は+X軸回転、右方向のマウス移動は+Y軸回転 */
        axis.x = 2.0f * FUTL_PI * dy / (float)screenHeight;
        axis.y = 2.0f * FUTL_PI * dx / (float)screenWidth;
        axis.z = 0.0f;

        /* モデル回転時、視点から見た回転で変換 */
        if (drag == 1)
        {
            FUTL_MatDirection(&axis, &invEyeRot, &axis);
        }

        /* ドラッグ開始位置からの回転角度、同時に回転軸を正規化 */
        rot = FUTL_VecNormalize(&axis);

        /* ドラッグ開始位置からの差分を元に回転クォータニオンを生成 */
        FUTL_QuatRotation(&dqtn, rot, axis.x, axis.y, axis.z);

        if (drag == 1)  /* モデル回転の場合 */
        {
            /* モデルの回転クォータニオンを合成 */
            FUTL_QuatMult(&mdlQtn, &dqtn, &startQtn);

            /* クォータニオンからマトリックスに変換 */
            FUTL_QuatToMatrix(&mdlRot, &mdlQtn);
        }
        else if (drag == 2)     /* 視点回転の場合 */
        {
            /* 環境の回転クォータニオンを合成 */
            FUTL_QuatMult(&eyeQtn, &dqtn, &startQtn);

            /* クォータニオンからマトリックスに変換 */
            FUTL_QuatToMatrix(&eyeRot, &eyeQtn);
        }
    }
}

プログラムでは、モデルを回転する場合(drag=1の場合)だけ、視点座標変換の逆行列invEyeRotにより回転軸を変換しています。これは視点座標系でのX軸とY軸をワールド座標に変換していることに相当します。

最新の7件

OpenGL

電子工作

玄箱HG

ホームページ

日記

Copyright (C) 2007 Arakin , All rights reserved.