今回は、ふたつの面を前後に置いて、y軸で水平に回してみたい(図1 ) 。インスタンスの数が増えると、座標の変換だけでなく、その重ね順を考えなければならない。やり方はさまざまで、厳密に処理するほど負荷も増えやすい。いかに単純に扱えるように設計するかが大切だ。
図1 インスタンスを前後に置いて水平に回す
動的に配置したビットマップのふたつの面をマウスポインタに応じて水平に回す
前回の第36回「Matrix3Dクラスの後から加える変換」で作成したスクリプト1 に手を加えるかたちで進める。まず準備として、これからのサンプルは面の数が増えていくため、[ライブラリ]のビットマップからBitmapインスタンスを生成して配置する処理は関数にしておこう。前回のスクリプト1 は、つぎのようにBitmapインスタンスをつくって、位置決めしていた。
// タイムライン: メイン
// [ライブラリ]のビットマップに[クラス]としてImage0を設定
var mySprite:Sprite = new Sprite();
var myBitmap:Bitmap = new Bitmap(new Image0(0, 0));
var nX:Number = stage.stageWidth / 2;
var nY:Number = stage.stageHeight / 2;
// ...[中略]...
mySprite.x = nX;
mySprite.y = nY;
myBitmap.x = -myBitmap.width / 2;
myBitmap.y = -myBitmap.height / 2;
addChild(mySprite);
mySprite.addChild(myBitmap);
// ...[後略]...
そこで、関数xCreateFace()を以下のように定義する。引数は5つで、第1がビットマップのクラス、第2~4はそのxyz座標、第5が面の水平方向の回転角だ。第5の引数を加えたのは、たとえば4つの面を四方に角柱のように配置するとき、ふたつの側面は90度、後面は180度回す必要があるからだ。この引数は、デフォルト値を0とした[1] 。
// タイムライン: メイン
// [ライブラリ]のビットマップに[クラス]としてImage0を設定
var mySprite:Sprite = new Sprite();
var myBitmap:Bitmap = xCreateFace(Image0, -50, -50, 0);
var nX:Number = stage.stageWidth / 2;
var nY:Number = stage.stageHeight / 2;
mySprite.x = nX;
mySprite.y = nY;
addChild(mySprite);
mySprite.addChild(myBitmap);
// Bitmapインスタンスの面を作成・配置して返す関数
function xCreateFace (myBitmapData:Class , nX:Number, nY:Number, nZ:Number, nRotationY:Number = 0):Bitmap {
var myBitmap:Bitmap = new Bitmap(new myBitmapData(0, 0));
myBitmap.x = nX;
myBitmap.y = nY;
myBitmap.z = nZ;
myBitmap.rotationY = nRotationY;
return myBitmap;
}
処理の内容については、とくに目新しいことはない。あえて補足するなら、関数xCreateFace()の第1引数として、クラスの参照をClass型で受け取っていることだ。このようにクラスの参照からコンストラクタを呼び出して、そのインスタンスがつくれる。[ムービープレビュー]を確かめれば、前回のスクリプト1 と同じように、ビットマップを納めたBitmapインスタンスがSpriteインスタンスに加えられて、ステージ中央に置かれる。
さて、準備は整ったので、ふたつの面を回してみよう。といっても、ふたつ別々に扱う必要などない。ふたつともひとつのSpriteインスタンスに入れるのだから、容れ物のSpriteごと回せば済む。つまり、回転の処理は、前回のスクリプト1 とまったく変わらない。もっとも、今回は水平にだけ回すので、垂直の処理は省くことになる。
まずは、Bitmapインスタンスの面の位置決めだ。面の数は、後でさらにふたつ増やす。正方形の計4つの面を、Spriteインスタンスの基準点が中心となるように定める。1辺の半分の長さを変数nUnitとすると、Bitmapインスタンスの左上角座標は、前面が(-nUnit, -nUnit, -nUnit)、後面は(nUnit, -nUnit, nUnit)となる(図2 ) 。
図2 Spriteインスタンスの基準点が中心となるように面の位置を定める
前回のスクリプト1 に手を加え、動的に配置したふたつのビットマップをマウスポインタの位置に応じて水平に回すようにしたのが、以下のスクリプト1 だ。前掲関数xCreateFace()を新たに定義し、ふたつのBitmapの面をSpriteインスタンスに納めた。また、DisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)のリスナー関数xRotate()は、インスタンスの垂直方向の動きは省いて、マウスポインタの水平座標に応じて水平にのみ回る。
スクリプト1 Sprite内にふたつのビットマップを置いてマウスポインタの位置に応じて水平に回す
// タイムライン: メイン
// [ライブラリ]のビットマップに[クラス]としてImage0とImage1を設定
var mySprite:Sprite = new Sprite();
var nX:Number = stage.stageWidth / 2;
var nY:Number = stage.stageHeight / 2;
var nUnit:Number = 100 / 2;
var frontBitmap:Bitmap = xCreateFace(Image0, -nUnit, -nUnit, -nUnit);
var backBitmap:Bitmap = xCreateFace(Image1, nUnit, -nUnit, nUnit, 180);
var nDeceleration:Number = 0.3;
mySprite.z = 0;
var myMatrix3D:Matrix3D = mySprite.transform.matrix3D;
mySprite.x = nX;
mySprite.y = nY;
addChild(mySprite);
mySprite.addChild(frontBitmap);
mySprite.addChild(backBitmap);
addEventListener(Event.ENTER_FRAME, xRotate);
function xCreateFace(myBitmapData:Class, nX:Number, nY:Number, nZ:Number, nRotationY:Number = 0):Bitmap {
var myBitmap:Bitmap = new Bitmap(new myBitmapData(0, 0));
myBitmap.x = nX;
myBitmap.y = nY;
myBitmap.z = nZ;
myBitmap.rotationY = nRotationY;
return myBitmap;
}
function xRotate(eventObject:Event):void {
var nRotationY:Number = (mouseX - nX) * nDeceleration;
myMatrix3D.appendTranslation(-nX, -nY, 0);
myMatrix3D.appendRotation(nRotationY, Vector3D.Y_AXIS);
myMatrix3D.appendTranslation(nX, nY, 0);
}
[ムービープレビュー]で確かめると、ふたつの面のBitmapインスタンスそのものは、マウスポインタの位置に応じて水平に回る。しかし、その重ね順が正しく表示されない(図3 ) 。これは、Flash Player 10でz座標が加わっても、インスタンスの重ね順はこれまでと変わらず、親インスタンスの表示リストにおける順序で決まるからだ。したがって、インスタンスのz座標に応じて、重ね順は変えなければならない。
図3 インスタンスのz座標が動いても表示の重ね順は変わらない
インスタンスの重ね順を定める
面の前後は、z座標値を比べるのが普通だ。しかし今回は、面がふたつで水平にしか回らない。すると、もっと楽な決め方がある。3次元座標空間を真上から見下ろしたyz平面で考えよう。図4 は円で表したSpriteインスタンスの中に、前面と後面が置かれている。初めはSpriteインスタンスのy軸回りの角度は0で、文字どおり前面が前にある(図4上図 ) 。
視線はz軸の向きだ。Spriteインスタンスのy軸角が反時計回りに-90度で、前後の面ともに視線と平行になる(図4中図 ) 。さらに回転すると、後面が手前にくる。逆に、インスタンスが時計回りで90度に至ると両面は視線と並行で(図4下図 ) 、それを超えれば後面が手前だ(表1 ) 。
図4 Spriteインスタンスのy軸回りの角度とふたつの面の位置回転角: 0°
回転角: -90°
回転角: 90°
表1 Spriteインスタンスのy軸回りの角度と手前に表示される面
Spriteインスタンスのy軸回りの角度 手前に表示される面
-180°~-90° 後面
-90°~90° 前面
90°~180° 後面
インスタンスのy軸回りの角度は、DisplayObject.rotationYプロパティで調べられた。すると、つぎのようなifステートメントで面の重ね順を変えればよさそうに思える。この考え方は間違っていない。しかし、y軸による回転角については、特別な仕様があるのだ。
var nRotationY:Number = mySprite.rotationY;
if (nRotationY > 90 || -90 > nRotationY) {
// 後面を手前に
} else {
// 前面を手前に
}
3次元空間における回転は、x軸およびz軸については角度が±180度の範囲で定められる。ところが、y軸回りの角度は±90度の範囲になっている[2] 。したがって、y軸による回転角が90度を超えたり、あるいは-90度を下回ることがそもそもないのだ。
しかし、それでどうやってy軸で1周の回転が表せるのか。からくりはこうだ。インスタンスをx軸とz軸でともに180度回転する。すると、y軸で180度回したのと同じ裏返しになる(図5 ) 。しかし、このときy軸の回転角度は0だ。ここからy軸を±90度の範囲で回転すれば、y軸のみで回した-180~-90度および90~180度に見せかけることができる。
図5 インスタンスをx軸とz軸でともに180度回転する
もとのインスタンス
→
x軸で180度回転
→
z軸で180度回転
では問題は、y軸による回転角の見かけが90度を超えたり、-90度を下回ったことをどうやって確かめるかということだ。幸いなことに、まさにy軸の回転角が90度を超えるか-90度を下回ったとき、x軸とz軸による180度の回転が起こる。だから、x軸(またはz軸)の回転角が0度か180度かによって判別すればよい。
インスタンスの重ね順つまり親インスタンスの表示リスト内の位置を変えるのは、DisplayObjectContainer.setChildIndex()メソッド()だった(第24回「インスタンスの管理と配列の並べ替え」4ページ 参照) 。また、表示リスト内のインスタンスの数は、DisplayObjectContainer.numChildrenプロパティ で調べられる。つぎのスクリプト2 には、面のインスタンスの重ね順を正しく定める関数xSetOrder()が新たに定義された。
スクリプト2 マウスポインタの位置に応じて水平に回したふたつの面の重ね順を設定する
// タイムライン: メイン
// [ライブラリ]のビットマップに[クラス]としてImage0とImage1を設定
var mySprite:Sprite = new Sprite();
var nX:Number = stage.stageWidth / 2;
var nY:Number = stage.stageHeight / 2;
var nUnit:Number = 100 / 2;
var frontBitmap:Bitmap = xCreateFace(Image0, -nUnit, -nUnit, -nUnit);
var backBitmap:Bitmap = xCreateFace(Image1, nUnit, -nUnit, nUnit, 180);
var nDeceleration:Number = 0.3;
mySprite.z = 0;
var myMatrix3D:Matrix3D = mySprite.transform.matrix3D;
mySprite.x = nX;
mySprite.y = nY;
addChild(mySprite);
mySprite.addChild(frontBitmap);
mySprite.addChild(backBitmap);
addEventListener(Event.ENTER_FRAME, xRotate);
function xCreateFace(myBitmapData:Class, nX:Number, nY:Number, nZ:Number, nRotationY:Number = 0):Bitmap {
var myBitmap:Bitmap = new Bitmap(new myBitmapData(0, 0));
myBitmap.x = nX;
myBitmap.y = nY;
myBitmap.z = nZ;
myBitmap.rotationY = nRotationY;
return myBitmap;
}
function xRotate(eventObject:Event):void {
var nRotationY:Number = (mouseX - nX) * nDeceleration;
myMatrix3D.appendTranslation(-nX, -nY, 0);
myMatrix3D.appendRotation(nRotationY, Vector3D.Y_AXIS);
myMatrix3D.appendTranslation(nX, nY, 0);
xSetOrder ();
}
function xSetOrder ():void {
var nTop:uint = mySprite.numChildren - 1;
if (mySprite.rotationX > 90) { // x軸で180度回転していたら
mySprite.setChildIndex (backBitmap, nTop);
} else {
mySprite.setChildIndex (frontBitmap, nTop);
}
}
関数xSetOrder()は、リスナー関数xRotate()から呼び出される。表示リスト内のインデックスは0から始まるので、最前面のインスタンスのインデックスはDisplayObjectContainer.numChildrenプロパティの値から1差引いた整数になる。そして、x軸で180度回転しているかどうかを判別して、正しいインスタンスを最前面に置いている。
なお、x軸回りの角度は0度または180度をとるものの、若干の誤差を含むことがある。そのため、上記スクリプトのif条件では間の90度を境にして切り分けた。[ムービープレビュー]を確かめると、インスタンスの重ね順はSpriteインスタンスの回転角度に応じて正しく入れ替わる(図6 ) 。
図6 インスタンスの重ね順が正しく変わる
次回は、さらに回す面の数を増やし、重ね順を決めるもう少し厳密な方法について説明したい。
[2]
第34回「3次元空間における回転 」で確かめたように、DisplayObject.rotationYプロパティに直接代入すれば、±90度の範囲はおろか±180度を超える値も設定できた。その場合であれば、DisplayObject.rotationYプロパティの値が90度を超えるか、-90度より小さいかという判定を加えることもできる。
2010年8月2日に開かれた「ActionScript 3.0による 三次元表現 in Apple Store, Ginza」では、そのような例も紹介した。なお、セミナーの映像が公開 されている。3次元空間の扱いについて、基本的な考え方を45分で解説した。これまでの復習だけでなく、これからの予習となる内容も若干含まれている。興味があれば、ぜひご覧いただきたい。
今回解説した次のサンプルファイルがダウンロードできます。