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

第6回スカイボックスの中に置いたドーナッツ型をカメラで追う

前回の第5回「スカイボックスで3次元空間に背景をつくる」は、3次元空間にスカイボックスで背景を定めて、その中をカメラで水平に見回した。今回は、真ん中にドーナッツ型を加えて背景を映し込むとともに、カメラの向きをインタラクティブに回り込ませる。お題のAway 3D TypeScriptサイトの作例Skybox and environment mappingに仕上げたい。

スカイボックスの中にドーナッツ型を置く

まずは、スカイボックスの中にドーナッツ型を置く。ドーナッツ型のつくり方は、すでに第1回「Away3D TypeScriptで基本的な3次元の形状をつくる」球体以外の基本的なかたちをつくるですでに説明した。PrimitiveTorusPrefabクラスでひながたをつくり、そこからPrefabBase.getNewObject()メソッドでドーナッツ型のMeshオブジェクトを得ればよい。

第5回コード23次元空間に定めたスカイボックスの中で水平にカメラを回すに手を加えていく。ドーナッツ型をつくる関数(createTorus())はつぎのように定め、初期設定の関数(initialize())から呼び出す。なお、今回のお題ではテクスチャをあてないので、TriangleMethodMaterial()コンストラクタに引数は渡していない。また、光源MaterialBase.lightPickerプロパティ)も与えない。

var PrimitiveTorusPrefab = require("awayjs-display/lib/prefabs/PrimitiveTorusPrefab");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");

var torus;

function initialize() {

  torus = createTorus(150, 60, 40, 20);
  view.scene.addChild(torus);

}

function createTorus(radius, tubeRadius, segmentsR, segmentsT) {
  var material = new TriangleMethodMaterial();
  var torus = new PrimitiveTorusPrefab(radius, tubeRadius, segmentsR, segmentsT)
  .getNewObject();
  torus.material = material;
  material.color = 0x111199;
  return torus;
}

ドーナッツ型のカラーMaterialBase.colorプロパティ)は確認用に暗い青(0x111199)を定めた。確かめてみると、スカイボックスの中に青いドーナッツ型が表れる。しかし、カメラが動いて画角から見切れてしまう図1⁠。これは、カメラが自分の居場所を中心にして回っているからだ。

図1 カメラが回るとドーナッツ型が見切れる
図1 カメラが回るとドーナッツ型が見切れる 図1 カメラが回るとドーナッツ型が見切れる

ドーナッツを真ん中に据えて、回り込むようにカメラを動かすにはどうしたらよいか。ちょっとしたパズルのような工夫をする。まず、カメラをドーナッツ型の中心である原点に置く。つぎにカメラを回転してから、とるべき間合いの分だけ後ろに下がればよい。

原点の座標(0, 0, 0)は、Vector3Dオブジェクトでつぎのように変数(zeroVector3D)にとっておく。引数なしのデフォルト値が原点だ。また、原点からカメラまでの距離も変数(cameraZ)に定めた。カメラの位置はDisplayObject.transformプロパティから得たTransformオブジェクトで操作する。位置座標を定めるのはTransform.positionプロパティ、後ろに下がるにはTransform.moveBackward()メソッドを用いる。

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

var zeroVector3D = new Vector3D();
var cameraZ = -600;

function render(timeStamp) {

  var transform = camera.transform;
  transform.position = zeroVector3D;
  camera.rotationY += 0.5;
  transform.moveBackward(-cameraZ); 
}

これで、ドーナッツ型を画面の真ん中に捉えつつ、カメラが回り込むように動く。だが、カメラの初期設定は加えておこう。その関数(setupCamera())を以下のように定めて、初期設定の関数(initialize())から呼び出す。DisplayObject.lookAt()メソッドメソッドでカメラの向きが定まる。デフォルトは原点なので、念のため加えた。また、PerspectiveProjection.fieldOfViewプロパティは、視野角を定める図2⁠。デフォルトは60度なので、少し広角に拡げたことになる[1]⁠。

図2 視野角と焦点距離
図2 視野角と焦点距離
var PerspectiveProjection = require("awayjs-core/lib/projections/PerspectiveProjection");
var cameraZ = -600;

function initialize() {

  setupCamera(view.camera, cameraZ, 90);

}

function setupCamera(camera, z, fieldOfView) {
  camera.z = z;
  camera.lookAt(zeroVector3D);
  camera.projection = new PerspectiveProjection(fieldOfView);
}

視野角を拡げたので、前掲図1より広い範囲が映る。その真ん中にドーナッツ型を捉えてカメラが回るようになった。なお、ドーナッツ型もx軸とy軸で回してみた図3⁠。ここまでのスクリプトはコード1にまとめたとおりだ。

図3 広がった視野角の中でドーナッツ型とそれを捉えるカメラが回る
図3 広がった視野角の中でドーナッツ型とそれを捉えるカメラが回る
コード1 スカイボックスの真ん中にドーナッツ型を捉えてカメラが回る
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 AssetLoaderContext = require("awayjs-core/lib/library/AssetLoaderContext");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var PerspectiveProjection = require("awayjs-core/lib/projections/PerspectiveProjection");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var View = require("awayjs-display/lib/containers/View");
var Skybox = require("awayjs-display/lib/entities/Skybox");
var SkyboxMaterial = require("awayjs-renderergl/lib/materials/SkyboxMaterial");
var PrimitiveTorusPrefab = require("awayjs-display/lib/prefabs/PrimitiveTorusPrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var view;
var torus;
var timer;
var baseUrl = "assets/skybox/";
var imageSkybox = "snow_texture.cube";
var zeroVector3D = new Vector3D();
var cameraZ = -600;
var centerX = 200;
var centerY = 150;
function initialize() {
  view = createView(centerX * 2, centerY * 2, 0xFFFF00);
  torus = createTorus(150, 60, 40, 20);
  setupCamera(view.camera, cameraZ, 90);
  view.scene.addChild(torus);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  loadAssets(baseUrl, imageSkybox);
  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 createTorus(radius, tubeRadius, segmentsR, segmentsT) {
  var material = new TriangleMethodMaterial();
  var torus = new PrimitiveTorusPrefab(radius, tubeRadius, segmentsR, segmentsT)
  .getNewObject();
  torus.material = material;
  material.color = 0x111199;
  return torus;
}
function setupCamera(camera, z, fieldOfView) {
  camera.z = z;
  camera.lookAt(zeroVector3D);
  camera.projection = new PerspectiveProjection(fieldOfView);
}
function loadAssets(base, image) {
  var assetLoaderContext = new AssetLoaderContext();
  assetLoaderContext.dependencyBaseUrl = base;
  AssetLibrary.load(new URLRequest(base + image), assetLoaderContext);
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (baseUrl + imageSkybox):
        setupSkybox(asset);
        break;
    }
  }
}
function setupSkybox(cubeTexture) {
  var skybox = new Skybox(new SkyboxMaterial(cubeTexture));
  view.scene.addChild(skybox);
}
function render(timeStamp) {
  var camera = view.camera;
  var transform = camera.transform;
  torus.rotationX += 2;
  torus.rotationY += 1;
  transform.position = zeroVector3D;
  camera.rotationY += 0.5;
  transform.moveBackward(-cameraZ);
  view.render();
}

マウスポインタの位置に応じてカメラを水平に回す

ここで、インタラクションを加えよう。マウスポインタの水平位置に応じて、カメラが水平に回る速さと向きを変える。マウスポインタの水平座標と画面の中心との差に比例して、カメラを水平に回せばよい。以下のように、マウスポインタのx座標を変数(lastMouseX)に定め、比例係数も変数(sencitivity)に与えておく。

マウスポインタの動きは、onmousemoveイベントハンドラで捉える。コールバック関数(getMousePosition())は、引数(eventObject)に受取ったイベントオブジェクトのclientXプロパティからポインタのx座標値を調べて変数(lastMouseX)に納める。そして、描画の関数(render())が、その座標と中心座標(centerX)の差に比例してカメラを水平に回している。

var viewWidth = centerX * 2;
var lastMouseX = centerX;
var sencitivity = 1 / 500;

function initialize() {

  // view = createView(centerX * 2, centerY * 2, 0xFFFF00);
  view = createView(viewWidth, centerY * 2, 0xFFFF00);

  document.onmousemove = getMousePosition;

}

function render(timeStamp) {

  // camera.rotationY += 0.5;
  camera.rotationY += (lastMouseX - centerX) * sencitivity;

}
function getMousePosition(eventObject) {
  lastMouseX = eventObject.clientX;
  if (lastMouseX > viewWidth) {
    lastMouseX = viewWidth;
  }
}

なお、onmousemoveイベントハンドラは、マウスポインタがViewオブジェクトの表示領域の外に出てもイベントを捉え、clientXプロパティは外の座標値になってしまう。そこで、イベントハンドラ(getMousePosition())は座標値(lastMouseX)をオブジェクトの幅(viewWidth)に抑えている。前掲コード1に以上の手直しを加えたのがつぎのコード2だ。マウスポインタの水平位置に応じて、カメラの水平に回る向きと速さが変わる。

コード2 マウスポインタの水平位置に応じてカメラの回る向きと速さを変える
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 AssetLoaderContext = require("awayjs-core/lib/library/AssetLoaderContext");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var PerspectiveProjection = require("awayjs-core/lib/projections/PerspectiveProjection");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var View = require("awayjs-display/lib/containers/View");
var Skybox = require("awayjs-display/lib/entities/Skybox");
var SkyboxMaterial = require("awayjs-renderergl/lib/materials/SkyboxMaterial");
var PrimitiveTorusPrefab = require("awayjs-display/lib/prefabs/PrimitiveTorusPrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var EffectEnvMapMethod = require("awayjs-methodmaterials/lib/methods/EffectEnvMapMethod");
var view;
var torus;
var timer;
var baseUrl = "assets/skybox/";
var imageSkybox = "snow_texture.cube";
var zeroVector3D = new Vector3D();
var cameraZ = -600;
var centerX = 200;
var centerY = 150;
var viewWidth = centerX * 2;
var viewHeight = centerY * 2;
var lastMouseX = centerX;
var lastMouseY = centerY;
var sencitivity = 1 / 500;
function initialize() {
  view = createView(viewWidth, centerY * 2, 0xFFFF00);
  torus = createTorus(150, 60, 40, 20);
  setupCamera(view.camera, cameraZ, 90);
  view.scene.addChild(torus);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  loadAssets(baseUrl, imageSkybox);
  document.onmousemove = getMousePosition;
  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 createTorus(radius, tubeRadius, segmentsR, segmentsT) {
  var material = new TriangleMethodMaterial();
  var torus = new PrimitiveTorusPrefab(radius, tubeRadius, segmentsR, segmentsT)
  .getNewObject();
  torus.material = material;
  material.color = 0x111199;
  return torus;
}
function setupCamera(camera, z, fieldOfView) {
  camera.z = z;
  camera.lookAt(zeroVector3D);
  camera.projection = new PerspectiveProjection(fieldOfView);
}
function loadAssets(base, image) {
  var assetLoaderContext = new AssetLoaderContext();
  assetLoaderContext.dependencyBaseUrl = base;
  AssetLibrary.load(new URLRequest(base + image), assetLoaderContext);
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (baseUrl + imageSkybox):
        setupSkybox(asset);
        break;
    }
  }
}
function setupSkybox(cubeTexture) {
  var skybox = new Skybox(new SkyboxMaterial(cubeTexture));
  view.scene.addChild(skybox);
  torus.material.addEffectMethod(new EffectEnvMapMethod(cubeTexture));
}
function render(timeStamp) {
  var camera = view.camera;
  var transform = camera.transform;
  var rotationX = camera.rotationX;
  torus.rotationX += 2;
  torus.rotationY += 1;
  transform.position = zeroVector3D;
  camera.rotationY += (lastMouseX - centerX) * sencitivity;
  rotationX += (lastMouseY - centerY) * sencitivity;
  if (rotationX > 30) {
    rotationX = 30;
  } else if (rotationX < -30) {
    rotationX = -30;
  }
  camera.rotationX = rotationX;
  transform.moveBackward(-cameraZ);
  view.render();
}
function getMousePosition(eventObject) {
  lastMouseX = eventObject.clientX;
  lastMouseY = eventObject.clientY;
  if (lastMouseX > viewWidth) {
    lastMouseX = viewWidth;
  }
  if (lastMouseY > viewHeight) {
    lastMouseY = viewHeight;
  }
}

スカイボックスの背景をドーナッツ型に映し込む

いよいよ仕上げだ。ドーナッツ型の表面にスカイボックスの背景を映し込む。環境マッピングと呼ばれる表現だ。それには、EffectEnvMapMethodオブジェクトをつくって、TriangleMethodMaterial.addEffectMethod()メソッドの引数に渡す。なお、EffectEnvMapMethod()コンストラクタの引数は、映し込むスカイボックスのテクスチャだ。スカイボックスをつくる関数(setupSkybox())には、つぎのようにステートメントを加えた。

var EffectEnvMapMethod = require("awayjs-methodmaterials/lib/methods/EffectEnvMapMethod");

function setupSkybox(cubeTexture) {

  torus.material.addEffectMethod(new EffectEnvMapMethod(cubeTexture));
}

これで、お題とした作例Skybox and environment mappingの表現ができた。だがこの際、もうひと手間加えよう。カメラに、マウスポインタの垂直位置に応じた縦の動きを与える。考え方は、水平の動きと変わらない。ただ、世界がひっくり返ってしまうのは避けたい。そこで、つぎのように±30度の範囲でカメラの角度を上下させる。

var viewHeight = centerY * 2;
var lastMouseY = centerY;

function render(timeStamp) {

  var rotationX = camera.rotationX;

  rotationX += (lastMouseY - centerY) * sencitivity;
  if (rotationX > 30) {
    rotationX = 30;
  } else if (rotationX < -30) {
    rotationX = -30;
  }
  camera.rotationX = rotationX;

}
function getMousePosition(eventObject) {

  lastMouseY = eventObject.clientY;

  if (lastMouseY > viewHeight) {
    lastMouseY = viewHeight;
  }
}

これで、スカイボックスのテクスチャが、回転するドーナッツ型に映り込む。そして、マウスポインタの位置に応じて、カメラがドーナッツ型を真ん中に捉えつつ、水平および垂直に回る図4⁠。書き上がったスクリプトをコード3にまとめた。

図4 背景の映り込んだドーナッツ型を捉えてカメラが上下左右に回るサンプルを別ウィンドウで開く
コード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 AssetLoaderContext = require("awayjs-core/lib/library/AssetLoaderContext");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var PerspectiveProjection = require("awayjs-core/lib/projections/PerspectiveProjection");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var View = require("awayjs-display/lib/containers/View");
var Skybox = require("awayjs-display/lib/entities/Skybox");
var SkyboxMaterial = require("awayjs-renderergl/lib/materials/SkyboxMaterial");
var PrimitiveTorusPrefab = require("awayjs-display/lib/prefabs/PrimitiveTorusPrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var EffectEnvMapMethod = require("awayjs-methodmaterials/lib/methods/EffectEnvMapMethod");
var view;
var torus;
var timer;
var baseUrl = "assets/skybox/";
var imageSkybox = "snow_texture.cube";
var zeroVector3D = new Vector3D();
var cameraZ = -600;
var centerX = 200;
var centerY = 150;
var viewWidth = centerX * 2;
var viewHeight = centerY * 2;
var lastMouseX = centerX;
var lastMouseY = centerY;
var sencitivity = 1 / 500;
function initialize() {
  view = createView(viewWidth, centerY * 2, 0xFFFF00);
  torus = createTorus(150, 60, 40, 20);
  setupCamera(view.camera, cameraZ, 90);
  view.scene.addChild(torus);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  loadAssets(baseUrl, imageSkybox);
  document.onmousemove = getMousePosition;
  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 createTorus(radius, tubeRadius, segmentsR, segmentsT) {
  var material = new TriangleMethodMaterial();
  var torus = new PrimitiveTorusPrefab(radius, tubeRadius, segmentsR, segmentsT)
  .getNewObject();
  torus.material = material;
  material.color = 0x111199;
  return torus;
}
function setupCamera(camera, z, fieldOfView) {
  camera.z = z;
  camera.lookAt(zeroVector3D);
  camera.projection = new PerspectiveProjection(fieldOfView);
}
function loadAssets(base, image) {
  var assetLoaderContext = new AssetLoaderContext();
  assetLoaderContext.dependencyBaseUrl = base;
  AssetLibrary.load(new URLRequest(base + image), assetLoaderContext);
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (baseUrl + imageSkybox):
        setupSkybox(asset);
        break;
    }
  }
}
function setupSkybox(cubeTexture) {
  var skybox = new Skybox(new SkyboxMaterial(cubeTexture));
  view.scene.addChild(skybox);
  torus.material.addEffectMethod(new EffectEnvMapMethod(cubeTexture));
}
function render(timeStamp) {
  var camera = view.camera;
  var transform = camera.transform;
  var rotationX = camera.rotationX;
  torus.rotationX += 2;
  torus.rotationY += 1;
  transform.position = zeroVector3D;
  camera.rotationY += (lastMouseX - centerX) * sencitivity;
  rotationX += (lastMouseY - centerY) * sencitivity;
  if (rotationX > 30) {
    rotationX = 30;
  } else if (rotationX < -30) {
    rotationX = -30;
  }
  camera.rotationX = rotationX;
  transform.moveBackward(-cameraZ);
  view.render();
}
function getMousePosition(eventObject) {
  lastMouseX = eventObject.clientX;
  lastMouseY = eventObject.clientY;
  if (lastMouseX > viewWidth) {
    lastMouseX = viewWidth;
  }
  if (lastMouseY > viewHeight) {
    lastMouseY = viewHeight;
  }
}

おすすめ記事

記事・ニュース一覧