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

第20回2つの光源のアニメーションとカメラのパン・チルト

前回の第19回4つのビットマップをパーティクルにしてアニメーションさせるでは、4つに増やしたロゴでパーティクルをつくり、最新の2015年10月9日付ライブラリに対応させた第19回サンプル1Away3D 15/10/09: Exploding logos using particles)。いよいよ本稿が最終回で、お題の仕上げにかかる。

点光源の数と色を変える

まず、点光源(PointLightオブジェクト)の数は2つに増やし、それぞれに異なる色を与える。点光源をつくる関数(createPointLight())には、第1引数に色(color⁠⁠、第2引数に環境光の強さ(ambient⁠⁠、そして第5引数(specular)には反射の強さを渡していた(再掲第17回表1参照⁠⁠。環境光はデフォルト値が白(0xFFFFFF)なので、色は反射光に表れる。確かめたいときは、つぎのように環境光の値を1より下げ、反射の強さは大きくするとわかりやすい図1⁠。

function createLights() {
  // light = createPointLight(0xFFFFFF, 1, 600, 100, 2);
  light = createPointLight(0x00FF00, 0.5, 600, 100, 10);
  return new StaticLightPicker([light]);
}

function createPointLight(color, ambient, fallOff, radius, specular) {

}
図1 環境光は下げて反射を強めると点光源の色の違いがわかる
図1 環境光は下げて反射を強めると点光源の色の違いがわかる
第17回表1 LightBaseクラスのプロパティ(再掲)
LightBaseクラスのプロパティ値と機能
ambient環境光の強さを示す0以上1以下の数値(デフォルト値0)
ambientColor環境光のカラー値を示す0から0xFFFFFFまでの整数(デフォルト値0xFFFFFF)
color光のカラー値(デフォルト値0xFFFFFF)
diffuse光の拡散する強さを示す0以上の数値(デフォルト値1)
fallOff光が届く距離の最大値(デフォルト値10000)
radius光が届く距離の最小値(デフォルト値9000)
specular光の反射する強さを示す0以上の数値(デフォルト値1)

PointLightオブジェクトとその色は、つぎのように緑(0x00FF00)と青(0x0000FF)の2つにした(greenLightとblueLight⁠⁠。環境光は一方(greenLight)で1を与えれば足りるため、他方(blueLight)は0にして反射のみ加えた。2つの点光源の色で陰影のような効果を与えるつもりだ。

// var light;
var greenLight;
var blueLight;

function createLights() {
  // light = createPointLight(0xFFFFFF, 1, 600, 100, 2);
  greenLight = createPointLight(0x00FF00, 1, 600, 100, 2);
  blueLight = createPointLight(0x0000FF, 0, 600, 100, 2);
  // return new StaticLightPicker([light]);
  return new StaticLightPicker([greenLight, blueLight]);
}

2つの点光源をアニメーションさせる

点光源はせっかく2つつくったのだから、互いに位置をずらし、水平に回すことにしよう。中心から距離rで角度θの2次元平面座標はつぎの図2のように(r cosθ, r sinθ)となる(第7回立方体を中心に三角関数でカメラを回す参照⁠⁠。3次元空間で水平に回すということは、xz平面で位置を動かせばよい。

図2 距離rで角度θの2次元平面における座標
図2 距離rで角度θの2次元平面における座標

アニメーションを描画する関数(render())に、2つの点光源(greenLightとblueLight)の回転をつぎのように定めた。角度をもつ変数(angle)に1度(π/180ラジアン)ずつ加え、xz平面で2つのオブジェクト(greenLightとgreenLight)の位置(lightXとlightZ)を決めている。それぞれのxz座標が正負逆なので、原点(0, 0, 0)を中心とした反対側に位置することになる。つまり、3次元空間の中で回りながらパーティクルを両端から照らすということだ。

var angle = 0;

function render(deltaTime) {
  var radius = 600;

  cameraController.panAngle += 0.2;

  angle += Math.PI / 180;
  var lightX = Math.sin(angle) * radius;
  var lightZ = Math.cos(angle) * radius;
  greenLight.x = lightX;
  greenLight.z = lightZ;
  blueLight.x = -lightX;
  blueLight.z = -lightZ;
  view.render();
}

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

仕上げに、カメラをマウスドラッグでパンあるいはチルトさせてみよう。書き加えるスクリプトは以下のとおりで、これまでのお題と処理はほとんど同じだ(第4回マウスドラッグでカメラをパンやチルトさせる参照⁠⁠。これで、マウスのドラッグに合わせて、カメラを上下左右に回すことができる。

var lastMouseX;
var lastMouseY;
var lastPanAngle;
var lastTiltAngle;

function setListeners() {

  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;
}

これで、本連載最後のお題となる画像をパーティクルに分解した弾けるアニメーションができ上がった。スクリプト全体は以下のコード1にまとめている。カメラをパンやチルトしてみれば、パーティクルがどのようにアニメーションしているのかわかるだろう。jsdo.itにサンプル1を公開したので試してほしい。青と緑の点光源の効果は、少しわかりづらいかもしれない。たとえば、つぎのように光源をつくる関数(createLights())で環境光をなくし、点光源の反射を強めてみるとよい図3⁠。

function createLights() {
  // greenLight = createPointLight(0x00FF00, 1, 600, 100, 2);
  greenLight = createPointLight(0x00FF00, 0, 600, 100, 10);
  // blueLight = createPointLight(0x0000FF, 0, 600, 100, 2);
  blueLight = createPointLight(0x0000FF, 0, 600, 100, 10);

}
図3 2つの点光源の効果
図3 2つの点光源の効果
サンプル1 Away3D 15/10/09: Exploding logos using particles with camerawork
サンプル1 Away3D 15/10/09: Exploding logos using particles with camerawork
 クリックでjsdo.itサイトのサンプルが開く
コード1 4つの画像ファイルからつくったパーティクルのアニメーションに向けて点光源とカメラを回す
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var ColorTransform = require("awayjs-core/lib/geom/ColorTransform");
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 HoverController = require("awayjs-display/lib/controllers/HoverController");
var Mesh = require("awayjs-display/lib/entities/Mesh");
var PointLight = require("awayjs-display/lib/entities/PointLight");
var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");
var ParticleAnimationSet = require("awayjs-renderergl/lib/animators/ParticleAnimationSet");
var ParticleAnimator = require("awayjs-renderergl/lib/animators/ParticleAnimator");
var ParticlePropertiesMode = require("awayjs-renderergl/lib/animators/data/ParticlePropertiesMode");
var ParticleBillboardNode = require("awayjs-renderergl/lib/animators/nodes/ParticleBillboardNode");
var ParticleBezierCurveNode = require("awayjs-renderergl/lib/animators/nodes/ParticleBezierCurveNode");
var ParticleInitialColorNode = require("awayjs-renderergl/lib/animators/nodes/ParticleInitialColorNode");
var ParticlePositionNode = require("awayjs-renderergl/lib/animators/nodes/ParticlePositionNode");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var ParticleGeometryHelper = require("awayjs-renderergl/lib/utils/ParticleGeometryHelper");
var MethodMaterial = require("awayjs-methodmaterials/lib/MethodMaterial");
var PARTICLE_SIZE = 2;
var view;
var cameraController;
var greenLight;
var blueLight;
var assetsURLs = [
  "assets/chrome.png",
  "assets/firefox.png",
  "assets/safari.png",
  "assets/ie.png"
];
var colorSeparations = [];
var bitmapDatas = new Array(assetsURLs.length);
var colorValues = [];
var colorPoints = [];
var colorMaterial;
var colorAnimationSet;
var colorAnimators;
var timer;
var time = 0;
var angle = 0;
var lastMouseX;
var lastMouseY;
var lastPanAngle;
var lastTiltAngle;
function initialize() {
  var lightPicker = createLights();
  view = createView(window.innerWidth, window.innerHeight, 0x0);
  cameraController = setupCameraController(view.camera, 1000, 225, 10);
  colorMaterial = createMaterial(lightPicker, true);
  setListeners();
}
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 setupCameraController(camera, distance, panAngle, tiltAngle) {
  var cameraController = new HoverController(camera);
  cameraController.distance = distance;
  cameraController.panAngle = panAngle;
  cameraController.tiltAngle = tiltAngle;
  return cameraController;
}
function createLights() {
  greenLight = createPointLight(0x00FF00, 1, 600, 100, 2);
  blueLight = createPointLight(0x0000FF, 0, 600, 100, 2);
  return new StaticLightPicker([greenLight, blueLight]);
}
function createPointLight(color, ambient, fallOff, radius, specular) {
  var light = new PointLight();
  light.color = color;
  light.ambient = ambient;
  light.fallOff = fallOff;
  light.radius = radius;
  light.specular = specular;
  return light;
}
function createMaterial(lightPicker, bothSides) {
  var material = new MethodMaterial();
  material.bothSides = bothSides;
  material.lightPicker = lightPicker;
  return material;
}
function onResourceComplete(eventObject) {
  var bitmapData = eventObject.assets[0];
  switch (eventObject.url) {
    case assetsURLs[0]:
      bitmapDatas[0] = bitmapData;
      break;
    case assetsURLs[1]:
      bitmapDatas[1] = bitmapData;
      break;
    case assetsURLs[2]:
      bitmapDatas[2] = bitmapData;
      break;
    case assetsURLs[3]:
      bitmapDatas[3] = bitmapData;
      break;
  }
  if (allDataExists(bitmapDatas)) {
    var colorGeometry = createParticles();
    startParticleAnimation(colorGeometry, assetsURLs.length);
  }
}
function allDataExists(array) {
  var count = array.length;
  for (var i = 0; i < count; i++) {
    if (!array[i]) {
      return false;
    }
  }
  return true;
}
function createParticles() {
  var count = bitmapDatas.length;
  colorAnimationSet = createColorAnimationSet(initColorParticle);
  for (var i = 0; i < count; i++) {
    setParticlesData(bitmapDatas[i], PARTICLE_SIZE, colorValues, colorPoints);
    colorSeparations[i] = colorPoints.length;
  }
  var primitive = new PrimitivePlanePrefab(PARTICLE_SIZE, PARTICLE_SIZE, 1, 1, false);
  var geometry = primitive.geometry;
  var colorGeometrySet = [];
  count = colorPoints.length;
  for (var j = 0; j < count; j++) {
    colorGeometrySet[j] = geometry;
  }
  var colorGeometry = ParticleGeometryHelper.generateGeometry(colorGeometrySet);
  return colorGeometry;
}
function setParticlesData(bitmapData, size, colorValues, colorPoints) {
  var bitmapWidth = bitmapData.width;
  var bitmapHeight = bitmapData.height;
  for (var i = 0; i < bitmapWidth; i++) {
    for (var j = 0; j < bitmapHeight; j++) {
      var point = new Vector3D(size * (i - bitmapWidth / 2), size * (-j + bitmapHeight / 2));
      var color = bitmapData.getPixel32(i, j);
      if (((color >> 24) & 0xff) > 0xb0) {
        var rgbColor = getRgbComponents(color);
        rgbColor.scaleBy(1 / 255);
        colorValues.push(rgbColor);
        colorPoints.push(point);
      }
    }
  }
}
function getRgbComponents(rgbColor) {
  var rgbVector = new Vector3D();
  rgbVector.x = (rgbColor & 0xff0000) >> 16;
  rgbVector.y = (rgbColor & 0xff00) >> 8;
  rgbVector.z = rgbColor & 0xff;
  return rgbVector;
}
function createColorAnimationSet(initParticleFunc) {
  var LOCAL_STATIC = ParticlePropertiesMode.LOCAL_STATIC;
  var colorAnimationSet = new ParticleAnimationSet();
  colorAnimationSet.addAnimation(new ParticleBillboardNode());
  colorAnimationSet.addAnimation(new ParticleBezierCurveNode(LOCAL_STATIC));
  colorAnimationSet.addAnimation(new ParticlePositionNode(LOCAL_STATIC));
  colorAnimationSet.addAnimation(new ParticleInitialColorNode(LOCAL_STATIC, true, false, new ColorTransform(0, 1, 0, 1)));
  colorAnimationSet.initParticleFunc = initParticleFunc;
  return colorAnimationSet;
}
function startParticleAnimation(colorGeometry, numAnimators) {
  var scene = view.scene;
  colorAnimators = new Array(numAnimators);
  var _colorParticleMesh = new Mesh(colorGeometry, colorMaterial);
  for (var i = 0; i < numAnimators; i++) {
    var colorParticleMesh = _colorParticleMesh.clone();
    var animator = new ParticleAnimator(colorAnimationSet);
    colorParticleMesh.rotationY = 180 / numAnimators * (i - 1);
    colorAnimators[i] = animator;
    colorParticleMesh.animator = animator;
    scene.addChild(colorParticleMesh);
  }
}
function setListeners() {
  var count = assetsURLs.length;
  document.onmousedown = startDrag;
  window.onresize = onResize;
  timer = new RequestAnimationFrame(render);
  timer.start();
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  for (var i = 0; i < count; i++) {
    AssetLibrary.load(new URLRequest(assetsURLs[i]));
  }
}
function initColorParticle(properties) {
  var BEZIER_END_VECTOR3D = ParticleBezierCurveNode.BEZIER_END_VECTOR3D;
  var size = 300 * PARTICLE_SIZE;
  var index = properties.index;
  var endPoint = new Vector3D();
  var rgb = colorValues[index];
  properties.startTime = 0;
  properties.duration = 1;
  if (index < colorSeparations[0]) {
    endPoint.x = size;
  } else if (index < colorSeparations[1]) {
    endPoint.x = -size;
  } else if (index < colorSeparations[2]) {
    endPoint.z = size;
  } else {
    endPoint.z = -size;
  }
  properties[BEZIER_END_VECTOR3D] = endPoint;
  properties[ParticleInitialColorNode.COLOR_INITIAL_COLORTRANSFORM] = new ColorTransform(rgb.x, rgb.y, rgb.z, 1);
  properties[ParticleBezierCurveNode.BEZIER_CONTROL_VECTOR3D] = getRandomVector3D(500);
  properties[ParticlePositionNode.POSITION_VECTOR3D] = colorPoints[index];
}
function getRandomVector3D(radius) {
  var angle0 = Math.random() * Math.PI * 2;
  var angle1 = Math.random() * Math.PI * 2;
  var x = radius * Math.cos(angle0) * Math.cos(angle1);
  var y = radius * Math.cos(angle0) * Math.sin(angle1);
  var z = radius * Math.sin(angle0);
  return new Vector3D(x, y, z);
}
function render(deltaTime) {
  var radius = 600;
  time += deltaTime;
  cameraController.panAngle += 0.2;
  if (colorAnimators) {
    var count = colorAnimators.length;
    for (var i = 0; i < count; i++) {
      var _time = 1000 * (Math.sin(time / 5000 + Math.PI * i / 4) + 1);
      colorAnimators[i].update(_time);
    }
  }
  angle += Math.PI / 180;
  var lightX = Math.sin(angle) * radius;
  var lightZ = Math.cos(angle) * radius;
  greenLight.x = lightX;
  greenLight.z = lightZ;
  blueLight.x = -lightX;
  blueLight.z = -lightZ;
  view.render();
}
function onResize(eventObject) {
  view.y = 0;
  view.x = 0;
  view.width = window.innerWidth;
  view.height = window.innerHeight;
}
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;
}

昨年の10月20日から始まった本連載はちょうど第20回を迎えた。本稿執筆時現在「Away3D」でGoogle検索すると、公式サイトについで、第2番目にこの第1回原稿が示されるまでに至った図4⁠。みなさんのご愛読の賜物と感謝申し上げたい。この連載を結ぶにあたって、これまでつくったお題4つを2015年10月9日付ビルドに合わせて書き改めた。そして、簡単な解説を筆者のサイトにAway3D: 2015年10月9日付ビルドに以前のコードを対応させるとして加えたので、参考にしていただけたら幸いだ。jsdo.itのサンプルとは別に、まとめてダウンロードしたい読者はこちらから保存してほしい(ただし、ライブラリの再配布はできないので、AwayJSのGitHubから別途ダウンロードして「lib」フォルダにコピーする必要がある⁠⁠。

おすすめ記事

記事・ニュース一覧