前回の第15回「Matrix2Dクラスで座標を回す」では、星形の2次元平面座標をPointオブジェクトで定めて直線で描き、平面上で回るアニメーションまでつくった(第15回コード3「マウスポインタの水平位置に応じてアニメーションが回る向きと速さを変える」)。今回は座標を3次元に拡げて、y軸で水平方向に回転させる。そのとき、遠近法の計算も加えていく。
z座標を加えてy軸で回す
前回述べた通り、EaselJSライブラリそのものには3次元空間を扱うクラスがない。Pointクラスもxy座標しかもたない。そこで、3次元空間の座標はObjectインスタンスでつくることにする。もちろん、プロパティはxyz座標だ。
第15回コード3に新たな関数(newPoint3D())を加え、戻り値は3次元座標のオブジェクトとする。すでに定めてあった星形の座標をつくる関数(createStarPoints())は、Point()コンストラクタの呼出しを、この新たな関数に差し替える。z座標値はすべて0でよい。
この書替えをしても、コードはそのまま動く。もちろん、z座標値など見てもいないし、回転は2次元平面、つまり第15回コード3と変わらないアニメーションになる(図1)。コードを書くときにはこのように小さな段階で、正しく動いているかどうか確かめられるように進めるとよい。
では、座標をy軸で水平に回そう。だが、どうやって。前回使ったMatrix2D.transformPoint()メソッドは、2次元平面の座標変換しか扱えない。ここで、文字どおり視点を変えよう。3次元空間を真上から見下ろす(図2)。今回のお題は座標をy軸で回すのだから、y座標はずっとそのままだ。つまり、xz平面で座標を変換すれば済むことになる[1]。
だったら、(x, z)座標を(x, y)に見立てて、Matrix2D.transformPoint()メソッドで回した座標を得る。そのy座標をz座標に入れてやればよい。回転するアニメーションの関数(rotate())につぎのような手を加える。Matrix2D.transformPoint()メソッドには3次元座標のxとzを渡す。結果をひとまず入れるPointオブジェクトは予め変数(_point)に与えた。そのうえで、結果のPointオブジェクトのxy座標値を、3次元座標オブジェクトのxとzにそれぞれ定める。
これで、3次元座標はy軸で回転する。星形が水平に回るアニメーションになった。第13回コード3を書直した全体がつぎのコード1だ。マウスポインタの水平位置に応じて回転の向きと速さも変わる。だが、アニメーションをじっと見続けていると違和感を覚えるだろう。見方によっては、回っているというより、単に水平に伸び縮みしているともいえる(図3)。この表現でよいなら、初めからShapeインスタンスを水平方向に伸縮した方が早い。こうなってしまうのは、座標の計算に遠近法が加えられていないからだ。
遠近法の投影(透視投影)
コンピュータグラフィックス(CG)の3次元座標空間の描画は、最終的に2次元平面のスクリーンに対して行われる。したがって、3次元空間で計算した座標に「遠近法」の効果を加えたうえで、スクリーンに投影するという処理になる。この処理を英語で"perspective projection"と呼び、「遠近法投影」とか「透視投影」と訳される。
遠近法では、奥行き(z座標)が遠いものほど小さく、また距離も縮めて表現する(デザインの世界では「パース」といわれる)。そして、かぎりなく遠ざかるにつれ、ある1点に向かって小さくなり、やがて見えなくなる。この点を「消失点」と呼ぶ(図4)。
また、カメラのレンズでは「焦点距離」ということばが使われる。たとえば、広角レンズは焦点距離が短く、同じ距離でも広い範囲が撮影できる。他方、焦点距離の長い望遠レンズは、写せる範囲は狭くなるものの、遠くのものを近寄せる。それだけでなく、画角は遠近感にも影響を及ぼす。
ふたつのものの奥行きの距離は、レンズが広角になり、焦点距離が短くなるほど、離れて見えるようになる。これを、遠近感の「誇張効果」という。逆に、焦点距離の長い望遠レンズでは、肉眼よりも距離が近づいて見える(図5)。一眼レフカメラでは、撮影する範囲や距離だけでなく、このような遠近感の効果も考えてレンズを選ぶ[2]。
焦点距離は、3次元座標空間における奥行きとなるz軸座標の視点から、投影面(スクリーン)までの距離を表す。焦点距離が長いほど、z位置の離れた(奥の)オブジェクトは、投影像がスクリーンに相対的に大きく表示される。また、視野角は視点から投影面全体を見た角度だ(図6)。したがって、焦点距離は、視野角と連動する。焦点距離を操作すれば、視野角もそれに応じて変わることになる。
実際のオブジェクトと投影像をそれぞれ底辺とし、ともに視点が頂点となる相似なふたつの三角形から、比率がつぎのように求められる。
したがって、3次元空間のz座標値に応じてxy座標値を2次元平面に透視投影する比率はつぎのとおりだ。予め焦点距離を定めたうえで、この投影比率をxy座標に乗じれば、透視投影された座標が定まる。
3次元空間座標を透視投影する
前掲コード1に、透視投影するための新たな関数(getProjetedPoint())を加えよう。引数は焦点距離と3次元座標のオブジェクトのふたつだ。投影された2次元座標をPointオブジェクトで返す。
焦点距離は変数(focalLength)に定めた。透視投影する関数(getProjetedPoint())は、アニメーションのリスナー関数(rotate())から、各3次元座標(point)を回転するごとに呼び出す。気をつけなければならないのは、透視投影された座標は2次元平面に描くための値で、3次元座標とは別ということだ。そのため投影後のPointオブジェクトを入れる配列を別に変数(points2D)として定めた。
透視投影の関数(getProjetedPoint())は、引数に受取った3次元座標のオブジェクト(_point3D)から、前項で示した式の比率(w)にもとづいてxy座標を求める。そして、座標値を新たなPointオブジェクト(point2D)に定めて返している。戻された値は、アニメーションの関数(rotate())が投影座標の配列(points2D)にすべて納めたうえで、線描の関数(draw())に渡されて透視投影した星形が描かれることになる。
なお、アニメーションのリスナー関数(rotate())で投影座標の配列(points2D)のArray.lengthプロパティを0にしているのは、エレメントが入っていない空っぽの状態にするためだ。もっとも今回のお題では、エレメント数が決まっていて上書きされるため、必ずしも空にしなくても構わない。ただ、このように配列を初期化しておけば、後でエレメント数が変わっても大丈夫だ。
前掲コード1にこれらの遠近法の計算を加えたのが、つぎのコード2だ。これで、水平に回る星のアニメーションに遠近感が与えられる(図7)。水平の伸び縮みではなく、回っているという感じがするだろう。
前掲コード2で焦点距離(focalLength)の値をどう決めたらよいか、疑問に思われた読者もおられよう。結論から申し上げれば、実際に試して各自の判断で決める。ただ、その際ステージの大きさを基準に考えるとよい。
コード2の値(300)はステージ幅(240)より気持ち大きめだ。もっと焦点距離が長くなれば、遠近感は弱まる(図8左)。あまり短くすると、遠近感の強い歪んだ表現になってくる(図8右)。
今回のお題の表現は、これででき上がりだ。jsdo.itにもサンプルコードを掲げた。次回は、スクリプトの組立てに手を入れて仕上げたい。具体的には、ごく簡単なクラスを定義してみる。