前回の第43回 は、Vector3Dインスタンスで3次元空間における正方形の4頂点座標をつくり、y軸で水平に回したうえで、2次元平面の座標に透視投影したワイヤーフレームを描いてみた。今回のお題は、頂点座標を8つに増やして立方体を回す(図1 ) 。
図1 立方体の8頂点をワイヤーフレームで結んで回す
立方体の8頂点の座標を決める
まず、立方体の8頂点の座標を決める。原点(0, 0, 0)は立方体の中心に据え、xyz軸が6面の中央を垂直に貫くように定めよう。1辺の半分の長さを変数(nUnit)に設定するなら、8頂点の3次元空間座標は図2 のとおりだ。なお、各頂点には0から始まる整数の通し番号を付した。
図2 原点を中心に定めた立方体の8頂点座標
前回書いたスクリプト1 (「 3次元空間の頂点座標から2次元平面に透視投影したワイヤーフレームを描く」 )は、Vectorオブジェクト(変数vertices)に納める3次元空間の頂点座標であるVector3Dエレメントの数をとくに定めていない。つまり、頂点座標数を正方形の4つから立方体の8つに増やしても、エラーを起こすことなく取りあえず動くはずだ。前回のスクリプト1について、Vectorオブジェクト(変数vertices)に4頂点でなく8頂点のVector3Dインスタンスを加えたのがつぎのフレームアクションだ。
var nUnit:Number = 100 / 2;
var mySprite:Sprite = new Sprite();
var vertices:Vector.<Vector3D> = new Vector.<Vector3D>();
var nDeceleration:Number = 0.3;
var myGraphics:Graphics = mySprite.graphics;
var nFocalLength:Number = transform.perspectiveProjection.focalLength;
mySprite.x = stage.stageWidth / 2;
mySprite.y = stage.stageHeight / 2;
/* 4頂点座標
vertices.push(new Vector3D(-nUnit, -nUnit, 0));
vertices.push(new Vector3D(nUnit, -nUnit, 0));
vertices.push(new Vector3D(nUnit, nUnit, 0));
vertices.push(new Vector3D(-nUnit, nUnit, 0));
*/ // 以下の8頂点座標に差替え
vertices.push(new Vector3D(-nUnit, -nUnit, -nUnit));
vertices.push(new Vector3D(nUnit, -nUnit, -nUnit));
vertices.push(new Vector3D(nUnit, nUnit, -nUnit));
vertices.push(new Vector3D(-nUnit, nUnit, -nUnit));
vertices.push(new Vector3D(-nUnit, -nUnit, nUnit));
vertices.push(new Vector3D(nUnit, -nUnit, nUnit));
vertices.push(new Vector3D(nUnit, nUnit, nUnit));
vertices.push(new Vector3D(-nUnit, nUnit, nUnit));
addChild(mySprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xRotate(eventObject:Event):void {
var nRotationY:Number = mySprite.mouseX * nDeceleration;
xTransform(vertices, nRotationY);
var vertices2D:Vector.<Point > = xGetVertices2D(vertices);
xDrawLines(vertices2D);
}
function xTransform(myVertices:Vector.<Vector3D>, myRotation:Number):void {
var nLength:uint = myVertices.length;
var myMatrix3D:Matrix3D = new Matrix3D();
myMatrix3D.prependRotation(myRotation, Vector3D.Y_AXIS);
for (var i:int = 0; i<nLength; i++) {
myVertices[i] = myMatrix3D.transformVector(myVertices[i]);
}
}
function xGetVertices2D(myVertices:Vector.<Vector3D>):Vector.<Point > {
var vertices2D:Vector.<Point> = new Vector.<Point>();
var nLength:uint = myVertices.length;
for (var i:uint = 0; i < nLength; i++) {
var myVector3D:Vector3D = myVertices[i].clone();
myVector3D.w = (nFocalLength + myVector3D.z) / nFocalLength;
myVector3D.project();
vertices2D.push(new Point(myVector3D.x, myVector3D.y));
}
return vertices2D;
}
function xDrawLines(vertices2D:Vector.<Point>):void {
var nLength:uint = vertices2D.length;
var myPoint:Point = vertices2D[nLength - 1];
myGraphics.clear();
myGraphics.lineStyle(2, 0x0000FF);
myGraphics.moveTo(myPoint.x, myPoint.y);
for (var i:uint = 0; i < nLength; i++) {
myPoint = vertices2D[i];
myGraphics.lineTo(myPoint.x, myPoint.y);
}
}
[ムービープレビュー]で確かめると、確かに8頂点を結ぶワイヤーフレームが3次元空間で水平に回る。もっとも、かたちは立方体というより、貧弱な泡立て器のようだ(図3 ) 。これは、スクリプトがすべての頂点を、単純にひと筆書きで結んでいることによる。
図3 8頂点を単純にひと筆書きで結んだワイヤーフレームが描かれる
しかし、3次元空間における8頂点の座標変換は正しく行われているようだ。ただあいにく、立方体の12辺はひと筆書きできない[1] 。つまり、いくつかの部品に分けて、ワイヤーフレームを描く必要がある。すると、手を加えるべきなのは2次元平面に線描する処理、具体的には関数xDrawLines()だ。
さて、この関数はどのように書き替えたらよいか。ひと筆書きでなく、複数のかたちを描けるように、機能を加える手ももちろんある。しかし、それらのひとつひとつを線描するには、結局関数xDrawLines()と同じ処理が必要だ。
だとすれば、新たに前処理の関数を定めれば済む。複数の部品の頂点座標をひと組ずつ取出し、それぞれ関数xDrawLines()に渡してワイヤーフレームを描かせる。いわば、仕事を切り分けて担当者に依頼する、ディレクター役の関数だ。
立方体のワイヤーフレームを描く
立方体はわかりやすさも考えて、図4 のように6つに切り分けて描くことにする。正方形を前面と背面にひとつずつ、あとはふたつの正方形の4頂点をそれぞれ結ぶ4つの辺だ。
図4 立方体をふたつの正方形と4辺で描く
さて、立方体の8頂点座標はすでにある。新たに用意しなければならないのは、ひとつひとつのワイヤーフレームがどの頂点を結んでつくられるのかという情報だ。前掲図2 にも書添えた頂点番号を用いれば、それらの組み合わせは示せる。
ひとつの部品を描くための頂点番号の組は、整数(uint型)をベース型とするVectorインスタンスに入れよう。そして、それら部品のVectorを6つエレメントとして、さらにVectorインスタンス(Vectorベース型)に入れ子で納める。入れ子の親Vectorオブジェクトを変数宣言(indices)して、部品となる6つのVectorエレメントを加えるスクリプトはつぎのとおりだ。
var indices:Vector.<Vector.<uint>> = new Vector.<Vector.<uint>>();
indices.push(new < uint>[ 0, 1, 2, 3] );
indices.push(new < uint>[ 4, 5, 6, 7] );
indices.push(new < uint>[ 0, 4] );
indices.push(new < uint>[ 1, 5] );
indices.push(new < uint>[ 2, 6] );
indices.push(new < uint>[ 3, 7] );
ふたつ補足しよう。まず、Vectorオブジェクトのベース型をVectorとする場合、そのベース型にもさらに子(入れ子のエレメント)のベース型を添える必要がある。そのため、ベース型を示す山括弧<>は、入れ子で二重になる。
Vector.<Vector.<子のベース型>>
つぎに、エレメントを予め納めたVectorインスタンスのつくり方だ。Arrayクラスと異なり、コンストラクタメソッドVector()の引数にエレメントを渡すことはできない。その代わり、Flash Professional CS5からはつぎのような一風変わったシンタックスを用いる(「 Vectorクラス 」参照。Flash CS4 Professionalでは使えないことに注意) 。
new <ベース型>[エレメント0, エレメント1, …, エレメントN]
これで描画するためのデータは整った。頂点座標と頂点番号の組合わせをそれぞれVectorインスタンスで受取って、立方体を描く関数を定めよう。関数名はxDraw()とし、つぎのように定義した。ふたつのforループが入れ子になっていることを除けば、とくに目新しいことはない。
function xDraw(vertices2D:Vector.<Point>, myIndices:Vector.<Vector.<uint>>):void {
var nLength:uint = myIndices.length;
myGraphics.clear();
for (var i:uint = 0; i < nLength; i++) {
var myVertices:Vector.<Point> = new Vector.<Point>();
var myIndex:Vector.<uint > = myIndices[i];
var nLength2:uint = myIndex.length;
for (var j:uint = 0; j < nLength2; j++) {
myVertices.push(vertices2D[myIndex[j]]);
}
xDrawLines(myVertices);
}
}
最初のforループでは第2引数(myIndices)のVectorインスタンスから頂点番号の組(整数ベース型Vector)を取出し、つぎのforループで各頂点番号に対応する座標(Point)を第1引数(vertices2D)のVectorから得る。それらの座標は新たなVector(myVertices)に加えていき、部品ひとつ分の組が揃ったら関数xDrawLines()に渡してワイヤーフレームを描かせる。
関数xDrawLines()は、つぎのようにGraphics.clear()メソッド呼出しのステートメント1行を除く。部品をひとつ描くたびに、前のワイヤーフレームを消してはまずいからだ。Graphics.clear()メソッドの呼出しは、上記xDraw()が立方体ごとにまとめて行う。
function xDrawLines(vertices2D:Vector.<Point>):void {
var nLength:uint = vertices2D.length;
var myPoint:Point = vertices2D[nLength - 1];
// myGraphics.clear();
myGraphics.lineStyle(2, 0x0000FF);
myGraphics.moveTo(myPoint.x, myPoint.y);
for (var i:uint = 0; i < nLength; i++) {
myPoint = vertices2D[i];
myGraphics.lineTo(myPoint.x, myPoint.y);
}
}
あとは、DisplayObject.enterFrameイベントのリスナー関数xRotate()からワイヤーフレーム描画のために呼出す関数を、xDrawLines()からxDraw()に書き替えればよい。引数には、頂点座標と頂点番号の組がそれぞれ納められたVectorインスタンス(vertices2Dとindices)を渡す。これで、立方体のワイヤーフレームが描かれ、マウスポインタの水平位置に応じて水平に回る(図5 ) 。
図5 立方体のワイヤーフレームがマウスポインタの水平位置に応じて水平に回る
できあがったフレームアクション全体は、以下のスクリプト1のとおりだ。前回のスクリプト1からどう書き替えたかはすでに説明した。しかし実は、関数xDrawLines()には、もうひと手間加えている。
スクリプト1 ワイヤーフレームの立方体をマウスポインタの水平位置に応じて水平に回す
// フレームアクション
var nUnit:Number = 100 / 2;
var mySprite:Sprite = new Sprite();
var vertices:Vector.<Vector3D> = new Vector.<Vector3D>();
var indices:Vector.<Vector.<uint>> = new Vector.<Vector.<uint>>();
var nDeceleration:Number = 0.3;
var myGraphics:Graphics = mySprite.graphics;
var nFocalLength:Number = transform.perspectiveProjection.focalLength;
mySprite.x = stage.stageWidth / 2;
mySprite.y = stage.stageHeight / 2;
vertices.push(new Vector3D(-nUnit, -nUnit, -nUnit));
vertices.push(new Vector3D(nUnit, -nUnit, -nUnit));
vertices.push(new Vector3D(nUnit, nUnit, -nUnit));
vertices.push(new Vector3D(-nUnit, nUnit, -nUnit));
vertices.push(new Vector3D(-nUnit, -nUnit, nUnit));
vertices.push(new Vector3D(nUnit, -nUnit, nUnit));
vertices.push(new Vector3D(nUnit, nUnit, nUnit));
vertices.push(new Vector3D(-nUnit, nUnit, nUnit));
indices.push(new <uint>[0, 1, 2, 3]);
indices.push(new <uint>[4, 5, 6, 7]);
indices.push(new <uint>[0, 4]);
indices.push(new <uint>[1, 5]);
indices.push(new <uint>[2, 6]);
indices.push(new <uint>[3, 7]);addChild(mySprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xRotate(eventObject:Event):void {
var nRotationY:Number = mySprite.mouseX * nDeceleration;
xTransform(vertices, nRotationY);
var vertices2D:Vector.<Point > = xGetVertices2D(vertices);
xDraw(vertices2D, indices);
}
function xTransform(myVertices:Vector.<Vector3D>, myRotation:Number):void {
var nLength:uint = myVertices.length;
var myMatrix3D:Matrix3D = new Matrix3D();
myMatrix3D.prependRotation(myRotation, Vector3D.Y_AXIS);
for (var i:int = 0; i<nLength; i++) {
myVertices[i] = myMatrix3D.transformVector(myVertices[i]);
}
}
function xGetVertices2D(myVertices:Vector.<Vector3D>):Vector.<Point > {
var vertices2D:Vector.<Point> = new Vector.<Point>();
var nLength:uint = myVertices.length;
for (var i:uint = 0; i < nLength; i++) {
var myVector3D:Vector3D = myVertices[i].clone();
myVector3D.w = (nFocalLength + myVector3D.z) / nFocalLength;
myVector3D.project();
vertices2D.push(new Point(myVector3D.x, myVector3D.y));
}
return vertices2D;
}
function xDraw(vertices2D:Vector.<Point>, myIndices:Vector.<Vector.<uint>>):void {
var nLength:uint = myIndices.length;
myGraphics.clear();
for (var i:uint = 0; i < nLength; i++) {
var myVertices:Vector.<Point> = new Vector.<Point>();
var myIndex:Vector.<uint > = myIndices[i];
var nLength2:uint = myIndex.length;
for (var j:uint = 0; j < nLength2; j++) {
myVertices.push(vertices2D[myIndex[j]]);
}
xDrawLines(myVertices);
}
}
function xDrawLines(vertices2D:Vector.<Point>):void {
var nLength:uint = vertices2D.length;
var myPoint:Point = vertices2D[nLength - 1];
if (nLength < 3) {
--nLength;
}
myGraphics.lineStyle(2, 0x0000FF);
myGraphics.moveTo(myPoint.x, myPoint.y);
for (var i:uint = 0; i < nLength; i++) {
myPoint = vertices2D[i];
myGraphics.lineTo(myPoint.x, myPoint.y);
}
}
加わったのは、ifステートメントだ。もともとのxDrawLines()関数は、閉じたかたちを描いた。つまり、初めと終わりの座標を結ぶ。するとそのままでは、2頂点しかもたない線分も、往復して描いてしまう。その復路の無駄を省くため、頂点数がふたつしかないとき、forループで開始点に戻って閉じる線描を減らしている。
図6 頂点数がふたつのとき復路の線描を省く
次回からは、新たなお題としてGraphics.drawTriangles()メソッドについて学ぶ。定められた領域を、ビットマップで塗りつぶせる。3次元空間に絡めると、面に素材のビットマップを貼りつける、いわゆる「テクスチャマッピング」に用いられるメソッドだ。
今回解説した次のサンプルファイルがダウンロードできます。