0

I have a very simple code for visualising geo terrain from tiles of different zoom levels, based on pseudo-quadtree algorithm.

Due to crossDomain restrictions and StackOverflow limits I am publishing code just for reference, the live demo is available at http://vault.vkuchinov.co.uk/lod/

There is a glitch while you're travelling, please check YouTube video

It seems that I have messed up with structure, but I don't know there.

Can anyone help me with fixing this glitch?

var folder = "17_50438_37354_50534_37450/satellite/";
var levels = [0x0B132B, 0x1C2541, 0x3A506B, 0x5BC0BE, 0x6FFFE9];  
var mouseDown = false;
    
var renderer, scene, camera, controls, loader, terrain, glsl, uniforms, root, tree;

var colors = [0xFFFFFF, 0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF];
    
class Node{
    
    constructor(level_, index_, centerX_, centerY_, width_, height_, resolution_){
        
        this.treeId = null;
        this.level = level_;
        this.index = index_;
        this.w = width_;
        this.h = height_;
        this.x = centerX_;
        this.y = centerY_;
        this.resolution = resolution_;
        
    }
    
}
    
class Quadtree{
    
    constructor(root_, levels_, distance_){
        
        var this_ = this;
        this.levels = levels_;
        this.distance = distance_;
        this.root = root_;
        
        this.tree = [ {id: "1", n: root_, active: true, children: null } ];
        this.generateLevels(this.tree[0]);

        this.lastTree = [...this.tree];
        this.tiles = {};
        
        this.generateTiles(this.tree[0]);

    }
    
    generateTiles = function(node_){
        
        if(node_.children == null){
            
            if(node_.active = true){ this.createTile(node_.n); }
            
        }
        else{
            
            this.generateTiles(node_.children[0]);
            this.generateTiles(node_.children[1]);
            this.generateTiles(node_.children[2]);
            this.generateTiles(node_.children[3]);
            
        }
        
    }
    
    generateLevels = function(parent_){

        if(parent_.n.level < this.levels && this.sqrtDistance(parent_.n) < this.distance){

            parent_.children = [];
            var parentID = parent_.id + ".";
            
            parent_.children.push({id: parentID + 1, n: new Node(parent_.n.level + 1, { x: parent_.n.index.x * 2, y: parent_.n.index.y * 2 }, parent_.n.x - parent_.n.w / 4, parent_.n.y - parent_.n.h / 4, parent_.n.w / 2, parent_.n.h / 2, parent_.n.resolution / 2), active: true, children: null});
            
            parent_.children.push({id: parentID + 2, n: new Node(parent_.n.level + 1, { x: parent_.n.index.x * 2, y: parent_.n.index.y * 2 + 1 }, parent_.n.x + parent_.n.w / 4, parent_.n.y - parent_.n.h / 4, parent_.n.w / 2, parent_.n.h / 2, parent_.n.resolution / 2), active: true, children: null});
            
            parent_.children.push({id: parentID + 3, n: new Node(parent_.n.level + 1, { x: parent_.n.index.x * 2 + 1, y: parent_.n.index.y * 2 }, parent_.n.x - parent_.n.w / 4, parent_.n.y + parent_.n.h / 4, parent_.n.w / 2, parent_.n.h / 2, parent_.n.resolution / 2), active: true, children: null});
            
            parent_.children.push({id: parentID + 4, n: new Node(parent_.n.level + 1, { x: parent_.n.index.x * 2 + 1, y: parent_.n.index.y * 2 + 1 }, parent_.n.x + parent_.n.w / 4, parent_.n.y + parent_.n.h / 4, parent_.n.w / 2, parent_.n.h / 2, parent_.n.resolution / 2), active: true, children: null});

            this.generateLevels(parent_.children[0]);
            this.generateLevels(parent_.children[1]);
            this.generateLevels(parent_.children[2]);
            this.generateLevels(parent_.children[3]);

            parent_.active = false;

         }

    }
    
    binToDec = function (bstr) {  return parseInt((bstr + '').replace(/[^01]/gi, ''), 2); }
    
    update = function(){
        
        var this_ = this;
        
        this.tree = [ {id: "1", n: this.root, active: true, children: null } ];
        this.generateLevels(this.tree[0]);
        
        var prev = {};
        this_.bfs(this.lastTree[0], "children", prev);
        
        var next = {};
        this_.bfs(this.tree[0], "children", next);
        
          Object.keys(next).forEach(function(key_){
            
            if(!prev.hasOwnProperty(key_)){
                
                
                if(next[key_].active = true) { this_.createTile(next[key_].n); }
                
            }
            
        })
        
        //remove old
        Object.keys(prev).forEach(function(key_){
            
            if(!next.hasOwnProperty(key_)){
                
                this_.deleteTile(prev[key_].n);
                
            }
            
        })
    
        
        this.lastTree = [...this.tree];
           
    
    }
    
    bfs = function(tree, key, collection) {
        
        if (!tree[key] || tree[key].length === 0) return;
        for (var i=0; i < tree[key].length; i++) {
            var child = tree[key][i]
            collection[child.id] = child;
            this.bfs(child, key, collection);
        }
        return;
    }
    
    getIndicesList = function(node_){
        
        var out = [];
        
        return out;
        
    }
    
    updateTree = function(node_, state_){
        
        if(!state_) {
            
            var id = "tile" + node_.n.index.x + "_" + node_.n.index.y;
            if(!this.validateNodeByIndex(this.tree, node_.id)){
                
                var tile = scene.getObjectByName(id);
                if(tile != undefined){
                    
                    tile.geometry.dispose();
                    tile.material.dispose();
                    scene.remove(tile);
                    
                }
                
            }
            
            
        }else{
            
            var id = node_.n.index.x + "_" + node_.n.index.y;
            if(!this.validateNodeByIndex(this.lastTree, node_.id)){
                
                //this.createTile(node_.n);
                
            }
        
        }
        
        if(node_.children != null){
            
            this.updateTree(node_.children[0]);
            this.updateTree(node_.children[1]);
            this.updateTree(node_.children[2]);
            this.updateTree(node_.children[3]);
            
        }
        
    }
    
    getNodeByIndex = function(tree_, index_){
        
        var n = index_.split(".");
        n = n.map(function(f_){ return Number(f_); })
        if( n.length == 2) { return tree_[0].children[n[1]]; }
        else if ( n.length == 3) { return tree_[0].children[n[1] - 1].children[n[2] - 1]; }
        else if ( n.length == 4) { return tree_[0].children[n[1] - 1].children[n[2] - 1].children[n[3] - 1]; }
        else if ( n.length == 5) { return tree_[0].children[n[1] - 1].children[n[2] - 1].children[n[3] - 1].children[n[4] - 1]; }
        else if ( n.length == 6) { return tree_[0].children[n[1] - 1].children[n[2] - 1].children[n[3] - 1].children[n[4] - 1].children[n[5] - 1]; }
       
    }
    
    validateNodeByIndex = function(tree_, index_){
        
        var out = false;
        var nodes = tree_[0]
       
        return out;
       
    }
    
    findNode = function(index_){
        
        var out = false;
        
        for (let node_ of this.nodes) {
            
            if(node_.index.x + "_" + node_.index.y == index_) { out = node_; break; } 
            
        }
        
        return out;
        
    }

    createTile = function(parent_){
        
        var id = parent_.index.x + "_" + parent_.index.y;
        var url = folder + "level" + parent_.level + "/" + parent_.index.x + "_" + parent_.index.y + ".jpg";
        var geometry = new THREE.PlaneGeometry(parent_.w, parent_.h, parent_.resolution, parent_.resolution);
        var material = new THREE.MeshBasicMaterial({ map: new THREE.TextureLoader().load(url) });
        var tile = new THREE.Mesh(geometry, material);
        tile.name = "tile" + parent_.index.x + "_" + parent_.index.y;
        tile.rotation.set(-Math.PI / 2, 0, 0);
        tile.position.set(parent_.x, 0, parent_.y);
        scene.add(tile);
                
    }

    deleteTile = function(parent_){

        var name = "tile" + parent_.index.x + "_" + parent_.index.y;
        var tile = scene.getObjectByName(name);
        if(tile != undefined){
            
            scene.remove(tile); 
            tile.geometry.dispose(); 
            tile.material.dispose();  
            
        }
   
    }
    
    bin_to_dec = function (bstr) {  return parseInt((bstr + '').replace(/[^01]/gi, ''), 2); }
    
    splitNode = function(index_, parent_, tree_, check_){

     if((parent_.level < this.levels && this.sqrtDistance(parent_) < this.distance) || !check_){

       tree_.children = [];
         
       var lt = new Node(parent_.level + 1, { x: parent_.index.x * 2, y: parent_.index.y * 2 }, parent_.x - parent_.w / 4, parent_.y - parent_.h / 4, parent_.w / 2, parent_.h / 2, parent_.resolution / 2);
       var rt = new Node(parent_.level + 1, { x: parent_.index.x * 2, y: parent_.index.y * 2 + 1 }, parent_.x + parent_.w / 4, parent_.y - parent_.h / 4, parent_.w / 2, parent_.h / 2, parent_.resolution / 2);
       var lb = new Node(parent_.level + 1, { x: parent_.index.x * 2 + 1, y: parent_.index.y * 2 }, parent_.x - parent_.w / 4, parent_.y + parent_.h / 4, parent_.w / 2, parent_.h / 2, parent_.resolution / 2);
       var rb = new Node(parent_.level + 1, { x: parent_.index.x * 2 + 1, y: parent_.index.y * 2 + 1 }, parent_.x + parent_.w / 4, parent_.y + parent_.h / 4, parent_.w / 2, parent_.h / 2, parent_.resolution / 2);
 
       tree_.children[0] = {id: lt.index.x + "_" + lt.index.y, n: lt, children: null };
       tree_.children[1] = {id: rt.index.x + "_" + rt.index.y, n: rt, children: null };
       tree_.children[2] = {id: lb.index.x + "_" + lb.index.y, n: lb, children: null };
       tree_.children[3] = {id: rb.index.x + "_" + rb.index.y, n: rb, children: null };

       lt.treeId = tree_.children[0];
       rt.treeId = tree_.children[1];
       lb.treeId = tree_.children[2];
       rb.treeId = tree_.children[3];

       return [lt, rt, lb, rb];
     
     }
    
     return [parent_];
        
    }
    
    getDifference = function(t1_, t2_) {

        function getD(object, parent) {
            function getKeys({ where, ...object }) {
                return Object.keys(object).flatMap(k => [k, ...(object[k] ? getKeys(object[k]) : [])]);
            }

            var types;
            Object
                .entries(object)
                .forEach(([k, { where, ...o }]) => {
                
                    if (where) {
                        let keys = getKeys(o);
                        if (!types) types = {};
                        if (!types[where]) types[where] = [];
                        types[where].push(keys.length ? [k, keys] : k);
                    } else {
                        getD(o, k);
                    }
                });

            if (types) {
                
                if (-1 in types) result.push([types[-1], parent]);
                if (1 in types) result.push([parent, types[1]]);
                
            }
        }

        var temp = {},
            add = (inc, tree) => ({ id, children }) => {
                tree[id] = tree[id] || { where: 0 };
                tree[id].where += inc;
                if (children) children.forEach(add(inc, tree[id]));
            },
            result = [];

        t1_.forEach(add(-1, temp));
        t2_.forEach(add(1, temp));

        getD(temp);

        return result;
        
    }   
    
    sqrtDistance = function(node_){
        
        var target = new THREE.Vector2(camera.position.x, camera.position.z).lerp(new THREE.Vector2(controls.target.x, controls.target.z), 1.0);
        
        var x1 = node_.x - node_.w / 2.0;
        var y1 = node_.y - node_.h / 2.0;
        var x2 = node_.x + node_.w / 2.0;
        var y2 = node_.y + node_.h / 2.0;

        var rx = (x1 + x2) / 2.0;
        var ry = (y1 + y2) / 2.0;
        var rwidth = node_.w;
        var rheight = node_.h;

        var dx = Math.max(Math.abs(target.x - rx) - rwidth / 2, 0);
        var dy = Math.max(Math.abs(target.y - ry) - rheight / 2, 0);
        return Math.sqrt(dx * dx + dy * dy);
        
    }
    
    draw = function(){
        
        this.nodes.forEach(function(node_){ node_.draw(); })
        
    }
    
}
    
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000);
document.body.appendChild(renderer.domElement);

scene = new THREE.Scene();
loader = new THREE.TextureLoader();
loader.crossOrigin = "";

camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 51200);
camera.position.set(-2048, 2048, -2048);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

controls.screenSpacePanning = false;

controls.minDistance = 8;
controls.maxDistance = 5120;
    
controls.maxPolarAngle = Math.PI / 2;
    
camera.position.set(208.48355078304965, 45.28894677815297, 310.34089790619583);
controls.target.set(233.437242880138, -1.1266992511037067e-14, 279.779814968453);
    
root = new Node(0, {x: 0, y: 0}, 0, 0, 2048, 2048, 64);
tree = new Quadtree(root, 5, 2048.0 / 16.0);
    
animate();

document.addEventListener("mousedown", function(){ mouseDown = true; }, false);
document.addEventListener("mouseup", function(){ mouseDown = false; }, false);
document.addEventListener("mousemove", onMouseUpdate, false);
renderer.domElement.addEventListener("wheel", onMouseUpdate, false);
        
function animate(){
    
    console.log(scene.children.length);
    controls.update();
    renderer.render(scene, camera);
    
    requestAnimationFrame(animate);

    
}
    
function onMouseUpdate(e_){
    
    tree.update();
    
}
body { margin: 0; }
<!DOCTYPE html>
<html>
<head>
    
    <meta charset="utf-8" />
    <title>GLSL Intersection</title>
  
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <script src="https://unpkg.com/three@0.116.0/build/three.min.js"></script>
    <script src="https://unpkg.com/three@0.116.0/examples/js/controls/OrbitControls.js"></script>
    <script src="stats.min.js"></script>
 
</head>
<body>
    
</body>
</html>
VVK
  • 435
  • 2
  • 27

1 Answers1

0

Your terrain tiles are z-fighting between different zoom levels. This is more noticeable before the second zoom-level loads because they're black and show the telltale signs of the depthbuffer trying to give precedence to two planes occupying the same space.

enter image description here

The same thing is happening in your YouTube video, but by then the terrain texture has loaded. Somewhere along your code the old tiles are not being destroyed in the either the deleteTile() or `updateTree() function (Not sure why you have two functions doing the same thing).

M -
  • 26,908
  • 11
  • 49
  • 81
  • deleteTile physically removes tile from the scene. 1. scene.remove(tile), 2. dispose geometry, 3. dispose material. While, updateTree() updates tile quadtree and check for tiles to be deleted or created. – VVK Jun 06 '20 at 11:58
  • I have added conditional at createTile() if(scene.getObjectByName(id) == undefined){ ... } just for case that it's already there and clones cause this glitch. No. No duplicates. – VVK Jun 06 '20 at 12:04
  • The most weird thing, if you stop moving camera, the glitch would disappear after 2-3 seconds. Is it normal for such kind of glitches? Is Three.js somehow solves it after # of frames? Don't think so. – VVK Jun 06 '20 at 12:08
  • It's only a hypothesis, but I have strong feeling that this glitch is caused by asynchronous tasks. Next update goes in the same time as previous loads new tiles. – VVK Jun 06 '20 at 14:31