シェーディングの下準備
フラットシェーディングを実装するために、ベクトル関係のメソッドをいくつか説明しておきます。まずはこのステップのプログラムから新しくVector3
クラスに加わっているメソッドからです。今までのVector3
クラスは主に頂点座標を表すために使ってきましたが、今回からベクトルらしくなってきます。
dot
メソッドは、ベクトルvとの内積を計算し、結果を返すメソッドです。
public function dot(v:Vector3):Number { return x * v.x + y * v.y + z * v.z; }
計算はとても単純で、x、y、z同士を掛け合わせたものを足すだけです。こんな計算で何ができるか疑問ですが、ベクトルの長さを返すlength
プロパティでは内積が役に立っています。また、両方のベクトルの長さが1のときの内積は特別な意味を持ちます。長さが1のベクトルとベクトルの間の角度をθとすると、cosθは内積と等しくなります。数式で内積を表すときには、ドット(・)を用います(例:a・b)。
cross
メソッドは、ベクトルvとの外積を計算し、結果を返すメソッドです。
public function cross(v:Vector3):Vector3 { return new Vector3( y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x ); }
この計算は少し複雑ですが、「ベクトル 外積」といったキーワードで検索すれば解説が見つかるかと思います。この外積を使うと、三角形の頂点座標を使って、三角形に垂直な方向のベクトル、つまり法線を計算することができます。数式で外積を表すときには、×を用います(例:a×b)。余談ですが、英語では内積をdot product、外積をcross productと呼びます。内積と外積を表す数式の記号は、まさにこれを表していると言えます。
length
プロパティは、ベクトルの長さを返すプロパティです。
public function get length():Number { return Math.sqrt(dot(this)); }
自分自身との内積の平方根がベクトルの長さとなります。なお、function get XXX()
という書き方をすると、メソッドではなくプロパティとなります。プロパティは、メソッド呼び出しと違って次のように()を使わずに呼び出すことができます。今回は出てきませんが、get
の反対でset
もあります。
var v:Vector3 = new Vector3(1, 2, 3); var len:Number = v.length;
Vector3
クラスの最後、normal
プロパティは、正規化されたベクトルを返すプロパティです。
public function get normal():Vector3 { return new Vector3(x / length, y / length, z / length); }
正規化とは、ベクトルの長さを1にすることです。length
プロパティで各要素を割ることで正規化しています。ベクトルの正規化はいろいろな所で出てきます。
Triangle
クラスにはnormal
プロパティが追加されています。
public function get normal():Vector3 { // a: v0からv1へ向かうベクトル var a:Vector3 = v1.sub(v0); // b: v0からv2へ向かうベクトル var b:Vector3 = v2.sub(v0); // 法線 = a×b return a.cross(b).normal; }
このnormal
は「法線」という意味で、直前に出てきたVector3
のnormal
の「正規化」とは意味が異なります。法線は外積のところでちらっと説明しましたが、面に垂直なベクトルのことです。プログラム中に出てくる頂点やベクトルを図にまとめました。
Triangle
クラスの説明でも書きましたが、三角形を表から見ると、それぞれの頂点は反時計回りにv0、v1、v2の順で並んでいます。これらの頂点を使い、三角形の辺を意味するベクトルaとbを計算します。
ある点からある点へ向かうベクトルは、終点の座標から始点の座標を引くことで計算することができます。そのため、aとbは次のように計算できます。
a = v1 - v0 b = v2 - v0
求まったaとbの外積a×bが、三角形の法線となります。a、b、そして外積a×bの関係は、前回も出てきたように右手の指を使うと簡単に説明できます。右手の親指をベクトルa、人差し指をベクトルbとしたとき、ベクトルの外積a×bは、中指の方向を向きます。外積は、aとb両方に対して垂直です。これが外積の幾何学的な意味です。外積の計算自体は複雑ですが、ベクトル同士の関係で考えると案外すっきりするものです。
なお、外積は可換ではないので、b×aは別の値になります。親指をb、人差し指をaと考えると、外積b×aである中指は下を向くと思います。紛らわしいですが、ここを間違えてしまうと、後ろを向いているはずの面が描画されてしまうなどの問題が発生してしまいます。
以上、少し複雑でしたが、外積を使うことによって三角形の頂点座標から法線を計算することができます。
フラットシェーディングの実装
いよいよランバートの法則を使ったフラットシェーディングの実装です。プログラムでは、Triangle
クラスのrender
メソッドがそれに当ります。
public function render(graphics:Graphics):void { var brightness:Number = normal.dot(new Vector3(0, 0, 1)); if (brightness <= 0) { return; } // RGBの値を0から255の範囲で適当に設定してみてください。 var r:uint = 127; var g:uint = 127; var b:uint = 255; // 0xRRGGBB形式にする var color:uint = (uint(r * brightness) << 16) | (uint(g * brightness) << 8) | uint(b * brightness); // 省略 }
まずnormal
とnew Vector(0, 0, 1)
の内積を計算し、結果をbrightness
としています。実はランバートの法則はこの1行だけです。ランバートの法則を図で表すと、次のようになります。
光の指す方向へ向かうベクトルがl、面の法線ベクトルがnです。今回は光は常に視点側から当たっていることにするので、lは(0, 0, 1)で固定です。面の法線ベクトルはTriangle
クラスのnormal
プロパティです。これらのベクトルの間の角度をθとします。面の明るさは、面が光の方向を向いているとき、つまりθが0度のときに最大となり、θが90度になると真っ暗になります。これをコサインで表すのがランバートの法則です。
面の明るさ = cosθ
コサインを使うことで、θが0度のときには明るさが1、90度(-90度でもOK)の時には明るさが0となります。コサインがとりうる値は-1~1ですが、0以下になってしまうになってしまうようなケース(面が真横を向いていたり後ろを向いていたり)では面を表示しません。
ここでθが何度なのか計算しなければならなさそうですが、そうではありません。Vector3#dot
メソッドで軽く触れたとおり、内積を使うことでcosθを計算することができます。ベクトルlとnが正規化されていれば、lとnの内積はcosθに等しくなります。
面の明るさ = cosθ = l・n (lとnの長さは1)
以上のことから、光源へ向かうベクトルと三角形の法線の内積が、面の明るさとなります。なお、明るさの計算結果が0以下であれば、何もせずに終了します。
次に、求めた明るさの値を使って、実際に塗りつぶす色を決定します。まずr
、g
、b
に最も明るいときのRGB値を設定します。そしてそれらの要素にbrightness
をかけ、ビット演算で組み合わせます。後はこの色を使って以前と同様に三角形を塗りつぶせば、フラットシェーディングした4面体を描くことができます。
マウスやキーボードを使った操作は前回と同じです。プログラムのほうはTriangle
クラスのメソッドを使った方法に書き換えてあります。
まとめ
ランバートの法則を使ってフラットシェーディングすることで、前回のワイヤーフレームから一歩進みました。これでそこそこ3Dらしくなったのではないでしょうか。ただし、まだ問題もあります。Triangle
クラスをたくさん使ってみると、前後関係がおかしくなることがあります。これはZソートという手法で改善されるのですが、今後の課題としておきます。