ActionScript 3.0をつかって疑似3D表示

ActionScript 2.0から3.0になって大幅に仕様が変更されました。これは、バージョンアップというよりむしろ別言語と行ったほうが正しいでしょう。

 ActionScript 3.0はできて間もない言語です。そのためか、すごい勢いで新たな機能が追加されています。実は3Dの機能もも最近追加されたのですが、ActionScript 3.0はマイナーバージョンアップによる仕様変更が著しいのです。
今まで動いていたプログラムがマイナーアップで動かなくなるといったことは非常に苦しいのです。
そのせいか私はこの新機能を容易に使い始めることができませんでした。

 じゃあ、基本的な機能だけを使って3Dを自分で実装すればいいじゃないか!!

ActionScript 3.0導入

 http://clockmaker.jp/blog/2009/07/tutorial-install-flashdevelop-flex-sdk/
↑このサイトにActionScript 3.0を無料で使う方法が乗っているので省略します。

ActionScript 3.0で3Dの面

 面を一つのクラスにすることを考える。まず、「面」を表すためには3次元上の点4つが必要である。
したがってVector3D型の変数を4つprivateに持つ、また、カメラのデータも同時に入れる。
そして、クラス内でカメラからその四つの点を見た時にどのように見えるのかを計算して、計算した結果を二次元の表示部分に出力する。このとき注意するのはカメラによって3次元を2次元に移すときには直線は直線のままだということです。つまり、四角形ならば頂点の見え方が変わっても、辺が曲線になったりはしないということです。

まず最も簡単な方法は平行投影といわれる軸を3次元上の点をそのまま平面に射影するものだ。


ただ、この方法だと全く奥行きが出ないので奥行きの出る方法である透視投影変換というものを使う。これは物体のz座標(奥行き)に反比例するようなx座表とy座標をとる。
具体的には
   x_2D = x_3D/z_3D*size*fov;
   y_2D = y_3D/z_3D*size*fov;
の式に従って、3次元上の点を二次元上の点に移します。このときsizeは画面サイズをいれ、fovには基準面の大きさ(視野角から求める)を入れる。
(透視投影変換ではさらに一般化するためにカメラの向きなどまで考慮に入れることがあるが、今回は表示するだけなので原点にカメラを持ってきて、さらにz軸正の方向を向いていると仮定した。)

package {
    import flash.display.*;
    import flash.geom.Matrix3D;
    import flash.geom.Point;
    import flash.geom.Vector3D;
    import flash.geom.Matrix;

    
    public class plane3D extends Sprite{
        private var p1:Vector3D = new Vector3D(-20, 10,   50,1);
        private var p2:Vector3D = new Vector3D( 20, 10,   50,1);
        private var p3:Vector3D = new Vector3D(-20, 10,  100,1);
        private var p4:Vector3D = new Vector3D( 20, 10,  100,1);
        //このworld全体にかかる行列
        private var TransThisWorld:Matrix3D = new Matrix3D;
        //描画領域
        private var canvas:Sprite = new Sprite();
        //テクスチャのマッピング用
        private var tex:mapping = new mapping;
        //カメラの情報
        public var angle:Number = 30;
        public var Width:Number  = 150;
        public var Height:Number  = 150;
        //カメラの情報から計算される値
        public var size:Number = 0;
        public var fov:Number  = 60;
        //設定
        public var pointEnable:int = 3;   //点を描画する時の半径
        public var lineEnable:int = 3;  //線を描画するときの太さ+
        
        public function plane3D(_x:Number=0,_y:Number=0,_z:Number=500) {
            
            addChild(canvas);
            canvas.x = 150;
            canvas.y = 150;
            
            size=((Width > Height) ? Width : Height) * 0.5;//視野の大きいほうの辺
            fov = 1 / Math.tan(angle * 0.5 * Math.PI / 180);//size*fov=スクリーンまでの距離
            
            z = (p1.z + p2.z + p3.z + p4.z) / 4;
            setPoints(p1,p2,p3,p4);
            
            changeHandler(null);
            addEventListener("enterFrame", changeHandler);
            
            TransThisWorld.appendTranslation(_x, _y, _z);
        }
        public function setPoints(np1:Vector3D, np2:Vector3D, np3:Vector3D, np4:Vector3D):void {
            
            p1=TransThisWorld.transformVector(np1);
            p2=TransThisWorld.transformVector(np2);
            p3=TransThisWorld.transformVector(np3);
            p4=TransThisWorld.transformVector(np4);
        }
        //イベントを用いて描画する用
        //changeHandler(null);addEventListener("enterFrame", changeHandler);
        public function changeHandler(event:Object):void {
            var pp1:Vector3D = rotate(p1);
            var pp2:Vector3D = rotate(p2);
            var pp3:Vector3D = rotate(p3);
            var pp4:Vector3D = rotate(p4);

            canvas.graphics.clear();
            if(pointEnable>0){
                drawPoint(pp1); drawPoint(pp2);
                drawPoint(pp3); drawPoint(pp4);
            }
            if(lineEnable>0){
                drawLine(pp1, pp2); drawLine(pp1, pp3); drawLine(pp1, pp4);
                drawLine(pp2, pp3); drawLine(pp2, pp4); drawLine(pp3, pp4);
            }
        }
        //親から呼び出されて描画する
        public function Render():void {
            var pp1:Vector3D = rotate(p1);
            var pp2:Vector3D = rotate(p2);
            var pp3:Vector3D = rotate(p3);
            var pp4:Vector3D = rotate(p4);

            canvas.graphics.clear();
            if(pointEnable>0){
                drawPoint(pp1); drawPoint(pp2);
                drawPoint(pp3); drawPoint(pp4);
            }
            if(lineEnable>0){
                drawLine(pp1, pp2); drawLine(pp1, pp3); drawLine(pp1, pp4);
                drawLine(pp2, pp3); drawLine(pp2, pp4); drawLine(pp3, pp4);
            }
        }
        //全体の変形、移動
        public function Trans(M:Matrix3D):void {
            var temp:Matrix3D = TransThisWorld.clone();
            temp.invert();
            M.prepend(temp);
            M.append(TransThisWorld);
            p1=M.transformVector(p1);
            p2=M.transformVector(p2);
            p3=M.transformVector(p3);
            p4 = M.transformVector(p4);
            tex.motify(projection(p1), projection(p2),
            projection(p3), projection(p4));
            z = (p1.z + p2.z + p3.z + p4.z) / 4;
            
        }
        
        private function drawPoint(p:Vector3D):void {
            var p2D:Point;
            p2D = projection(p);
            canvas.graphics.beginFill(0xffffff);
            canvas.graphics.drawCircle(p2D.x, p2D.y, 5);
            canvas.graphics.endFill();
        }

        private function drawLine(p1:Vector3D, p2:Vector3D):void {
            var p12D:Point;
            p12D = projection(p1);
            var p22D:Point;
            p22D = projection(p2);
            canvas.graphics.lineStyle(3, 0xffffff);
            canvas.graphics.moveTo(p12D.x, p12D.y);
            canvas.graphics.lineTo(p22D.x, p22D.y);
            canvas.graphics.lineStyle();
        }
        
        private function projection(p:Vector3D) :Point{
            var ret:Point = new Point();
            ret.x = p.x/p.z*size*fov;
            ret.y = p.y/p.z*size*fov;
            return ret;
        }
        
        
        private function rotate(_p:Vector3D):Vector3D {
            var ret:Vector3D = new Vector3D(_p.x, _p.y, _p.z,1);
            var p:Point;
            return ret;
        }
        public function setTexture(name:String = "RED"):void {
            tex.pictureName = name;
            tex.motify(projection(p1), projection(p2),
            projection(p3), projection(p4));
            canvas.addChild(tex);
        }
        
    }
}

マッピング

このままでは3Dとはいってもワイヤーフレーム(つまり骨格)のみになってしまいます。そこで、これらの面に画像を張り付けることを考えます。しかし、ここで困ったことにFlashには画像を変形できるような仕組みがかなり限られていて、射影変換と呼ばれる変換を行うメソッドは用意されていないようでした。

そこで、疑似的に射影変換をする方法をとります。画像を次のように上下左右4つの3角形に分割して変換するのです。

cut.png

ただ3角形に分割して変換するだけなら、さまざまな分割方法が考えられますが、とりあえずこの分割方法をとることにします。

この分割して変形するという作業を自動的に行ってくれるメソッドが用意されています、それがdrawTriangles()です。
Sprite.graphics.drawTriangles(vertices, indices, uvtData);
vertices:Vector.<Number> 分割するための頂点を指定
indices:Vector.<Int>
頂点の何番目をつないで3角形を作るか、したがって3の倍数のサイズになるはず。verticesでの配列のインデックスで指定
uvtData:Vector.<Number>  それぞれの頂点がどのUV座標に対応するか。UV座標とは画像の左上を( 0.0 , 0.0 )右下を( 1.0 , 1.0 )としたときの画像上の座標。

これを使って画像を変形させます。

package {
    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.display.Loader;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.geom.Point;
    /**
     * ...
     * @author naegawa
     */
    public class  mapping_test extends Sprite
    {
        public var vertices:Vector.<Number> = new Vector.<Number>;
        public var indices:Vector.<int> = new Vector.<int>;
        public var uvtData:Vector.<Number> = new Vector.<Number>;
        public var bitmap:Bitmap = new Bitmap;
        [Embed(source='cut.png')]
        private var IMG_PICT:Class;
        
        private var p1:Point = new Point(0, 0);
        private var p2:Point = new Point(100, 0);
        private var p3:Point = new Point(0, 100);
        private var p4:Point = new Point(130, 130);
        
        private var pc:Point = new Point;
        public function mapping_test(){
            
            divide(p1, p2, p3, p4);
            vertices.push(p1.x, p1.y);
            vertices.push(p2.x, p2.y);
            vertices.push(pc.x, pc.y);
            vertices.push(p3.x, p3.y);
            vertices.push(p4.x, p4.y);
            
            indices.push(0, 1, 2);
            indices.push(1, 2, 4);
            indices.push(2, 4,  3);
            indices.push(3, 2, 0);
            
            uvtData.push(0, 0);
            uvtData.push(1, 0);
            uvtData.push(0.5, 0.5);
            uvtData.push(0, 1);
            uvtData.push(1, 1);
            
            
            this.addEventListener(Event.ENTER_FRAME, rewrite);
            //this.addChild(tex);
            this.mouseEnabled = false;
        }
        public function rewrite(e:Event):void {
            //if (tex.flag == 1) {
                var bmp:Bitmap = new IMG_PICT;
                this.graphics.clear();
                this.graphics.beginBitmapFill(bmp.bitmapData);
                this.graphics.drawTriangles(vertices, indices, uvtData);
            //}
        }
        public function motify(np1:Point, np2:Point, np3:Point, np4:Point):void {
            
            p1 = np1;
            p2 = np2;
            p3 = np3;
            p4 = np4;
            divide(p1, p2, p3, p4);
            var i:Number=0;
            for (i = 0;i<10;i++){
            vertices.pop();
            }
            vertices.push(np1.x, np1.y);
            vertices.push(np2.x, np2.y);
            vertices.push(pc.x, pc.y);
            vertices.push(np3.x, np3.y);
            vertices.push(np4.x, np4.y);
            
        }
        private function divide(np1:Point, np2:Point, np3:Point, np4:Point):void {
            pc.x = (np1.x + np2.x + np3.x + np4.x) / 4;
            pc.y = (np1.y + np2.y + np3.y + np4.y) / 4;
        }
    }
    
}

このマッピングを使うと下のようなプログラムも組むことができます

以上の二つのクラスをあわせると3D空間上に平面を作り、さらにそこにビットマップ等の絵をマッピングすることができます。


これらのクラスの利用例

改善点・今後の展望

  1. 移動後の分割点の位置をしっかり計算していない。(実際には行列計算を行うとしっかり求まる)
  2. 光の計算等をしてより立体的に見せる。
  3. カメラの移動を可能にする。