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

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

この記事を読むのに必要な時間:およそ 7 分

前回の第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()に渡してワイヤーフレームを描かせる。いわば,仕事を切り分けて担当者に依頼する,ディレクター役の関数だ。

※1
ひと筆書きの禁じ手である,同じ線をなぞって構わなければ,立方体も描ける。ただ,描線の重複という無駄が生じるので,今回この手は採らない。

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

立方体はわかりやすさも考えて,図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次元空間に絡めると,面に素材のビットマップを貼りつける,いわゆる「テクスチャマッピング」に用いられるメソッドだ。

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

著者プロフィール

野中文雄(のなかふみお)

ソフトウェアトレーナー,テクニカルライター,オーサリングエンジニア。上智大学法学部卒,慶応義塾大学大学院経営管理研究科修士課程修了(MBA)。独立系パソコン販売会社で,総務・人事,企画,外資系企業担当営業などに携わる。その後,マルチメディアコンテンツ制作会社に転職。ソフトウェアトレーニング,コンテンツ制作などの業務を担当する。2001年11月に独立。Web制作者に向けた情報発信プロジェクトF-siteにも参加する。株式会社ロクナナ取締役(非常勤)。

URLhttp://www.FumioNonaka.com/

著書

コメント

コメントの記入