Logarithmic Z Buffer Zoom

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Verge3D webgl - cameras - logarithmic depth buffer</title>
    <meta charset="windows-1252">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <style>
      body {
        color: #808080;
        font-family:Monospace;
        font-size:13px;
        text-align:center;
        background-color: #000;
        margin: 0px;
        overflow: hidden;
      }
      #info {
        position: absolute;
        top: 0px; width: 100%;
        padding: 5px;
        z-index: 100;
        color: #ddd;
        text-shadow: 0 0 1px rgba(0,0,0,1);
      }
      a {
        color: #0080ff;
      }
      b { color: lightgreen }
      .renderer_label {
        position: absolute;
        bottom: 1em;
        width: 100%;
        color: white;
        z-index: 10;
        display: block;
        text-align: center;
      }
      #container {
        white-space: nowrap;
      }
      #container_normal {
        width: 50%;
        display: inline-block;
        position: relative;
        overflow: hidden;
      }
      #container_logzbuf {
        width: 50%;
        display: inline-block;
        position: relative;
        overflow: hidden;
      }
      #renderer_border {
        position: absolute;
        top: 0;
        bottom: 0;
        width: 2px;
        z-index: 10;
        opacity: .8;
        background: #ccc;
        border: 1px inset #ccc;
        cursor: col-resize;
      }
    </style>
  </head>
  <body>

    <div id="container">
      <div id="container_normal"><h2 class="renderer_label">normal z-buffer</h2></div><div id="container_logzbuf"><h2 class="renderer_label">logarithmic z-buffer</h2></div>
      <div id="renderer_border"></div>
    </div>

    <div id="info">
      <a href="https://www.soft8soft.com/verge3d" target="_blank" rel="noopener">Verge3D</a> - cameras - logarithmic depth buffer<br/>
      Zoom through scene with objects ranging in size from 1µm to 100,000,000 light years using the mousewheel<br/>
      Linear z-buffer handles close-up objects well, but fails spectacularly at distant objects<br/>
      Logarithmic handles all but the smallest objects with ease
    </div>

    <script src="../build/v3d.js"></script>
    <script src="js/libs/stats.min.js"></script>

    <script>
      // 1 micrometer to 100 billion light years in one scene, with 1 unit = 1 meter?  preposterous!  and yet...
      var NEAR = 1e-6, FAR = 1e27;
      var SCREEN_WIDTH = window.innerWidth;
      var SCREEN_HEIGHT = window.innerHeight;
      var screensplit = .25, screensplit_right = 0;
      var mouse = [.5, .5];
      var zoompos = - 100, minzoomspeed = .015;
      var zoomspeed = minzoomspeed;
      var container, border, stats;
      var objects = {};
      // Generate a number of text labels, from 1µm in size up to 100,000,000 light years
      // Try to use some descriptive real-world examples of objects at each scale
      var labeldata = [
        { size: .01, scale: 0.0001, label: "microscopic (1µm)" }, // FIXME - triangulating text fails at this size, so we scale instead
        { size: .01, scale: 0.1, label: "minuscule (1mm)" },
        { size: .01, scale: 1.0, label: "tiny (1cm)" },
        { size: 1, scale: 1.0, label: "child-sized (1m)" },
        { size: 10, scale: 1.0, label: "tree-sized (10m)" },
        { size: 100, scale: 1.0, label: "building-sized (100m)" },
        { size: 1000, scale: 1.0, label: "medium (1km)" },
        { size: 10000, scale: 1.0, label: "city-sized (10km)" },
        { size: 3400000, scale: 1.0, label: "moon-sized (3,400 Km)" },
        { size: 12000000, scale: 1.0, label: "planet-sized (12,000 km)" },
        { size: 1400000000, scale: 1.0, label: "sun-sized (1,400,000 km)" },
        { size: 7.47e12, scale: 1.0, label: "solar system-sized (50Au)" },
        { size: 9.4605284e15, scale: 1.0, label: "gargantuan (1 light year)" },
        { size: 3.08567758e16, scale: 1.0, label: "ludicrous (1 parsec)" },
        { size: 1e19, scale: 1.0, label: "mind boggling (1000 light years)" },
        { size: 1.135e21, scale: 1.0, label: "galaxy-sized (120,000 light years)" },
        { size: 9.46e23, scale: 1.0, label: "... (100,000,000 light years)" }
      ];
      init();
      function init() {
        container = document.getElementById('container');
        var loader = new v3d.FontLoader();
        loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
          var scene = initScene(font);
          // Initialize two copies of the same scene, one with normal z-buffer and one with logarithmic z-buffer
          objects.normal = initView(scene, 'normal', false);
          objects.logzbuf = initView(scene, 'logzbuf', true);
          animate();
        });
        stats = new Stats();
        container.appendChild(stats.dom);
        // Resize border allows the user to easily compare effects of logarithmic depth buffer over the whole scene
        border = document.getElementById('renderer_border');
        border.addEventListener('mousedown', onBorderMouseDown);
        window.addEventListener('mousemove', onMouseMove, false);
        window.addEventListener('resize', onWindowResize, false);
        window.addEventListener('wheel', onMouseWheel, false);
      }
      function initView(scene, name, logDepthBuf) {
        var framecontainer = document.getElementById('container_' + name);
        var camera = new v3d.PerspectiveCamera(50, screensplit * SCREEN_WIDTH / SCREEN_HEIGHT, NEAR, FAR);
        scene.add(camera);
        var renderer = new v3d.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: logDepthBuf });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(SCREEN_WIDTH / 2, SCREEN_HEIGHT);
        renderer.domElement.style.position = "relative";
        renderer.domElement.id = 'renderer_' + name;
        framecontainer.appendChild(renderer.domElement);
        return { container: framecontainer, renderer: renderer, scene: scene, camera: camera };
      }
      function initScene(font) {
        var scene = new v3d.Scene();
        scene.add(new v3d.AmbientLight(0x222222));
        var light = new v3d.DirectionalLight(0xffffff, 1);
        light.position.set(100, 100, 100);
        scene.add(light);
        var materialargs = {
          color: 0xffffff,
          specular: 0x050505,
          shininess: 50,
          emissive: 0x000000
        };
        var geometry = new v3d.SphereBufferGeometry(0.5, 24, 12);
        for (var i = 0; i < labeldata.length; i++) {
          var scale = labeldata[i].scale || 1;
          var labelgeo = new v3d.TextBufferGeometry(labeldata[i].label, {
            font: font,
            size: labeldata[i].size,
            height: labeldata[i].size / 2
          });
          labelgeo.computeBoundingSphere();
          // center text
          labelgeo.translate(- labelgeo.boundingSphere.radius, 0, 0);
          materialargs.color = new v3d.Color().setHSL(Math.random(), 0.5, 0.5);
          var material = new v3d.MeshPhongMaterial(materialargs);
          var group = new v3d.Group();
          group.position.z = - labeldata[i].size * scale;
          scene.add(group);
          var textmesh = new v3d.Mesh(labelgeo, material);
          textmesh.scale.set(scale, scale, scale);
          textmesh.position.z = - labeldata[i].size * scale;
          textmesh.position.y = labeldata[i].size / 4 * scale;
          group.add(textmesh);
          var dotmesh = new v3d.Mesh(geometry, material);
          dotmesh.position.y = - labeldata[i].size / 4 * scale;
          dotmesh.scale.multiplyScalar(labeldata[i].size * scale);
          group.add(dotmesh);
        }
        return scene;
      }
      function updateRendererSizes() {
        // Recalculate size for both renderers when screen size or split location changes
        SCREEN_WIDTH = window.innerWidth;
        SCREEN_HEIGHT = window.innerHeight;
        screensplit_right = 1 - screensplit;
        objects.normal.renderer.setSize(screensplit * SCREEN_WIDTH, SCREEN_HEIGHT);
        objects.normal.camera.aspect = screensplit * SCREEN_WIDTH / SCREEN_HEIGHT;
        objects.normal.camera.updateProjectionMatrix();
        objects.normal.camera.setViewOffset(SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, SCREEN_WIDTH * screensplit, SCREEN_HEIGHT);
        objects.normal.container.style.width = (screensplit * 100) + '%';
        objects.logzbuf.renderer.setSize(screensplit_right * SCREEN_WIDTH, SCREEN_HEIGHT);
        objects.logzbuf.camera.aspect = screensplit_right * SCREEN_WIDTH / SCREEN_HEIGHT;
        objects.logzbuf.camera.updateProjectionMatrix();
        objects.logzbuf.camera.setViewOffset(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_WIDTH * screensplit, 0, SCREEN_WIDTH * screensplit_right, SCREEN_HEIGHT);
        objects.logzbuf.container.style.width = (screensplit_right * 100) + '%';
        border.style.left = (screensplit * 100) + "%";
      }
      function animate() {
        requestAnimationFrame(animate);
        render();
      }
      function render() {
        // Put some limits on zooming
        var minzoom = labeldata[0].size * labeldata[0].scale * 1;
        var maxzoom = labeldata[labeldata.length - 1].size * labeldata[labeldata.length - 1].scale * 100;
        var damping = (Math.abs(zoomspeed) > minzoomspeed ? .95 : 1.0);
        // Zoom out faster the further out you go
        var zoom = v3d.Math.clamp(Math.pow(Math.E, zoompos), minzoom, maxzoom);
        zoompos = Math.log(zoom);
        // Slow down quickly at the zoom limits
        if ((zoom == minzoom && zoomspeed < 0) || (zoom == maxzoom && zoomspeed > 0)) {
          damping = .85;
        }
        zoompos += zoomspeed;
        zoomspeed *= damping;
        objects.normal.camera.position.x = Math.sin(.5 * Math.PI * (mouse[0] - .5)) * zoom;
        objects.normal.camera.position.y = Math.sin(.25 * Math.PI * (mouse[1] - .5)) * zoom;
        objects.normal.camera.position.z = Math.cos(.5 * Math.PI * (mouse[0] - .5)) * zoom;
        objects.normal.camera.lookAt(objects.normal.scene.position);
        // Clone camera settings across both scenes
        objects.logzbuf.camera.position.copy(objects.normal.camera.position);
        objects.logzbuf.camera.quaternion.copy(objects.normal.camera.quaternion);
        // Update renderer sizes if the split has changed
        if (screensplit_right != 1 - screensplit) {
          updateRendererSizes();
        }
        objects.normal.renderer.render(objects.normal.scene, objects.normal.camera);
        objects.logzbuf.renderer.render(objects.logzbuf.scene, objects.logzbuf.camera);
        stats.update();
      }
      function onWindowResize() {
        updateRendererSizes();
      }
      function onBorderMouseDown(ev) {
        // activate draggable window resizing bar
        window.addEventListener("mousemove", onBorderMouseMove);
        window.addEventListener("mouseup", onBorderMouseUp);
        ev.stopPropagation();
        ev.preventDefault();
      }
      function onBorderMouseMove(ev) {
        screensplit = Math.max(0, Math.min(1, ev.clientX / window.innerWidth));
        ev.stopPropagation();
      }
      function onBorderMouseUp() {
        window.removeEventListener("mousemove", onBorderMouseMove);
        window.removeEventListener("mouseup", onBorderMouseUp);
      }
      function onMouseMove(ev) {
        mouse[0] = ev.clientX / window.innerWidth;
        mouse[1] = ev.clientY / window.innerHeight;
      }
      function onMouseWheel(ev) {
        var amount = ev.deltaY;
        if (amount === 0) return;
        var dir = amount / Math.abs(amount);
        zoomspeed = dir / 10;
        // Slow down default zoom speed after user starts zooming, to give them more control
        minzoomspeed = 0.001;
      }
    </script>
  </body>
</html>