Search code examples
three.jstextureszoominglevels

THREE.JS Level of Detail Texture Loading


Here is the small Three.JS sketch with THREE.LOD() objects. As you can see there are 4 levels with their unique textures.

enter image description here

By now, all these textures are preloaded on start.

Is there any way to load 1, 2, 3 levels textures on the fly while zooming in?

Yes, I could do same thing without THREE.LOD() just by coding my own custom algorithm, which would generate/remove planes on zooming, but I'm very interesting in built-in THREE.LOD().

    
var folder = "http://vault.vkuchinov.co.uk/test/assets";
var levels = [0xF25E6B, 0x4EA6A6, 0x8FD9D1, 0xF2B29B, 0xF28E85];  
    
var renderer, scene, camera, controls, loader, lod, glsl, uniforms;
    
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;

lod = new THREE.LOD();
lod.name = "0,0";
generateTiles(lod, 2048, 2048, 2048, 0, 0x00FFFF);
  
scene.add(lod);
    
animate();
        
function animate(){
         
    controls.update();
    renderer.render(scene, camera);
    
    requestAnimationFrame(animate);

}

function generateTiles(parent_, width_, height_, zoom_, level_, hex_){
    
    var id = parent_.name.split(",");
    var colors = [0xFFFF00, 0xFF000, 0x00FF00, 0x0000FF, 0xFF00FF, 0xF0F0F0];
    
    var group = new THREE.Group(), geometry, plane;

    var dx = 0, dy  = 0;
    dy *= Math.pow(2, level_); dx *= Math.pow(2, level_); 
    
    var url = folder + "/textures/level" + level_ + "/" + id[0] + "_" + id[1] + ".jpg";
    
    if(level_ < 3){
        
        var uniforms = {

            satellite: {
                type: "t",
                value: loader.load(url)
            }
            
        };

        var glsl = new THREE.ShaderMaterial({

        uniforms: uniforms,
        vertexShader: document.getElementById("vertexTerrain").textContent,
        fragmentShader: document.getElementById("fragmentTerrain").textContent,
        lights: false,
        fog: false,
        transparent: true

        });

        glsl.extensions.derivatives = true;
        
        geometry = new THREE.PlaneGeometry(width_, height_, 256, 256);
        plane = new THREE.Mesh(geometry, glsl); 
        plane.rotation.set(-Math.PI / 2, 0, 0);
        parent_.addLevel(plane, zoom_);

        geometry = new THREE.PlaneGeometry(width_ / 2, height_ / 2, 128, 128);

        var ix = (Number(id[0]) * 2);
        var iy  = (Number(id[1]) * 2);

        var lod1 = new THREE.LOD();
        var url1 = getURL(ix + "," + iy, width_ / 2, height_ / 2, zoom_ / 2, level_ + 1);
        
        var uniforms1 = {

            satellite: {
                type: "t",
                value: loader.load(url1)
            }
            
        };

        var glsl1 = new THREE.ShaderMaterial({

        uniforms: uniforms1,
        vertexShader: document.getElementById("vertexTerrain").textContent,
        fragmentShader: document.getElementById("fragmentTerrain").textContent,
        lights: false,
        fog: false,
        transparent: true

        });

        glsl1.extensions.derivatives = true;
        
        plane = new THREE.Mesh(geometry, glsl1);
        plane.rotation.set(-Math.PI / 2, 0, 0);
        lod1.addLevel(plane, zoom_ / 2);
        lod1.position.set(-width_ / 4, 0, -height_ / 4);
        lod1.name = ix + "," + iy;
        group.add(lod1);

        var lod2 = new THREE.LOD();
        var url2 = getURL(ix + "," + (iy + 1), width_ / 2, height_ / 2, zoom_ / 2, level_ + 1);
        
        var uniforms2 = {

            satellite: {
                type: "t",
                value: loader.load(url2)
            }
            
        };

        var glsl2 = new THREE.ShaderMaterial({

        uniforms: uniforms2,
        vertexShader: document.getElementById("vertexTerrain").textContent,
        fragmentShader: document.getElementById("fragmentTerrain").textContent,
        lights: false,
        fog: false,
        transparent: true

        });

        glsl2.extensions.derivatives = true;
        
        plane = new THREE.Mesh(geometry, glsl2);
        plane.rotation.set(-Math.PI / 2, 0, 0);
        lod2.addLevel(plane, zoom_ / 2);
        lod2.position.set(width_ / 4, 0, -height_ / 4);
        lod2.name = ix + "," + (iy + 1);
        group.add(lod2);

        var lod3 = new THREE.LOD();
        var url3 = getURL((ix + 1) + "," + iy, width_ / 2, height_ / 2, zoom_ / 2, level_ + 1);
        
        var uniforms3 = {

            satellite: {
                type: "t",
                value: loader.load(url3)
            }
            
        };

        var glsl3 = new THREE.ShaderMaterial({

        uniforms: uniforms3,
        vertexShader: document.getElementById("vertexTerrain").textContent,
        fragmentShader: document.getElementById("fragmentTerrain").textContent,
        lights: false,
        fog: false,
        transparent: true

        });

        glsl3.extensions.derivatives = true;
        
        plane = new THREE.Mesh(geometry, glsl3);
        plane.rotation.set(-Math.PI / 2, 0, 0);
        lod3.addLevel(plane, zoom_ / 2);
        lod3.position.set(-width_ / 4, 0, height_ / 4);
        lod3.name = (ix + 1) + "," + iy;
        group.add(lod3);

        var lod4 = new THREE.LOD();
        var url4 = getURL((ix + 1) + "," + (iy + 1), width_ / 2, height_ / 2, zoom_ / 2, level_ + 1);
        
        var uniforms4 = {

            satellite: {
                type: "t",
                value: loader.load(url4)
            }
            
        };

        var glsl4 = new THREE.ShaderMaterial({

        uniforms: uniforms4,
        vertexShader: document.getElementById("vertexTerrain").textContent,
        fragmentShader: document.getElementById("fragmentTerrain").textContent,
        lights: false,
        fog: false,
        transparent: true

        });

        glsl4.extensions.derivatives = true;
        
        plane = new THREE.Mesh(geometry, glsl4);
        plane.rotation.set(-Math.PI / 2, 0, 0);
        lod4.addLevel(plane, zoom_ / 2);
        lod4.position.set(width_ / 4, 0, height_ / 4);
        lod4.name = (ix + 1) + "," + (iy + 1);
        group.add(lod4);

        parent_.addLevel(group, zoom_ / 2);

        generateTiles(lod1, width_ / 2, height_ / 2, zoom_ / 2, level_ + 1, colors[level_]);
        generateTiles(lod2, width_ / 2, height_ / 2, zoom_ / 2, level_ + 1, colors[level_]);
        generateTiles(lod3, width_ / 2, height_ / 2, zoom_ / 2, level_ + 1, colors[level_]);
        generateTiles(lod4, width_ / 2, height_ / 2, zoom_ / 2, level_ + 1, colors[level_]);
        
    }

}
    
function getURL(name_, width_, height_, zoom_, level_){
    
    var id = name_.split(",");  
    return folder + "/textures/level" + level_ + "/" + id[0] + "_" + id[1] + ".jpg"; 

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

</head>
<body>

<script id="vertexTerrain" type="x-shader/x-vertex">

uniform sampler2D satellite;
varying vec2 vUv;

void main() {

  vUv = uv;
  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
  gl_Position = projectionMatrix * mvPosition;


}

</script>
    
<script id="fragmentTerrain" type="x-shader/x-fragment">

precision highp float;
precision highp int;

uniform sampler2D satellite;

varying vec2 vUv;

void main() {

    gl_FragColor = texture2D(satellite, vUv);

}
        
</script>

    
</body>
</html>


Solution

  • Looking at the code you could scan your lods, see what their current level is, and check if it's loaded or not?

    body {
      margin: 0;
    }
    #c {
      width: 100vw;
      height: 100vh;
      display: block;
    }
    <canvas id="c"></canvas>
    <script type="module">
    import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js';
    import {OrbitControls} from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/examples/jsm/controls/OrbitControls.js';
    
    function main() {
      const canvas = document.querySelector('#c');
      const renderer = new THREE.WebGLRenderer({canvas});
    
      const fov = 75;
      const aspect = 2;  // the canvas default
      const near = 0.1;
      const far = 500;
      const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
      camera.position.z = 2;
    
      const controls = new OrbitControls(camera, canvas);
      controls.update();
    
      const scene = new THREE.Scene();
    
      {
        const color = 0xFFFFFF;
        const intensity = 1;
        const light = new THREE.DirectionalLight(color, intensity);
        light.position.set(-1, 2, 4);
        scene.add(light);
      }
    
      const numLevels = 4;
      const lodInfos = [];
      function createLod(pos) {
        const lod = new THREE.LOD();
        lod.position.set(...pos);
        scene.add(lod);
        for (let level = 0; level < numLevels; ++level) {
          const obj = new THREE.Object3D();
          lod.addLevel(obj, 3 + Math.pow(2, level));
        }
        lodInfos.push({
          lod,
          levels: [],
        });
      }
      
      createLod([0, 0, 0]);
      
      function scanLods() {
        for (const {lod, levels} of lodInfos) {
          const level = lod.getCurrentLevel();
          if (!levels[level]) {
            // this level is not loaded
            levels[level] = true; // mark it as loaded
            
            // load it
            loadLodLevel(level, lod.levels[level].object);
            
            // optimization: if all levels are loaded 
            // remove this from the lodInfos
          }
        }
      }
      
      function loadLodLevel(level, obj) {
        // obviously I'd use some kind of data structure but just to
        // get something working
        let geometry;
        let material;
        switch(level) {
          case 0:
            geometry = new THREE.BoxBufferGeometry(1, 1, 1);
            material = new THREE.MeshPhongMaterial({color: 'red'});
            break;
          case 1:
            geometry = new THREE.SphereBufferGeometry(0.5, 12, 6);
            material = new THREE.MeshPhongMaterial({color: 'yellow'});
            break;
          case 2:
            geometry = new THREE.ConeBufferGeometry(0.5, 1, 12);
            material = new THREE.MeshPhongMaterial({color: 'green'});
            break;
          case 3:
            geometry = new THREE.CylinderBufferGeometry(0.5, 0.5, 1, 12);
            material = new THREE.MeshPhongMaterial({color: 'purple'});
            break;
        }
        const lodMesh = new THREE.Mesh(geometry, material);
        obj.add(lodMesh);
      }
    
      function resizeRendererToDisplaySize(renderer) {
        const canvas = renderer.domElement;
        const width = canvas.clientWidth;
        const height = canvas.clientHeight;
        const needResize = canvas.width !== width || canvas.height !== height;
        if (needResize) {
          renderer.setSize(width, height, false);
        }
        return needResize;
      }
    
      function render(time) {
        time *= 0.001;
    
        if (resizeRendererToDisplaySize(renderer)) {
          const canvas = renderer.domElement;
          camera.aspect = canvas.clientWidth / canvas.clientHeight;
          camera.updateProjectionMatrix();
        }
    
        scanLods();
    
        renderer.render(scene, camera);
    
        requestAnimationFrame(render);
      }
    
      requestAnimationFrame(render);
    }
    
    main();
    </script>

    The solution above adds a THREE.Object3D for each lod and then childs a mesh to it when it's made visible.

    You could also replace the THREE.Object3D so instead of

    obj.add(lodMesh);
    

    it would be something like

    obj.levels[level].object = lodMesh;
    obj.parent.add(lodMesh);
    obj.parent.remove(obj);