AS3.0 で 3D プログラミングを1から勉強する (5) - テクスチャを張る

面を塗ることができたので、面に画像を貼り付けるのも簡単。画像を6つ用意して、立方体の上に貼り付けてみることにする。
といっても前回までとほとんど変わらなくて、各頂点の 2D 上の座標を求めてから、画像を歪めて描画するだけでよい。
問題は「どうやって画像を歪ませるか」という一点のみ。
ActionScript 3 で画像を歪ませる方法
ここからは完全に AS3 に限定したノウハウになる。
答えは Graphics.beginBitmapFill() メソッドにある。
public function beginBitmapFill(
bitmap:BitmapData, // 表示するビットマップ
matrix:Matrix = null, // 変形方法を Matrix で指定
repeat:Boolean = true, // リピートするか
smooth:Boolean = false // スムース化するか
):void
第二引数の Matrix をうまく渡してやれば、望みの通りの形に歪ませて描画できる。
計算方法がちょっと複雑なので、既成のライブラリを使うのも手だろう。いずれも内部では beginBitmapFill() を利用している。
特に、前者の 四角形の自由変形 では、Matrix を生成する仕組みが詳しく解説されている。図つきなのでとても分かりやすい。敬意を表して、このページで紹介されている TransformUtil クラスを活用してみることにする。
ちょっと重めなので、クリックしたらスタートするようにした。やってることの大半は前回とほとんど同じ。前回は lineTo() で描画していた部分を、TransformUtil クラスを使って描画するようにしたぐらい。最後にソースコードを掲載しておくので興味ある人はご覧あれ。
Flash Player 10 では
現在β版が公開されている Flash Player 10 では、3D 表示に便利なクラスやメソッドが追加されている。テクスチャに関しては、Graphics.drawTriangles() メソッドが便利そうだ。
以下のブログが先陣を切って調査している。
- [FlashPlayer10]drawTrianglesで球面にテクスチャリング
- unbland.net blog - [Flash Player 10 #2] 3D のプリミティブにテクスチャを適用する。
自前でがんばる Flash Player 9 に比べて、Flash Player 本体に定義されているメソッドを利用できる分、かなりの高速化が実現できるようだ。
まとめ
テクスチャを張る方法を紹介した。今回は立方体に画像を貼り付ける方法を紹介した。球面のようななめらかな面の場合は、球の表面をいくつもの三角形に分解して同じ方法を適用する。原理は全く同じ。
ソースコードは以下に(198行):
package {
import flash.display.*;
import flash.events.Event;
import flash.utils.Dictionary;
import flash.utils.setInterval;
import flash.text.TextField;
import five3D.geom.Matrix3D;
import five3D.geom.Point3D;
[SWF(backgroundColor="0x000000")]
public class Study3d5 extends Sprite{
private var canvas:Sprite;
private var cubes:Array;
private var rad:Number;
public function Study3d5(){
stage.scaleMode = "noScale";
stage.align = "TL";
cubes = [];
cubes.push(new Cube(0, 0, 0, 120));
canvas = new Sprite();
addChild(canvas);
canvas.x = 200;
canvas.y = 150;
var textField:TextField = new TextField();
textField.textColor = 0xffffff;
textField.text = "click to start";
addChild(textField);
rad = 0;
var f:Boolean = true;
stage.addEventListener("click", function(event:Event):void{
if(f){
textField.text = "click to stop";
addEventListener("enterFrame", changeHandler);
}else{
textField.text = "click to start";
addChild(textField);
removeEventListener("enterFrame", changeHandler);
}
f = !f;
});
}
private function changeHandler(event:Object):void {
canvas.graphics.clear();
// 回転行列を作成
var matrix:Matrix3D = new Matrix3D();
matrix.rotateX(Math.PI / 6);
matrix.rotateY(rad / 180 * Math.PI * 3);
matrix.rotateZ(rad / 180 * Math.PI);
// それぞれの立方体の中心のZ座標を取得する
var dic:Dictionary = new Dictionary();
for each(var c:Cube in cubes){
var center:Point3D = matrix.transformPoint(c.center);
dic[c] = center.z;
}
// Zソート (奥のものから順番に並べる)
cubes.sort(function(a:Cube, b:Cube):Number {
return dic[b] - dic[a];
});
// 奥から描画
for each(c in cubes){
c.draw(canvas.graphics, matrix, 200);
}
// 角度更新
rad = (rad + 1) % 360;
}
}
}
import flash.display.Graphics;
import flash.geom.Point;
import flash.utils.Dictionary;
import five3D.geom.Point3D;
import five3D.geom.Matrix3D;
class Cube {
[Embed(source="1.jpg")]
private static var Img1:Class;
[Embed(source="2.jpg")]
private static var Img2:Class;
[Embed(source="3.jpg")]
private static var Img3:Class;
[Embed(source="4.jpg")]
private static var Img4:Class;
[Embed(source="5.jpg")]
private static var Img5:Class;
[Embed(source="6.jpg")]
private static var Img6:Class;
private var images:Array = [];
private var points:Array = [];
private var _center:Point3D;
public function get center():Point3D {
return _center;
}
public function Cube(x:Number, y:Number, z:Number, len:Number){
_center = new Point3D(x, y, z);
images.push(new Img1());
images.push(new Img2());
images.push(new Img3());
images.push(new Img4());
images.push(new Img5());
images.push(new Img6());
var diff:Function = function(f:Boolean):Number{return f ? len / 2 : -len / 2;};
// 立方体の頂点8つを作成する
for(var i:int = 0; i < 8; i++){
var p:Point3D = new Point3D(x + diff(i % 4 % 3 == 0), y + diff(i % 4 < 2), z + diff(i < 4));
points.push(p);
}
}
public function draw(g:Graphics, matrix:Matrix3D, f:Number):void {
// 回転後の座標を計算
var p:Array = [];
for(var i:int = 0; i < points.length; i++){
var pt:Point3D = matrix.transformPoint(points[i]);
p.push(pt);
}
// 面の一覧
var planes:Array = [
[p[0], p[1], p[2], p[3], images[0].bitmapData],
[p[7], p[6], p[5], p[4], images[1].bitmapData],
[p[0], p[4], p[5], p[1], images[2].bitmapData],
[p[1], p[5], p[6], p[2], images[3].bitmapData],
[p[2], p[6], p[7], p[3], images[4].bitmapData],
[p[3], p[7], p[4], p[0], images[5].bitmapData]
];
// 面の中心のZ座標を求める
var z:Dictionary = new Dictionary();
for(i = 0; i < planes.length; i++){
z[planes[i]] = (planes[i][0].z + planes[i][1].z + planes[i][2].z + planes[i][3].z) / 4;
}
// Zソート (奥のものから順番に並べる)
planes.sort(function(a:Array, b:Array):Number {
return z[b] - z[a];
});
// 奥から順番に面を描画
var index:int = 0;
for each(var plane:Array in planes){
drawPlane(g, plane[4], plane[0], plane[1], plane[2], plane[3]);
}
}
private function drawPlane(g:Graphics, bmd:BitmapData, p1:Point3D, p2:Point3D, p3:Point3D, p4:Point3D):void {
// 単位法線ベクトル
var v1:Point3D = p2.subtract(p1);
var v2:Point3D = p4.subtract(p1);
var n:Point3D = cross(v1, v2);
n.normalize(1);
// 裏側の面は描画しない
var l:Point3D = new Point3D(0, 0, -1);
var product:Number = n.dot(l);
if(product < 0){
return;
}
// 透視投影しつつ2次元座標に変換する
var p:Point3D;
var pp1:Point, pp2:Point, pp3:Point, pp4:Point;
p = p1.clone(); p.project(p.getPerspective(500)); pp1 = new Point(p.x, p.y);
p = p2.clone(); p.project(p.getPerspective(500)); pp2 = new Point(p.x, p.y);
p = p3.clone(); p.project(p.getPerspective(500)); pp3 = new Point(p.x, p.y);
p = p4.clone(); p.project(p.getPerspective(500)); pp4 = new Point(p.x, p.y);
// 変形してビットマップを表示
TransformUtil.drawBitmapQuadrangle(g, bmd,
new Point(0, 0), new Point(100, 0), new Point(0, 100), new Point(100, 100),
pp1, pp2, pp4, pp3);
}
}
// 外積
function cross(p1:Point3D, p2:Point3D):Point3D {
return new Point3D(p1.y * p2.z - p1.z * p2.y,
p1.z * p2.x - p1.x * p2.z,
p1.x * p2.y - p1.y * p2.x);
}