今回は、面に色を塗るフラットシェーディングです。
実はてっく煮のオリジナルのプログラムである「AS3.0 で 3D プログラミングを1から勉強する (4) - 面に色を塗る」は、フラットシェーディングなので光源の位置によって面の色の濃さが変わるはずなんですが、半透明かつ回転しているためか色の濃さが変わっているように見えなかったわけです。
最初に実際に移植してみても面の色の濃さが変わらず、当時は原因が分からなかったため、内積と外積および面の明るさ(ランベルトの法則)をちゃんと理解した上で移植することにしました。
このフラットシェーディングを理解するまでに内積と外積と正規化の記事を勉強しながら書いたため、内容に間違いがあるかも知れません。これから衝突や物理を学んでいく上で内積と外積の理解が深まったら訂正および追記していきます。
実際に理解した上で移植し直したわけですが、結論から言うとプログラムの考え方は間違っていなかったのですが、小数の値のまま色の指定をしていたために正常に値が反映されなかったのです。
小数を整数に変換(ビット演算による小数の整数化)することで解決しました。
var b = (63 * product + 192); ↓ var b = (63 * product + 192) | 0; var rgb = 'rgb(' + b + ',' + b + ',' + b + ')';
フラットシェーディングの仕組み自体は、てっく煮のサイトと正規化のところで記載してあるので、このプログラムを移植する上で悩んだ部分を書いていきます。
■Zソート
Zソートは、Z座標が大きい(奥のもの)から順番に描画する方法です。
下記の2点でZソートを使用しています。
・奥の立方体から描画するために並べ替える
・立方体の面についても奥の面から描画するために並べ替える
Zソートなしの場合では、黄色の立方体が奥にあるのに白の立方体の後に描画されてしまいます。
オリジナルのActionscriptでは、Dictionaryクラスを使用してZソートしているのですが、Javascriptでは、Dictionaryクラスが存在しなかったのでZソート (奥のものから順番に並べる)のところは少し悩みました。
Array.sortメソッドはそのまま利用したかったので、面のZソートでは面クラス(Plane)を作成して解決させました。
//面クラス var Plane = (function () { function Plane(p1, p2, p3, p4) { this.p1 = p1; this.p2 = p2; this.p3 = p3; this.p4 = p4; this.z = (p1.z + p2.z + p3.z + p4.z) / 4; } return Plane; })(); var planes = []; planes.push(new Plane(p[0], p[1], p[2], p[3])); planes.push(new Plane(p[7], p[6], p[5], p[4])); planes.push(new Plane(p[0], p[4], p[5], p[1])); planes.push(new Plane(p[1], p[5], p[6], p[2])); planes.push(new Plane(p[2], p[6], p[7], p[3])); planes.push(new Plane(p[3], p[7], p[4], p[0])); // Zソート (奥のものから順番に並べる) planes.sort(function (a, b) { return b["z"] - a["z"]; });
立方体クラスのZソートでは、zsortプロパティを作成してArray.sortメソッドで並び替えています。
for(var i = 0; i < this.cubes.length; i++) { this.cubes[i].setZSort(matrix.transformPoint(this.cubes[i].center)); } //並び替え this.cubes.sort(function (a, b) { return b["zsort"] - a["zsort"]; });
■内積と外積の角度
面の明るさを求める際に、内積と外積を使用して光源に対する値を計算するのですが、内積はcosθの値、外積はsinθの値のまま使用します。
今回は角度の情報を表示して、法線ベクトル(sin)が90°に近いと光源の角度(cos)が0に近くなって、面の明るさが最大値255に近くなるのを実感したかったわけです。
角度を実際に出力するには、逆正弦(asin)と逆余弦(acos)を使います。ラジアン値で返るので角度に変換(ラジアン * 180 / Math.PI)します。
// 情報表示 Cube.prototype.drawInfo = function (g, n, p, b, i) { g.fillStyle = "white"; //sin=法線ベクトル cos=光源 color=色 255(最大) 光源角度が小さくなる程、 //色が最大値になる。 g.fillText('No.' + (i + 1) + ' sin=' + ((Math.asin(n.z) * 180 / Math.PI) | 0) + ' cos=' + ((Math.acos(p) * 180 / Math.PI) | 0) + ' color=' + b, 10, (i + 2) * 10); };
■プログラム
オリジナルとは違いは、小さい立方体の色を分けたところです。また、各設定のチェックボックスや範囲指定を追加して、違いを見えるようにしました。