環境光によるライティング

実行結果

今回のサンプルの実行結果

平行光源の弱点

前回は平行光源によるライティングに挑戦しました。平行光源では、光の向きは常に一定です。そして、それをシミュレートするためにモデル変換行列の逆行列を生成する必要があったり、モデルデータに法線情報を追加したりする必要がありましたね。

平行光源は比較的計算の負荷も小さく、ある程度それらしいライティングが可能なため、3D プログラミングの世界ではよく使われています。しかし平行光源にも当然弱点があります。それは陰になる部分、つまり光が当たっていない部分をうまく演出できないことです。

たとえば前回のサンプルの頂点シェーダをよく見てみると、ライトベクトルと法線との内積を取っている部分で、実はちょっとしたズルをしています。

前回のサンプルの一部分を抜粋

float diffuse  = clamp(dot(normal, invLight), 0.1, 1.0);

さて、ここでは GLSL の組み込み関数である clamp を使っていますね。この関数は引数として与えられた数値を一定の範囲に収めてくれます。先ほどのコードのように指定すれば、結果は必ず 0.1 ~ 1.0 の範囲に収まることになります。

しかし、ライトベクトルと法線との内積を取ると、場合によっては結果がマイナスの数値になることもあります。しかし clamp を使っている以上、マイナスの数値であっても指定された最小値 0.1 に丸められますね。仮にこの clamp の丸めの範囲を、0.0 ~ 1.0 に変更するとどうなるか想像がつきますでしょうか。それをやってみたのが以下の画像です。

ゼロクランプバージョン

はい、このように完全に光が当たらない部分は、真っ暗――というか真っ黒になります。こうなると、場所によっては背景の黒に溶け込んでしまってモデルの輪郭すらわからない状態になってしまいますね。これが平行光源を用いたライティングによるデメリットでもあるわけです。

前回のサンプルのように、丸めの範囲をある程度広く設けることで今回の問題には対処できます。しかし、環境光という新しい光の概念を用いれば、この問題にすんなりと対応することが可能になります。

環境光とは

環境光(アンビエントライト)とは、現実世界における自然光の乱反射をシミュレートする概念です。

現実世界では、太陽や照明器具から発せられた光が、物や大気中の塵などに当たって乱反射し世界を照らします。たとえば真っ暗な部屋に、たった一つ電球が置かれている状態を思い浮かべてみてください。電球を背にして立つと、自分の影が壁や床に映るのが見えますね。そして、自分自身の身体の前面も、直接照明に照らされてはいませんがうっすらと見えるはずです。

壁や天井、床や大気中の塵などが電球の光を乱反射することで、直接光が当たっていない部分であっても電球から発せられる光の影響を受けるのです。このような、光の乱反射を再現しようとするのが環境光の考え方です。

環境光は三次元空間上のあらゆる部分を照らすために使われます。つまり、頂点ごとに異なる情報を扱う attribute 変数としてではなく、uniform 変数としてシェーダに情報を渡します。さらに言うと、環境光は最終的にコンテキスト上に出力される色に影響を与えるものですので、四つの要素を持つ色情報として扱います。

環境光を定義する一例

// 環境光の色
var ambientColor = [0.1, 0.1, 0.1, 1.0];

環境光を用いる際には、その色の強度に注意しましょう。環境光は先ほども書いたようにありとあらゆるものを照らすことになるので、たとえば上記のコードで 0.1 を指定している部分を全部 1.0 などにしてしまうと描画されるモデルは全て真っ白になってしまいます。平行光源による陰影付けがどうなっていようと関係なく、全てを真っ白になるまで照らしてしまいますので気をつけましょう。

環境光の色は、せいぜい 0.2 くらいまでの範囲に収めておいたほうが無難です。ちなみに今回のサンプルは 0.1 で設定しています。

頂点シェーダと javascript を修正

さて、それでは各種ソースの修正箇所を見ていきます。まずは頂点シェーダから。

頂点シェーダのソース

attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform   mat4 mvpMatrix;
uniform   mat4 invMatrix;
uniform   vec3 lightDirection;
uniform   vec4 ambientColor;
varying   vec4 vColor;

void main(void){
	vec3  invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
	float diffuse  = clamp(dot(normal, invLight), 0.0, 1.0);
	vColor         = color * vec4(vec3(diffuse), 1.0) + ambientColor;
	gl_Position    = mvpMatrix * vec4(position, 1.0);
}

前回から追加されている uniform 変数が一つありますね。それが vec4 として宣言されている ambientColor です。環境光は、平行光源による一連の計算が終わり、最終的に出力される色を決める段階で加算します。

ここで仮に足し合わせではなく掛け合わせで処理してしまうと、逆にシーン全体が暗くなってしまうので注意しましょう。

続いてはメインプログラムを修正します。

とは言っても、頂点シェーダに環境光のパラメータを渡すだけですので、追加でやることはほんの少しです。

まずは環境光のパラメータをプログラムの中で定義しておきます。

環境光のパラメータを追加

// 環境光の色
var ambientColor = [0.1, 0.1, 0.1, 1.0];

さらに、これを正しく頂点シェーダに渡すために、シェーダの uniformLocation を取得する部分を追加します。

// uniformLocationを配列に取得
var uniLocation = new Array();
uniLocation[0] = gl.getUniformLocation(prg, 'mvpMatrix');
uniLocation[1] = gl.getUniformLocation(prg, 'invMatrix');
uniLocation[2] = gl.getUniformLocation(prg, 'lightDirection');
uniLocation[3] = gl.getUniformLocation(prg, 'ambientColor');

さらに、これを uniform 変数として恒常ループの中でシェーダに送れば OK ですね。

シェーダへ環境のパラメータを送る

// uniform変数の登録
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[1], false, invMatrix);
gl.uniform3fv(uniLocation[2], lightDirection);
gl.uniform4fv(uniLocation[3], ambientColor);

これで、頂点シェーダと javascript プログラムの両方を修正できましたね。

ちなみに、今回のサンプルでは環境光を導入しましたので、平行光源の計算を行なっている部分ではクランプの範囲を 0.0 ~ 1.0 の範囲に変更しています。平行光源によってシミュレートされる光が当たらない部分は、純粋に環境光によって照らされることになります。

まとめ

環境光は、自然界の光の乱反射をシミュレートする概念で、平行光源の弱点をうまくカバーしてくれます。普通、これらの二つの光は同時に利用されます。環境光だけでライティングするとモデルの凹凸が表現されませんし、平行光源だけでライティングすると陰影がきつくなりすぎてしまうからですね。

3D プログラミングにおける代表的なライティングである環境光と平行光源による拡散光。今回のサンプルでそこまで実現できました。次回は、反射光と呼ばれるライティングについて解説します。

サンプルへのリンクは以下です。実際に動作する様子を確認できますので参考にしてみてください。

entry

PR

press Z key