ActionScript 3.0で始めるオブジェクト指向スクリプティング

第44回ワイヤーフレームの立方体を回す

前回の第43回は、Vector3Dインスタンスで3次元空間における正方形の4頂点座標をつくり、y軸で水平に回したうえで、2次元平面の座標に透視投影したワイヤーフレームを描いてみた。今回のお題は、頂点座標を8つに増やして立方体を回す図1⁠。

図1 立方体の8頂点をワイヤーフレームで結んで回す
図1 立方体の8頂点をワイヤーフレームで結んで回す

立方体の8頂点の座標を決める

まず、立方体の8頂点の座標を決める。原点(0, 0, 0)は立方体の中心に据え、xyz軸が6面の中央を垂直に貫くように定めよう。1辺の半分の長さを変数(nUnit)に設定するなら、8頂点の3次元空間座標は図2のとおりだ。なお、各頂点には0から始まる整数の通し番号を付した。

図2 原点を中心に定めた立方体の8頂点座標
図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頂点を単純にひと筆書きで結んだワイヤーフレームが描かれる

しかし、3次元空間における8頂点の座標変換は正しく行われているようだ。ただあいにく、立方体の12辺はひと筆書きできない[1]⁠。つまり、いくつかの部品に分けて、ワイヤーフレームを描く必要がある。すると、手を加えるべきなのは2次元平面に線描する処理、具体的には関数xDrawLines()だ。

さて、この関数はどのように書き替えたらよいか。ひと筆書きでなく、複数のかたちを描けるように、機能を加える手ももちろんある。しかし、それらのひとつひとつを線描するには、結局関数xDrawLines()と同じ処理が必要だ。

だとすれば、新たに前処理の関数を定めれば済む。複数の部品の頂点座標をひと組ずつ取出し、それぞれ関数xDrawLines()に渡してワイヤーフレームを描かせる。いわば、仕事を切り分けて担当者に依頼する、ディレクター役の関数だ。

立方体のワイヤーフレームを描く

立方体はわかりやすさも考えて、図4のように6つに切り分けて描くことにする。正方形を前面と背面にひとつずつ、あとはふたつの正方形の4頂点をそれぞれ結ぶ4つの辺だ。

図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 立方体のワイヤーフレームがマウスポインタの水平位置に応じて水平に回る
図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 頂点数がふたつのとき復路の線描を省く
図6 頂点数がふたつのとき復路の線描を省く

次回からは、新たなお題としてGraphics.drawTriangles()メソッドについて学ぶ。定められた領域を、ビットマップで塗りつぶせる。3次元空間に絡めると、面に素材のビットマップを貼りつける、いわゆる「テクスチャマッピング」に用いられるメソッドだ。

今回解説した次のサンプルファイルがダウンロードできます。

おすすめ記事

記事・ニュース一覧