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

第4回床の追加とカメラのパン・チルト

前回は、第2回に書上げたコード第2回コード2を、2014年11月5日付のAway3D最新ビルドで動くように手直しした第3回コード1⁠。今回は、それにふたつ手を加えて仕上げよう。第1に、テクスチャが貼られた床を3次元空間に加える。そして第2に、マウスドラッグでカメラの視界が変えられるようにしたい。

テクスチャが貼られた床を加える

床は矩形の平面として3次元空間に加える。貼りつけるテクスチャは、ビーチボールと同じくAway 3D TypeScriptサイトの作例GitHub: StageGL Examplesにある素材から、floor_diffuse.jpgを使う。升目に敷石を並べたようなテクスチャだ図1⁠。

図1 石畳のようなテクスチャ
図1 石畳のようなテクスチャ

矩形の平面もボールのように、別に定める関数(createPlane())でつくることにする。引数には矩形の幅と高さ、およびライトに加えて、ボールの下に置きたいので垂直位置を与えた。

createPlane(幅, 高さ, ライト, 垂直位置)

平面をつくる関数(createPlane())は、つぎのように初期設定の関数(initialize())から呼び出す。床のテクスチャは、AssetLibrary.load()メソッドで読み込む。なお、View.sceneプロパティは、球体(sphere)と平面(plane)のふたつで参照するため、あらかじめローカル変数(scene)にとった。また、アニメーションで秒間60回の再描画をするため、ここであえてView.render()メソッドを呼び出すのは止めた。

var plane;

var planeDiffuse = "assets/floor_diffuse.jpg";
function initialize() {

  view = createView(240, 180, 0x0);
  var scene = view.scene;

  plane = createPlane(800, 800, directionalLight, -300);
  // view.scene.addChild(sphere);
  scene.addChild(sphere);
  scene.addChild(plane);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);

  AssetLibrary.load(new URLRequest(planeDiffuse));

  // view.render();
  // view.render();
}

平面をつくる関数(createPlane())は、以下のように定めた。用いるプレハブのクラスがPrimitivePlanePrefabであることを除けば、組立ては球体をつくる関数(createSphere())と同じだ。PrimitivePlanePrefab()コンストラクタには幅と高さを引数に渡す。そして、球と同じくPrefabBase.getNewObject()メソッドでオブジェクトを得る。そして、マテリアルとライトを定め、垂直位置を動かしたら、平面のオブジェクトを返している。

new PrimitivePlanePrefab(幅, 高さ)
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");

function createPlane(width, height, light, y) {
  var material = new TriangleMethodMaterial();
  var plane = new PrimitivePlanePrefab(width, height).getNewObject();
  plane.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  plane.y = y;
  return plane;
}

読み込んだテクスチャをLoaderEvent.RESOURCE_COMPLETEイベントのリスナー関数(onResourceComplete())で扱うことは変わらない。ただし、素材がビーチボールと石畳のふたつになったので、そのどちらが読込まれたのかを確かめなければならない。そのために、引数のイベントオブジェクトのLoaderEvent.urlプロパティを用いる。

LoaderEvent.urlプロパティは、読み込んだ素材のURLを文字列で示す。それを、あらかじめ変数(imageDiffuseとplaneDiffuse)に与えてあったURLと比べればよい。リスナー関数(onResourceComplete())は大幅に書き替えることになるため、つぎに関数本体すべてを抜書きした。LoaderEvent.assetsプロパティは素材の配列なので、for文ですべてを取り出す。そして、switch文でLoaderEvent.urlプロパティがどちらのURLかを確かめたうえで、そのマテリアルのテクスチャMaterialBase.textureプロパティ)に素材を定めた。

function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  // var material = sphere.material;
  var material;
  var count = assets.length;
  var url = eventObject.url;
  // material.texture = assets[0];
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (imageDiffuse):
        material = sphere.material;
        material.texture = asset;
        break;
      case (planeDiffuse):
        material = plane.material;
        material.texture = asset;
        break;
    }
  }
  // view.render();
}

これらの手が加えられたJavaScriptコードを以下にまとめたコード1⁠。これで、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 PrimitiveSpherePrefab = require("awayjs-display/lib/prefabs/PrimitiveSpherePrefab");
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var view;
var sphere;
var plane;
var timer;
var imageDiffuse = "assets/beachball_diffuse.jpg";
var planeDiffuse = "assets/floor_diffuse.jpg";
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);
  view = createView(240, 180, 0x0);
  var scene = view.scene;
  sphere = createSphere(300, 32, 24, directionalLight);
  plane = createPlane(800, 800, directionalLight, -300);
  scene.addChild(sphere);
  scene.addChild(plane);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(imageDiffuse));
  AssetLibrary.load(new URLRequest(planeDiffuse));
  timer = new RequestAnimationFrame(rotate);
  timer.start();
}
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 createSphere(radius, segmentsH, segmentsV, light) {
  var material = new TriangleMethodMaterial();
  var sphere = new PrimitiveSpherePrefab(radius, segmentsH, segmentsV)
  .getNewObject();
  sphere.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  return sphere;
}
function createPlane(width, height, light, y) {
  var material = new TriangleMethodMaterial();
  var plane = new PrimitivePlanePrefab(width, height).getNewObject();
  plane.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  plane.y = y;
  return plane;
}
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;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (imageDiffuse):
        material = sphere.material;
        material.texture = asset;
        break;
      case (planeDiffuse):
        material = plane.material;
        material.texture = asset;
        break;
    }
  }
}

コントローラでカメラをパンやチルトさせる

3次元の見せ方はオブジェクトそのものを動かすだけでなく、カメラの位置や向きで視界を変えることによっても表せる。そうしたカメラワークに用いるのがコントローラだ。HoverControllerオブジェクトは、カメラをパンやチルトできる。HoverController()コンストラクタの引数にはカメラのオブジェクトを与える。

new HoverController(カメラ)

HoverControllerクラスのプロパティは、つぎの表1に掲げた。そして、プロパティHoverController.panAngleHoverController.tiltAngleでパンあるいはチルトを定めると、カメラは直ちにその角度に切り替わるのではなく、ステップ(デフォルトは8)に分けてトゥイーンされる。

表1 HoverControllerクラスに備わる基本的なプロパティ
HoverControllerクラスのプロパティプロパティの値
distanceカメラと撮影対象との距離で、デフォルト値は1000
panAngleカメラがy軸を中心に回る度数の角度で、デフォルト値は0
tiltAngleカメラの仰角を示す度数で、デフォルト値は90
maxTiltAngle
minTiltAngle
チルトできる角度の範囲の最大度数(デフォルト値90)と最小度数(デフォルト値-90)

HoverControllerクラスでコントローラのオブジェクトをつくる関数(setupCameraController())は、つぎのように定める。引数にはカメラオブジェクトのほか、表1のHoverControllerクラスのプロパティに定める値も加えた。

setupCameraController(カメラ, 距離, チルト最小度数, チルト最大度数, パン度数, チルト度数)

カメラのコントローラ(cameraController)をつくる関数(setupCameraController())は、つぎのように初期設定の関数(initialize())から呼び出した。新たにつくられるHoverControllerオブジェクトに、引数の5つのプロパティ値を与えて返す。

var HoverController = require("awayjs-display/lib/controllers/HoverController");

var cameraController;

function initialize() {

  cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);

}

function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  var cameraController = new HoverController(camera);
  cameraController.distance = distance;
  cameraController.minTiltAngle = minTiltAngle;
  cameraController.maxTiltAngle = maxTiltAngle;
  cameraController.panAngle = panAngle;
  cameraController.tiltAngle = tiltAngle;
  return cameraController;
}

前述のとおり、HoverControllerオブジェクトでパンやチルトをするとトゥイーンアニメーションで表される。そこで、球体も回すのは止め、カメラワークだけを確かめられるようにしよう。したがって、RequestAnimationFrame()コンストラクタに与える引数のコールバックは、View.render()メソッドのみ呼出す。また、関数名(render())もそれに合わせて変えた。

function initialize() {

  // timer = new RequestAnimationFrame(rotate);
  timer = new RequestAnimationFrame(render);

}

// function rotate(timeStamp) {
function render(eventObject) {
  // sphere.rotationX = (sphere.rotationX + 1) % 360;
  // sphere.rotationY = (sphere.rotationY + 1) % 360;
  view.render();
}

これで、HoverControllerオブジェクトによりカメラにパンとチルトが与えられた。しかも、定められた角度にトゥイーンアニメーションする図3⁠。ここまでをまとめたのが、以下のコード2だ。

図3 コントローラに定めたカメラのパンやチルトにトゥイーンアニメーションする
図3 コントローラに定めたカメラのパンやチルトにトゥイーンアニメーションする 図3 コントローラに定めたカメラのパンやチルトにトゥイーンアニメーションする
コード2 HoverControllerクラスでカメラをパンおよびチルトする
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 PrimitiveSpherePrefab = require("awayjs-display/lib/prefabs/PrimitiveSpherePrefab");
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var HoverController = require("awayjs-display/lib/controllers/HoverController");
var view;
var sphere;
var plane;
var cameraController;
var timer;
var imageDiffuse = "assets/beachball_diffuse.jpg";
var planeDiffuse = "assets/floor_diffuse.jpg";
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);
  view = createView(240, 180, 0x0);
  var scene = view.scene;
  sphere = createSphere(300, 32, 24, directionalLight);
  plane = createPlane(800, 800, directionalLight, -300);
  scene.addChild(sphere);
  scene.addChild(plane);
  cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);  //
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(imageDiffuse));
  AssetLibrary.load(new URLRequest(planeDiffuse));
  timer = new RequestAnimationFrame(render);
  timer.start();
}
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 createSphere(radius, segmentsH, segmentsV, light) {
  var material = new TriangleMethodMaterial();
  var sphere = new PrimitiveSpherePrefab(radius, segmentsH, segmentsV)
  .getNewObject();
  sphere.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  return sphere;
}
function createPlane(width, height, light, y) {
  var material = new TriangleMethodMaterial();
  var plane = new PrimitivePlanePrefab(width, height).getNewObject();
  plane.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  plane.y = y;
  return plane;
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  var cameraController = new HoverController(camera);
  cameraController.distance = distance;
  cameraController.minTiltAngle = minTiltAngle;
  cameraController.maxTiltAngle = maxTiltAngle;
  cameraController.panAngle = panAngle;
  cameraController.tiltAngle = tiltAngle;
  return cameraController;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (imageDiffuse):
        material = sphere.material;
        material.texture = asset;
        break;
      case (planeDiffuse):
        material = plane.material;
        material.texture = asset;
        break;
    }
  }
}
function render(timeStamp) {
  view.render();
}

マウスドラッグでカメラをパンやチルトさせる

お題の仕上げとして、カメラのパンやチルトをマウスドラッグで変えられるようにしよう。もっとも、マウス操作の扱いは、JavaScriptに組込み済みのイベントハンドラ(コールバック関数)を使って行う。Away3Dのプロパティは、パンとチルトの角度を変えるだけだ。マウスイベントについては、⁠1)マウスボタンを押す、⁠2)マウスポインタを動かす、⁠3)マウスボタンを放すという3つの操作のコールバック関数を以下のように定める。

第1に、マウスボタンを押すdocument.onmousedownイベントのコールバック(startDrag())は、ふたつのことを行う。まず、ボタンを押したときのマウスポインタ座標とカメラのパンおよびチルト角をそれぞれ変数(lastMouseXとlastMouseY、lastPanAngle、lastTiltAngle)に覚えさせる。つぎに、マウスを動かしたときとボタンを放したときのハンドラ(drag()とstopDrag())を定めている。

第2に、マウスを動かすdocument.onmousemoveイベントのコールバック(drag())は、今のマウスポインタの位置とボタンを押したときの座標の差にもとづいて、カメラのパンHoverController.panAngleプロパティ)とチルトHoverController.tiltAngleプロパティ)の角度を変える。なお、座標の差に乗じている小数値(0.5と0.3)は調整係数だ。

第3に、マウスボタンを放すdocument.onmouseupイベントのコールバック(stopDrag())は、マウスを動かすときとボタンを放したときのコールバックをイベントハンドラから除くnullを与える⁠⁠。これで、初めの状態に戻ることになる。

var lastMouseX;
var lastMouseY;
var lastPanAngle;
var lastTiltAngle;
function initialize() {

  document.onmousedown = startDrag;

}

function startDrag(eventObject) {
  lastMouseX = eventObject.clientX;
  lastMouseY = eventObject.clientY;
  lastPanAngle = cameraController.panAngle;
  lastTiltAngle = cameraController.tiltAngle;
  document.onmousemove = drag;
  document.onmouseup = stopDrag;
}
function drag(eventObject) {
  cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
}
function stopDrag(eventObject) {
  document.onmousemove = null;
  document.onmouseup = null;
}

3つのマウスイベントのハンドラを組み入れると、ドラッグでカメラがパンあるいはチルトできるようになるサンプル1⁠。試してみるとわかるのは、水平方向のパンは何周でも回せるのに対して、垂直方向のチルトは0度から90度までしか動かせないことだ。この範囲が、HoverController.minTiltAngleHoverController.maxTiltAngleプロパティで決められている。でき上がりをコード3にまとめた。

サンプル1 Away3D 14/11/05: Panning and tilting the camera in the 3D space

コード3 HoverControllerオブジェクトによるカメラのパンとチルトをマウスドラッグで動かす
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 PrimitiveSpherePrefab = require("awayjs-display/lib/prefabs/PrimitiveSpherePrefab");
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var HoverController = require("awayjs-display/lib/controllers/HoverController");
var view;
var sphere;
var plane;
var cameraController;
var timer;
var imageDiffuse = "assets/beachball_diffuse.jpg";
var planeDiffuse = "assets/floor_diffuse.jpg";
var lastMouseX;
var lastMouseY;
var lastPanAngle;
var lastTiltAngle;
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);
  view = createView(240, 180, 0x0);
  var scene = view.scene;
  sphere = createSphere(300, 32, 24, directionalLight);
  plane = createPlane(800, 800, directionalLight, -300);
  scene.addChild(sphere);
  scene.addChild(plane);
  cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(imageDiffuse));
  AssetLibrary.load(new URLRequest(planeDiffuse));
  document.onmousedown = startDrag;
  timer = new RequestAnimationFrame(render);
  timer.start();
}
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 createSphere(radius, segmentsH, segmentsV, light) {
  var material = new TriangleMethodMaterial();
  var sphere = new PrimitiveSpherePrefab(radius, segmentsH, segmentsV)
  .getNewObject();
  sphere.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  return sphere;
}
function createPlane(width, height, light, y) {
  var material = new TriangleMethodMaterial();
  var plane = new PrimitivePlanePrefab(width, height).getNewObject();
  plane.material = material;
  material.lightPicker = new StaticLightPicker([light]);
  plane.y = y;
  return plane;
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {  //
  var cameraController = new HoverController(camera);
  cameraController.distance = distance;
  cameraController.minTiltAngle = minTiltAngle;
  cameraController.maxTiltAngle = maxTiltAngle;
  cameraController.panAngle = panAngle;
  cameraController.tiltAngle = tiltAngle;
  return cameraController;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (imageDiffuse):
        material = sphere.material;
        material.texture = asset;
        break;
      case (planeDiffuse):
        material = plane.material;
        material.texture = asset;
        break;
    }
  }
}
function render(timeStamp) {
  view.render();
}
function startDrag(eventObject) {
  lastMouseX = eventObject.clientX;
  lastMouseY = eventObject.clientY;
  lastPanAngle = cameraController.panAngle;
  lastTiltAngle = cameraController.tiltAngle;
  document.onmousemove = drag;
  document.onmouseup = stopDrag;
}
function drag(eventObject) {
  cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
}
function stopDrag(eventObject) {
  document.onmousemove = null;
  document.onmouseup = null;
}

おすすめ記事

記事・ニュース一覧