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); }