In case Flash no longer exists; a copy of this site is included in the Flashpoint archive's "ultimate" collection.

Dead Code Preservation :: Archived AS3 works from wonderfl.net

Progressive LOD terrain page loading

Planning out asynchronous terrain paging/loading scheme across nested hierarchical grids (somewhat like a quadtree) of varying LODs (level-of-details). For further terrain pieces away from the camera position (denoted by mouse position over stage), lower resolutions are used by having them cover a much larger area.

By right, there's supposed to be a global low resolution heightmap that is always displayed at the back for a generic starting LOD which will always be shown before higher LODs are loaded. This will be done in the actual 3d terrain implementation so at least some (low-detail) terrain is shown before building it up gradually at the relevant nearby areas if required (on-demand).

But anyway, I'm testing with Google maps images as a concept proof first to ensure the loading sequence is correct.
Get Adobe Flash player
by Glidias 11 Mar 2013

    Talk

    Glidias at 11 Mar 2013 14:42
    Each terrain page at the highest LOD square (ie. smallest grid square) can typically be 2km x 2km and 1024 tiles x 1024 tiles at highest LOD detail. Currently, I only have working single-page LOD terrain examples at https://github.com/Glidias/alternterrain for alternativa3d engine and am seeing how to implement this for a much larger worldset.

    Tags

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

package
{
    import com.greensock.loading.core.LoaderCore;
    import com.greensock.loading.ImageLoader;
    import com.greensock.loading.LoaderMax;
    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.display.DisplayObject;
    import flash.display.Graphics;
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.events.Event;
    import flash.geom.Point;
    import flash.geom.Rectangle;
    import flash.text.TextField;
    
    /**
     * Planning out terrain paging scheme across nested hierarchical grids. 
     * 
     * Currently, this is a debug mode sprite in 2D for testing..
     * 
     * @author Glenn Ko
     */
    public class TerrainQueryBox extends Sprite
    {
        public const BASE_PATCH:int = 4;  // increase this for more page loads from within the center
        public const QUERY_SIZE:int = 256;
        
        private var boundingSpace:Rectangle = new Rectangle(0, 0, 2048, 1024);
        private var boundingTiles:Rectangle = new Rectangle(boundingSpace.x/32, boundingSpace.y/32, boundingSpace.width / 32, boundingSpace.height / 32);
        private var offset6:Vector.<int>= new Vector.<int>(6*4, true);
        
        private var _lmx:int = -int.MAX_VALUE;
        private var _lmy:int = -int.MAX_VALUE;
        
        private var testLoadingLayer:Sprite = new Sprite();
        private var graphicsLayer:Sprite = new Sprite();
        override public function get graphics():Graphics {
            return graphicsLayer.graphics;
        }
        
        
        static private const ORIGIN:Point = new Point();
        
        private var levelPxOffsets:Vector.<int> = new Vector.<int>();
        
        
        private var gridLookup:BitmapData;
        
        private var stateLookup:BitmapData;
        private var lastStatelookup:BitmapData;
        private var loaderStates:Vector.<LoaderState> = new Vector.<LoaderState>();
        private var loaderOffsets:Vector.<int> = new Vector.<int>();
        private var loaderMax:LoaderMax = new LoaderMax({onProgress:progressHandler});
        
        
        private var previewChangeState:BitmapData;
        
        
        public function TerrainQueryBox() 
        {
            LoaderMax.defaultAuditSize = false;

            if (stage.stageWidth < 500) {
                scaleX = .5;
                scaleY = .5;
            }
            
            
            gridLookup = new BitmapData(boundingTiles.width, boundingTiles.height, false, 0);
            stateLookup = new BitmapData(boundingTiles.width*2, boundingTiles.height, false, 0);
            lastStatelookup = new BitmapData(stateLookup.width, stateLookup.height, false, 0);
            previewChangeState = new BitmapData(stateLookup.width, stateLookup.height, true, 0);
            ///*
            offset6[0] = 0;  offset6[1] = 1;  // nw  (0)
            offset6[2] = 1;  offset6[3] = 1;
            offset6[4] = 1;  offset6[5] = 0;
            
            offset6[6] = -1;  offset6[7] = 0;  // ne  (0)
            offset6[8] = -1;  offset6[9] = 1;
            offset6[10] = 0;  offset6[11] = 1;
            
            offset6[12] =0;  offset6[13] = -1;  //   sw (|=2,)  10
            offset6[14] = 1;  offset6[15] = -1;
            offset6[16] = 1;  offset6[17] = 0;
            
            offset6[18] = -1;  offset6[19] = 0;   // se (|=2,|=1) 11
            offset6[20] = -1;  offset6[21] = -1;
            offset6[22] = 0;  offset6[23] = -1;
            //*/
            
            // bounding box query on level 1 grid - 32,  1
            // bounding box query on level 2 grid - 64  , 2
            // bounding box query on level 3 grid - 128, 4,
            // bounding box query on level 4 grid - 256, 8
            
            var qSize:uint = 32;
            levelPxOffsets.push(0);
            var count:int = 0;
            var count2:int = 0;
            var level:int  = 0;
            loaderOffsets.push(0);
            boundWidth = int(boundingSpace.width) / 32;
            boundHeight = int(boundingSpace.height) / 32;
            while (qSize < QUERY_SIZE ) {
                
                qSize *= 2;
                count += (boundWidth >> level);
                
                count2 += (boundWidth >> level) * (boundHeight >> level);
                levelPxOffsets.push(count );
                loaderOffsets.push(count2);
                level++;
            }
            
            loaderStates.length = count2 + boundWidth*boundHeight;
            loaderStates.fixed = true;
            loaderOffsets.fixed = true;
            
            
             stage.align = StageAlign.TOP_LEFT;
             stage.scaleMode = StageScaleMode.NO_SCALE;
             
             addEventListener(Event.ENTER_FRAME, onEnterFrame);
             
             
             scrollRect = new Rectangle(0, 0, 1680, 1024);
             
            
             addChild(testLoadingLayer);
             addChild( graphicsLayer);
             
             addChild( new Bitmap(stateLookup));
              addChild( new Bitmap(previewChangeState));
              
            //loaderMax.skipFailed= true;
            loaderMax.skipPaused = true;
            
            addChild(_debugField);
            _debugField.autoSize = "left";
            _debugField.background = true;
            _debugField.backgroundColor  = 0xFFFFFF;
        
            
        }
        
        private static const START_ZOOM:int = 7;
        private static const START_X:int = 50;
        private static const START_Y:int = 7;
        
        
        private var _progressCount:int = 0;
        private function progressHandler(e:Event):void {
        //    _debugField.text =String( loaderMax.numChildren + ', '+_progressCount++ );
        }

        // if already downloading, get by grid id location
        // if not already downlaoding
        
        private function onEnterFrame(e:Event):void 
        {
            var mx:int = mouseX;
            var my:int = mouseY;
            if (mx < 0) mx = 0;
            if (my < 0) my = 0;
            var mix:int = Math.round(mx/32);
            var miy:int = Math.round(my / 32);
            if (mix >= boundWidth) mix = boundWidth - 1;    // clamp round up cases
            if (miy >= boundHeight) miy = boundHeight - 1;
            
            if (_lmx === mix && _lmy === miy) {
                return;
            }
            
            _lmx = mix;
            _lmy = miy;
            
            
            graphics.clear(); 
            gridLookup.fillRect(gridLookup.rect, 0xFF0000);
            lastStatelookup.copyPixels(stateLookup, stateLookup.rect, ORIGIN);
            previewChangeState.fillRect(previewChangeState.rect, 0);
            stateLookup.fillRect(stateLookup.rect, 0);
            
            var rect:Rectangle;
            
            
            rect = new Rectangle(mix * 32 - 32 * 4 * BASE_PATCH, miy * 32 - 32 * 4* BASE_PATCH, 32 * 8* BASE_PATCH, 32 * 8* BASE_PATCH);
            graphics.lineStyle(0, 0xFF0000, 1);
            
            graphics.drawRect(rect.x, rect.y, rect.width, rect.height);
            
            graphics.lineStyle(0, 0, 1);
            
            var qSize:int;
            
            qSize = 32;
            

            rect.width = 32*4;
            rect.height = 32 * 4;
            rect.x = mix * 32 - 32 * 2
            rect.y = miy * 32 - 32 * 2
            var level:int = 0;
            drawOntoGrid(rect, qSize, level);
        
            while (qSize < QUERY_SIZE ) {
                rect.x -= rect.width*.5; rect.y -= rect.height*.5; rect.width *= 2, rect.height *= 2;
                drawOntoGrid(rect, qSize*=2, ++level);
            }
            level++;
            // determine any change of state from last lookup
            while (--level > -1) {
                checkGrid(level);
            }
            
            // prioritize current quad and 3 nearest quads to load first

                                
            // Prioritize loading of 4 nearest squares
            mx /= 32;
            my /= 32;
            if (mx >= boundWidth) mx = boundWidth - 1;    // clamp round up cases
            if (my >= boundHeight) my = boundHeight - 1;
            var xRound:int = mx < mix ? 1 : -1;  
            var yRound:int = my < miy ? 1 : -1;      
            loaderStates[miy * boundWidth + mix].loader.prioritize();  // current  square i'm on
            var x:int;
            var y:int;  // get 3 rounded squares
            x = mx+xRound; y = my + yRound; // round both xy
            if (x>=0 && x< boundWidth && y >=0 && y< boundHeight) loaderStates[y * boundWidth +x].loader.prioritize(); 
            x = mx+xRound; y = my; // round x only
            if (x>=0 && x< boundWidth) loaderStates[y * boundWidth +x].loader.prioritize(); 
            x = mx; y = my + yRound; // round y only
            if (y >=0 && y< boundHeight) loaderStates[y * boundWidth +x].loader.prioritize();  
        
            loaderMax.resume();    
        }
        
        

        
        private function checkGrid(level:int):void 
        {
            var xOffset:int = levelPxOffsets[level];
            var yLen:int = boundHeight >> level;
            var xLen:int = boundWidth >> level;
            for (var y:int = 0; y < yLen; y++) {
                for (var x:int = 0; x < xLen; x++) {
                    var xer:int = xOffset + x;
                    var last:Boolean = lastStatelookup.getPixel(xer, y) !=0; 
                    var now:Boolean = stateLookup.getPixel(xer, y) != 0; 
                    if (last != now) {
                        resolveState(now, x,y, level);  // should resolve state index
                    }
                }
            }
        }
        
            //  http://mt0.google.com/vt/x=50&y=7&z=7    
            private static const DUMMY_START_X:int = 50;
            private static const DUMMY_START_Y:int = 7;
            private static const DUMMY_START_ZOOM:int = 7;
            
            
    private function getTileURLGoogle(x:int, y:int, zoom:int):String {
        return "http://mt0.google.com/vt/x="+x+"&y="+y+"&z="+zoom;
    }
        
        private function resolveState(doLoad:Boolean, x:int, y:int, level:int):void 
        {
            var child:DisplayObject;
            var rowsAcross:int = (boundWidth >> level);
            var offset:int = loaderOffsets[level];
            
            
            
            offset += y * rowsAcross + x;
            var gridSize:int = (32 << level);
            var loaderState:LoaderState = loaderStates[offset];
            if (loaderState == null) {
                loaderStates[offset] = loaderState= new LoaderState();
            }
            if (doLoad) {
                if (loaderState.state === LoaderState.STATE_DOWNLOADED) {
                    throw new Error("Should not happen!");
                    return;
                }
                var spr:Sprite;
                if (loaderState.state === LoaderState.STATE_EMPTY) {
                    var targZoom:int = loaderOffsets.length - level - 1;
                    child = testLoadingLayer.addChild(spr = new Sprite());
                    child.x = gridSize * x;
                    child.y = gridSize * y;
                    
                    var targScale:Number = gridSize / 256;  // divide by google map size 256
                    child.scaleX = targScale;
                    child.scaleY = targScale;
                    
                    loaderState.load( getTileURLGoogle((DUMMY_START_X << targZoom) + x, (DUMMY_START_Y << targZoom) + y, DUMMY_START_ZOOM + targZoom),  child);
                    loaderMax.prepend(loaderState.loader);
                    
                    //loaderState.loader.pause();  // enable this to test nearest-4-square priority load trails
                }  
                else if (loaderState.state < LoaderState.STATE_DOWNLOADING) {  
                    
                    loaderState.resume();
                    loaderState.prioritize();
                }
                else {
                    // prioritize
                    loaderState.prioritize();
                }
                
                
            }
            else {
                if (loaderState.state >= LoaderState.STATE_DOWNLOADING) {
                ///*
                var ref:LoaderCore = loaderState.loader;
                    loaderState.dispose();
                    //loaderMax.resume();
                    loaderMax.remove(ref);
                    //loaderStates[offset]  = null;
                    //loaderMax.resume();
                    
                    
                    //*/
                    //loaderState.remove();
                
                }
            }
            
            //loaderOffsets[level] + y *
            previewChangeState.setPixel32( levelPxOffsets[level] + x, y, 0xFF0000FF);
            //throw new Error("DIFFERENT:" + level + ","+[x+levelPxOffsets[level],y]);
            
            //if (!doLoad) throw new Error("TO UNLOAD!" + level + ","+[x+levelPxOffsets[level],y]);
            
        }
        
        private var rectFiller:Rectangle = new Rectangle();
        private var boundWidth:int;
        private var boundHeight:int;
        private var _debugField:TextField = new TextField();
        
        private function drawOntoGrid(sampleRegion:Rectangle, gridSize:int, level:int):void {
    
            var xLimit:int = Math.ceil( (sampleRegion.x+sampleRegion.width)  / gridSize );
            var yLimit:int = Math.ceil( (sampleRegion.y + sampleRegion.height)  / gridSize );
            var parentGridSize:int = gridSize * 2;
            if (xLimit > boundingTiles.x + boundingTiles.width) xLimit = boundingTiles.x + boundingTiles.width ;
            if (yLimit > boundingTiles.y + boundingTiles.height) yLimit = boundingTiles.y + boundingTiles.height ;
            var yStart:int = sampleRegion.y / gridSize;
            var xStart:int = sampleRegion.x / gridSize;
            if (xStart < boundingTiles.x) xStart = boundingTiles.x;
            if (yStart < boundingTiles.y) yStart = boundingTiles.y;
            
            rectFiller.width = gridSize / 32;
            rectFiller.height = gridSize / 32;
            
                var levelPxOffset:int = levelPxOffsets[level];
                
        
            var randColor:uint = 0xFF + (level + 1);// * (65535/8);
            
            var gotParent:Boolean = parentGridSize <= QUERY_SIZE;
            
            for (var yi:int =  yStart; yi < yLimit; yi++) {
                    for (var xi:int = xStart; xi < xLimit; xi ++) {
                        var xis:int;
                         var yis:int;
                        rectFiller.x = xis =( xi - boundingTiles.x) * rectFiller.width;
                        rectFiller.y = yis = (yi - boundingTiles.y) * rectFiller.height;
                    
    
                        if ( (gridLookup.getPixel(xis, yis) & 0xFFFF00) != 0xFF0000) {
                            // this is already filled up by something
                            //BitmapData().compare()
                            continue;
                        }
                
                        if (gridSize == 32 ) graphics.lineStyle(2, 0xFF)
                        else graphics.lineStyle(0, 0, 1);
                        graphics.beginFill(0xFF0000, .2);
                        graphics.drawRect(xi * gridSize, yi * gridSize, gridSize, gridSize);
                         gridLookup.fillRect(rectFiller, randColor);
                            stateLookup.setPixel(xi+levelPxOffset, yi, 0xFF0000);
                        
                        if (gotParent) {
                            if (gridSize == 32) graphics.lineStyle(1, 0xFF0000, 1)
                            else graphics.lineStyle(0, 0, 1)
                            
                            var result:int = 0;
                            // based on current region, which quadrant and I'm in?? draw rect on 3 other quadrants.
                            var xii:int = xi*gridSize ;
                            var yii:int = yi*gridSize;
                        
                            
                            
                            result|= ((xi*gridSize) % parentGridSize)  != 0 ? 1 : 0;
                            result |= ((yi * gridSize) % parentGridSize) != 0 ? 2 : 0;
                    
                        
                            
                            //if (gridSize == 32 ) {
                                //graphics.lineStyle(0, 0xFF, 1);
                                
                                result *= 6;
                                
                                rectFiller.x = (xis) + offset6[result]*rectFiller.width;
                                rectFiller.y = (yis) + offset6[result + 1] * rectFiller.height;
                                if (gridLookup.getPixel(rectFiller.x, rectFiller.y) === 0xFF0000) {
                                    gridLookup.fillRect(rectFiller, randColor);
                                    graphics.beginFill(0xFF0000, .2);
                                    graphics.drawRect(xii+ offset6[result] * gridSize,
                                    yii+(offset6[result + 1]) * gridSize,
                                    gridSize, gridSize);
                                    stateLookup.setPixel(xi+ levelPxOffset+offset6[result], yi+ offset6[result+1], 0xFF0000);
                                }
                
                                rectFiller.x = (xis) + offset6[result+2]*rectFiller.width;
                                rectFiller.y = (yis) + offset6[result+3]*rectFiller.height;
                                if (gridLookup.getPixel(rectFiller.x, rectFiller.y) === 0xFF0000) {
                                    gridLookup.fillRect(rectFiller, randColor);
                                    graphics.beginFill(0xFF0000, .2);
                                    graphics.drawRect(xii+ offset6[result + 2] * gridSize,
                                    yii+(offset6[result + 3]) * gridSize,
                                    gridSize, gridSize);
                                    stateLookup.setPixel(xi+ levelPxOffset+offset6[result+2], yi+ offset6[result+3], 0xFF0000);
                                }
                                
                                rectFiller.x = (xis) + offset6[result+4]*rectFiller.width;
                                rectFiller.y = (yis) + offset6[result + 5] * rectFiller.height;
                                if (gridLookup.getPixel(rectFiller.x, rectFiller.y) === 0xFF0000) {
                                    gridLookup.fillRect(rectFiller, randColor);
                                    graphics.beginFill(0xFF0000, .2);
                                    graphics.drawRect(xii+ offset6[result + 4] * gridSize,
                                    yii+offset6[result + 5] * gridSize,
                                    gridSize, gridSize);
                                    stateLookup.setPixel(xi+ levelPxOffset+offset6[result+4], yi+ offset6[result+5], 0xFF0000);
                                    
                                }
                                
                            //}
                            
                        
                        
                        }
                }
            }
            
            
            
            
            
        
            
        }
        
    }

}
import com.greensock.loading.ImageLoader;
import com.greensock.loading.LoaderStatus;
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;

        
class LoaderState {
    
    public static const STATE_EMPTY:int = 0;
    public static const STATE_PAUSED_PENDING_REMOVAL:int = 1;
    public static const STATE_DOWNLOADED_PENDING_REMOVAL:int = 2;
    public static const STATE_DOWNLOADING:int = 3; 
    public static const STATE_DOWNLOADED:int = 4;
    
    private var _loader:ImageLoader;
    private var _container:Sprite;
    
    private var _state:int = 0;
    
    
    public function get loaderState():int {
        if (_loader == null) return STATE_EMPTY;
        
        var status:int =  _loader.status;    
        if (status === LoaderStatus.COMPLETED) {
            return STATE_DOWNLOADED;
        }
        else if (status === LoaderStatus.READY || status === LoaderStatus.LOADING) {
            return STATE_DOWNLOADING;
        }
        else if (status === LoaderStatus.PAUSED) {
            return STATE_PAUSED_PENDING_REMOVAL;
        }
        
        return STATE_DOWNLOADED_PENDING_REMOVAL;
    }
    
    public function get state():int 
    {
        return _state;
    }
    
    public function set state(value:int):void 
    {
        _state = value;
        
    }
    
    public function get loader():ImageLoader 
    {
        return _loader;
    }
    
    public function get container():Sprite 
    {
        return _container;
    }
    
    
    public function getRawContent():* {
        return _loader.rawContent;
    }

    //private var _preloaderBar:Shape = new Shape();
    
    public function load(url:String, container:*):void {

        _state = STATE_DOWNLOADING;
        _container = container;
        _loader = new ImageLoader(url, { container:container, onComplete:notifyComplete } );
        _container.visible = true;
        
        //_preloaderBar.graphics.beginFill(0x00FF00, 0);
        //    _preloaderBar.graphics.drawRect(, 1);
        
        //container.addChild(_preloaderBar);
        //    _loader.load();

    }
    private function notifyComplete(e:Event):void {
        _state = STATE_DOWNLOADED;
    }
    
    public function resume():void {
        _container.visible = true;
        if (_state === STATE_PAUSED_PENDING_REMOVAL) _loader.resume();
    }
    
    
    public function remove():void {
        if (_loader.status === STATE_DOWNLOADED) {
            _state = STATE_DOWNLOADED_PENDING_REMOVAL;
        }
        else {
            _loader.pause();
            _state = STATE_PAUSED_PENDING_REMOVAL;
        }
        _container.visible = false;
    }
 
    
    public function dispose():void {
        if (_container.parent) _container.parent.removeChild(_container);
        _container = null;
        _state = 0;
        // _loader.dispose(true);
        //loader.unload();
        //if (_state === STATE_DOWNLOADED)
        _loader.dispose(true);
        //else 
        //loader.unload();
        _loader = null;
    }
    
    public function prioritize():void 
    {
        _loader.prioritize();
        _container.visible = true;
    }
    
}