前回の第20回「立方体のワイヤーフレームを水平に回す」でつくった立方体の四方の面を塗りたい。ただしその場合は、面をどの順序で塗るか、考えておかなければならない。そうしないと、面の重ね順が崩れる(図1)。今回は、「ベクトルの外積」を使って解決することにしよう。
面の塗り順をどう扱うか
3次元空間でオブジェクトを描く順序については、第19回「3次元空間で弾むオブジェクトとz座標による重ね順の並べ替え」で学んだ。このときは、Array.sort()メソッドにより、「オブジェクトの重なりをz座標値の順に並べ替える」ことにしたのだった。今回も、配列の並べ替えで塗り順を決める手は使える。だが、Array.sort()メソッドの処理の負荷は決して軽くない。第19回※1をつぎに改めて引用した。この動画を見れば、JavaScriptの手間もうかがい知れよう。
実は、今回扱う立方体は、閉じた凸の多面体という扱いやすいかたちだ。まず、面の裏側は外から決して見えない。つまり、裏返った面は描かなければ済む。つぎに、表向きの面は、互いに重なることはない。したがって、表の面を描く順序は考えなくて構わないのだ。
- 閉じた凸の多面体
2次元ベクトルの外積で面の裏表を調べる
そこで、面の裏表をどうやって調べるかだ。ベクトルには「外積」という計算がある。この外積をうまくつかえば、面が裏か表か、たやすくわかる。外積は、3次元ベクトルで用いられることが多い。けれども、今回は透視投影した後の2次元平面のベクトルで考える。
ベクトルは大きさと向きをもつ。けれど、どこを始点とするかは問わない。つまり、始点が違っても、向きが等しく平行で、長さの等しいベクトルは、互いに等しい(図2左)。そして、始点を原点に揃えると、ベクトルは終点の座標のみで表せる(図2右)。原点を始点とするベクトルは「位置ベクトル」という。
2次元平面の位置ベクトルA(ax, ay)とB(bx, by)の外積はA×Bで表し[1]、つぎの簡単な四則演算の式で定められる。この式の数学的な意味については、興味をもたれた読者向けに後で解説する。計算そのものは、ただの掛け算と引き算だ。
- 2次元ベクトルA(ax, ay)とB(bx, by)の外積
- A×B =axby - aybx
ここで覚えていただきたいのは、上記の外積の式はふたつの項の引き算なので、A×BとB×Aは正負が逆になるということだ。つまり、外積には交換法則が成立たない。外積A×Bについて幾何学的に説明すると、ベクトルBがAに対して右ネジの位置にあると正、左ネジの位置にあれば負になる(図3)。
では、2次元ベクトルの外積で面の裏表を確かめよう。あらかじめ表向きの面の3頂点からふたつのベクトルを定め、位置関係を(たとえば右ネジに)決めておく(図4)。すると、面がどう回転しようが、表向きであるかぎり、ふたつのベクトルの間の位置は(右ネジのまま)変わらない。
実はこのために、前回立方体の4面の頂点座標はすべて、つぎのようにそれぞれの面ごとに時計回りのインデックスを与えておいた(第20回図3再掲)。したがって、各面の第1頂点から第2頂点へのベクトルと第1頂点から第3頂点へのベクトルの位置は、みな右ネジに揃う。
表向きの面のふたつのベクトルの位置関係を右ネジに定めれば、外積が正なら表のままで、負になったら裏返ったことがわかる(図5)。そして、立方体の面のうち、ふたつのベクトルの外積が正の場合だけ描けば、それですべて丸く治まる。
立方体の表向きの面のみを塗る
まずは、第20回につくったサンプル(コード1および2)の立方体の4面を塗ろう。まだ、面の裏表は考えない。面のクラス(Face)には、つぎのように塗り色のプロパティ(color)を加える。また、塗り色はランダムに決めたいので、ランダムな整数を返す関数がほしい。そこで、クラスのように扱える名前空間のオブジェクト(MathUtils)を定めて、その静的メソッド(getRandomInt())として加えた。引数の最大値と最小値(minとmax)の順序は、逆でも値が求まるように条件判定の処理を入れている。
面のクラス(Face)のコンストラクタに塗り色の引数が加わったので、面の配列をつくる関数(getFacesVertices())からの呼出しにランダムなカラーを与える。ランダムなカラーを返す関数(getRandomColor())は、前掲のランダムな整数が得られるメソッド(MathUtils.getRandomInt())を用いた。
そして、ひとつひとつの面を描く関数(draw())にも塗り色を引数(color)として加える。面の線描は除いた。立方体を描く関数(drawFaces())は、面のオブジェクト(face)のプロパティ(color)から塗り色を定めている。
これで、立方体の面がランダムな色で塗られる。もちろん、まだ塗り重ねについて考えていないので、面の前後関係は崩れている(前掲図1参照)。面の裏表を調べるために必要なメソッドをクラス(MathUtils)に加えよう。
外積を計算するには、ベクトルを定めなければならない。始点(vector0)と終点(vector1)のふたつの座標から、位置ベクトルを求めるメソッド(MathUtils.subtractVectors())は、xy座標それぞれを引き算してPointオブジェクトで返す。ふたつのベクトル(vector0とvector1)から外積を導く関数(crossProduct2D())は、前項で示した2次元ベクトルの外積の計算式にしたがって値を返している。
立方体を描く関数(drawFaces())は、表向きの面だけを描く。面が表向きかどうかは、関数(isFront())で調べる。引数(facePoints)は4頂点座標をもつ面のオブジェクトだ。初めの3頂点座標から、表の面に対して右ネジに定められたふたつのベクトル(vector0とvector1)を得る。そのうえで、外積が0以上、つまりふたつのベクトルが右ネジの位置にあるかどうかをブール(論理)値で返している。
これで、立方体は表向きの面だけが塗られる。面を描く順序はとくに変えることなく、水平に回る立方体が正しく表現される。ベクトルを求めたり、外積を計算したりするのは、簡単な四則演算だ。Array.sort()メソッドより、ずっと負荷は軽い。しかも、見えない面は描かないので、無駄な描画も省ける。かなりお得な処理といえる。
書上げたJavaScriptコードを以下にまとめよう。まずは、クラスの定めがコード1だ。面のクラス(Face)には塗り色のプロパティ(color)を加えた。また、数学的な計算のためのクラス(MathUtils)を新たに備えた。このクラスのメソッドはすべて静的に(クラスを直接参照して)呼び出す。
回転する立方体を描くつぎのコード2は、表向きの面だけを塗り重ねた。新たに定めた関数(isFront())は、2次元ベクトルの外積(MathUtils.crossProduct2D())から面が表か裏かを確かめて返している。
サンプルのコードをjsdo.itに掲げた。クラス定義のscript要素(前掲コード1)は[HTML]の欄に書き分けてある。次回は、立方体の6面をすべて塗ったうえで、さらにx軸で垂直にも回るようにしたい。
ベクトルの外積とは
ベクトルの外積についてもう少し知りたい読者のために、数学的な説明を補っておく。3次元空間のベクトルAとBの外積はA×Bで表され、つぎのような新たなベクトルとして求められる(表1)。そして、ふたつのベクトルAからBに回したとき右ネジが進む向きに定められる(図7左)。つまり、外積には交換法則が成り立たない。
表1 3次元ベクトルの外積A×Bで求められるベクトル
外積の要素 | 外積のベクトルとふたつのベクトルの関係 |
---|
角度 | ふたつのベクトルAとBを含む平面に垂直(図7左) |
方向 | ベクトルAからBに向かう回転を考えたとき、その回し方で右ネジの進む方向(図7左) |
大きさ | ベクトルAとBを隣り合う2辺とした平行四辺形の面積(図7右) |
3次元空間のベクトルAとBの位置座標を、それぞれ(ax, ay, az)および(bx, by, bz)とすると、外積はつぎの式で定められる。
2次元平面で考えると、z座標値(azとbz)はつねに0だ。したがって、A×B = (0, 0, axby - aybx)となる。そこで、2次元平面のベクトルA(ax, ay)とB(bx, by)の外積は、3次元の外積のz座標値で表す。ベクトルAに対してBが右ネジの位置にあるとき値は正、左ネジなら負となる。
2次元ベクトルの外積は、ほかにも使い道がある。興味がある読者は、「珍味ベクトル外積3種盛り」をお読みいただきたい。