第15回からこれまで、3次元表現のお題をふたつ仕上げた。3次元座標や透視投影にも慣れ、簡単なクラスの扱いもわかってきたことだろう。そこでもうひとつ、3次元空間のインタラクティブなお題に取り組みたい。立方体を、マウスポインタの位置に応じて水平および垂直に回してみる。今回を含めて、3回ほどでつくるつもりのサンプルが、つぎのjsdo.itのコードだ。
立方体の頂点をワイヤーフレームで結ぶ
今回書くコードは、ワイヤーフレームの立方体を、マウスポインタの水平座標に応じてy軸で回すところまでになる。ワイヤーフレームの星形をy軸で回すというのが、ひとつ目のお題だった。そのでき上がりとなる第17回コード2「冒頭に3次元座標のクラスと透視投影のメソッドを定義したy軸で星形が回るアニメーション」を書き替えていくことにする(再掲第17回図1)。ご参考までに、このjsdo.itのサンプルも以下に添えた。
第17回コード2では、y軸で回した3次元の頂点座標は、透視投影したうえで、2次元の頂点を直線で結んだ。描くかたちは、星形にはかぎられない。そこで、座標を立方体の8頂点に差し替えてみよう。原点(0, 0, 0)は立方体の真ん中に置く。すると、一辺の半分の長さ(halfEdge)を決めれば、立方体の8頂点座標は定まる(図1)。
一辺の半分の長さから8頂点座標(Point3Dオブジェクト)の配列を返す関数(createCubePoints())はつぎのように定めて、第17回コード2の星形座標の関数(createStarPoints())と差し替える。なお、回り始める角速度(angle)は0にして、初めは止まった状態とした。
この書替えだけで、まがりなりにもワイヤーフレームのアニメーションとしては動く。もっとも、頂点はひと筆書きで結ばれてしまうため、6面の立方体には見えない(図2)。したがって、つぎは面ごとに分けて描くにはどうしたらよいかだ。
立方体の面を頂点番号で定める
立方体のそれぞれの面を、分けて扱わなければならないことはわかった。とはいえ、面ごとに座標をもたせるのは無駄だ。四角形の4頂点座標に6面を掛けると、24座標になる。けれど、立方体はひとつの頂点を3つの四角形が共有しているので、座標そのものは8つで済む。
そこで、立方体の8頂点にインデックス(整数番号)を与える(図3)。すると、立方体の各面はその4頂点の番号で定められる。そして、回転の変換と透視投影は8頂点座標に加え、面を描くときに頂点番号から座標を求めればよい。そのため、頂点に定めるインデックスは、8頂点座標を入れた配列内のインデックスと揃えておく。一般に、3次元座標の変換や透視投影より、インデックスから値を探して取出す方が負荷は低い。
3次元座標(Point3D)と同じく、面もつぎのようにクラス(Face)で定めよう。コンストラクタの引数は、4頂点番号だ。また、配列(Arrayオブジェクト)と同じように扱いたいので、頂点番号はインスタンスの整数インデックス(0~3)にエレメントとして納め、インスタンスのlengthプロパティも定めた[1]。
この面のクラス(Face)のインスタンスを配列に納めて返す関数(getFacesVertices())は、つぎのとおりだ。とりあえず、立方体の前と後ろの2面だけで試す。立方体の8頂点座標(points)と面の頂点番号(facesVertices)から面を描く関数(drawFaces())は、この後定める。
面(Face)のオブジェクトの配列(facesVertices)から順にエレメントを取出して、面の頂点番号を座標に直したうえでひとつひとつ描く。ひとつの面の座標の組(配列)からひと筆書きで線描する仕事は、第17回コード2の関数(draw())がほぼそのまま使える。
新たにほしいのは、立方体の8頂点の座標から面の頂点番号に対応したものを拾って返す関数だ。これは、面のクラス(Face)のメソッド(getFacePoints())として定めよう。面の頂点番号はインスタンスがもっているのだから、引数には立方体の8頂点の座標の配列を渡せばよい。戻り値は、面の頂点の座標を納めた配列だ。
このメソッドはできたとしよう。論文をあらすじから考えるのと同じで、いきなり細かい実装に手をつけるより、処理の大きな流れを先に考えた方が見通しはよくなる。また、実装の中身もはっきりするだろう。
面(Face)のオブジェクトの配列(facesVertices)から順にエレメントを取出して、すべての面を描く関数(drawFaces())はつぎのように定めた。引数は、立方体の8頂点座標が納められた配列(points)と面のオブジェクトの配列(faces)だ。この関数は、前掲の初期化の関数(initialize())に加え、アニメーションのリスナー関数(rotate())からも呼び出される。
面のクラス(Face)のメソッド(getFacePoints())ができたことにしてしまえば、とくに難しいところはない。ひとつ補っておくと、1面を線描する関数(draw())につぎのような手直しが加わる。複数の面を描くことになったので、Graphics.clear()メソッドの呼出しは前掲のすべての面を描く関数(drawFaces())から行う。Stage.update()メソッドの呼出しも同じだ。
今回の課題で残るは、面のクラス(Face)に新たに加えるメソッド(getFacePoints())だ。透視投影した立方体の8頂点座標が納められた配列を引数(points)に受け取って、自らの4頂点番号に合った座標エレメントを取り出し、頂点番号の順に新たな配列(facePoints)に入れて返す。予め、8頂点座標の配列内のインデックスと頂点番号は揃えておいた。したがって、自らに定められた4つの頂点番号から、そのインデックスの座標エレメントを取り出せばよい。
これで、アニメーションの骨組みはできた。確かめてみると、立方体の前面と後面が、マウスポインタの位置に応じて水平に回る(図4)。では、以下のように立方体の両側面も座標の配列を返す関数(getFacesVertices())に含めよう。また、面のワイヤーフレームを描く関数(draw())における線幅の指定は細くする。
読者の中には、初めから立方体の4面を入れておけば早かったと思われた方もあろう。けれど実は、筆者が初めてコードを試したとき、立方体の1面しか描かれなかった。理由を探るために、ワイヤーフレームを描く関数(draw())が呼び出されているか、引数の座標は正しいかといったことを調べた。原因は関数に、前述したGraphics.clear()メソッドの呼出しが残っていたためだった。こうした場合、面の数が多いと、その分確かめる情報が増えて煩わしいことになるのだ。
今回書上げたコード全体を以下にまとめよう。見やすさも考えて、クラス定義はscript要素を分けた(コード1)。クラスを使い回す機会ができたら、このJavaScriptコードはJavaScript(JS)ファイルにして読み込んでもよいだろう。
立方体の4面をワイヤーフレームで描き、マウスポインタの位置に応じて水平に回すアニメーションのスクリプトは以下のとおりだ(コード2)。つぎは、立方体の4面を塗りたい。その場合、前回第19回「3次元空間で弾むオブジェクトとz座標による重ね順の並べ替え」で学んだように、面の重ね順を考えなければならない。次回は、配列の並べ替えではなく、ベクトルの外積を使って解決する。
いつものとおり、jsdo.itにもサンプルを掲げた。クラス定義のscript要素は[HTML]の欄に加えてある。