今回からのお題は、3次元でインターフェイス風の動きをつくる。サンプル1は、マウスポインタの位置に応じて空間に浮かぶ箱が回り、それぞれの箱はロールオーバーとロールアウト、およびクリックに反応する。今回は箱の配置と回転までで、これから都合4回で仕上げるつもりだ。
3次元空間の真ん中に箱を置いて回す
まずは、3次元空間の真ん中に箱を置いて回す。JavaScriptコードの組立ては第3回コード1「球体にテクスチャを貼って3次元空間で回す」と同じで、球体が立方体に変わるだけだ。立方体はPrimitiveCubePrefabクラスでひながたをつくる(第1回の「球体以外の基本的なかたちをつくる」の項を参照)。PrimitiveCubePrefab()コンストラクタの基本となる引数は幅と高さおよび奥行きの3つで、後はデフォルト値が与えられている[1]。
だが、今回は7つ目の引数を使いたい。これはテクスチャが6面の展開図で用意されているかどうかをブール値で示す。デフォルト値はtrueで展開図を使う。falseを渡すと、ひとつのテクスチャを1面分として、それを6面それぞれに貼ることになる。お題で使ったのは、公式サイトのawayjs-examplesからダウンロードできるtrinket_diffuse.jpgだ(図1)。なお、引数の水平・垂直・奥行き分割数のデフォルト値は、それぞれ1が与えられている。
前述のとおり、第3回コード1と同じ組立てで、3次元空間に立方体を置いて回したのが後に掲げるコード1だ。おもな違いをつぎに抜出した。ひながたはPrimitiveCubePrefabクラスで定め、立方体をつくる関数(createCube())の引数はPrimitiveCubePrefab()コンストラクタに合わせた。テクスチャは6面それぞれに用いるよう、7番目の引数がfalseになっている。後は、変数や関数の名前を立方体(cube)に合わせて変えたくらいだ。
これで3次元空間の真ん中にテクスチャの貼られた立方体が置かれ、水平および垂直軸で回る(図2)。箱を回したのは、とりあえずかたちやテクスチャを確かめるためだ。お題では箱は動かさずに、カメラのほうを回り込ませる。
立方体を中心に三角関数でカメラを回す
つぎは、カメラを動かそう。立方体を真ん中にして回るようにする。記憶のよい読者は、前回第6回の「スカイボックスの中にドーナッツ型を置く」の項で行ったと思い出したかもしれない。しかし、おさらいばかりではつまらない。引出しを増やす意味でも別のやり方を使ってみよう。
カメラの向きは後に回し、立方体を中心として水平に円を描くように動かす。円軌道の座標は、三角関数のsinとcosで求める。原点O(0, 0)を中心に描いた半径1の円(「単位円」と呼ぶ)について、原点Oからx軸正方向に対して角度θの直線と交わる点Pのxy座標は三角関数により(cosθ, sinθ)と定められている(図3)。つまり、原点からの距離は1で角度がθの点のxy座標は(cosθ, sinθ)ということだ[2]。すると、原点からの距離と角度をもとに、円周上のxy座標はつぎの式で導かれる。
では、カメラはどうやって立方体に向ければよいか。動かすたびに、カメラに対してDisplayObject.lookAt()メソッドを呼び出せばよい(第6回の「スカイボックスの中にドーナッツ型を置く」の項を参照)。立方体は3次元空間の真ん中に置いたので、向ける座標は原点(0, 0, 0)だ。
カメラを動かす関数(setCamera())は、つぎのように定めよう。引数にはCameraオブジェクトと距離および角度を渡す。Mathクラスの三角関数を使うので、角度の単位はラジアンとする。
前掲コード1は、つぎのように手直しする。カメラを動かす関数(setCamera())は、まず初期設定の関数(initialize())から呼び出す。距離と角度は変数(distanceとangle)に定めた。なお、角度の初期値(-π/2 = -90度)は立方体の手前正面になる。そして、アニメーションの関数(rotate())が、経過時間(timeStamp)に応じて角度を増し、そのたびにカメラを動かす関数が呼び出される。
この機会に、描画領域(view)の幅と高さは変数(stageWidthとstageHeight)に定めた。
これで、立方体を3次元空間の真ん中に捉えつつ、カメラがその周りを回る(図4)。手を加えた後のスクリプト全体は以下のコード2のとおりだ。お題の土台ができたといえる。
すでに述べたとおり、このようなカメラの動きは前回と同じようにTransform.moveBackward()メソッドを用いてもできる。三角関数を使うことで、何か得はあるのだろうか。たとえば、sinとcosを使って楕円軌道を描くこともできる。その場合には、x座標とy座標それぞれを求める距離の大きさを変えればよい。
試しに、前掲コード2のカメラを動かす関数(setCamera())で、座標を求める式の一方につぎのように比率(0.5)を乗じてみる。すると、カメラは楕円軌道を動くので、立方体に近づいたり離れたりすることになる(図5)。もし、Transform.moveBackward()メソッドで後ろに下げようとすれば、その距離を求めなければならず、三角関数を使わざるを得ない。それなら、初めから三角関数でカメラそのものを動かすほうが早い。
原点からの円周上に複数のオブジェクトを並べる
真ん中に置いた箱の周りに、衛星のようにとりあえず5つほど箱を置きたい。これらの位置も円軌道のうえに定めるので、やはり三角関数を使う。衛星は真ん中の箱を複製して用いることにする。Mesh.clone()がそのためのメソッドだ。引数はとらない。衛星の箱をつくる関数(cloneMesh())はつぎのように定めて、複製もとのオブジェクト(箱)とつくる数を引数に渡す。
複製のオブジェクトをつくったら、円軌道上に置く。その位置決めは別の関数(getPolarPosition())で行う。引数には距離と角度を渡して、3次元座標をVector3Dオブジェクトの戻り値で得る。角度は度数としよう。このとき、角度の方向に気をつけたい。
オブジェクトの位置は、y軸周りの角度で決める。このとき、コンピュータグラフィックスではy軸は下が正の向きだ。そして、その向きに時計回りが角度の正方向とされる。つまり、座標空間を上から見下ろしたxz平面で、時計回りが正となる(図6)。これは三角関数で扱う角度と向きが逆だ[3]。したがって、三角関数には角度の値を、正負逆にして与えなければならない。
オブジェクトを複製する関数(cloneMesh())は、つぎのように初期設定の関数(initialize())から呼び出す。複製するオブジェクトは、引数に受け取った個数(count)にもとづいて均等の角度(degrees)で置くことにした。原点からの距離(distance)も等しくし、位置決めの関数(getPolarPosition())にその値と角度を渡して得た戻り値(position)から、オブジェクトの3次元座標が定められている。なお前述のとおり、位置決めの関数の中で、y軸周りの角度(rotationY)は正負逆にしてある。
ひとつ補っておきたいのは、オブジェクトを複製する関数(cloneMesh())の中でオブジェクト(clone)そのものを配置したのと同じ角度(rotationY)回したことだ。こうしないと、オブジェクトはすべて真ん中の箱と同じ向きになる。観覧車のゴンドラと同じ状態だ。そうではなく、原点と自分を結んだ直線の向きになるようにした。つまり、カメラがオブジェクトを画面の真ん中に捉えたとき、つねに正面を向くことになる。
これで、真ん中のオブジェクトを中心とした円周上に、等しい間隔で複数のオブジェクトが並べられた(図7)。カメラはその外の円軌道を回り込んで、真ん中の箱を画面の中心に捉える。以下のコード3に、ここまででき上がったスクリプトをまとめた。同じコードは、jsdo.itにサンプル2として掲げてある。次回は、周りに配置するオブジェクトの位置や大きさをランダムにしてみよう。