前回の第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 ビーチボールのテクスチャ
外部ファイルを読込んで使うときには、ロード待ちしなければならない。そこで、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回コード1 「3次元空間に球体をひとつ置く 」に手を加えていこう。素材用のフォルダ(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 テクスチャが貼り損なわれた球体
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 球体にビーチボールのテクスチャが貼られた
コード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();
}