Away3D TypeScriptではじめる3次元表現

第7回回り込むカメラの真ん中にオブジェクトを捉える

今回からのお題は、3次元でインターフェイス風の動きをつくる。サンプル1は、マウスポインタの位置に応じて空間に浮かぶ箱が回り、それぞれの箱はロールオーバーとロールアウト、およびクリックに反応する。今回は箱の配置と回転までで、これから都合4回で仕上げるつもりだ。

サンプル1 Away3D 14/11/05: Dealing with the mouse interactions

3次元空間の真ん中に箱を置いて回す

まずは、3次元空間の真ん中に箱を置いて回す。JavaScriptコードの組立ては第3回コード1球体にテクスチャを貼って3次元空間で回すと同じで、球体が立方体に変わるだけだ。立方体はPrimitiveCubePrefabクラスでひながたをつくる(第1回の球体以外の基本的なかたちをつくるの項を参照⁠⁠。PrimitiveCubePrefab()コンストラクタの基本となる引数は幅と高さおよび奥行きの3つで、後はデフォルト値が与えられている[1]⁠。

new PrimitiveCubePrefab(幅, 高さ, 奥行き, 水平分割数, 垂直分割数, 奥行き分割数, 6面テクスチャ)

だが、今回は7つ目の引数を使いたい。これはテクスチャが6面の展開図で用意されているかどうかをブール値で示す。デフォルト値はtrueで展開図を使う。falseを渡すと、ひとつのテクスチャを1面分として、それを6面それぞれに貼ることになる。お題で使ったのは、公式サイトのawayjs-examplesからダウンロードできるtrinket_diffuse.jpg図1⁠。なお、引数の水平・垂直・奥行き分割数のデフォルト値は、それぞれ1が与えられている。

図1 立方体の各面に貼るテクスチャ
図1 立方体の各面に貼るテクスチャ

前述のとおり、第3回コード1と同じ組立てで、3次元空間に立方体を置いて回したのが後に掲げるコード1だ。おもな違いをつぎに抜出した。ひながたはPrimitiveCubePrefabクラスで定め、立方体をつくる関数(createCube())の引数はPrimitiveCubePrefab()コンストラクタに合わせた。テクスチャは6面それぞれに用いるよう、7番目の引数がfalseになっている。後は、変数や関数の名前を立方体(cube)に合わせて変えたくらいだ。

var PrimitiveCubePrefab = require("awayjs-display/lib/prefabs/PrimitiveCubePrefab");

var cube;
var imageDiffuse = "assets/trinket_diffuse.jpg";

function initialize() {

  cube = createCube(400, 400, 400, directionalLight);

}

function createCube(width, height, depth, light) {
  var material = new TriangleMethodMaterial();
  var cube = new PrimitiveCubePrefab(width, height, depth, 1, 1, 1, false)
  .getNewObject();
  cube.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  return cube;
}

これで3次元空間の真ん中にテクスチャの貼られた立方体が置かれ、水平および垂直軸で回る図2⁠。箱を回したのは、とりあえずかたちやテクスチャを確かめるためだ。お題では箱は動かさずに、カメラのほうを回り込ませる。

図2 3次元空間の真ん中でテクスチャの貼られた箱が回る
図2 3次元空間の真ん中でテクスチャの貼られた箱が回る
コード1 3次元空間の真ん中に立方体を置いて水平および垂直に回す
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var AssetLibrary = require("awayjs-core/lib/library/AssetLibrary");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var View = require("awayjs-display/lib/containers/View");
var DirectionalLight = require("awayjs-display/lib/entities/DirectionalLight");
var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitiveCubePrefab = require("awayjs-display/lib/prefabs/PrimitiveCubePrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var view;
var cube;
var imageDiffuse = "assets/trinket_diffuse.jpg";
var timer;
function initialize() {
  var directionalLight = createDirectionalLight(0.5, 0xFFFFFF);
  view = createView(240, 180, 0x0);
  cube = createCube(400, 400, 400, directionalLight);
  view.scene.addChild(cube);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(imageDiffuse));
  timer = new RequestAnimationFrame(rotate);
  timer.start();
  view.render();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer();
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createCube(width, height, depth, light) {
  var material = new TriangleMethodMaterial();
  var cube = new PrimitiveCubePrefab(width, height, depth, 1, 1, 1, false)
  .getNewObject();
  cube.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  return cube;
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material = cube.material;
  material.texture = assets[0];
  view.render();
}
function rotate(timeStamp) {
  cube.rotationX = (cube.rotationX + 1) % 360;
  cube.rotationY = (cube.rotationY + 1) % 360;
  view.render();
}

立方体を中心に三角関数でカメラを回す

つぎは、カメラを動かそう。立方体を真ん中にして回るようにする。記憶のよい読者は、前回第6回のスカイボックスの中にドーナッツ型を置くの項で行ったと思い出したかもしれない。しかし、おさらいばかりではつまらない。引出しを増やす意味でも別のやり方を使ってみよう。

カメラの向きは後に回し、立方体を中心として水平に円を描くように動かす。円軌道の座標は、三角関数のsinとcosで求める。原点O(0, 0)を中心に描いた半径1の円(⁠⁠単位円」と呼ぶ)について、原点Oからx軸正方向に対して角度θの直線と交わる点Pのxy座標は三角関数により(cosθ, sinθ)と定められている図3⁠。つまり、原点からの距離は1で角度がθの点のxy座標は(cosθ, sinθ)ということだ[2]⁠。すると、原点からの距離と角度をもとに、円周上のxy座標はつぎの式で導かれる。

x = 距離×cos角度
y = 距離×sin角度
図3 原点から距離が1で角度θのxy座標は(cosθ, sinθ)
図3 原点から距離が1で角度θのxy座標は(cosθ, sinθ)

では、カメラはどうやって立方体に向ければよいか。動かすたびに、カメラに対してDisplayObject.lookAt()メソッドを呼び出せばよい(第6回のスカイボックスの中にドーナッツ型を置くの項を参照⁠⁠。立方体は3次元空間の真ん中に置いたので、向ける座標は原点(0, 0, 0)だ。

カメラを動かす関数(setCamera())は、つぎのように定めよう。引数にはCameraオブジェクトと距離および角度を渡す。Mathクラスの三角関数を使うので、角度の単位はラジアンとする。

setCamera(カメラ, 距離, 角度)

前掲コード1は、つぎのように手直しする。カメラを動かす関数(setCamera())は、まず初期設定の関数(initialize())から呼び出す。距離と角度は変数(distanceとangle)に定めた。なお、角度の初期値(-π/2 = -90度)は立方体の手前正面になる。そして、アニメーションの関数(rotate())が、経過時間(timeStamp)に応じて角度を増し、そのたびにカメラを動かす関数が呼び出される。

var Vector3D = require("awayjs-core/lib/geom/Vector3D");

var ORIGIN = new Vector3D();
var angle = -Math.PI / 2;
var distance = 1500;

function initialize() {

  setCamera(view.camera, distance, angle);

}

function rotate(timeStamp) {
  // cube.rotationX = (cube.rotationX + 1) % 360;
  // cube.rotationY = (cube.rotationY + 1) % 360;
  var camera = view.camera;
  angle += timeStamp / 2000;
  setCamera(camera, distance, angle);

}
function setCamera(camera, distance, angle) {
  camera.x = Math.cos(angle) * distance;
  camera.z = Math.sin(angle) * distance;
  camera.lookAt(ORIGIN);
}

この機会に、描画領域(view)の幅と高さは変数(stageWidthとstageHeight)に定めた。

var stageWidth = 240;
var stageHeight = 180;
function initialize() {

  // view = createView(240, 180, 0x0);
  view = createView(stageWidth, stageHeight, 0x0);

}

これで、立方体を3次元空間の真ん中に捉えつつ、カメラがその周りを回る図4⁠。手を加えた後のスクリプト全体は以下のコード2のとおりだ。お題の土台ができたといえる。

図4 真ん中に置いた立方体の周りをカメラが回る
図4 真ん中に置いた立方体の周りをカメラが回る
コード2 3次元空間の真ん中に立方体を捉えてカメラが回る
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var AssetLibrary = require("awayjs-core/lib/library/AssetLibrary");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var View = require("awayjs-display/lib/containers/View");
var DirectionalLight = require("awayjs-display/lib/entities/DirectionalLight");
var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitiveCubePrefab = require("awayjs-display/lib/prefabs/PrimitiveCubePrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var view;
var cube;
var imageDiffuse = "assets/trinket_diffuse.jpg";
var timer;
var ORIGIN = new Vector3D();
var angle = -Math.PI / 2;
var distance = 1500;
var stageWidth = 240;
var stageHeight = 180;
function initialize() {
  var directionalLight = createDirectionalLight(0.5, 0xFFFFFF);
  view = createView(stageWidth, stageHeight, 0x0);
  cube = createCube(400, 400, 400, directionalLight);
  setCamera(view.camera, distance, angle);
  view.scene.addChild(cube);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(imageDiffuse));
  timer = new RequestAnimationFrame(rotate);
  timer.start();
  view.render();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer();
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createCube(width, height, depth, light) {
  var material = new TriangleMethodMaterial();
  var cube = new PrimitiveCubePrefab(width, height, depth, 1, 1, 1, false)
  .getNewObject();
  cube.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  return cube;
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material = cube.material;
  material.texture = assets[0];
  view.render();
}
function rotate(timeStamp) {
  var camera = view.camera;
  angle += timeStamp / 2000;
  setCamera(camera, distance, angle);
  view.render();
}
function setCamera(camera, distance, angle) {
  camera.x = Math.cos(angle) * distance;
  camera.z = Math.sin(angle) * distance;
  camera.lookAt(ORIGIN);
}

すでに述べたとおり、このようなカメラの動きは前回と同じようにTransform.moveBackward()メソッドを用いてもできる。三角関数を使うことで、何か得はあるのだろうか。たとえば、sinとcosを使って楕円軌道を描くこともできる。その場合には、x座標とy座標それぞれを求める距離の大きさを変えればよい。

x = 距離×cos角度×比率
y = 距離×sin角度

試しに、前掲コード2のカメラを動かす関数(setCamera())で、座標を求める式の一方につぎのように比率(0.5)を乗じてみる。すると、カメラは楕円軌道を動くので、立方体に近づいたり離れたりすることになる図5⁠。もし、Transform.moveBackward()メソッドで後ろに下げようとすれば、その距離を求めなければならず、三角関数を使わざるを得ない。それなら、初めから三角関数でカメラそのものを動かすほうが早い。

function setCamera(camera, distance, angle) {
  camera.x = Math.cos(angle) * distance * 0.5;
  camera.z = Math.sin(angle) * distance;
  camera.lookAt(ORIGIN);
}
図5 楕円軌道を回るとカメラは箱に近づいたり離れたりする
図5 楕円軌道を回るとカメラは箱に近づいたり離れたりする 図5 楕円軌道を回るとカメラは箱に近づいたり離れたりする

原点からの円周上に複数のオブジェクトを並べる

真ん中に置いた箱の周りに、衛星のようにとりあえず5つほど箱を置きたい。これらの位置も円軌道のうえに定めるので、やはり三角関数を使う。衛星は真ん中の箱を複製して用いることにする。Mesh.clone()がそのためのメソッドだ。引数はとらない。衛星の箱をつくる関数(cloneMesh())はつぎのように定めて、複製もとのオブジェクト(箱)とつくる数を引数に渡す。

cloneMesh(複製もとオブジェクト, 数)

複製のオブジェクトをつくったら、円軌道上に置く。その位置決めは別の関数(getPolarPosition())で行う。引数には距離と角度を渡して、3次元座標をVector3Dオブジェクトの戻り値で得る。角度は度数としよう。このとき、角度の方向に気をつけたい。

getPolarPosition(距離, 角度)

オブジェクトの位置は、y軸周りの角度で決める。このとき、コンピュータグラフィックスではy軸は下が正の向きだ。そして、その向きに時計回りが角度の正方向とされる。つまり、座標空間を上から見下ろしたxz平面で、時計回りが正となる図6⁠。これは三角関数で扱う角度と向きが逆だ[3]⁠。したがって、三角関数には角度の値を、正負逆にして与えなければならない。

図6 xz平面でy軸周りの角度は時計回りが正
図6 xz平面でy軸周りの角度は時計回りが正

オブジェクトを複製する関数(cloneMesh())は、つぎのように初期設定の関数(initialize())から呼び出す。複製するオブジェクトは、引数に受け取った個数(count)にもとづいて均等の角度(degrees)で置くことにした。原点からの距離(distance)も等しくし、位置決めの関数(getPolarPosition())にその値と角度を渡して得た戻り値(position)から、オブジェクトの3次元座標が定められている。なお前述のとおり、位置決めの関数の中で、y軸周りの角度(rotationY)は正負逆にしてある。

function initialize() {

  cloneMesh(cube, 5);

}

function cloneMesh(mesh, count) {
  var scene = view.scene;
  var distance = 1000;
  var degrees = 360 / count;
  for (var i = 0; i clone();
    var rotationY = degrees * i;
    var position = getPolarPosition(distance, rotationY);
    clone.x = position.x;
    clone.y = position.y;
    clone.z = position.z;
    clone.rotationY = rotationY;
    scene.addChild(clone);
  }
}

function getPolarPosition(distance, rotationY) {
  var vector = new Vector3D();
  vector.x = distance * Math.cos(-rotationY * Math.PI / 180);
  vector.z = distance * Math.sin(-rotationY * Math.PI / 180);
  return vector;
}

ひとつ補っておきたいのは、オブジェクトを複製する関数(cloneMesh())の中でオブジェクト(clone)そのものを配置したのと同じ角度(rotationY)回したことだ。こうしないと、オブジェクトはすべて真ん中の箱と同じ向きになる。観覧車のゴンドラと同じ状態だ。そうではなく、原点と自分を結んだ直線の向きになるようにした。つまり、カメラがオブジェクトを画面の真ん中に捉えたとき、つねに正面を向くことになる。

これで、真ん中のオブジェクトを中心とした円周上に、等しい間隔で複数のオブジェクトが並べられた図7⁠。カメラはその外の円軌道を回り込んで、真ん中の箱を画面の中心に捉える。以下のコード3に、ここまででき上がったスクリプトをまとめた。同じコードは、jsdo.itにサンプル2として掲げてある。次回は、周りに配置するオブジェクトの位置や大きさをランダムにしてみよう。

図7 真ん中の箱を中心とした円周上に複数の箱が置かれた
図7 真ん中の箱を中心とした円周上に複数の箱が置かれた
サンプル2 Away3D 14/11/05: Cubes on abcircle with the camera rotated around them
コード3 円軌道上に複数のオブジェクトを配置してカメラで回り込む
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var AssetLibrary = require("awayjs-core/lib/library/AssetLibrary");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var View = require("awayjs-display/lib/containers/View");
var DirectionalLight = require("awayjs-display/lib/entities/DirectionalLight");
var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitiveCubePrefab = require("awayjs-display/lib/prefabs/PrimitiveCubePrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var view;
var cube;
var imageDiffuse = "assets/trinket_diffuse.jpg";
var timer;
var ORIGIN = new Vector3D();
var angle = -Math.PI / 2;
var distance = 1500;
var stageWidth = 240;
var stageHeight = 180;
function initialize() {
  var directionalLight = createDirectionalLight(0.5, 0xFFFFFF);
  view = createView(stageWidth, stageHeight, 0x0);
  cube = createCube(400, 400, 400, directionalLight);
  setCamera(view.camera, distance, angle);
  view.scene.addChild(cube);
  cloneMesh(cube, 5);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(imageDiffuse));
  timer = new RequestAnimationFrame(rotate);
  timer.start();
  view.render();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer();
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createCube(width, height, depth, light) {
  var material = new TriangleMethodMaterial();
  var cube = new PrimitiveCubePrefab(width, height, depth, 1, 1, 1, false)
  .getNewObject();
  cube.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  return cube;
}
function cloneMesh(mesh, count) {
  var scene = view.scene;
  var distance = 1000;
  var degrees = 360 / count;
  for (var i = 0; i < count; i++) {
    var clone = mesh.clone();
    var rotationY = degrees * i;
    var position = getPolarPosition(distance, rotationY);
    clone.x = position.x;
    clone.y = position.y;
    clone.z = position.z;
    clone.rotationY = rotationY;
    scene.addChild(clone);
  }
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material = cube.material;
  material.texture = assets[0];
  view.render();
}
function rotate(timeStamp) {
  var camera = view.camera;
  angle += timeStamp / 2000;
  setCamera(camera, distance, angle);
  view.render();
}
function setCamera(camera, distance, angle) {
  camera.x = Math.cos(angle) * distance;
  camera.z = Math.sin(angle) * distance;
  camera.lookAt(ORIGIN);
}
function getPolarPosition(distance, rotationY) {
  var vector = new Vector3D();
  vector.x = distance * Math.cos(-rotationY * Math.PI / 180);
  vector.z = distance * Math.sin(-rotationY * Math.PI / 180);
  return vector;
}

おすすめ記事

記事・ニュース一覧