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

第47回遠近法を適用した3次元空間座標へのテクスチャマッピング

前回の第46回は、矩形を三角形に分けてビットマップで塗るいわゆる「テクスチャマッピング」について解説した。今回は、いよいよ3次元空間座標を2次元平面に透視投影して、ビットマップの面を貼ってみたい。3次元空間座標の透視投影は、第43回Vector3Dオブジェクトの座標に遠近法を適用するで学んだ。このときは、3次元空間で回転する正方形をワイヤーフレームで描いた第43回図7再掲⁠⁠。今回は、この正方形にテクスチャマッピングしてみよう。

第43回図7 回転するワイヤーフレームの四角形にパースペクティブがかかった(再掲)
第43回図7 回転するワイヤーフレームの四角形にパースペクティブがかかった(再掲) 第43回図7 回転するワイヤーフレームの四角形にパースペクティブがかかった(再掲) 第43回図7 回転するワイヤーフレームの四角形にパースペクティブがかかった(再掲)

3次元空間で水平回転する正方形の頂点座標にテクスチャマッピングする

第43回スクリプト1「3次元空間の頂点座標から2次元平面に透視投影したワイヤーフレームを描く」フレームアクションだ。今回はこのフレームアクションに手を加える。スクリプトの解説やファイルのダウンロードは、第43回2ページをご覧いただきたい。

[ライブラリ]に納めた塗りのビットマップには、クラス「Image」を設定しておく第45回図1再掲⁠⁠。⁠ライブラリ]のビットマップへの[クラス]の設定方法ついて詳しくは、第34回3次元空間における回転「ビットマップのインスタンスを動的に配置する」を参照してほしい。

第45回図1 塗りで使う[ライブラリ]のビットマップにクラス名「Image」を設定(再掲)
第45回図1 塗りで使う[ライブラリ]のビットマップにクラス名「Image」を設定(再掲)

それでは、第43回スクリプト1をテクスチャマッピングできるように書き替えよう。できあがるフレームアクションは、スクリプト1として後にまとめて掲げた。手を加えるスクリプトは抜き書きして順に示す。まずは、初期設定から考えよう。

加えた内容は大きくふたつある図1⁠。第1は、テクスチャとして用いるビッマップのインスタンス(myTexture)をつくることだ。第2に、Graphics.drawTriangles()メソッドに渡すVectorオブジェクトをつくった。第2引数となる分割した3角形の頂点番号の組と、第3引数のuv座標だ。そして、それらのVectorオブジェクトに初期値のエレメントを与えている。なお、第1引数の頂点座標はアニメーションさせるため、後で書直す関数(xGetVertices2D())で計算する。

var nUnit:Number = 100 / 2;
var mySprite:Sprite = new Sprite();
var myTexture:BitmapData = new Image();   // 追加: ビットマップのインスタンス生成
var vertices:Vector.<Vector3D> = new Vector.<Vector3D>();
// 追加: Graphics.drawTriangles()メソッドに渡すVectorオブジェクト
var indices:Vector.<int> = new Vector.<int>();   // 頂点番号(第2引数)
var uvData:Vector.<Number> = new Vector.<Number>();   // uv座標(第3引数)
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, 0));
vertices.push(new Vector3D(nUnit, -nUnit, 0));
vertices.push(new Vector3D(nUnit, nUnit, 0));
vertices.push(new Vector3D(-nUnit, nUnit, 0));
// 追加: 頂点番号の組
indices.push(0, 1, 3);
indices.push(1, 2, 3);
// 追加: uv座標値
uvData.push(0, 0);
uvData.push(1, 0);
uvData.push(1, 1);
uvData.push(0, 1);
addChild(mySprite);
図1 初期設定にGraphics.drawTriangles()メソッドの引数となるVectorオブジェクトを加える
図1 初期設定にGraphics.drawTriangles()メソッドの引数となるVectorオブジェクトを加える

つぎは、イベントリスナーの設定だ。ここでもふたつ書き替える。第1は、透視投影した頂点座標のデータ型だ。第43回スクリプト1ではVectorオブジェクトのベース型をPointで定めた。しかし、Graphics.drawTriangles()メソッドに渡すには、ベース型をNumberにしなければならない。第2に、描画はワイヤーフレームでなくテクスチャマッピングにするので、関数(xDraw())を新たに定める。

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);
  var vertices2D:Vector.<Number> = xGetVertices2D(vertices);   // 変更: ベース型をNumberに
  // xDrawLines(vertices2D);
  xDraw(vertices2D);   // 変更: テクスチャマッピングの関数を呼出す
}

そして、上記の変更にもとづく関数の修正だ。3次元空間の座標を2次元平面に透視投影する関数(xGetVertices2D())は、つぎのように戻り値であるVectorオブジェクトのベース型をPointからNumberに変える。その他の実質的な中身はそのままだ。

// function xGetVertices2D(myVertices:Vector.<Vector3D>):Vector.<Point> {
function xGetVertices2D(myVertices:Vector.<Vector3D>):Vector.<Number> {
  // var vertices2D:Vector.<Point> = new Vector.<Point>();
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  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));
    vertices2D.push(myVector3D.x, myVector3D.y);
  }
  return vertices2D;
}

最後に、テクスチャマッピングの関数(xDraw())を新たに定める。引数は、2次元平面に透視投影した座標のVectorオブジェクトだ。ワイヤーフレームを描く関数(xDrawLines)はもはや要らない。新たな関数の基本的な処理は、前回のスクリプト3のリスナー関数(xDraw())と変わらない。とくに補足は不要だろう。

/* 削除
function xDrawLines(vertices2D:Vector.<Point>):void {
  // ...[中略]...
}
*/
function xDraw(vertices2D:Vector.<Number>):void {
  myGraphics.clear();
  myGraphics.beginBitmapFill(myTexture);
  myGraphics.drawTriangles(vertices2D, indices, uvData);
  myGraphics.endFill();
}

でき上がったフレームアクションの全体は、つぎのスクリプト1のとおりだ。さほど大きな手を加えずにできたように見える。

スクリプト1 3次元空間の回転する正方形を2次元平面に透視投影してテクスチャマッピング(暫定)
// フレームアクション
var nUnit:Number = 100 / 2;
var mySprite:Sprite = new Sprite();
var myTexture:BitmapData = new Image();
var vertices:Vector.<Vector3D> = new Vector.<Vector3D>();
var indices:Vector.<int> = new Vector.<int>();
var uvData:Vector.<Number> = new Vector.<Number>();
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, 0));
vertices.push(new Vector3D(nUnit, -nUnit, 0));
vertices.push(new Vector3D(nUnit, nUnit, 0));
vertices.push(new Vector3D(-nUnit, nUnit, 0));
indices.push(0, 1, 3);
indices.push(1, 2, 3);
uvData.push(0, 0);
uvData.push(1, 0);
uvData.push(1, 1);
uvData.push(0, 1);
addChild(mySprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xRotate(eventObject:Event):void {
  var nRotationY:Number = mySprite.mouseX * nDeceleration;
  xTransform(vertices, nRotationY);
  var vertices2D:Vector.<Number> = xGetVertices2D(vertices);
  xDraw(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.<Number> {
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  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(myVector3D.x, myVector3D.y);
  }
  return vertices2D;
}
function xDraw(vertices2D:Vector.<Number>):void {
  myGraphics.clear();
  // myGraphics.lineStyle(1, 0x0000FF);   // 確認用
  myGraphics.beginBitmapFill(myTexture);
  myGraphics.drawTriangles(vertices2D, indices, uvData);
  myGraphics.endFill();
}

テクスチャがゆがむ謎

前掲スクリプト1のキャプションに「⁠⁠暫定⁠⁠」とあるのが気になる。取りあえず、⁠ムービープレビュー]を確かめよう。マウスポインタの水平位置に応じた向きと速さで、正方形が水平に回り、ビットマップはそのかたちに合わせて塗られる。しかしよく見ると、回るときにテクスチャの真ん中当たりが上下に歪み、ペンギンが2頭身になったり4頭身になったりする図2上段⁠⁠。

図2 三角形に分割したテクスチャが回転でゆがむ
図2 三角形に分割したテクスチャが回転でゆがむ 図2 三角形に分割したテクスチャが回転でゆがむ 図2 三角形に分割したテクスチャが回転でゆがむ 図2 三角形に分割したテクスチャが回転でゆがむ 図2 三角形に分割したテクスチャが回転でゆがむ 図2 三角形に分割したテクスチャが回転でゆがむ

前掲スクリプト1の関数xDraw()内に、コメントアウトが1行ある。この確認用のGraphics.lineStyle()メソッドを有効にすると、分けた三角形の外枠が描かれ、どのように画像がゆがむのか少し見やすくなる図2下段⁠⁠。

myGraphics.lineStyle(1, 0x0000FF);   // 確認用

とくに画像中程の水平線に注目すると、三角形に分けた対角線のところが、上下にゆがんでいるとわかる。ふたつの三角形の変形に食い違いがあるようだ。ゆがみをさらにはっきりと見るため、使うビットマップを格子状の線が引かれた正方形に変えてみる。

正方形を回してゆがみを確かめると、線のつながりは保たれているものの、正方形のます目が平行四辺形になっている図3上段⁠⁠。これはつまり、テクスチャに遠近法が適用されていないことを示す。いわゆるパースペクティブがかかれば、ます目は奥に向かって狭まった台形にならなければいけないはずだ図3下段⁠⁠。

図3 テクスチャに遠近法が適用されていないと三角形の継ぎ目がゆがむ
テクスチャに遠近法の適用なし

図3 テクスチャに遠近法が適用されていないと三角形の継ぎ目がゆがむ 図3 テクスチャに遠近法が適用されていないと三角形の継ぎ目がゆがむ 図3 テクスチャに遠近法が適用されていないと三角形の継ぎ目がゆがむ
テクスチャに遠近法を適用
図3 テクスチャに遠近法が適用されていないと三角形の継ぎ目がゆがむ 図3 テクスチャに遠近法が適用されていないと三角形の継ぎ目がゆがむ 図3 テクスチャに遠近法が適用されていないと三角形の継ぎ目がゆがむ

前掲スクリプト1は、正方形の頂点座標には遠近法を適用して透視投影した。しかし、テクスチャは単に三角形の頂点に合わせて変形しただけで、遠近法つまり奥行きを考えていない。Graphics.drawTriangles()メソッドの第3引数には、uv座標に奥行きのt軸を加えたuvt座標が渡せる。そうすれば、テクスチャにも遠近法が適用される。次回はこのuvt座標について解説する。

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

おすすめ記事

記事・ニュース一覧