法線マップの生成
今回のサンプルの実行結果
法線マップとはなんぞや
バンプマッピングなどの特殊なライティング処理を行う場合に欠かせない法線マップ。多くのゲームやデモプログラムに使われているので、3D プログラミングの世界では多少はメジャーな存在でもあります。
法線マップとは、二次元に展開された、法線の情報をマッピングした画像のことを言います。法線マップの生成には、専用のツールやペイントソフトのフィルタ機能、あるいはフォトレタッチソフトの法線マップ専用プラグインを用いるのが一般的です。たかが法線マップ、されど法線マップ――というわけでもないのですが、利用したツールによって高品位な法線マップが出力できるかどうかは変わってきます。内部的に用いられているアルゴリズムの違いによって、品質に差が出るのですね。
以前、バンプマッピングの解説を行った際には、フリーソフトの法線マップ変換ツールをダウンロードしてきてサンプル用の法線マップを手動で用意しました。もちろん、そのような手順でフリーソフトなどを利用して事前に法線マップを準備できるときはいいのですが、どうしても動的に法線マップを生成する必要がある場面に出くわすことがあります。
例えば、フレームバッファや canvas にあらかじめ描き込まれている情報を、そのまま法線マップに変換してリアルタイムに利用したい場合などが考えられます。こういったケースでは、動的に生成されたビットマップからさらに動的に法線マップを生成しなければなりません。今回はこれを canvas でやってみることにしましょう。
法線マップ生成手順
そもそも、法線マップはその名のとおり法線を操作するために利用されます。動的に法線マップを生成したいシーンを考えてみると、たとえば金属のような質感を持つモデルに凹凸をつけたい場合(戦闘で傷つくと表面に裂傷ができるとか)や、水面などの動きのあるものに対して法線を適用して見せる場合などが考えられますね。
WebGL や canvas のような、ブラウザ上で動作することが前提となるものは特に、インターネット上でリアルタイムに更新されるデータをもとにシーンを構成することができます。このことから考えても、動的に法線を生成し適用したい場面は少なからずあるような気がします。
法線マップを動的に生成するには、二次元にマッピングされた高さに関する情報が必要です――と言ってもイメージしにくいと思いますが、要は凹凸を表現するために法線マップが必要なわけですから、この凹凸に関する情報、つまり高低を表すデータが二次元上に展開されていることが前提です。
色を数値で表す場合、普通は明るい色のほうが大きな数字で表現されますね。ですから、白に近い場所ほど高い、黒に近い場所ほど低い、と考えるのが普通です。そして、隣接するピクセルの色との比較を行いながら、色の明るさに応じて高低や傾きを求め、それを法線に変換してマッピングします。これが動的に法線マッピングを行うための基本的な考え方になります。
高低に関する情報だけで法線マップは生成できます。当然、単純に高低を表すだけであれば色成分を表す RGB の三つの領域は必要ありません。R 成分ひとつあれば、それで高さを表現することはできてしまいます。これは逆に言うと、処理のしかた次第では、法線マップ生成の対象となるビットマップが白黒の場合に限り正しく法線マップが生成されるということでもあります。
今回のサンプルでは、白黒ビットマップだけでなく、RGB のすべての要素を用いたカラービットマップでも処理できるように、内部でピクセルの輝度を計算してそれを元に法線を算出するロジックを採用してやってみましょう。
法線マップ生成に利用する関数群
法線マップの生成には、いくつか補助関数を用意しておくほうがスマートに処理を記述できます。
今回のサンプルでも補助関数を使っていますので、先にそちらから見ていきます。
補助関数群
function returnLuminance(v, index){
return v[index] * 0.298912 + v[index + 1] * 0.586611 + v[index + 2] * 0.114478;
}
function vec3Normalize(v){
var e;
var n = [0.0, 0.0, 0.0];
var l = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
if(l > 0){
e = 1.0 / l;
n[0] = v[0] * e;
n[1] = v[1] * e;
n[2] = v[2] * e;
}
return n;
}
function vec3Cross(v1, v2){
n = [0.0, 0.0, 0.0];
n[0] = v1[1] * v2[2] - v1[2] * v2[1];
n[1] = v1[2] * v2[0] - v1[0] * v2[2];
n[2] = v1[0] * v2[1] - v1[1] * v2[0];
return n;
}
三つの関数を今回は定義しました。一つ目の returnLuminance
は RGB から輝度を算出するために利用します。やっていることとしては、グレイスケール変換を行うときと同じように、RGB の各要素に一定の係数を掛けることで輝度を算出する感じですね。
続いての補助関数二つ目は vec3Normalize
という名前からも想像できると思いますが、ベクトルを正規化する関数です。そして続く三つ目の補助関数も読んで字の如くで、この vec3Cross
という関数を用いることでベクトル同士の外積を計算できます。
これら三つの補助関数は次の項で登場します。その役割を覚えておいてください。
法線マップ生成のコード
それでは法線マップを生成するためのコードを見てみます。
法線マップ生成関数
function normalMap(src){
var i, j, k, l, m, n, o;
var g, f;
var width = src.width;
var height = src.height;
var ctx = src.getContext('2d');
f = ctx.getImageData(0, 0, width, height);
g = ctx.createImageData(f);
for(i = 0; i < width; i++){
for(j = 0; j < height; j++){
k = (i - 1 < 0 ? 0 : i - 1) + j * width;
m = returnLuminance(f.data, k * 4);
k = (i + 1 > width - 1 ? i : i + 1) + j * width;
n = returnLuminance(f.data, k * 4);
l = (n - m) * 0.5;
k = i + ((j - 1) < 0 ? 0 : j - 1) * width;
m = returnLuminance(f.data, k * 4);
k = i + ((j + 1) > height - 1 ? j : j + 1) * width;
n = returnLuminance(f.data, k * 4);
o = (n - m) * 0.5;
var dyx = [0.0, l, 1.0];
var dyz = [1.0, -o, 0.0];
var dest = vec3Normalize(vec3Cross(dyx, dyz));
k = i + j * width;
g.data[k * 4] = Math.floor((dest[2] + 1.0) * 0.5 * 255);
g.data[k * 4 + 1] = Math.floor((dest[0] + 1.0) * 0.5 * 255);
g.data[k * 4 + 2] = Math.floor((dest[1] + 1.0) * 0.5 * 255);
g.data[k * 4 + 3] = 255;
}
}
return g;
}
この関数は引数を一つ取ります。引数には、処理対象となる canvas を与えます。また、この関数は戻り値を canvas の imageData
として返しますので、適宜描き出したい canvas に対して適用してあげてください。
関数の冒頭では、与えられた canvas 要素から幅や高さを取得したり、2D コンテキストを取得したりといろいろやっていますね。まず最初のポイントとなるのは変数 f
の中身です。変数 f
は引数として与えられた canvas の中身、つまり canvas の imageData
です。この配列変数 f
の内容を参照することによって、変換の元となるデータの色情報を抜き出して処理します。
抜き出した色情報を元に法線マップを生成するわけですが、その処理を行っているのがネストを含む for ループですね。このループ処理によってすべてのピクセルを走査処理します。
ループの中では大量の三項演算子(条件演算子)が出てくるので非常に紛らわしいと思いますが、ここでは対象となるピクセルが上下左右の端に位置するかどうかを調べています。今回のサンプルでは、各端部では反対側の端を見るような処理になっていますが、ここは必要に応じて適宜変更して利用するといいでしょう。
対象のピクセルが上下左右の端でなかった場合には、隣接する上下左右のピクセルの輝度を一度変数に代入しておきます。それぞれ、差分を元に三次元ベクトルを生成して外積を取ることで、法線に相当する情報を算出します。最終的に、法線を正規化してから書き出し用の imageData
にデータを出力しているのが、コードを見てもらえればわかると思います。
もし白黒のビットマップから法線マップを生成する場合には、輝度を生成するための補助関数 returnLuminance
は使わずに処理できます。この場合は若干ですが処理が高速化できるはずです。
プログラムの理解とプログラムの利用
と、ここまでを読んでもベクトルやら外積やらわかりませんという人もいるでしょう。そういう人は、むしろ難しく考えずにコードをコピーするのもありだと思います。というのは、この手の処理や 3D プログラミングの多くに言えることですが、理解できていることと利用できることとはまったく関係ありません。最近よく感じますが中身でなにが起こっているかわからなくても、正しく処理する手段だけを持っていればプログラミングはできます。いずれ、どうしてこれで法線マップが生成できるのか、その仕組みを深く理解したくなったら再度コードと向き合えばいいのです。
仕組みを深く理解していることは、自分自身で応用処理を記述したり、素早く効率的にコードを記述できたり、またバグの原因追及時にかなり有利になるなどいいことづくめです。しかし、理解を強制する必要はないと思っています。自分自身の成長のためにコードを読み解くならそれもよし。とりあえず使ってみようということであれば、まず使ってみたらいいと思います。
まとめ
久しぶりに canvas ネタをお届けしましたがいかがでしたか。
あまり実用的なコードではないかもしれませんし、出来上がる法線マップも大していいものでもないのですが、よほど高い解像度でアップで表示されるような部分でなければ、この程度の簡易な法線マップを用いたバンプマッピングでもそれほど粗は目立たないでしょう。
実際問題、このコードは非常に極端なエッジの強い法線マップしか生成できません。高品位な法線マップを生成するためには、もっと広い範囲をサンプリングしてやる必要があるでしょう。今回のサンプルは本当に最小限の構成ですが、ビットマップから法線に相当する情報を算出する基本は押さえてあると思います。サンプルでは実際にローカルの画像を参照して法線マップに変換できます。白黒やカラーなど、いろいろな画像を使ってテストしてみてください。