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

第8回オブジェクトの位置と大きさをランダムにする

前回の第7回回り込むカメラの真ん中にオブジェクトを捉えるでは、真ん中のオブジェクトが中心となる円周上に、等しい間隔で複数のオブジェクトを並べ、カメラはその外の円軌道から回り込ませた第7回コード3⁠。今回は、オブジェクトの位置や大きさをランダムにしてみよう。

Matrix3Dクラスで座標を回転する

オブジェクトをランダムに変換する前に、第7回コード3で円周上の座標を求めた関数(getPolarPosition())は書き替える。座標の回転や伸縮、さらに移動まで一手に引受けるMatrix3Dクラスを使ってみたい。"matrix"というのは数学の「行列」を意味する映画を思い浮かべてはいけない⁠⁠。難しげに聞こえるかもしれないが、つまるところ数値をパラメータで変換する仕組みだ[1]⁠。

たとえば、Photoshop CCやFireworks CS6のフィルタやカラー調整は、内部的に行列を用いて行われている図1⁠。これらの機能は、画像のピクセルごとのカラー値を変換する。

図1 Fireworks CS6の「色相・彩度」フィルタ
図1 Fireworks CS6の「色相・彩度」フィルタ

Away3DのMatrix3Dクラスが変換するのは3次元座標だ。パラメータを変えることにより、回転や伸縮、あるいは移動ができる。あまり難しく考えすぎずに、つぎの図2のようなパネルを思い浮かべてもらえばよい。ただし、スライダではなく、もちろんJavaScriptコードを書いて変換しなければならない。

図2 Matrix3Dクラスは座標を回転・伸縮・移動する
図2 Matrix3Dクラスは座標を回転・伸縮・移動する

Matrix3Dクラスに備わる回転・伸縮・移動の座標変換メソッドは、つぎの表1のとおりだ。円周上の座標を求めるには、回転のappendRotation()メソッドを使う。引数には度数の角度と回転軸を与える。y軸で回したいので、回転軸はVector3D.Y_AXISとする。なお、今のところ前変換(prepend)と後変換(append)が何かは気にしなくてよい[2]⁠。まずは、Matrix3Dクラスに慣れることを心がけよう。

appendRotation(角度, 回転軸)
表1 Matrix3Dクラスの回転・伸縮・移動の座標変換メソッド
座標変換前変換後変換
回転 prependRotation(degrees, axis)appendRotation(degrees, axis)
伸縮prependScale(xScale, yScale, zScale)appendScale(xScale, yScale, zScale)
移動prependTranslation(x, y, z)appendTranslation(x, y, z)

Matrix3Dクラスで、距離と角度から円周上の座標をどう求めるか。x軸上の点(距離, 0, 0)をその角度回せばよい。ただし、Matrix3Dオブジェクトは変換行列を表し、3次元座標ではない。Matrix3D.transformVector()メソッドに座標のVector3Dオブジェクトを渡せば、変換されたVector3Dオブジェクトが得られる。距離と角度から円周上の座標を返す関数(getPolarPosition())は、以下のようにほぼすべて書き替えた[3]⁠。

Matrix3Dオブジェクト.transformVector(Vector3Dオブジェクト)
var Matrix3D = require("awayjs-core/lib/geom/Matrix3D");

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

これで、円軌道上に置くオブジェクトは、Matrix3Dオブジェクトの行列変換により位置が決まる。書き替えたスクリプト全体は、つぎのコード1のとおりだ。もちろん、動きの見た目は第7回コード3と変わらない。

コード1 円軌道上に複数のオブジェクトを行列変換により配置してカメラで回り込む
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var Matrix3D = require("awayjs-core/lib/geom/Matrix3D");
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(distance, 0, 0);
  var matrix = new Matrix3D();
  matrix.appendRotation(rotationY, Vector3D.Y_AXIS);
  return matrix.transformVector(vector);
}
第7回図6 xz平面でy軸周りの角度は時計回りが正
第7回図6 xz平面でy軸周りの角度は時計回りが正

オブジェクトの位置をランダムに定める

では、オブジェクトの位置をランダムに定めたい。そこで、引数に渡した最小値と最大値の範囲で、ランダムな数値が返される関数(getRandom())を以下のように定めた。

getRandom(最小値, 最大値)

そして、距離と角度から位置を返す関数には、y軸だけでなくx軸回りの角度も引数に加えた。Matrix3Dクラスを使った行列変換は、このように複数の変換を組み合わせたり、複雑な変換が求められる場合も、面倒な計算そのものは行列に任せてしまえるのが助かる。

getPolarPosition(距離, x軸回りの角度, y軸回りの角度)
function cloneMesh(mesh, count) {

  // var distance = 1000;
  // var degrees = 360 / count;
  for (var i = 0; i < count; i++) {

    var distance = getRandom(500, 1200);
    var rotationX = getRandom(-60, 60);
    // var rotationY = degrees * i;
    var rotationY = getRandom(-180, 180);
    // var position = getPolarPosition(distance, rotationY);
    var position = getPolarPosition(distance, rotationX, rotationY);

  }
}

function getRandom(min, max) {
  var random = Math.random() * (max - min) + min;
  return random;
}
// function getPolarPosition(distance, rotationY) {
function getPolarPosition(distance, rotationX, rotationY) {

  matrix.appendRotation(rotationY, Vector3D.Y_AXIS);
  matrix.appendRotation(rotationX, Vector3D.X_AXIS);

}

これだけの変更で、周囲に置くオブジェクトの位置がランダムに定まる図3⁠。なお、x軸回りつまり垂直方向の角度の範囲は大きくなりすぎないように、±60度に抑えた。スクリプト全体は、つぎのコード2のとおりだ。ただ、距離に対して箱が大きすぎるため、互いにぶつかってめり込んでしまうことが多い。つぎに、大きさもランダムに変えよう。

図3 周囲に置く箱の位置がランダムに定まる
図3 周囲に置く箱の位置がランダムに定まる
コード2 周囲に置く箱の距離とx軸およびy軸周りの角度をランダムに定める
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var Matrix3D = require("awayjs-core/lib/geom/Matrix3D");
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;
  for (var i = 0; i < count; i++) {
    var clone = mesh.clone();
    var distance = getRandom(500, 1200);
    var rotationX = getRandom(-60, 60);
    var rotationY = getRandom(-180, 180);
    var position = getPolarPosition(distance, rotationX, 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 getRandom(min, max) {
  var random = Math.random() * (max - min) + min;
  return random;
}
function getPolarPosition(distance, rotationX, rotationY) {
  var vector = new Vector3D(distance, 0, 0);
  var matrix = new Matrix3D();
  matrix.appendRotation(rotationY, Vector3D.Y_AXIS);
  matrix.appendRotation(rotationX, Vector3D.X_AXIS);
  return matrix.transformVector(vector);
}

Transformクラスでオブジェクトの大きさをランダムに定める

それでは、周囲のオブジェクトは少し小さめにして、ランダムなサイズにしよう。ここではTransformオブジェクトを用いる。表示オブジェクトは、DisplayObject.transformプロパティに自らの変換情報を備えている。変形であれば、Transformオブジェクトのつぎの表2のプロパティにVector3Dオブジェクトを与えれば、回転や伸縮、あるいは移動できる。

表2 Transformクラスの回転・伸縮・移動の変形プロパティ
変形プロパティ
回転rotation
伸縮scale
移動position

オブジェクトを複製する関数(cloneMesh())は、新たに定めるサイズ変更の関数(setScale())に複製したオブジェクト(clone)と伸縮率(scale)を引数に渡して呼び出す。すると、オブジェクトのDisplayObject.transformから得たTransformオブジェクトのTransform.scaleプロパティに、xyz方向とも同じ伸縮率を定めたVector3Dオブジェクトが与えられる。

setScale(オブジェクト, 伸縮率)
function cloneMesh(mesh, count) {

  for (var i = 0; i < count; i++) {

    var scale = getRandom(0.3, 0.5);

    setScale(clone, scale);

  }
}
function setScale(mesh, scale) {
  mesh.transform.scale = new Vector3D(scale, scale, scale);
}

これで、周囲に置かれるオブジェクトは位置だけでなく、サイズも小さめのランダムな値に定められた図4⁠。ここまでのスクリプトをまとめたのが、以下のコード1だ。jsdo.itのコードも、サンプル1として掲げた。次回は、マウスインタラクションを加えよう。

図4 周囲に置かれるオブジェクトの位置とサイズがランダムに定められた
図4 周囲に置かれるオブジェクトの位置とサイズがランダムに定められた
コード3 周囲に置くオブジェクトの位置と大きさをランダムに定める
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var Matrix3D = require("awayjs-core/lib/geom/Matrix3D");
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;
  for (var i = 0; i < count; i++) {
    var clone = mesh.clone();
    var distance = getRandom(500, 1200);
    var scale = getRandom(0.3, 0.5);
    var rotationX = getRandom(-60, 60);
    var rotationY = getRandom(-180, 180);
    var position = getPolarPosition(distance, rotationX, rotationY);
    clone.x = position.x;
    clone.y = position.y;
    clone.z = position.z;
    setScale(clone, scale);
    clone.rotationY = rotationY;
    scene.addChild(clone);
  }
}
function setScale(mesh, scale) {
  mesh.transform.scale = new Vector3D(scale, scale, scale);
}
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 getRandom(min, max) {
  var random = Math.random() * (max - min) + min;
  return random;
}
function getPolarPosition(distance, rotationX, rotationY) {
  var vector = new Vector3D(distance, 0, 0);
  var matrix = new Matrix3D();
  matrix.appendRotation(rotationY, Vector3D.Y_AXIS);
  matrix.appendRotation(rotationX, Vector3D.X_AXIS);
  return matrix.transformVector(vector);
}
サンプル1 Away3D 14/11/05: Cubes of random positions and scales with the camera rotated around them

おすすめ記事

記事・ニュース一覧