5. 照明モデルの実装例

照明モデルとは 3D シーンでライティングがどのように起こっているかを表す数学的なモデルのことで、光源の色、マテリアルの色、また光がどのように反射されるかといった情報がそこに含まれています。

典型的な照明モデルはいくつかの項-環境光(ambient)、放射光(emissive)、拡散光(diffuse)、鏡面光(specular)-に分解されます。

フラグメントライティングではプライマリカラーとセカンダリカラーの 2 つのカラーが出力されます。プライマリカラーは環境光、放射光、拡散光の和ですが、セカンダリカラーは 2 つの鏡面光の和であり、単純な鏡面光以外の表現にも対応することができます。

5.1. 微細面反射(microfacet Reflection)

microfacet とは肉眼では認識できないほど微細で小さな面のことです。粗い表面の物質が microfacet の集合で構成されていると考えれば、ライトベクトルと視線ベクトルのハーフベクトルに一致する法線を持つ microfacet は視線方向に光を反射させていることになります。つまり、視線方向に光を反射させる microfacet の数が多いほど、より強い反射光となります。

ここで法線の角度を引数にとり、その角度の法線を持つ microfacet の割合を返す分布関数を導入すれば、物質の反射光量はその関数に比例することになります。この関数は microfacet の勾配(slope)がどのように分布しているかを示すものでもあるため、勾配分布(slope distribution)関数とも呼ばれます。

コード 5-1. 微細面反射の実装例
GLfloat ms0[] = {1.0f, 1.0f, 1.0f, 1.0f};
GLfloat ms1[] = {0.0f, 0.0f, 0.0f, 1.0f};
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG0_DMP);
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular0"), 
        1, ms0);
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular1"), 
        1, ms1);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD0"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor1"), 
        GL_FALSE);

GLfloat LUT_D0[512];
for( i = 0; i < 256; i++) LUT_D0[i] = slopeDist((float) i / 256.0f);
for( i = 0; i < 255; i++) LUT_D0[i + 256] = LUT_D0[i + 1] - LUT_D0[i];
LUT_D0[511] = slopeDist(1.0f) - LUT_D0[255];

glBindTexture(GL_LUT_TEXTURE0_DMP, lutTexD0);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D0);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerD0"), 0);

分布 0(D0)の参照テーブルに勾配分布関数のサンプリング値を設定しています。

5.2. ブリン・フォンモデル(Blinn-Phong Model)

Blinn-Phong の反射モデルはべき乗関数を分布関数とする単純な微細面反射モデルです。また、Blinn-Phong の一般化された反射モデル(generalized Blinn-Phong model)は、法線とハーフベクトルの内積を引数にとる、べき乗関数以外の分布関数を使って表現されます。例えば Gaussian 関数を分布関数として用いることができます。

単純な Blinn-Phong の反射モデルであれば、微細面反射の実装例で分布関数 slopeDist() を以下のように定義するだけです。

コード 5-2. 単純な Blinn-Phong モデルでの分布関数
float slopeDist(float NH)
{
    return powf(NH, 2.0f);
}

セカンダリカラーに 2 つある鏡面光を使って、2 層からなる Blinn-Phong モデルを表現することもできます。これには、分布 1(D1)の参照テーブルに 2 層目の勾配分布関数のサンプリング値を設定し、D0 と D1 の両方を使用するレイヤコンフィグレーションに変更することで対応することができます。

コード 5-3. 2 層からなる Blinn-Phong モデルの実装例
GLfloat ms0[] = {1.0f, 1.0f, 1.0f, 1.0f};
GLfloat ms1[] = {1.0f, 1.0f, 1.0f, 1.0f};
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG2_DMP);
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular0"), 
        1, ms0);
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular1"), 
        1, ms1);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD0"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1"), 
        GL_LIGHT_ENV_NH_DMP);

glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor0"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor1"), 
        GL_FALSE);

GLfloat LUT_D0[512], LUT_D1[512];
for( i = 0; i < 256; i++) LUT_D0[i] = slopeDistD0((float) i / 256.0f);
for( i = 0; i < 255; i++) LUT_D0[i + 256] = LUT_D0[i + 1] - LUT_D0[i];
LUT_D0[511] = slopeDistD0(1.0f) - LUT_D0[255];

for( i = 0; i < 256; i++) LUT_D1[i] = slopeDistD1((float) i / 256.0f);
for( i = 0; i < 255; i++) LUT_D1[i + 256] = LUT_D1[i + 1] - LUT_D1[i];
LUT_D1[511] = slopeDistD1(1.0f) - LUT_D1[255];

glBindTexture(GL_LUT_TEXTURE0_DMP, lutTexD0);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D0);
glBindTexture(GL_LUT_TEXTURE1_DMP, lutTexD1);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D1);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD0"), 0);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD1"), 1);

Blinn-Phong モデルについては以下の文献を参照してください。

Blinn, James F., Models of Light Reflection for Computer Synthesised Pictures, ACM Computer Graphics, SIGGRAPH 1977 Proceedings, 11(4), pp. 192-198

5.3. クック・トーランスモデル(Cook-Torrance Model)

より正確な光の反射量を表現するのが Cook-Torrance のモデルです。このモデルでは、光の反射量が分布関数にフレネル項と呼ばれるファクタを乗算したものに比例すると定義しています。透過した光の屈折と反射が入射角と屈折率の関数で表せることを利用し、さらに物質によって異なる吸光係数を考慮することで様々な物質の反射を表現することができます。フレネル項はその反射をライトベクトルと法線ベクトルの関数として表現したものです。

microfacet のフレネル項を考えるとき、ここでいう法線は microfacet の法線であり、微細面反射と同様にそれはハーフベクトルと一致します。そのため、このモデルをフラグメントライティングで表現する際には、(ハーフベクトルは視線とライトのハーフベクトルであるので)視線ベクトルとハーフベクトルをフレネル項の入力に使用し、分布関数には分母の m2 を除いた Beckmann 関数を使用します。また、microfacet 同士が影を落とすことも考慮しなければなりませんが、角度によって microfacet からの反射が視点に到達する割合は、ジオメトリファクタ(Cook-Torrance の geometric factor に角度を考慮したもの)としてセカンダリカラーの計算式にすでに含まれています。3DS のジオメトリファクタは Cook-Torrance の geometric factor を以下のように近似したものです。

Lfi は i 番目のライトのライトベクトル、NfVf は法線と視線ベクトルです。

フレネル項の屈折率と吸光係数は色に依存します。そのため、フレネル項は RGB の各成分での屈折率と吸光係数で計算することができ、フラグメントライティングでは各成分の反射として扱うことになります。

以上のことから、Cook-Torrance モデルをフラグメントライティングで表現するには、反射、ディストリビューションファクタ、ジオメトリファクタを利用し、反射の入力に視線ベクトルとハーフベクトルの内積、ディストリビューションファクタの入力に法線とハーフベクトルの内積を利用することがわかります。これが可能なのはレイヤコンフィグ 4、5、7 の 3 つです。また、マテリアルの鏡面光 1 が反射に置き換わり、ライトからの鏡面光 1 で色が決定されることに注意が必要です。

コード 5-4. Cook-Torrance モデルの実装例
GLfloat ms[] = {0.0f, 0.0f, 0.0f, 0.0f};
GLfloat ls[] = {1.0f, 1.0f, 1.0f, 1.0f};

glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular0"), 
        1, ms);
glUniform4fv(glGetUniformLocation(progID, 
        "dmp_FragmentLightSource[0].specular1"), 1, ls);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG4_DMP);

glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRR"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRG"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRB"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRR"), 
        GL_LIGHT_ENV_VH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRG"), 
        GL_LIGHT_ENV_VH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRB"), 
        GL_LIGHT_ENV_VH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRR"), 2.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRG"), 2.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRB"), 2.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleD1"), 2.0f);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor1"), 
        GL_TRUE);

GLfloat LUT_RR[512], LUT_RG[512], LUT_RB[512], LUT_D1[512];
for( i = 0; i < 128; i++) LUT_RR[i] = 
        nk_fresnel((float) i / 128.0f, FRESNEL_N_R, FRESNEL_K_R);
for( i = 0; i < 127; i++) LUT_RR[i + 256] = LUT_RR[i + 1] - LUT_RR[i];
LUT_RR[383] = nk_fresnel(1.0f, FRESNEL_N_R, FRESNEL_K_R) - LUT_RR[127];

for( i = 0; i < 128; i++) LUT_RG[i] = 
        nk_fresnel((float) i / 128.0f, FRESNEL_N_G, FRESNEL_K_G);
for( i = 0; i < 127; i++) LUT_RG[i + 256] = LUT_RG[i + 1] - LUT_RG[i];
LUT_RG[383] = nk_fresnel(1.0f, FRESNEL_N_G, FRESNEL_K_G) - LUT_RG[127];

for( i = 0; i < 128; i++) LUT_RB[i] = 
        nk_fresnel((float) i / 128.0f, FRESNEL_N_B, FRESNEL_K_B);
for( i = 0; i < 127; i++) LUT_RB[i + 256] = LUT_RB[i + 1] - LUT_RB[i];
LUT_RB[383] = nk_fresnel(1.0f, FRESNEL_N_B, FRESNEL_K_B) - LUT_RB[127];

for( i = 0; i < 128; i++) LUT_D1[i] = beckmann((float) i / 128.0f, 1.0f);
for( i = 0; i < 127; i++) LUT_D1[i + 256] = LUT_D1[i + 1] - LUT_D1[i];
LUT_D1[383] = beckmann(1.0f, 1.0f) - LUT_D1[127];

glBindTexture(GL_LUT_TEXTURE0_DMP, lutTexRR);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RR);
glBindTexture(GL_LUT_TEXTURE1_DMP, lutTexRG);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RG);
glBindTexture(GL_LUT_TEXTURE2_DMP, lutTexRB);
glTexImage1D(GL_LUT_TEXTURE2_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RB);
glBindTexture(GL_LUT_TEXTURE3_DMP, lutTexD1);
glTexImage1D(GL_LUT_TEXTURE3_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D1);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRR"), 0);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRG"), 1);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRB"), 2);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD1"), 3);

nk_fresnel()beckmann() はそれぞれ吸光係数を考慮したフレネル反射関数、beckmann 関数です。

dmp_LightEnv.lutScaleXX で参照テーブルからの出力を倍化しています。

Cook-Torrance モデルについては以下の文献を参照してください。

Cook, Robert L., and Torrance, Kenneth E., A Reflectance Model for Computer Graphics, ACM Computer Graphics, SIGGRAPH 1981 Proceedings, 15(4), pp. 307-316.

ジオメトリファクタについては以下の文献を参照してください。

Alan Watt, 3D Computer Graphics, 3rd edition, Addison-Wesley Publishing Ltd, Addison-Wesley Publishing Company Inc., 2000, pp. 216

様々な物質の屈折率と吸光係数については以下の文献を参照してください。

Glassner, Andrew S., Principles of Digital Image Synthesis, Morgan Kaufmann Publishers, Inc., San Francisco, CA, 1995.

5.4. シュリック異方性モデル(Schlick Anisotropic Model)

これまでに紹介してきた照明モデルは、向きに対してすべてのサーフェスが同じ性質を持っている(等方性: isotropic)と仮定し、反射は法線に対して常に対称となっていました。それに対し、織物の繊維が特定の方向で反射を強めるように、向きに対してサーフェスが異なる性質を持つ場合を異方性(anisotropic)と呼びます。

異方性のあるサーフェスの光の散乱特性はサーフェスに沿った方向の関数として変化します。Schlick は異方性反射を表現するために、分布関数を 2 つの関数で構成することを提案しました。1 つは法線とハーフベクトルに依存し、もう 1 つは接線とハーフベクトルの接平面への投影ベクトルのなす角Φに依存する関数で、以下の式で表されます。

r はサーフェスの荒さを特徴づけるパラメータ、p はサーフェスの異方性を特徴づけるパラメータです。

このモデルでの geometric factor は Cook-Torrance モデルのものと異なりますが、フラグメントライティングで使用しているジオメトリファクタとは同様の特性を持っています。

コード 5-5. Schlick Anisotropic モデルの実装例
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG7_DMP);

glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRR"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRG"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRB"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRR"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRG"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRB"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1"), 
        GL_LIGHT_ENV_CP_DMP);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRR"), 1.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRG"), 1.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRB"), 1.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleD1"), 1.0f);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor0"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor1"), 
        GL_FALSE);

GLfloat LUT_RR[512], LUT_RG[512], LUT_RB[512], LUT_D1[512];
for( i = 0; i < 256; i++) LUT_RR[i] = 
        z_schlick(0.7f, (float) i / 256.0f, true);
for( i = 0; i < 255; i++) LUT_RR[i + 256] = LUT_RR[i + 1] - LUT_RR[i];
LUT_RR[511] = z_schlick(0.7f, 1.0f, true) - LUT_RR[255];

for( i = 0; i < 256; i++) LUT_RG[i] = 
        z_schlick(0.7f, (float) i / 256.0f, true);
for( i = 0; i < 255; i++) LUT_RG[i + 256] = LUT_RG[i + 1] - LUT_RG[i];
LUT_RG[511] = z_schlick(0.7f, 1.0f, true) - LUT_RG[255];

for( i = 0; i < 256; i++) LUT_RB[i] = 
        z_schlick(0.7f, (float) i / 256.0f, true);
for( i = 0; i < 255; i++) LUT_RB[i + 256] = LUT_RB[i + 1] - LUT_RB[i];
LUT_RB[511] = z_schlick(0.5f, 1.0f, true) - LUT_RB[255];

for( i = 0; i < 256; i++) LUT_D1[i] = 
        a_schlick(0.0015f, (float) i / 256.0f, true);
for( i = 0; i < 255; i++) LUT_D1[i + 256] = LUT_D1[i + 1] - LUT_D1[i];
LUT_D1[511] = a_schlick(0.0015f, 1.0f, true) - LUT_D1[255];

glBindTexture(GL_LUT_TEXTURE0_DMP, lutTexRR);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RR);
glBindTexture(GL_LUT_TEXTURE1_DMP, lutTexRG);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RG);
glBindTexture(GL_LUT_TEXTURE2_DMP, lutTexRB);
glTexImage1D(GL_LUT_TEXTURE2_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RB);
glBindTexture(GL_LUT_TEXTURE3_DMP, lutTexD1);
glTexImage1D(GL_LUT_TEXTURE3_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D1);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerRR"), 0);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerRG"), 1);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerRB"), 2);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerD1"), 3);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.bumpMode"), 
        GL_LIGHT_ENV_BUMP_AS_BUMP_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.bumpSelector"), 
        GL_TEXTURE0);

z_schlick()a_schlick() は schlick 関数です。

バンプマッピングを行っているのは、摂動された接線が入力で必要なためです。

シュリック異方性モデルについては以下の文献を参照してください。

Schlick, Christophe, An Inexpensive BRDF Model for Physically-Based Rendering, Computer Graphics Forum, 13(3), pp. 233-246 (1994).

5.5. 表面下散乱モデル(Subsurface-Scattering Model)

肌、ロウソクのロウ、大理石のような半透明の物体は強い表面下散乱を持ち、光は内部で多くの散乱を起こした後に外部に放出されています。このような物体では光の反射位置は入射位置とは異なるため、これまで紹介したモデルと同じアプローチでは表現することができません。

Jensen らが提案した拡散光理論をベースにしたモデルは、入射してくる光と拡散光反射関数をサーフェスのある領域で積分するというものです。その積分領域は平均自由行程(mean free-path)ほどの大きさであり、典型的な素材では 1 から数ミリメートルになります。

人間の手のように、半透明の素材でできたオブジェクトは、周囲が暗い環境でたった 1 つの光源で照らされると、凸面部分の透明度が最も高く感じられます。これは複雑な照明下においても同様で、すなわち表面下散乱を持つオブジェクトの描画では凸面部分のレンダリングが最も重要であると考えることができます。

そこで凸面部分を半径 S の球の一部分とみなして、この部分に Jensen らによる式の積分を適用(半径 S は "mean free-path" の長さよりずっと大きい)し、荒いサーフェスを持つオブジェクトであると考えると、その反射モデルは以下の式で与えられます。

は Lambertian(拡散光)項と wrapping(光のあたらない表面領域への光の侵入を表す)項の 2 項を足し合わせたものです。



α'(albedo)と Fdr については、以下の文献を参照してください。

Jensen, H. W., Marschner, S., Levoy, M., and Hanrahan, P., A practical model for subsurface light transport, SIGGRAPH 2001 Proceedings, E. Fiume, Ed., Annual Conference Series, pp. 511–518.

コード 5-6. 表面下散乱モデルの実装例
 /* Lighting Environment */
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config" ), 
    GL_LIGHT_ENV_LAYER_CONFIG7_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.fresnelSelector" ), 
    GL_LIGHT_ENV_PRI_SEC_ALPHA_FRESNEL_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights" ), 
    GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD0" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD1" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRR" ), 
    GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRG" ), 
    GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRB" ), 
    GL_FALSE);
/* below we use GL_TRUE to have non-zero for negative NV because negative NV values happen on silhouette */
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD0" ), 
    GL_FALSE);
/* below we use GL_TRUE to have non-zero for negative NV because negative NV values happen on silhouette */
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputFR" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRR" ), 
    GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRG" ), 
    GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRB" ), 
    GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1" ), 
    GL_LIGHT_ENV_NV_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD0" ), 
    GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputFR" ), 
    GL_LIGHT_ENV_NV_DMP);

/* Material */
GLfloat ms2[] = {0.28f, 0.28f, 0.28f, 1.f};
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular1"), 1, ms2);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRR"), 0);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRG"), 1);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRB"), 2);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD1"), 3);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD0"), 4);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerFR"), 5);

/* Light Source */
GLfloat ld0[] = {1.f, 1.f, 1.f, 1.f}; /* light0 diffuse */
GLfloat ls0[] = {0.35f, 0.35f, 0.35f, 1.f}; /* light0 specular */
GLfloat ls1[] = {0.28f, 0.28f, 0.28f, 1.f}; /* light0 specular2 */
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentLightSource[0].diffuse"), 
    1, ld0);
glUniform4fv(glGetUniformLocation(progID, 
    "dmp_FragmentLightSource[0].specular0"), 1, ls0);
glUniform4fv(glGetUniformLocation(progID, 
    "dmp_FragmentLightSource[0].specular1"), 1, ls1);
glUniform1i(glGetUniformLocation(progID, 
    "dmp_FragmentLightSource[0].geomFactor0" ), GL_FALSE);
glUniform1i(glGetUniformLocation(progID, 
    "dmp_FragmentLightSource[0].geomFactor1" ), GL_FALSE);

/* Texture Combiner 0 */
glUniform1i(glGetUniformLocation(progID, "dmp_TexEnv[0].combineRgb"), 
    GL_ADD);
glUniform1i(glGetUniformLocation(progID, "dmp_TexEnv[0].combineAlpha"), 
    GL_REPLACE);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[0].operandRgb"), 
    GL_SRC_COLOR, GL_SRC_COLOR, GL_SRC_COLOR);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[0].operandAlpha"), 
    GL_SRC_ALPHA, GL_SRC_ALPHA, GL_SRC_ALPHA);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[0].srcRgb"), 
    GL_FRAGMENT_PRIMARY_COLOR_DMP, GL_FRAGMENT_SECONDARY_COLOR_DMP, 
    GL_CONSTANT);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[0].srcAlpha"), 
    GL_CONSTANT, GL_CONSTANT, GL_CONSTANT);

/* Texture Combiner 1 */
glUniform1i(glGetUniformLocation(progID, "dmp_Texture[0].samplerType" ), 
    GL_TEXTURE_CUBE_MAP);
glUniform1i(glGetUniformLocation(progID, "dmp_TexEnv[1].combineRgb"), 
    GL_MULT_ADD_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_TexEnv[1].combineAlpha"), 
    GL_REPLACE);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[1].operandRgb"), 
    GL_SRC_COLOR, GL_SRC_ALPHA, GL_SRC_COLOR);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[1].operandAlpha"), 
    GL_SRC_ALPHA, GL_SRC_ALPHA, GL_SRC_ALPHA);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[1].srcRgb"), 
    GL_TEXTURE0, GL_FRAGMENT_PRIMARY_COLOR_DMP, GL_PREVIOUS);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[1].srcAlpha"), 
    GL_PREVIOUS, GL_PREVIOUS, GL_PREVIOUS);

/* LUT */
GLfloat qlut[3][512], lut[512];
int j, co;

GLuint lutids[6];
glGenTextures(6, lutids);
/* RR, RG, RB */
for (co = 0; co < 3; co++) {
    for (j = 0; j < 128; j++) {
        LN = (float) j/128.f;
        qlut[co][j] = calc_lamb(LN, co) + calc_wrap(LN, co);
    }
    for (j = 128; j < 256; j++) {
        LN = (float) (j - 256) /128.f;
        qlut[co][j] = calc_wrap(LN, co);
    }
    for (j = 0; j < 127; j++)
        qlut[co][j + 256] = qlut[co][j + 1] - qlut[co][j];
    qlut[co][127 + 256] = calc_lamb(1.f, co) + calc_wrap(1.f, co) - 
        qlut[co][127];
    for (j = 128; j < 255; j++)
        qlut[co][j + 256] = qlut[co][j + 1] - qlut[co][j];
    qlut[co][255 + 256] = qlut[co][0] - qlut[co][255];
}
glBindTexture(GL_LUT_TEXTURE0_DMP, lutids[0]);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, qlut[0]);
glBindTexture(GL_LUT_TEXTURE1_DMP, lutids[1]);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, qlut[1]);
glBindTexture(GL_LUT_TEXTURE2_DMP, lutids[2]);
glTexImage1D(GL_LUT_TEXTURE2_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, qlut[2]);
/* D1 */
for (j = 0; j < 256; j++) lut[j] = calc_t((float) j / 255.9375f);
for (j = 0; j < 255; j++) lut[j + 256] = lut[j + 1] - lut[j];
lut[255 + 256] = calc_t(1.f) - lut[255];
glBindTexture(GL_LUT_TEXTURE3_DMP, lutids[3]);
glTexImage1D(GL_LUT_TEXTURE3_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, lut);
/* D0 */
memset(lut, 0, sizeof(lut));
for (j = 0; j < 128; j++) lut[j] = beckmann((float)j / 128.f, 0.5f);
for (j = 128; j < 256; j++) lut[j] = 0.f;
for (j = 0; j < 127; j++) lut[j + 256] = lut[j + 1] - lut[j];
lut[127 + 256] = 1.f - lut[127];
for (j = 128; j < 256; j++) lut[j + 256] = 0;
glBindTexture(GL_LUT_TEXTURE4_DMP, lutids[4]);
glTexImage1D(GL_LUT_TEXTURE4_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, lut);
/* FR */
memset(lut, 0, sizeof(lut));
for (j = 0; j < 256; j++) 
lut[j] = r_fresnel((float)j / 255.9375f, 2.f, 0.35f, 0.f);
for (j = 0; j < 255; j++) lut[j + 256] = lut[j + 1] - lut[j];
    lut[255 + 256] = r_fresnel(1.f, 2.f, 0.35f, 0.f) - lut[255];
glBindTexture(GL_LUT_TEXTURE5_DMP, lutids[5]);
glTexImage1D(GL_LUT_TEXTURE5_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, lut);

calc_lambcalc_wrapcalc_t はそれぞれ、r(L・N)、W(L・N)、T(L・N) を計算する関数です。

反射モデルは RR、RG、RB および D1 の参照テーブルで表されています。D0 はスペキュラ、FR は周辺光からの影響を表しています。

コンバイナ 0 でオブジェクトのプライマリカラーと反射モデルで計算されたセカンダリカラーを足し合わせています。コンバイナ 1 においてカラーのオペランドに GL_MULT_ADD_DMP を指定することで、周辺光からの影響を計算しながらオブジェクトのカラーと足し合わせています。

5.6. トゥーンシェーディング(Toon-Shading)

トゥーンシェーディングは、色が一定となる領域が 2 段階から 3 段階程度になるように陰影をつける表現です。トゥーンシェーディングを行うには、内積値 N・H または L・N の入力に対して範囲ごとに一定値を出力する関数を用意します。その関数で作成された参照テーブルから出力される段階的な値と定数カラーを乗算することで、アニメ調のはっきりした陰影表現が可能となります。

ライトの位置関係のみで陰影が変化する場合は、入力する内積値に L・N(GL_LIGHT_ENV_LN_DMP)を選択し、視線方向も加味する場合は N・H(GL_LIGHT_ENV_NH_DMP)を選択してください。

ライトの鏡面光 1 の明度だけを変化させるならば反射(RR)の参照テーブルに値を設定し、反射の各成分がその参照テーブルを使用するレイヤコンフィグを指定するだけです。段階の分け方は同じでも、各成分がそれぞれの参照テーブルを使用すれば極端な色味の変化を表現することも可能です。

入力する内積値に N・V(GL_LIGHT_ENV_NV_DMP)を使用し、ディストリビューションファクタの参照テーブル(D0, D1)を入力値 0.0 付近で 0.0、それ以外は 1.0 にすれば、視線と法線の向きが極端に異なる領域が暗く(黒く)なることで鏡面光のみでトゥーンシェーディングを行っているオブジェクトの輪郭を描画することができます。

コード 5-7. トゥーンシェーディングの実装例
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG2_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRR"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRR"), 
        GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD0"), 
        GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1"), 
        GL_LIGHT_ENV_NV_DMP);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentLightSource[0].geomFactor0"), GL_FALSE);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentLightSource[0].geomFactor1"), GL_FALSE);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentLightSource[0].spotEnabled"), GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);

float delta[] = {1.0f, 0.7f, 0.5f, -1.0f};
for (i = 0, j = 127; j >= 0; j--) {
    LN = (float) j / 128.0f ;
    if (LN > delta[i]) lut[j] = previous;
    else {
        lut[j] = LN; previous = lut[j]; i++;
    }
}
for (j = 0 ; j < 127; j++) lut[j + 256] = lut[j+1] - lut[j];
lut[127 + 256] = 0.0f;
for (i = 0, j = 255; j >= 128; j--) {
    LN = (float)(j - 256) /128.0f;
    if (LN > delta[i]) lut[j] = previous;
    else {
        lut[j] = LN; previous = lut[j]; i++;
    }
}
for (j = 128; j < 255; j++) lut[j + 256] = lut[j+1] - lut[j];
lut[255 + 256] = lut[0] - lut[255];
glBindTexture(GL_LUT_TEXTURE0_DMP, lutids[0]);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0,
        GL_LUMINANCEF_DMP, GL_FLOAT, lut);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRR"), 0);

float highlight_eps = 0.01f;
for (j = 0; j < 256; j++)
    if ((float) j / 256.0f <= 1.0f - highlight_eps) lut[j] = 0.0f;
    else                                            lut[j] = 1.0f;
for (j = 0; j < 255; j++) lut[j + 256] = lut[j+1] - lut[j];
lut[255 + 256] = 0.0f;
glBindTexture(GL_LUT_TEXTURE1_DMP, lutids[1]);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0,
        GL_LUMINANCEF_DMP, GL_FLOAT, lut);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD0"), 1);

float outline = 0.15f;
for (j = 0; j < 256; j++)
    if ((float) j / 256.0f < outline) lut[j] = 0.0f; 
    else                              lut[j] = 1.0f; 
for (j = 0; j < 255; j++) lut[j + 256] = lut[j+1] - lut[j];
lut[255 + 256] = 1.0f - lut[255];
glBindTexture(GL_LUT_TEXTURE2_DMP, lutids[2]);
glTexImage1D(GL_LUT_TEXTURE2_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, lut);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD1"), 2);

delta が段階的な変化を起こす内積値の配列、highlight_eps がハイライトとなる内積値の範囲、outline が影となる内積値の範囲です。これらの値でトゥーンシェーディングの表現を変化させることができます。