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

第48回テクスチャに遠近法を適用する - uvt座標の指定

前回の第47回「3次元空間の回転する正方形を2次元平面に透視投影してテクスチャマッピング」スクリプト1した。ところが、テクスチャに遠近法が適用されていないため、分けた三角形の継ぎ目がゆがんでしまった(第47回図3再掲⁠⁠。これはGraphics.drawTriangles()メソッドに渡す第3引数のuv座標が、2次元で奥行きの座標を含まないからだ。実は、第3引数には奥行きのt軸を加えたuvt座標が渡せる。今回は、このuvt座標により、遠近法を適用したテクスチャマッピングに挑戦したい。

第47回図3 テクスチャに遠近法が適用されていないと三角形の継ぎ目がゆがむ(再掲)
画像 画像 画像
テクスチャに遠近法の適用なし
画像 画像 画像
テクスチャに遠近法を適用

Utils3D.projectVectors()メソッドの仕組み

前回の「3次元空間の回転する正方形を2次元平面に透視投影してテクスチャマッピング」するスクリプト1に手を加えていく。uvt座標のt値は、3次元頂点座標のz座標値から計算式で導ける。けれど、座標をひとつひとつ式で計算しなくてもよい。実は、Utils3D.projectVectors()という優れものの静的メソッドがある。

このメソッドを使えば、3次元空間の頂点座標が納められたVectorオブジェクトから、Graphics.drawTriangles()メソッドに渡す第3引数のt座標値をまとめて求めてくれる。それだけではない。第1引数にする2次元平面に透視投影された座標値のVectorオブジェクトも自動的につくってくれるのだ。

つまり、前回のスクリプト1の3次元空間座標を2次元平面に透視投影する関数(xGetVertices2D())がそっくり要らなくなる。今までの苦労はどうなる、という声が聞こえてきそうだ。しかし、透視投影の考え方を知っておくことは大切だ。それに、Utils3D.projectVectors()メソッドのありがたみも実感できよう。

/* さくっと削除
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;
}
*/

静的メソッドUtils3D.projectVectors()の仕組みが少し変わっている。というのは、メソッドがやることはふたつある。第1に、Graphics.drawTriangles()メソッドの第1引数に渡す、透視投影した2次元座標がエレメントに納められたVectorオブジェクトをつくる。そして第2は、Graphics.drawTriangles()メソッドの第3引数にするuvt座標のt値を計算することだ。

そこでメソッドから値を返そうとすると、戻り値はひとつしか定められない。Graphics.drawTriangles()メソッドの設計者は、大胆にも戻り値はなし(void)とした。その代わり、第1引数と第3引数にするVectorオブジェクトを引数として渡す。すると、Utils3D.projectVectors()メソッドはふたつの引数のVectorオブジェクトを直接書替えてしまうのだ。

Utils3D.projectVectors()メソッドは、つぎのように4つの引数をとる。第1引数が透視投影の変換を行うMatrix3Dオブジェクト、第2引数は変換される3次元座標値が納められたNumberベース型のVectorオブジェクトだ。そして、第3引数が透視投影した2次元座標を納めるNumberベース型のVectorオブジェクトだ。エレメントとなる座標値はすべてメソッドが計算するので、なんとこのVectorオブジェクトは空っぽで構わない。第4引数にはuvt座標のNumberベース型のVectorオブジェクトを渡す。t座標はメソッドが書替えるので、仮のエレメント値として0を入れておけばよい。

Utils3D.projectVectors(投影Matrix3D:Matrix3D, 3次元座標Vector, 2次元座標Vector, uvt座標Vector)

Utils3D.projectVectors()メソッドを使う

前回のスクリプト1にUtils3D.projectVectors()メソッドを組込もう。まずは引数に渡す変数の初期設定に手を入れる。第1に、3次元頂点座標を納めるVectorインスタンス(vertices)は、Utils3D.projectVectors()メソッドの第2引数に合わせて、ベース型をVector3DからNumber型に変える。第2に、uv座標のVectorインスタンス(uvData)にはt値のエレメントを加えた(変数名もuvtDataに変更⁠⁠。前述のとおり、t値はすべて0で構わない。

// var vertices:Vector.<Vector3D> = new Vector.<Vector3D>();
var vertices:Vector.<Number> = new Vector.<Number>();
// ...[中略]...
/*
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));
*/
vertices.push(-nUnit, -nUnit, 0);
vertices.push(nUnit, -nUnit, 0);
vertices.push(nUnit, nUnit, 0);
vertices.push(-nUnit, nUnit, 0);
// ...[中略]...
/*
uvData.push(0, 0);
uvData.push(1, 0);
uvData.push(1, 1);
uvData.push(0, 1);
*/
uvtData.push(0, 0, 0);
uvtData.push(1, 0, 0);
uvtData.push(1, 1, 0);
uvtData.push(0, 1, 0);

つぎに、Utils3D.projectVectors()メソッドを使う流れだ。DisplayObject.enterFrameイベントのリスナー関数(xRotate())からは、前述のとおり3次元空間座標を2次元平面に透視投影する関数(xGetVertices2D())はもはや呼出さない。替わりに、空のNumberベース型Vectorオブジェクト(vertices2D)を座標変換の関数(xTransform())に引数として渡す。そして、このVectorオブジェクトは関数の中で、Utils3D.projectVectors()メソッドの第3引数となる。したがって、メソッドにより透視投影された2次元座標が、Vectorエレメントとして納められるのだ。

var worldMatrix3D:Matrix3D = new Matrix3D();
// ...[中略]...
function xRotate(eventObject:Event):void {
  var nRotationY:Number = mySprite.mouseX * nDeceleration;
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  // xTransform(vertices, nRotationY);
  xTransform(vertices2D, nRotationY);
  // var vertices2D:Vector.<Number> = xGetVertices2D(vertices);
  xDraw(vertices2D);
}
// function xTransform(myVertices:Vector.<Vector3D>, myRotation:Number):void {
function xTransform(vertices2D:Vector.<Number>, myRotation:Number):void {
  // var nLength:uint = myVertices.length;
  // var myMatrix3D:Matrix3D = new Matrix3D();
  // myMatrix3D.prependRotation(myRotation, Vector3D.Y_AXIS);
  worldMatrix3D.prependRotation(myRotation, Vector3D.Y_AXIS);
  Utils3D.projectVectors(worldMatrix3D, vertices, vertices2D, uvtData);
  /*
  for (var i:int = 0; i<nLength; i++) {
    myVertices[i] = myMatrix3D.transformVector(myVertices[i]);
  }
  */
}

もうひとつ、Matrix3Dオブジェクトを変数(worldMatrix3D)として宣言した。これは前回のスクリプト1とは考え方が変わったためだ。前回のスクリプトでは、回転の変換を3次元空間座標に加えてVectorオブジェクト(Vector3Dベース型)の変数(vertices)に納めた。しかし、今回3次元空間の頂点座標は動かさない。変換のMatrix3Dオブジェクトに回転を加えて、その結果のオブジェクトを変数(worldMatrix3D)に残す。つまり、座標ではなく変換行列で座標変換を管理するのだ。

これでスクリプトにUtils3D.projectVectors()メソッドが組込めた。フレームアクション全体は、以下のスクリプト1のとおりだ図1上段⁠。しかし、まだタイトルに「暫定」のふた文字がある。理由は[ムービープレビュー]を試せばわかる。回る正方形にパースペクティブがかかっていない図1中下段⁠。これは、Utils3D.projectVectors()メソッドの第1引数として渡したMatrix3Dオブジェクトに透視投影が加えられていないためだ。

スクリプト1 3次元空間の回転する正方形を2次元平面に透視投影してテクスチャマッピング(暫定2)
// フレームアクション
var nUnit:Number = 100 / 2;
var mySprite:Sprite = new Sprite();
var myTexture:BitmapData = new Image();
var vertices:Vector.<Number> = new Vector.<Number>();
var indices:Vector.<int> = new Vector.<int>();
var uvtData:Vector.<Number> = new Vector.<Number>();
var nDeceleration:Number = 0.3;
var myGraphics:Graphics = mySprite.graphics;
var nFocalLength:Number = transform.perspectiveProjection.focalLength;
var worldMatrix3D:Matrix3D = new Matrix3D();
mySprite.x = stage.stageWidth / 2;
mySprite.y = stage.stageHeight / 2;
vertices.push(-nUnit, -nUnit, 0);
vertices.push(nUnit, -nUnit, 0);
vertices.push(nUnit, nUnit, 0);
vertices.push(-nUnit, nUnit, 0);
indices.push(0, 1, 3);
indices.push(1, 2, 3);
uvtData.push(0, 0, 0);
uvtData.push(1, 0, 0);
uvtData.push(1, 1, 0);
uvtData.push(0, 1, 0);
addChild(mySprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xRotate(eventObject:Event):void {
  var nRotationY:Number = mySprite.mouseX * nDeceleration;
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  xTransform(vertices2D, nRotationY);
  xDraw(vertices2D);
}
function xTransform(vertices2D:Vector.<Number>, myRotation:Number):void {
  worldMatrix3D.prependRotation(myRotation, Vector3D.Y_AXIS);
  Utils3D.projectVectors(worldMatrix3D, vertices, vertices2D, uvtData);
}
function xDraw(vertices2D:Vector.<Number>):void {
  myGraphics.clear();
  myGraphics.beginBitmapFill(myTexture);
  myGraphics.drawTriangles(vertices2D, indices, uvtData);
  myGraphics.endFill();
}
図1 Utils3D.projectVectors()メソッドによる変換に遠近法が投影されていない
図1 Utils3D.projectVectors()メソッドによる変換に遠近法が投影されていない
画像 画像 画像 画像

Matrix3Dオブジェクトに透視投影の変換を加える

Utils3D.projectVectors()メソッドの第1引数として渡したMatrix3Dオブジェクト(worldMatrix3D)には、Matrix3D.prependRotation()メソッドで回転は加えた。しかし、透視投影は行っていなかった。実際、スクリプト1をよく見直してみると、焦点距離の数値(nFocalLength)がどこにも使われていない。

それでは、Matrix3Dオブジェクトにどうやって透視投影を加えればよいか。今回はPerspectiveProjection.toMatrix3D()メソッドを使うことにしよう。タイムラインに置いたDisplayObjectインスタンスには、そのタイムラインのPerspectiveProjectionオブジェクトにより遠近法が適用された(第33回「遠近法の投影」PerspectiveProjectionクラスで遠近法を操作する参照⁠⁠。PerspectiveProjection.toMatrix3D()メソッドは、PerspectiveProjectionオブジェクトがもつ遠近法の設定をMatrix3Dオブジェクトにして返してくれる。

ただし、PerspectiveProjection.toMatrix3D()メソッドから得たMatrix3Dオブジェクトの遠近法の変換は、いわばカメラのレンズを決めただけだ。3次元空間座標を2次元平面に透視投影するには、カメラと座標空間との距離を定めなければならない。これは焦点距離を基本にするとよい。

具体的には、前掲スクリプト1に透視投影のMatrix3Dオブジェクト(viewMatrix3D)をつぎのように加える。タイムラインのPerspectiveProjectionオブジェクト(myPerspective)からMatrix3DオブジェクトをPerspectiveProjection.toMatrix3D()メソッドにより得たうえで、奥行きを焦点距離の分平行移動する。この移動の変換は、Matrix3D.prependTranslation()メソッドにより透視投影の前に加える。カメラからの距離を先に決めなければ、透視投影できないからだ。

// var nFocalLength:Number = transform.perspectiveProjection.focalLength;
var myPerspective:PerspectiveProjection = transform.perspectiveProjection;
// ...[中略]...
// 透視投影のMatrix3Dオブジェクトを得る
var viewMatrix3D:Matrix3D = myPerspective.toMatrix3D();
// 焦点距離分奥に平行移動
viewMatrix3D.prependTranslation(0, 0, myPerspective.focalLength); 

そして、Utils3D.projectVectors()メソッドの第1引数に渡すMatrix3Dオブジェクト(worldMatrix3D)には、この透視投影のMatrix3Dオブジェクト(viewMatrix3D)Matrix3D.append()メソッドで乗じる。ただし、もととなる3次元空間座標の変換結果は、そのままMatrix3Dオブジェクトとして残しておかなければならない。つまり、このオブジェクトを直接2次元平面に透視投影してはだめだ。そのため、Matrix3D.clone()メソッドで複製したオブジェクト(myMatrix3D)に変換を加える。

function xTransform(vertices2D:Vector.<Number>, myRotation:Number):void {
  worldMatrix3D.prependRotation(myRotation, Vector3D.Y_AXIS);
  var myMatrix3D:Matrix3D = worldMatrix3D.clone();
  myMatrix3D.append(viewMatrix3D);   // 透視投影の変換
  // Utils3D.projectVectors(worldMatrix3D, vertices, vertices2D, uvtData);
  Utils3D.projectVectors(myMatrix3D, vertices, vertices2D, uvtData);
} 

2回にわたって取組んだお題がようやく整った。これでマウスポインタの水平座標に応じて水平に回した正方形の3次元空間の頂点座標を、2次元平面に透視投影したうえでテクスチャがマッピングされる図2⁠。でき上がったフレームアクション全体を、スクリプト2に掲げる。

スクリプト2 3次元空間の回転する正方形を2次元平面に透視投影してテクスチャマッピング(完成)
// フレームアクション
var nUnit:Number = 100 / 2;
var mySprite:Sprite = new Sprite();
var myTexture:BitmapData = new Image();
var vertices:Vector.<Number> = new Vector.<Number>();
var indices:Vector.<int> = new Vector.<int>();
var uvtData:Vector.<Number> = new Vector.<Number>();
var nDeceleration:Number = 0.3;
var myGraphics:Graphics = mySprite.graphics;
var myPerspective:PerspectiveProjection = transform.perspectiveProjection;
var worldMatrix3D:Matrix3D = new Matrix3D();
var viewMatrix3D:Matrix3D = myPerspective.toMatrix3D();
viewMatrix3D.prependTranslation(0, 0, myPerspective.focalLength);
mySprite.x = stage.stageWidth / 2;
mySprite.y = stage.stageHeight / 2;
vertices.push(-nUnit, -nUnit, 0);
vertices.push(nUnit, -nUnit, 0);
vertices.push(nUnit, nUnit, 0);
vertices.push(-nUnit, nUnit, 0);
indices.push(0, 1, 3);
indices.push(1, 2, 3);
uvtData.push(0, 0, 0);
uvtData.push(1, 0, 0);
uvtData.push(1, 1, 0);
uvtData.push(0, 1, 0);
addChild(mySprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xRotate(eventObject:Event):void {
  var nRotationY:Number = mySprite.mouseX * nDeceleration;
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  xTransform(vertices2D, nRotationY);
  xDraw(vertices2D);
}
function xTransform(vertices2D:Vector.<Number>, myRotation:Number):void {
  worldMatrix3D.prependRotation(myRotation, Vector3D.Y_AXIS);
  var myMatrix3D:Matrix3D = worldMatrix3D.clone();
  myMatrix3D.append(viewMatrix3D);
  Utils3D.projectVectors(myMatrix3D, vertices, vertices2D, uvtData);
}
function xDraw(vertices2D:Vector.<Number>):void {
  myGraphics.clear();
  myGraphics.beginBitmapFill(myTexture);
  myGraphics.drawTriangles(vertices2D, indices, uvtData);
  myGraphics.endFill();
}
図2 回転のMatrix3Dオブジェクトに透視(遠近法)投影の変換を加える
図2 回転のMatrix3Dオブジェクトに透視(遠近法)投影の変換を加える
画像 画像 画像 画像

今回書いたスクリプトで、3次元空間の頂点座標はひとつの平面上でなければいけない、というつくりにはしてない。そこで次回は、立方体の頂点座標をテクスチャでくるんでみよう。

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

おすすめ記事

記事・ニュース一覧