forked from: KD-Image City (WIP: Alternativa3D-powered)

by Ludd forked from KD-Image City (WIP: Alternativa3D-powered) (diff: 497)
フラクタルで画像を描画

パクリ元ネタ:
fladdict » コンピューターに絵画を描かせる
http://fladdict.net/blog/2009/05/computer-painting.html

標準偏差:
http://www.cap.or.jp/~toukei/kandokoro/html/14/14_2migi.htm

画像の読み込み処理:
http://wonderfl.kayac.com/code/3fb2258386320fe6d2b0fe17d6861e7da700706a

RGB->HSB変換:
http://d.hatena.ne.jp/flashrod/20060930#1159622027

WIP: Real-time processed image using Alternativa3D's KDContainer to generate a "growing"  *  city.

Since image is used, can consider using perlin noise or other procedural means to 
generate city layout.

I was thinking a Blade runner style city would be nice.
Would be good to add in geometry/splitting animations (buildings pushing in/out) as part 
of the "growing" effect.

W-S-A-D to fly around with mouse drag look.

@author Glidias
♥0 | Line 341 | Modified 2012-10-23 16:11:48 | MIT License
play

ActionScript3 source code

/**
 * Copyright Ludd ( http://wonderfl.net/user/Ludd )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/1LPP
 */

// forked from Glidias's KD-Image City (WIP: Alternativa3D-powered) 
/**
フラクタルで画像を描画

パクリ元ネタ:
fladdict » コンピューターに絵画を描かせる
http://fladdict.net/blog/2009/05/computer-painting.html

標準偏差:
http://www.cap.or.jp/~toukei/kandokoro/html/14/14_2migi.htm

画像の読み込み処理:
http://wonderfl.kayac.com/code/3fb2258386320fe6d2b0fe17d6861e7da700706a

RGB->HSB変換:
http://d.hatena.ne.jp/flashrod/20060930#1159622027

**/
package 
{
    import alternativ7.engine3d.containers.KDContainer;
    import alternativ7.engine3d.controllers.SimpleObjectController;
    import alternativ7.engine3d.core.Debug;
    import alternativ7.engine3d.core.Object3D;
    import alternativ7.engine3d.core.Object3DContainer;
    import alternativ7.engine3d.core.Vertex;
    import alternativ7.engine3d.materials.FillMaterial;
    import alternativ7.engine3d.materials.Material;
    import alternativ7.engine3d.objects.Mesh;
    import alternativ7.engine3d.objects.SkyBox;
    import alternativ7.engine3d.primitives.Box;
    import alternativ7.engine3d.primitives.Plane;
    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.display.Loader;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.KeyboardEvent;
    import flash.events.MouseEvent;
    import flash.net.URLRequest;
    import flash.system.LoaderContext;
    import flash.text.TextField;
    import flash.ui.Keyboard;

    import alternativ7.engine3d.alternativa3d;
    import alternativ7.engine3d.core.Camera3D;
    import alternativ7.engine3d.core.View;
    

    use namespace alternativa3d;

    /**
     * WIP: Real-time processed image using Alternativa3D's KDContainer to generate a "growing"  *  city.
     * 
     * Since image is used, can consider using perlin noise or other procedural means to 
     * generate city layout.
     * 
     * I was thinking a Blade runner style city would be nice.
     * Would be good to add in geometry/splitting animations (buildings pushing in/out) as part 
     * of the "growing" effect.
     
     *  W-S-A-D to fly around with mouse drag look.
     * 
     * @author Glidias
     */
    [SWF(width = "465", height = "465", frameRate = "30", backgroundColor = "#ffffff")]
    
    public class KDTreeImage extends Sprite 
    {
        
        private const IMAGE_URL:String = "http://farm4.static.flickr.com/3639/3538831894_cca4aabd68.jpg";
        //標準偏差の閾値。小さくすると細かくなるけど、小さすぎるとただのモザイクみたくなる。
        private const THRESHOLD:Number = 0.1;

        private var fillRectangleArray:Array;
        private var image:Bitmap;
        private var imageData:BitmapData;
        private var _canvas:Sprite;
        
        private var camera:Camera3D;
        private var kdContainer:KDContainer;
        private var cameraController:SimpleObjectController;
        private var KD_NODE:Class;
        private var _lastKDNode:*;
        private static const WORLD_SCALE:Number = 128;
        private static const INV_255:Number = 1 / 255;
        private static const MAX_HEIGHT:Number = 12000;
        private var rootContainer:Object3DContainer;
        
        private var _tint:Number;

        
        public function KDTreeImage():void 
        {
            if (stage) init();
            else addEventListener(Event.ADDED_TO_STAGE, init);
            
            
            

        }
        
        private function init(e:Event = null):void 
        {
            initA3D(); 
            removeEventListener(Event.ADDED_TO_STAGE, init);
            //画像の読み込み
                        var req:URLRequest = new URLRequest(IMAGE_URL);
                        var loader:Loader = new Loader();
                        loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadComplete);    
                        loader.load( req, new LoaderContext(true));
                       
        }
        
        private function initA3D():void 
        {
            camera = new Camera3D();
            camera.view = new View(stage.stageWidth, stage.stageHeight);
            stage.addEventListener(Event.RESIZE, onStageResize);
            
            addChild(camera.view);
            
            kdContainer = new KDContainer();
            KD_NODE = getKDNodeClass();
            
        
            cameraController = new SimpleObjectController(stage, camera, 800, 8);
            
            rootContainer = new Object3DContainer();
            rootContainer.addChild(camera);
            
            //camera.addToDebug(Debug.BOUNDS, Box);
            camera.addToDebug(Debug.NODES, KDContainer);
            //camera.addToDebug(Debug.BOUNDS, KDContainer);
    
            stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
            
        }
        
        private var _doSpawn:Boolean = true;
        
        private function onKeyDown(e:KeyboardEvent):void 
        {
            if (e.keyCode === Keyboard.TAB) {
                camera.debug = !camera.debug;
            }
            if (e.keyCode === Keyboard.BACKSPACE) {
                _doSpawn = !_doSpawn;
            }
        }
        
        private function getKDNodeClass():Class {
            var dummy:KDContainer = new KDContainer();
            dummy.createTree(new <Object3D>[new Box(8,8,8)]);
            return Object(dummy.root).constructor;
        }
        
        private function onStageResize(e:Event):void 
        {
            camera.view.width = stage.stageWidth;
            camera.view.height = stage.stageHeight;
        }
        
        //画像読み込み後の処理
        public function loadComplete(e:Event = null):void 
        {
            e.target.removeEventListener(Event.COMPLETE, loadComplete);
            
            image = e.target.loader.content as Bitmap;
            imageData = image.bitmapData;

            //キャンバス用スプライト
            _canvas = new Sprite;
            
            var p:RectanglePiece = new RectanglePiece();
            var node:*;
            var threshold:Number = kdContainer.threshold;
            
            p.x0 = 0;
            p.y0 = 0;
            p.x1 = imageData.width;
            p.y1 = imageData.height;
            p.c = 0;
            
            // Setup root starting
            kdContainer.root = setupNode(p);
            kdContainer.boundMinX = 0;
            kdContainer.boundMinY = 0;
            kdContainer.boundMaxX = p.x1 * WORLD_SCALE;
            kdContainer.boundMaxY = p.y1 * WORLD_SCALE;
            kdContainer.boundMinZ = 0;
            kdContainer.boundMaxZ = MAX_HEIGHT;
            
            
            camera.x = kdContainer.boundMaxX * .5;
            camera.y = kdContainer.boundMaxY * .5;
            camera.z = MAX_HEIGHT + 63400;
            cameraController.updateObjectTransform();
            cameraController.lookAtXYZ(camera.x, camera.y, 0);
            
            var skybox:SkyBox = new SkyBox(99999999);
            skybox.setMaterialToAllFaces( new FillMaterial(0xEEFEFF) );
            rootContainer.addChild(skybox);
            
            
            var floor:Plane = new Plane(kdContainer.boundMaxX, kdContainer.boundMaxY,1,1,false,false,false,null, new FillMaterial(0xAAAAAA) );
            floor.clipping = 2;
            rootContainer.addChild(floor);
            floor.x = kdContainer.boundMaxX * .5;
            floor.y = kdContainer.boundMaxY * .5;
            
            rootContainer.addChild(kdContainer);
            
            //フラクタルデータ保持用配列に初期値挿入
            fillRectangleArray = new Array(p);
            
            
            
            addChild(_canvas);
            addEventListener(Event.ENTER_FRAME, onEnterFrame);
        }
        
        private function setupNode(p:RectanglePiece):* {
            var node:*;
            p.node = node =  new KD_NODE();
            node.boundMinX = p.x0 * WORLD_SCALE;
            node.boundMinY = p.y0 * WORLD_SCALE;
            node.boundMinZ = 0;
            node.boundMaxX = p.x1 * WORLD_SCALE;
            node.boundMaxY = p.y1 * WORLD_SCALE;
            node.boundMaxZ = MAX_HEIGHT;
            return node;
        }
        

    
        // todo: optimization and pooling?
        private function createBuilding(p:RectanglePiece, color:uint):void
        {
            ///*
            
            //node.boundMaxZ = 0;
            //*/
            var node:* = p.node;
            var mat:Material = new FillMaterial(color);
            var height:Number = 1 + _tint * MAX_HEIGHT;
            var w:Number = (p.x1 - p.x0) * WORLD_SCALE;
            var h:Number = (p.y1 - p.y0) * WORLD_SCALE;
            var mesh:Mesh = new Box(w, h, height, 1, 1, 1, false, false, mat, mat, mat, mat, mat, mat);        

            ///*
            for (var v:Vertex = mesh.vertexList; v != null; v = v.next) {
                v.x += w * .5 + p.x0 * WORLD_SCALE;
                v.y += h * .5 + p.y0 * WORLD_SCALE;
                v.z += height * .5;
            }
    
            mesh.calculateBounds();
            mesh.calculateFacesNormals();
            //*/
            
            //mesh.x = (w * .5 ) + p.x0 * WORLD_SCALE;
            //mesh.y = (h * .5) + p.y0 * WORLD_SCALE;
            //mesh.z = height * .5;
            
            //kdContainer.addChild(mesh);
            
            //node.boundMaxZ = height;

            //if ( !Bounds3D.getBoundsOf(node).equals2D(Bounds3D.getBoundsOf(mesh))) throw new Error("Not equal!:"+Bounds3D.getBoundsOf(node) + ", "+Bounds3D.getBoundsOf(mesh) );
            
            node.objectList = mesh;
            node.objectBoundList = mesh;
        }
        

        
        //ループ
        private function onEnterFrame(e:Event):void 
        {
            var node:*;
            
            if (!_doSpawn) {
                cameraController.update();
                camera.render();
                return;
            }
            
            
            //フラクタル処理終了
            if (fillRectangleArray.length < 1) {
                removeEventListener(Event.ENTER_FRAME, onEnterFrame);
                var tx:TextField = new TextField();
                tx.text = '終了';
                tx.textColor = 0xFFFFFF;
                addChild(tx);
            }else {
                //フラクタルデータ保持用配列から1つ取り出す
                var rect:RectanglePiece = fillRectangleArray.shift();
                var cArray:Array = deviationLogic(rect.x0, rect.y0, rect.x1, rect.y1);
                rect.c = cArray[0];
                rect.color = cArray[1];
                
                var halfWidth:Number = (rect.x1 - rect.x0) * .5;
                var halfHeight:Number = (rect.y1 - rect.y0) * .5;

                // 指定した矩形内の輝度の標準偏差値が閾値以上なら2分木して処理続行
                if (rect.c > THRESHOLD && (halfWidth > 2 || halfHeight > 2)) {
                    //矩形を書くよ
                    /*
                    _canvas.graphics.lineStyle(0, 0xAAAAAA);
                    _canvas.graphics.beginFill(cArray[1]);
                    _canvas.graphics.drawRect(rect.x0, rect.y0, (rect.x1 - rect.x0), (rect.y1 - rect.y0));
                    */
                    
                    node  = rect.node;
                    var removeObj:Object3D;
                    if (rect.parent != null ) {
                        if ((removeObj=rect.parent.node.objectList) != null) {
                            
                            rect.parent.node.objectList = null;
                            // todo: cut road along parent splitter.
                            
                            if (rect.positive) {
                                rect.parent.node.positive = node;
                                rect.parent.node.negative = rect.sibling.node;
                            }
                            else {
                                rect.parent.node.negative = node;
                                rect.parent.node.positive = rect.sibling.node;
                            }
                            createBuilding(rect, rect.color);
                            createBuilding(rect.sibling, rect.parent.color);
                            
                        }
                        else {  // fill up remaining branch (either negative or postiive)
                            if (rect.positive) rect.parent.node.positive = node;
                            else rect.parent.node.negative = node;
                            createBuilding(rect, rect.color);
                        }
                    }
                    else { // Root node case!
                        createBuilding(rect, rect.color);
                    }
                    
                    
                    //矩形を2分割してフラクタルデータ保持用配列に突っ込む
                    
                    var rect0:RectanglePiece = new RectanglePiece();
                    var rect1:RectanglePiece = new RectanglePiece();
                    
                    // Rather hackish pointers here!
                    rect0.positive = false;
                    rect1.positive = true;
                    rect0.sibling = rect1;
                    rect1.sibling  = rect0;
                    rect0.parent = rect;
                    rect1.parent = rect;
                    
                    if (halfWidth > halfHeight) {
                        
                        node.axis = 0;
                        node.coord = (rect.x0 + halfWidth) * WORLD_SCALE;
                        node.minCoord = node.coord - kdContainer.threshold;
                        node.maxCoord = node.coord + kdContainer.threshold;
                        
                        rect0.x0 = rect.x0;   // negative x
                        rect0.y0 = rect.y0;
                        rect0.x1 = rect.x0+halfWidth;
                        rect0.y1 = rect.y1;
                        fillRectangleArray.push(rect0);
                        
                        rect.node.negative
                        setupNode(rect0);
                        

                        rect1.x0 = rect.x0+halfWidth;  // postive x
                        rect1.y0 = rect.y0;
                        rect1.x1 = rect.x1;
                        rect1.y1 = rect.y1;
                        fillRectangleArray.push(rect1);
                        
                        
                        rect.node.positive;
                        setupNode(rect1);
                    

                    }else {
                        node.axis = 1;
                        node.coord = (rect.y0 + halfHeight) * WORLD_SCALE;
                        node.minCoord = node.coord - kdContainer.threshold;
                        node.maxCoord = node.coord + kdContainer.threshold;

                        rect0.x0 = rect.x0;  // negative y
                        rect0.y0 = rect.y0;
                        rect0.x1 = rect.x1;
                        rect0.y1 = rect.y0+halfHeight;
                        fillRectangleArray.push(rect0);
                        
                        rect.node.negative;
                        setupNode(rect0);
                        

                        rect1.x0 = rect.x0;  //postive y
                        rect1.y0 = rect.y0+halfHeight;
                        rect1.x1 = rect.x1;
                        rect1.y1 = rect.y1;
                        fillRectangleArray.push(rect1);
                        
                        
                        rect.node.positive
                        setupNode(rect1);
                        
                    }
                }
            }
            
            cameraController.update();
            camera.render();
        }
        /**
         * 指定した矩形間の輝度の標準偏差を求める
         * @param    x0    左上のx座標
         * @param    y0    左上のy座標
         * @param    x1    右下のx座標
         * @param    y1    右下のy座標
         * @return    標準偏差値とカラーの平均
         */
        private function deviationLogic(x0:Number,y0:Number,x1:Number,y1:Number):Array {
            var rgb:uint = 0;
            var r:uint = 0;
            var g:uint = 0;
            var b:uint = 0;
            var hsb:Array = new Array();
            var bArray:Array = new Array();
            var br:Number = 0;
            var av:Number = 0;

            //輝度の平均を計算
            for (var i:int = x0; i < x1;i++ ) {
                for (var j:int = y0; j < y1; j++ ) {
                    rgb = imageData.getPixel(i, j);
                    r += (rgb >> 16) & 255;
                    g += (rgb >> 8) & 255;
                    b += rgb & 255;
                    hsb = uintRGBtoHSB(rgb);
                    br += hsb[2];
                    bArray.push(hsb[2]);
                }
            }
            av = br / bArray.length;
            r = r / bArray.length;
            g = g / bArray.length;
            b = b / bArray.length;
            rgb = (r << 16) | (g << 8) | (b << 0);
            _tint = (255 - ( 0.21 * r + 0.71 * g + 0.07 * b )) * INV_255;
            //標準偏差を計算
            br = 0;
            for (i = 0; i < bArray.length; i++ ) {
                br += (bArray[i] - av) *(bArray[i] - av);
            }
            return [Math.sqrt(br / bArray.length),rgb];
            
        }
        /**
         * 
         * @param    rgb    RGB成分(uint)
         * @return HSB配列([0]=hue, [1]=saturation, [2]=brightness)
         */
        private function uintRGBtoHSB(rgb:uint):Array {
            var r:uint = (rgb >> 16) & 255;
            var g:uint = (rgb >> 8) & 255;
            var b:uint = rgb & 255;
            return RGBtoHSB(r, g, b);
        }
        /** RGBからHSBをつくる
         * @param r    色の赤色成分(0~255)
         * @param g 色の緑色成分(0~255)
         * @param b 色の青色成分(0~255)
         * @return HSB配列([0]=hue, [1]=saturation, [2]=brightness)
         */
        private function RGBtoHSB(r:int, g:int, b:int):Array {
            var cmax:Number = Math.max(r, g, b);
            var cmin:Number = Math.min(r, g, b);
            var brightness:Number = cmax / 255.0;
            var hue:Number = 0;
            var saturation:Number = (cmax != 0) ? (cmax - cmin) / cmax : 0;
            if (saturation != 0) {
                var redc:Number = (cmax - r) / (cmax - cmin);
                var greenc:Number = (cmax - g) / (cmax - cmin);
                var bluec:Number = (cmax - b) / (cmax - cmin);
                if (r == cmax) {
                    hue = bluec - greenc;
                } else if (g == cmax) {
                    hue = 2.0 + redc - bluec;
                } else {
                    hue = 4.0 + greenc - redc;
                }
                hue = hue / 6.0;
                if (hue < 0) {
                    hue = hue + 1.0;
                }
            }
            return [hue, saturation, brightness];
        }
    }    
}

    /**
     * ...
     * @author DefaultUser (Tools -> Custom Arguments...)
     */
    class RectanglePiece 
    {
        public var x0:Number;
        public var y0:Number;
        public var x1:Number;
        public var y1:Number;
        public var c:Number;
        public var node:*;
        public var parent:RectanglePiece;
        public var positive:Boolean;
        public var color:uint;
        public var sibling:RectanglePiece;
        
        public function RectanglePiece() 
        {
             this.x0 = 0;
             this.y0 = 0;
             this.x1 = 0;
             this.x1 = 0;
             this.c = 0;            
        }
        
    }