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

第2回物体にテクスチャを貼って回す

前回の第1回Away3D TypeScriptで基本的な3次元の形状をつくるは、基本的なかたちとして球体をつくって3次元空間に置いた。もっとも、表面の素材(マテリアル)はデフォルトのままで動きもしないため、3次元表現としては冴えない(再掲第1回サンプル2⁠。そこで、今回は素材にテクスチャを貼って、アニメーションで回してみよう。

第1回サンプル2 Away3D 14/08/26: Creating a sphere in the 3D space(再掲)

3次元空間でボールを回す

まずは、球体に貼るテクスチャのビットマップが要る。これは、Away 3D TypeScriptサイトの作例GitHub: StageGL Examplesにある素材を使わせてもらおう。beachball_diffuse.jpgがビーチボールのようなテクスチャだ図1⁠。といっても、ご覧のとおり球体の展開図などつくらなくてよい。矩形のビットマップをAway3Dが伸び縮みさせて球体に貼りつけてくれる。JPEG形式で大きさは512×256ピクセルだ。3次元のCGではテクスチャの1辺を2の累乗にするのがお約束になっている。

図1 ビーチボールのテクスチャ
図1 ビーチボールのテクスチャ

外部ファイルを読込んで使うときには、ロード待ちしなければならない。そこで、LoaderEvent.RESOURCE_COMPLETEイベントに静的メソッドAssetLibrary.addEventListener()でリスナー関数を加えて、読込み終えたときの処理を定める。そして、ロードを始めるのが静的メソッドAssetLibrary.load()だ。引数はURLRequestオブジェクトで、コンストラクタには読込むURLを文字列で渡す。

away.library.AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, リスナー関数)

away.library.AssetLibrary.load(new away.net.URLRequest(URL))

それでは、第1回コード13次元空間に球体をひとつ置くに手を加えていこう。素材用のフォルダ(assets)を設けてテクスチャを納め、URLは変数(imageDiffuse)に定めた。初期設定の関数(initialize())でつくる平行光源(directionalLight)の色は、自然な白に戻す。そして、LoaderEvent.RESOURCE_COMPLETEイベントのリスナー(onResourceComplete())の追加とテクスチャの読込みを加えた。なお、AssetLibraryクラスの完全修飾名(名前空間)が長いので、予めクラスと同名のローカル変数にとっている。

さらに、素材にはテクスチャを読込んで与えるので、球体をつくる関数(createSphere())でデフォルトのテクスチャは定めなくてよい。第1回コード1を、つぎのように書替える。

var sphere;
var imageDiffuse = "assets/beachball_diffuse.jpg";
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);    // 0x00FFFF);
  var AssetLibrary = away.library.AssetLibrary;
  AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new away.net.URLRequest(imageDiffuse));

}

function createSphere(radius, segmentsH, segmentsV, light) {
  // var defaultTexture = away.materials.DefaultMaterialManager.getDefaultTexture();
  // var material = new away.materials.TriangleMethodMaterial(defaultTexture);
  var material = new away.materials.TriangleMethodMaterial();
  var sphere = new away.prefabs.PrimitiveSpherePrefab(radius, segmentsH, segmentsV)
  .getNewObject();
  sphere.material = material;

}

つぎに、LoaderEvent.RESOURCE_COMPLETEイベントのリスナー関数(onResourceComplete())だ。引数に受取るイベントオブジェクトのLoaderEvent.assetsプロパティから素材の配列が得られる。今回読込んでいるテクスチャはひとつしかないので、インデックス0の素材をMaterialBase.textureプロパティに与えれば表面素材が定まる。

function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material = sphere.material;
  material.texture = assets[0];
  view.render();
}

ところが、このリスナー関数ではテクスチャが貼られなかったりする図2⁠。テクスチャがキャッシュされていない場合、LoaderEvent.RESOURCE_COMPLETEイベントのリスナー関数(onResourceComplete())が呼び出されてLoaderEvent.assetsプロパティの参照がとれても、その配列は空のときがある[1]⁠。しかも、たちの悪いことに、その後はもうイベントが起こらない。したがって、配列が空のときは改めてイベントリスナーを定め、テクスチャも読込み直さなければならない。その修正を加えたのが、以下の関数だ。

図2 テクスチャが貼り損なわれた球体
図2 テクスチャが貼り損なわれた球体
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  if (assets.length > 0) {
    var material = sphere.material;
    material.texture = assets[0];
    view.render();
  } else {
    var AssetLibrary = away.library.AssetLibrary;
    var RESOURCE_COMPLETE = away.events.LoaderEvent.RESOURCE_COMPLETE;
    AssetLibrary.removeEventListener(RESOURCE_COMPLETE, onResourceComplete);
    AssetLibrary.addEventListener(RESOURCE_COMPLETE, onResourceComplete);
    AssetLibrary.load(new away.net.URLRequest(eventObject.url));
  }
}

LoaderEvent.RESOURCE_COMPLETEイベントのリスナー関数(onResourceComplete())が呼び出されたら、LoaderEvent.assetsプロパティの配列の長さを調べる。そして、エレメントの素材があることを確かめて、MaterialBase.textureプロパティに与える。配列が空のときはイベントリスナーは定め直して、改めてAssetLibrary.load()メソッドで素材ファイルを読み込んだ。なお、そのURLはLoaderEvent.urlプロパティで得られる。これでようやく球体にビーチボールのテクスチャが貼られた図3⁠。ここまでをコード1として以下にまとめておこう。

図3 球体にビーチボールのテクスチャが貼られた
図3 球体にビーチボールのテクスチャが貼られた
コード1 3次元空間に置いた球体にテクスチャを貼る
var view;
var sphere;
var imageDiffuse = "assets/beachball_diffuse.jpg";
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);
  var AssetLibrary = away.library.AssetLibrary;
  view = createView(240, 180, 0x0);
  sphere = createSphere(300, 32, 24, directionalLight);
  view.scene.addChild(sphere);
  AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new away.net.URLRequest(imageDiffuse));
  view.render();
  view.render();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new away.render.DefaultRenderer();
  var view = new away.containers.View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createSphere(radius, segmentsH, segmentsV, light) {
  var material = new away.materials.TriangleMethodMaterial();
  var sphere = new away.prefabs.PrimitiveSpherePrefab(radius, segmentsH, segmentsV)
  .getNewObject();
  sphere.material = material;
  material.lightPicker = new away.materials.StaticLightPicker([light]);
  return sphere;
}
function createDirectionalLight(ambient, color) {
  var light = new away.entities.DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  if (assets.length > 0) {
    var material = sphere.material;
    material.texture = assets[0];
    view.render();
  } else {
    var AssetLibrary = away.library.AssetLibrary;
    var RESOURCE_COMPLETE = away.events.LoaderEvent.RESOURCE_COMPLETE;
    AssetLibrary.removeEventListener(RESOURCE_COMPLETE, onResourceComplete);
    AssetLibrary.addEventListener(RESOURCE_COMPLETE, onResourceComplete);
    AssetLibrary.load(new away.net.URLRequest(eventObject.url));
  }
}

3次元空間でボールを回す

つぎに、ボールを3次元空間で回す。今回はまだインタラクションはなしで、Away3D TypeScriptのアニメーションの仕組みを学んでおこう。アニメーションにはRequestAnimationFrameクラスを用いる[2]⁠。RequestAnimationFrame()コンストラクタの引数に与えた関数が、1秒間に約60回の頻度で呼出される。ただし、RequestAnimationFrame.start()メソッドでアニメーションを始めないといけないことに気をつけてほしい。

そこで、初期化の関数(initialize())に、つぎのようにRequestAnimationFrameクラスによるアニメーションの定めを加えた。コールバック関数(rotate())は、ボール(sphere)をx軸とy軸周りに1度ずつ回す。なお、角度を360度の剰余で与えているのは、数値が大きくなりすぎて意図しない動きになるのを防ぐためだ。

var timer;
function initialize() {

  timer = new away.utils.RequestAnimationFrame(rotate);
  timer.start();

}

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

物体の回転について、細かいことをひとつ補っておく。 DisplayObject.rotationZプロパティを使えばz軸で回すこともできる。だが、x軸とy軸で、数学的にはz軸周りにも回せる。z軸による回転は、xy平面で回すことを意味する。同じように、x軸ならyz平面上の回転になる。yz平面はy軸で90度回せば、xy平面に重なる。つまり、x軸とy軸で、z軸と同じ回転が表せるということだ。3次元空間における回転を少し突っ込んで扱うようになったときのために、このことは頭の片隅に置いてほしい。

これでビーチボールが水平および垂直に回るようになった。サンプル1としてjsdo.itに掲げよう。スクリプトは以下にコード2としてまとめた。次回は、テクスチャつきの床を加えるとともに、ボールそのものを回すのでなく、ドラッグでカメラの視点が移るようにインタラクションを与えて仕上げたい。

サンプル1 Away3D 14/08/26: Rotating a texture mapped sphere in the 3D space
コード2 球体にテクスチャを貼って3次元空間で回す
var view;
var sphere;
var imageDiffuse = "assets/beachball_diffuse.jpg";
var timer;
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);
  var AssetLibrary = away.library.AssetLibrary;
  view = createView(240, 180, 0x0);
  sphere = createSphere(300, 32, 24, directionalLight);
  view.scene.addChild(sphere);
  AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new away.net.URLRequest(imageDiffuse));
  timer = new away.utils.RequestAnimationFrame(rotate);
  timer.start();
  view.render();
  view.render();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new away.render.DefaultRenderer();
  var view = new away.containers.View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createSphere(radius, segmentsH, segmentsV, light) {
  var material = new away.materials.TriangleMethodMaterial();
  var sphere = new away.prefabs.PrimitiveSpherePrefab(radius, segmentsH, segmentsV)
  .getNewObject();
  sphere.material = material;
  material.lightPicker = new away.materials.StaticLightPicker([light]);
  return sphere;
}
function createDirectionalLight(ambient, color) {
  var light = new away.entities.DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  if (assets.length > 0) {
    var material = sphere.material;
    material.texture = assets[0];
    view.render();
  } else {
    var AssetLibrary = away.library.AssetLibrary;
    var RESOURCE_COMPLETE = away.events.LoaderEvent.RESOURCE_COMPLETE;
    AssetLibrary.removeEventListener(RESOURCE_COMPLETE, onResourceComplete);
    AssetLibrary.addEventListener(RESOURCE_COMPLETE, onResourceComplete);
    AssetLibrary.load(new away.net.URLRequest(eventObject.url));
  }
}
function rotate(timeStamp) {
  sphere.rotationX = (sphere.rotationX + 1) % 360;
  sphere.rotationY = (sphere.rotationY + 1) % 360;
  view.render();
}

おすすめ記事

記事・ニュース一覧