点光源によるライティング
ポイントライト
今回のテーマは点光源(ポイントライト)の実装です。
点光源はその名の通り、頂点と同じように光源が点として表される光源です。
今までは、全ての光源処理に平行光源を使ってきました。
平行光源
無限遠から降り注ぐような一定の方向を示す光源でした。
三次元空間上の全てのモデルは、同じ指向性の光によってライティングされていましたね。
一方で点光源を用いた処理では、
光源の位置が三次元空間上に固定されます。
これにより、三次元空間上のどこにモデルが描画されるかによって、
光の当たり方が変わってきます。
現実世界では、電球などがこの点光源と似た効果を生み出します。
ただし今回実装する点光源の処理では、この光の減衰は考慮しません。
点光源の考え方
光源から各頂点へのライトベクトルを計算して、あとは同様のことをすればいい
「光源から各頂点へのライトベクトルを計算」
ここで計算がひと手間ある分、若干負荷が増える
頂点シェーダを修正する
今回は前回と同様にフォンシェーディングでライティングします。修正の大半はフラグメントシェーダ側になりますが、頂点シェーダ側にも若干の修正が必要です。
ライトベクトルの計算のために、光源の頂点座標をシェーダに渡す必要がある
フォンシェーディングなら、フラグメントシェーダにわたす
頂点の位置情報は、
通常ローカル座標として頂点シェーダに渡されます。
グローバル座標への変換は、一緒に渡した行列にかけて行う
mvpにかけたら、viewとperspeciveまで変換されてしまう
点光源は、グローバル座標で渡すはず
なので、pvの掛け算の前の、mだけかけた、グローバル座標としての頂点で計算をしないといけない
頂点シェーダにはモデル座標変換行列を新たに渡す必要があります。それを踏まえて、頂点シェーダのソースを修正してみましょう。
頂点シェーダのソース
code:glsl.vert
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mvpMatrix;
uniform mat4 mMatrix;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vColor;
void main(void){
vPosition = (mMatrix * vec4(position, 1.0)).xyz;
vNormal = normal;
vColor = color;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
前回からの変更点は大きく分けると二つあります。
一つ目の変更点は頂点の位置情報をフラグメントシェーダに渡すための varying 変数である vPosition の追加です。頂点の位置情報を表すデータであるため vec3 として定義してあります。
二つ目の変更点は新しい uniform 変数である mMatrix を追加していることです。先ほども書いたように、頂点シェーダに入ってくる頂点の位置情報はローカル座標系ですので、これをモデル座標変換行列を適用したあとの形(つまりワールド座標系)に変換するために、 uniform 修飾子付き変数を使ってモデル座標変換行列をシェーダ側が受け取れるようにしているわけですね。
フラグメントシェーダへ頂点の位置情報を渡す際には、モデル座標変換行列を表す mMatrix と、頂点のローカル座標を表す position とを掛け合わせてから vPosition に代入します。これによりフラグメントシェーダは、モデル座標変換が適用されたあとの頂点の位置を知ることができるようになるのですね。
フラグメントシェーダを修正する
続いてはフラグメントシェーダ側の修正です。フラグメントシェーダでは、頂点の位置と点光源の位置とを使ってライトベクトルをその都度算出しなければなりません。
このときのライトベクトルを計算する方法は非常に簡単で、単なるベクトルの減算のみで求めることができます。
また、今回は点光源による処理ですので、今まで使っていたライトベクトル用の uniform 変数(lightDirection)に代わり、点光源の位置を示す uniform 変数である lightPosition が登場します。
フラグメントシェーダのソース
code:glsl
precision mediump float;
uniform mat4 invMatrix;
uniform vec3 lightPosition;
uniform vec3 eyeDirection;
uniform vec4 ambientColor;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vColor;
void main(void){
vec3 lightVec = lightPosition - vPosition;
vec3 invLight = normalize(invMatrix * vec4(lightVec, 0.0)).xyz;
vec3 invEye = normalize(invMatrix * vec4(eyeDirection, 0.0)).xyz;
vec3 halfLE = normalize(invLight + invEye);
float diffuse = clamp(dot(vNormal, invLight), 0.0, 1.0) + 0.2;
float specular = pow(clamp(dot(vNormal, halfLE), 0.0, 1.0), 50.0);
vec4 destColor = vColor * vec4(vec3(diffuse), 1.0) + vec4(vec3(specular), 1.0) + ambientColor;
gl_FragColor = destColor;
}
シェーダ内の main 関数の一行目、変数 lightVec に点光源から頂点へと向かうライトベクトルが入ります。先述のとおり、これは単なる減算だけで求められますので簡単ですね。
そして、ここで得られたライトベクトルを使って、平行光源のときと同じように逆行列を適用したり、あるいはハーフベクトルを求めたりしながら拡散光や反射光を計算していきます。
仕組みさえ理解できてしまえば、前回までのサンプルとそれほど変わらないのがわかると思います。要は、ライトベクトルの扱いが異なるだけで、ライティング手法自体はほとんど同じなのですね。
javascript の修正
シェーダが修正できたら、次はメインプログラムである javascript の修正です。
今回は結構細かい部分での修正が多くなっているので、ポイントを絞って解説します。今までのサンプルではトーラスだけを用いてレンダリングしてきましたが、今回はトーラスに加え球体を使ってレンダリングしているのが冒頭の画像を見るとわかると思います。トーラスの頂点データ、さらには球体用の頂点データを別途用意しなければなりません。
球体モデルの頂点データを生成するのは、以下の関数。実装としてはトーラスの頂点データを生成する関数と似たような感じになっています。
球体の頂点データを生成する関数
code:js
// 球体を生成する関数
function sphere(row, column, rad, color){
var pos = new Array(), nor = new Array(),
col = new Array(), idx = new Array();
for(var i = 0; i <= row; i++){
var r = Math.PI / row * i;
var ry = Math.cos(r);
var rr = Math.sin(r);
for(var ii = 0; ii <= column; ii++){
var tr = Math.PI * 2 / column * ii;
var tx = rr * rad * Math.cos(tr);
var ty = ry * rad;
var tz = rr * rad * Math.sin(tr);
var rx = rr * Math.cos(tr);
var rz = rr * Math.sin(tr);
if(color){
var tc = color;
}else{
tc = hsva(360 / row * i, 1, 1, 1);
}
pos.push(tx, ty, tz);
nor.push(rx, ry, rz);
col.push(tc0, tc1, tc2, tc3); }
}
r = 0;
for(i = 0; i < row; i++){
for(ii = 0; ii < column; ii++){
r = (column + 1) * i + ii;
idx.push(r, r + 1, r + column + 2);
idx.push(r, r + column + 2, r + column + 1);
}
}
return {p : pos, n : nor, c : col, i : idx};
}
球体を形成する頂点は、一枚の大きなポリゴン群で出来た膜を、球の形に丸めるような方法で定義します。この sphere 関数は四つの引数を取ります。第一引数には、球体を形成する膜状のポリゴンの板の縦の分割数(頂点数)です。地球にたとえると緯度の方向ですね。第二引数は横の分割数になりますので、こちらは地球で言うなら経度の方向ということになります。
第三引数には球体の半径が入ります。第四引数には、球体に色をつける場合にはその色を四つの要素を持つ配列として渡します。色が指定されていない場合には HSV カラーが自動的に適用されるようになっています。
この関数の使い方としては、適切に引数を指定して呼び出し、その戻り値を変数に受け取ります。戻り値はオブジェクトなので適切にプロパティを参照します。実際にメインプログラムの中で使っている部分を見てみましょう。
関数 sphere の使用箇所を抜粋
code:js
// 球体の頂点データからVBOを生成し配列に格納
var sPosition = create_vbo(sphereData.p);
var sNormal = create_vbo(sphereData.n);
var sColor = create_vbo(sphereData.c);
// 球体用IBOの生成
var sIndex = create_ibo(sphereData.i);
上記のようにすると縦横それぞれ 64 頂点からなる球体が生成されますね。半径は 2.0 で、今回は青みがかった色も付くように指定しています。ここでのポイントは、後々の処理のためにあらかじめ VBO を配列に格納してリスト化している部分です。こうしておくことによって、attributeLocation と VBO を紐付ける作業が非常にスムーズに行なえます。これについては後述します。
さてどんどん行きます。次は uniformLocation の取得部分。今回は平行光源から点光源に変更したことによって、ライトの向きを指定していた部分がライトの位置を指定する形に変わっています。
uniform 周辺の処理
code:js
// uniformLocationを配列に取得
var uniLocation = new Array();
uniLocation0 = gl.getUniformLocation(prg, 'mvpMatrix'); uniLocation1 = gl.getUniformLocation(prg, 'mMatrix'); uniLocation2 = gl.getUniformLocation(prg, 'invMatrix'); uniLocation3 = gl.getUniformLocation(prg, 'lightPosition'); uniLocation4 = gl.getUniformLocation(prg, 'eyeDirection'); uniLocation5 = gl.getUniformLocation(prg, 'ambientColor'); // 中略
// 点光源の位置
シェーダのほうで行なった uniform 修飾子付き変数の変更を、こちらでもしっかり反映させておきます。また、今回のサンプルでは点光源の位置は原点としています。
点光源の効果をわかりやすくするために、サンプルでは点光源の位置(つまり原点)を中心にトーラスト球体が回転するようにモデル座標変換行列を生成します。二つのモデルを同時に描画するので、恒常ループの中で適切に VBO や IBO を適用しながらモデルをレンダリングしていきます。
少し長いコードになりますが、注意深く見ていけばわかると思います。ポイントとなるのは、先ほども書いたとおり、VBO を格納した配列を使って自作関数による VBO のバインド処理を行なっている箇所ですね。
恒常ループ内の描画処理
code:js
// カウンタをインクリメントする
count++;
// カウンタを元にラジアンと各種座標を算出
var rad = (count % 360) * Math.PI / 180;
var tx = Math.cos(rad) * 3.5;
var ty = Math.sin(rad) * 3.5;
var tz = Math.sin(rad) * 3.5;
// トーラスのVBOとIBOをセット
set_attribute(tVBOList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);
// モデル座標変換行列の生成
m.identity(mMatrix);
m.rotate(mMatrix, -rad, 0, 1, 1, mMatrix); m.multiply(tmpMatrix, mMatrix, mvpMatrix);
m.inverse(mMatrix, invMatrix);
// uniform変数の登録と描画
gl.uniformMatrix4fv(uniLocation0, false, mvpMatrix); gl.uniformMatrix4fv(uniLocation1, false, mMatrix); gl.uniformMatrix4fv(uniLocation2, false, invMatrix); gl.uniform3fv(uniLocation3, lightPosition); gl.uniform3fv(uniLocation4, eyeDirection); gl.uniform4fv(uniLocation5, ambientColor); gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);
// 球体のVBOとIBOをセット
set_attribute(sVBOList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sIndex);
// モデル座標変換行列の生成
m.identity(mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
m.inverse(mMatrix, invMatrix);
// uniform変数の登録と描画
gl.uniformMatrix4fv(uniLocation0, false, mvpMatrix); gl.uniformMatrix4fv(uniLocation1, false, mMatrix); gl.uniformMatrix4fv(uniLocation2, false, invMatrix); gl.drawElements(gl.TRIANGLES, sphereData.i.length, gl.UNSIGNED_SHORT, 0);
// コンテキストの再描画
gl.flush();
各種座標変換行列の生成、さらには逆行列の生成が済んだら、点光源の位置や視点ベクトルなどと一緒にシェーダにプッシュします。さらに、VBO と IBO のバインド処理を行なった上で描画命令を発行します。
二つのモデルを描画するために必要となる一連の処理が、繰り返し行なわれていることに注意すれば、特に難しいことはやっていませんので、焦らずじっくり考えてみてください。
まとめ
点光源を用いたライティングは、基本的なライティングの概念は平行光源と同様です。ライトベクトルと頂点の法線や視点ベクトルとの内積を取ることによって陰影付けを行ないます。平行光源と異なるのは、ライトベクトルがあらかじめ一定の値なのか、そうではないのか、簡単に言ってしまえばそれだけです。点光源ではモデル座標変換を行なったあとの頂点の位置と光源の位置とを使って、その都度ライトベクトルを算出するようにしますので若干ですが計算量が増えます。
平行光源では光の向きが一定だったので、全体的に光が均等に当たります。しかし点光源では頂点の座標に応じて詳細に光の当たり具合が変化します。今回のサンプルも前回同様フラグメントシェーダ内でライトの計算を行なうフォンシェーディングですので、非常に綺麗なライティング処理が行なわれます。
今回のテキストでとりあえずライティングに関する基礎的な部分は実装できたと考えていいでしょう。WebGL ではシェーダを工夫することで様々なエフェクトや演出効果を実装できますので、ここからは応用力も必要になってきますね。特殊なテクニックに関してはまたいずれ解説できればと思っています。
さて、今回も実際に動作するサンプルを用意してあります。後述するリンクからサンプルページに飛べます。
次回はいよいよテクスチャをやります。お楽しみに。