前回 のあらすじは、3次元空間座標のVector3Dインスタンスで正方形の4頂点をつくり、y軸で水平に回してみたのだった。ところが、パースペクティブがかかっていないため、ワイヤーフレームは水平に伸び縮みする四角形にしか見えなかった(第42回図7 再掲) 。2次元平面の座標に引き写すとき、どのように遠近法の投影(透視投影)を加えればよいか。これが今回のお題だ。
第42回図7 回転するワイヤーフレームの四角形にパースペクティブがかかっていない(再掲)
遠近法と焦点距離
第33回「遠近法の投影 」の「[ プロパティ]インスペクタを使った3次元空間の操作」では、「 視野角」( 「 画角」 )をご紹介した。[ プロパティ]インスペクタの[3D位置とビュー]セクションでは[遠近の角度]というプロパティとして示され、1から179までの数値で定める。値が大きいほど、遠近の差は広がって見えた(第33回図3 再掲) 。
第33回図3 [ プロパティ]インスペクタの[3D位置とビュー]セクションにおける[遠近の角度]( 再掲)
視野角に関わる大きさとして「焦点距離」がある。3次元空間の像を投影する2次元のスクリーンと視点との距離で表される(図1 ) 。視野角が狭いと焦点距離は長く、視野角を拡げれば焦点距離は短くなる[1] 。遠近法の投影された座標は、この焦点距離を用いて計算できる。
図1 焦点距離
3次元空間のオブジェクトと2次元平面に投影された像との大きさの比率を求めよう。視点を頂点とし、底辺をそれぞれ投影像ともとのオブジェクトとするふたつの三角形は相似だ。したがって、つぎの比例式が導かれる。
投影像の大きさ/オブジェクトの大きさ = 焦点距離 / (焦点距離 + z位置)
投影像の大きさ = オブジェクトの大きさ×焦点距離 / (焦点距離 + z位置)
この比例式は、オブジェクトの大きさだけでなく、3次元空間の座標すべてに対して成立つ。たとえば、横断歩道のように、同じ幅の水平線が等間隔で前方に並んでいたとする。これを2次元平面に投影すると、同じ幅の線つまり2点間の水平距離は、遠くなるほど短くなる。また、線と線との間隔も遠くにいくほど詰まって見える(図2 ) 。
図2 同じ幅の水平線が奥に向かって等間隔に並ぶ透視投影図
このように3次元空間の座標を2次元平面のxy座標に変換するとき、上記の比例式で求められた遠近法投影(透視投影)の比率を用いればよい。
3次元空間の座標を2次元平面に透視投影する比率
焦点距離 / (焦点距離 + z位置)
図3 視野角と焦点距離
焦点距離にもとづく透視投影の計算
3次元空間座標を2次元平面に透視投影するには、まず焦点距離を定めなければならない。もちろん、その値は自由に決められる。とはいえ、何か目安がほしい。視野角([ 遠近の角度] )と異なり、焦点距離は[プロパティ]インスペクタには見当たらない(前掲第33回図3 参照) 。しかし、PerspectiveProjectionクラスのプロパティPerspectiveProjection.focalLength として、備わっている。
前述第33回「遠近法の投影」の「PerspectiveProjectionクラスで遠近法を操作する 」でご説明したとおり、PerspectiveProjectionオブジェクトはDisplayObject.transformプロパティの参照からTransform.perspectiveProjectionプロパティにより得られる。PerspectiveProjection.focalLengthプロパティの値は、視野角(PerspectiveProjection.fieldOfViewプロパティ )とステージの幅(Stage.stageWidthプロパティ )に応じて変わる(図4 ) 。
図4 PerspectiveProjection.focalLengthプロパティで焦点距離を調べる
それでは、前回第42回のスクリプト3 「3次元空間の頂点座標にもとづいて2次元平面にワイヤーフレームを描く」に、2次元平面への透視投影を加えてみよう。具体的には、Vector3Dオブジェクトの座標を2次元平面のPointインスタンスに換えて、Vectorオブジェクトのエレメントに納めて返す関数xGetVertices2D()を書き直す。
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];
// 透視投影の処理を加える
vertices2D.push(new Point(myVector3D.x, myVector3D.y));
}
return vertices2D;
}
焦点距離は、デフォルトのPerspectiveProjection.focalLengthプロパティの値を使おう。この値は変数(nFocalLength)にとっておく。すると、関数xGetVertices2D()は、焦点距離とVector3Dオブジェクトのz座標値(Vector3D.zプロパティ)から透視投影の比率(前掲「3次元空間の座標を2次元平面に透視投影する比率」 )を求め(変数nProjectionRatio) 、xy座標に乗じればよい。関数xGetVertices2D()につぎのような手を加えれば、3次元空間座標が2次元平面に透視投影できる(図5 ) 。
var nFocalLength:Number = transform.perspectiveProjection.focalLength ; // 追加
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];
var nProjectionRatio:Number =
nFocalLength / (nFocalLength + myVector3D.z) ; // 追加
vertices2D.push(new Point(myVector3D.x * nProjectionRatio,
myVector3D.y * nProjectionRatio)); // 修正
}
return vertices2D;
}
図5 デフォルトの焦点距離にもとづいて3次元空間座標を2次元平面に透視投影する
この関数の書替えで、ことさら困ることはない。透視投影は正しく行われる。ただ、Vector3Dクラスをもう少し紹介するために、Vector3Dクラスのプロパティとメソッドを使って同じ処理を書いてみたい。
Vector3D.wプロパティとVector3D.project()メソッドで透視投影を行う
Vector3Dクラスには3次元座標空間における位置ベクトル(x, y, z)の成分となるVector3D.xやVector3D.y、Vector3D.zプロパティに加えて、4つめの成分としてVector3D.w がある[2] 。このプロパティは、座標値ではなくオプションとして用いる。Vector3D.wプロパティに透視投影の比率を与えると、Vector3D.project()メソッド の呼出しにより、3次元空間座標に透視投影の計算が加えられる。
Vector3Dオブジェクト.project()
このメソッドを使うときには、気をつけるべきことがふたつある。第1は、3次元座標(Vector3D.x/Vector3D.y/Vector3D.zプロパティ)値が、Vector3D.wプロパティの値で割り算されることだ。よって、前掲「3次元空間の座標を2次元平面に透視投影する比率」は、分子分母を入替えて(逆数にして)Vector3D.wプロパティに設定する必要がある。
第2に、Vector3D.project()メソッドは、参照するVector3Dオブジェクトの3次元座標値をそのまま書替える。したがって、もとの3次元空間座標を残しておくには、Vector3Dオブジェクトは複製したうえで操作しなければならない。Vector3Dオブジェクトを複製するには、Vector3D.clone()メソッドを用いる。
このVector3D.project()メソッドとVector3D.wプロパティを使って透視投影を行ったのが、以下のスクリプト1の関数xGetVertices2D()だ(図6 ) 。
図6 Vector3D.project()メソッドとVector3D.wプロパティで透視投影を行う
スクリプト1 3次元空間の頂点座標から2次元平面に透視投影したワイヤーフレームを描く
// フレームアクション
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;
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));
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 ();
// Vector3D.w プロパティ = (焦点距離 + z位置) / 焦点距離
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);
}
}
[ムービープレビュー]を確かめると、回るワイヤーフレームの四角形はパースペクティブがかかって描かれる。次回は面の数を増やしてみよう。
図7 回転するワイヤーフレームの四角形にパースペクティブがかかった
今回解説した次のサンプルファイルがダウンロードできます。