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

第33回遠近法の投影

今回から、Flash Player 10で備わった3次元空間の扱いに入りたい。したがって、Flash CS4 Professional以降が前提となり、サンプルファイルもCS4形式での提供となる。まずは、遠近法の捉え方について説明しよう。

[プロパティ]インスペクタを使った3次元空間の操作

コンピュータグラフィックス(CG)で3次元空間を描くとき、最終的には2次元平面のスクリーンに映さなければならない。つまり、3次元空間で計算した座標を、遠近法にしたがって計算し直し、その結果をスクリーンに投影することになる。この処理は英語で"perspective projection"と呼ばれ、⁠遠近法投影」とか「透視投影」と訳される。この遠近法を扱うのが、PerspectiveProjectionクラスだ。

だが、インスタンスをいきなりスクリプトで動かす前に、[プロパティ]インスペクタを用いて、3次元空間のプロパティの操作がどのような表現になるのか試してみよう。ステージに置いたインスタンスを選んだら、[プロパティ]インスペクタの[3D位置とビュー]セクションで3次元空間の操作ができる。たとえば、z座標値を大きくすると、インスタンスはサイズが縮まり、ステージの真ん中に向かって動く図1⁠。

図1 [プロパティ]インスペクタの[3D位置とビュー]セクションで3次元表現を操作
図1 [プロパティ]インスペクタの[3D位置とビュー]セクションで3次元表現を操作

これは、インスタンスに遠近法の表現が加えられたためだ。試しに、同じシンボルのインスタンスをいくつかを同じxy座標に重ねたうえで、z座標値だけを段階的に増やしてみるとよくわかる。z座標値が大きく奥行きの遠いインスタンスほど、サイズは小さくなり、位置はある1点に近づく。この点を「消失点」と呼ぶ図2左図⁠⁠。消失点は、デフォルトではステージの中央にある。[プロパティ]インスペクタの[3D位置とビュー]セクションでxy座標値を設定すれば動かせる図2右図。※1⁠。

図2 インスタンスが遠ざかるにつれて消失点に向かって小さくなる
図2 インスタンスが遠ざかるにつれて消失点に向かって小さくなる

[プロパティ]インスペクタの[3D位置とビュー]セクションには、もうひとつ[遠近の角度]というプロパティがある。これは一般には、カメラに関わる用語で「視野角」とか「画角」と呼ばれる角度だ図3右図⁠⁠。1から179までの数値で定める。値が大きいほど、遠近の差は広がって見える図3左図⁠⁠。

図3 [プロパティ]インスペクタの[3D位置とビュー]セクションにおける[遠近の角度]
図3 [プロパティ]インスペクタの[3D位置とビュー]セクションにおける[遠近の角度]

ステージにランダムなシェイプを配置する

遠近法を操作すると表現がどう変わるかは、インスタンスひとつだけではわかりにくい。そこで、PerspectiveProjectionクラスを試す前に、ステージ一杯に数多くのランダムなシェイプを配置しよう。シェイプは、Shapeクラスで生成する。Shapeインスタンスには、スクリプトで円を描く。その描画をするときに参照するのが、インスタンスのShape.graphicsプロパティだ。

Shape.graphicsプロパティは、ShapeインスタンスがもつGraphicsオブジェクトを参照する。そのオブジェクトに対してGraphicsクラスのメソッドを呼び出せば、Shapeインスタンスにベクターが描ける[2]⁠。ベクターを描くには、まず塗り(もしくは線)を設定する必要がある。塗りはGraphics.beginFill()メソッドでカラーとアルファを決める。

Graphicsオブジェクト.beginFill(カラー値, アルファ値)

カラーは通常16進数の整数、アルファは0から1までの数値(デフォルト値は1)を渡す。そして、塗りを終えるときには、引数のないGraphics.endFill()メソッドを呼出す。円を描くメソッドは、Graphics.drawCircle()メソッドだ。引数は3つで、中心のxy座標値と半径の長さになる。

Graphicsオブジェクト.drawCircle(中心のx座標値, 中心のy座標値, 半径の長さ)

これらの知識をもとにして、中心は基準点(0, 0)で、指定したカラーと半径の円が描かれたShapeインスタンスを返す関数xCreateCircle()は、つぎのように定義される。なお、カラーはデフォルト値を黒(0x000000)とした。

// フレームアクション
function xCreateCircle(nRadius:Number, nColor:uint = 0):Shape {
  var myShape:Shape = new Shape();
  var myGraphics:Graphics = myShape.graphics;
  myGraphics.beginFill(nColor);
  myGraphics.drawCircle(0, 0, nRadius);
  myGraphics.endFill();
  return myShape;
}

同じタイムラインのフレームアクションからつぎのように関数xCreateCircle()を呼び出すと、ステージ中央に半径10ピクセルで青い円の描かれたShapeインスタンスが配置される図4⁠。

var myShape:Shape = xCreateCircle(10, 0x0000FF);
addChild(myShape);
myShape.x = stage.stageWidth / 2;
myShape.y = stage.stageHeight / 2;
図4 円の描かれたShapeインスタンスを関数でつくってステージ中央に配置
図4 円の描かれたShapeインスタンスを関数でつくってステージ中央に配置

このShapeインスタンスをいくつもステージ上につくるとき、3次元座標の位置と大きさ、およびカラーをランダムに決めたい。そこで、指定した範囲でランダムな数値が得られる関数xGetRandom()を定義しておこう。0以上1未満のランダムな数値を返すメソッドはMath.random()だ。このランダムな数値を最大値と最小値の範囲に定めるのがつぎの式になる。

Math.random() * (最大値 - 最小値) + 最小値

カラー値は整数にする必要がある。ランダムな整数値を求める場合、最小値を戻り値の範囲に含め、小数点以下を切捨てるため、式はつぎのように変わる(※3)。

Math.floor(Math.random() * (最大値 - 最小値 + 1) + 最小値)

そこで、指定した範囲のランダムな数値を返す関数xGetRandom()は、以下のように定義した。3つの引数は順に、最大値と最小値、および整数で返すかどうかのブール(論理)値だ。引数を渡さなければ、Math.random()メソッドの戻り値がそのまま返される。また、関数の最初のifステートメントは、第1および第2引数が最小値、最大値の順でも処理できるように、値を入替えている。

function xGetRandom(nMax:Number = 1, nMin:Number = 0, bInt:Boolean = false):Number {
  if (nMax < nMin) {
    var nTemp:Number = nMax;
    nMax = nMin;
    nMin = nTemp;
  }
  var nRandom:Number = Math.random();
  if (bInt) {
    return Math.floor(nRandom * (nMax - nMin + 1) + nMin);
  } else {
    return nRandom * (nMax - nMin) + nMin;
  }
}

以下のスクリプト1には、上記ふたつの関数を使ってステージ一杯に任意の数のランダムなShapeインスタンスをつくる関数xCreateShapes()が加えられている。[ムービープレビュー]を確かめると、ステージ上にランダムな位置とサイズ(半径10ピクセル未満)およびカラーの円のShapeインスタンスが100個つくられる図5⁠。

スクリプト1 ステージ上にランダムな位置とサイズおよびカラーの円のShapeインスタンスをつくる
// タイムライン: メイン
// フレームアクション
xCreateShapes(100);
function xCreateShapes(nCount:uint):void {
  for (var i:uint = 0; i < nCount; i++) {
    var nColor:uint = xGetRandom(0xFFFFFF, 0, true);
    var nRadius:Number = xGetRandom(10);
    var myShape:Shape = xCreateCircle(nRadius, nColor);
    addChild(myShape);
    myShape.x = xGetRandom(stage.stageWidth);
    myShape.y = xGetRandom(stage.stageHeight);
    myShape.z = xGetRandom(stage.stageWidth);
  }
}
function xCreateCircle(nRadius:Number, nColor:uint = 0):Shape {
  var myShape:Shape = new Shape();
  var myGraphics:Graphics = myShape.graphics;
  myGraphics.beginFill(nColor);
  myGraphics.drawCircle(0, 0, nRadius);
  myGraphics.endFill();
  return myShape;
}
function xGetRandom(nMax:Number = 1, nMin:Number = 0, bInt:Boolean = false):Number {
  if (nMax < nMin) {
    var nTemp:Number = nMax;
    nMax = nMin;
    nMin = nTemp;
  }
  var nRandom:Number = Math.random();
  if (bInt) {
    return Math.floor(nRandom * (nMax - nMin + 1) + nMin);
  } else {
    return nRandom * (nMax - nMin) + nMin;
  }
}
図5 ステージ上にランダムな位置とサイズおよびカラーの円のShapeが配置された
図5 ステージ上にランダムな位置とサイズおよびカラーの円のShapeが配置された

もっとも、このままでは見える大きさの違いが、もともとのサイズによるのか、遠近法のせいなのかがわからない。そこでつぎに、PerspectiveProjectionクラスを使ってみよう。

PerspectiveProjectionクラスで遠近法を操作する

PerspectiveProjectionクラスを使って、メインタイムラインの遠近法を変えてみる。DisplayObjectインスタンスがもつPerspectiveProjectionオブジェクトには、ふたつのプロパティを経てアクセスする。

まず、DisplayObject.transformプロパティは、DisplayObjectインスタンスに対する2次元ないし3次元の座標やカラーの変換を管理するTransformオブジェクトへの参照となる。そしてつぎに、Transform.perspectiveProjectionプロパティにより、DisplayObjectインスタンスに適用される遠近法の設定となるPerspectiveProjectionオブジェクトの参照が得られる図6⁠。なお、後の回で解説する3次元空間の座標変換を扱うMatrix3Dオブジェクトも、Transformクラスのプロパティから参照する。

図6 プロパティDisplayObject.transformからTransform.perspectiveProjectionの参照を得る
図6 プロパティDisplayObject.transformからTransform.perspectiveProjectionの参照を得る

それでは、前掲スクリプト1の消失点を動かしてみよう。用いるプロパティは、PerspectiveProjection.projectionCenterだ。xy座標をPointオブジェクトで設定する。前掲フレームアクション(スクリプト1)につぎのステートメントを加えると、消失点がマウスポインタの座標に合わせて動く図7⁠。

var myPerspective:PerspectiveProjection = transform.perspectiveProjection;
addEventListener(Event.ENTER_FRAME, xMoveViewPoint);
function xMoveViewPoint(eventObject:Event):void {
  myPerspective.projectionCenter = new Point(mouseX, mouseY);
}
図7 消失点がマウスポインタの座標に合わせて動く
図7 消失点がマウスポインタの座標に合わせて動く

さらに、視野角も変えてみたい。PerspectiveProjection.fieldOfViewプロパティに、0より大きく180より小さい度数値で定める。前掲フレームアクション(スクリプト1)にさらにつぎのスクリプトを加えると、ステージをクリックしたとき、現行の視野角に50度を加え図8右図⁠⁠、再度クリックするともとの視野角に戻る図8左図⁠⁠。前述のとおり、視野角は大きいほど遠近の差が際立つ。

var standardView:Number = myPerspective.fieldOfView;
var bStandard:Boolean = true;
stage.addEventListener(MouseEvent.CLICK, xChangeFieldOfView);
function xChangeFieldOfView(eventObject:MouseEvent):void {
  if (bStandard = ! bStandard) {
    myPerspective.fieldOfView = standardView;
  } else {
    myPerspective.fieldOfView = standardView + 50;
  }
}
図8 ステージをクリックすると視野角が拡大する
図8 ステージをクリックすると視野角が拡大する

なお、前掲リスナー関数xChangeFieldOfView()のif条件は、式の両辺を比較しているのではなく、代入文であることに注意してほしい。演算子!は論理否定で、ブール(論理)値を反転する。したがって、変数のtrueとfalseを反転して代入したうえで、その反転後の値を条件として評価している。

さて、以上に述べた消失点と視野角の操作を前掲スクリプト1に加えたのが、つぎのスクリプト2だ。

スクリプト2 Shapeインスタンスが配置された3次元空間の消失点と視野角を変える
// タイムライン: メイン
// フレームアクション
var myPerspective:PerspectiveProjection = transform.perspectiveProjection;
var standardView:Number = myPerspective.fieldOfView;
var bStandard:Boolean = true;
addEventListener(Event.ENTER_FRAME, xMoveViewPoint);
stage.addEventListener(MouseEvent.CLICK, xChangeFieldOfView);
xGreateShapes(100);
function xMoveViewPoint(eventObject:Event):void {
  myPerspective.projectionCenter = new Point(mouseX, mouseY);
}
function xChangeFieldOfView(eventObject:MouseEvent):void {
  if (bStandard = ! bStandard) {
    myPerspective.fieldOfView = standardView;
  } else {
    myPerspective.fieldOfView = standardView + 50;
  }
}
function xGreateShapes(nCount:uint):void {
  for (var i:uint = 0; i < nCount; i++) {
    var nColor:uint = xGetRandom(0xFFFFFF, 0, true);
    var nRadius:Number = xGetRandom(10);
    var myShape:Shape = xCreateCircle(nRadius, nColor);
    addChild(myShape);
    myShape.x = xGetRandom(stage.stageWidth);
    myShape.y = xGetRandom(stage.stageHeight);
    myShape.z = xGetRandom(stage.stageWidth);
  }
}
function xCreateCircle(nRadius:Number, nColor:uint = 0):Shape {
  var myShape:Shape = new Shape();
  var myGraphics:Graphics = myShape.graphics;
  myGraphics.beginFill(nColor);
  myGraphics.drawCircle(0, 0, nRadius);
  myGraphics.endFill();
  return myShape;
}
function xGetRandom(nMax:Number = 1, nMin:Number = 0, bInt:Boolean = false):Number {
  if (nMax < nMin) {
    var nTemp:Number = nMax;
    nMax = nMin;
    nMin = nTemp;
  }
  var nRandom:Number = Math.random();
  if (bInt) {
    return Math.floor(nRandom * (nMax - nMin + 1) + nMin);
  } else {
    return nRandom * (nMax - nMin) + nMin;
  }
}

ところで、動作結果を注意深く見ると、ある不自然なことに気づくかもしれない。それは、インスタンスのz座標値の大小と画面上の表示の重ね順が、必ずしも一致していないということだ。つまり、z座標は奥のはずのインスタンスが、手前のインスタンスの前に表示されたりする。なぜなら、Flash Playerでは、インスタンスのz座標値とはまったく別に表示の重ね順が決まるからだ。

したがって、表示の重ね順をいかにz座標値と一致させるかは、3次元空間を扱ううえでのひとつの大きな課題となる。この問題については、3次元空間の操作についてもっと詳しく説明したあとで改めて採上げたい。つぎは、3次元空間座標の回転について解説する。

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

おすすめ記事

記事・ニュース一覧