Search code examples
three.jsglslwebglshadervertex-shader

Buffered Geometry billboarding vertices in the vertex shader


I came across this truly excellent example of how to implement billboarding via a vertex shader to offload the hard work of drawing and rotating a large number of labels to always face the camera.

var scene;
    var book;
    var shaderMaterial;

    var renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    renderer.setClearColor(0x000000);
    document.body.appendChild(renderer.domElement);

    var camera = new THREE.PerspectiveCamera(55, 1, 0.1, 40000);


    window.onresize = function () {
        renderer.setSize(window.innerWidth, window.innerHeight);
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
    };
    window.onresize();

    scene = new THREE.Scene();

    camera.position.z = 25;
    camera.position.y = 15;
    scene.add(camera);


    var grid = new THREE.GridHelper(100, 10);
    scene.add(grid);


    var controls = new THREE.OrbitControls(camera);
    controls.damping = 0.2;
    var lettersPerSide = 16;

    function createGlpyhSheet() {

        var fontSize = 64;

        var c = document.createElement('canvas');
        c.width = c.height = fontSize * lettersPerSide;
        var ctx = c.getContext('2d');
        ctx.font = fontSize + 'px Monospace';
        var i = 0;

        for (var y = 0; y < lettersPerSide; y++) {
            for (var x = 0; x < lettersPerSide; x++, i++) {
                var ch = String.fromCharCode(i);
                ctx.fillText(ch, x * fontSize, -(8 / 32) * fontSize + (y + 1) * fontSize);
            }
        }

        var tex = new THREE.Texture(c);
        tex.flipY = false;
        tex.needsUpdate = true;

        return tex;
    }


    function createLabels(textArrays, positions) {
        //console.log(textArrays, positions);

        var master_geometry = new THREE.Geometry();


        for (var k = 0; k < textArrays.length; k++) {

            var geo = new THREE.Geometry();
            geo.dynamic = true;

            var str = textArrays[k];
            var vec = positions[k];
            //console.log(shaderMaterial);

            //console.log('str is', str, 'vec is', vec);


            var j = 0,
                ln = 0;

            for (i = 0; i < str.length; i++) {

                //console.log('creating glyph', str[i]);

                var code = str.charCodeAt(i);
                var cx = code % lettersPerSide;
                var cy = Math.floor(code / lettersPerSide);
                var oneDotOne = .55;

                geo.vertices.push(
                new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 0.05, 0),
                new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 0.05, 0),
                new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 1.05, 0),
                new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 1.05, 0));
                shaderMaterial.attributes.labelpos.value.push(vec);
                shaderMaterial.attributes.labelpos.value.push(vec);
                shaderMaterial.attributes.labelpos.value.push(vec);
                shaderMaterial.attributes.labelpos.value.push(vec);

                var face = new THREE.Face3(i * 4 + 0, i * 4 + 1, i * 4 + 2);
                geo.faces.push(face);
                face = new THREE.Face3(i * 4 + 0, i * 4 + 2, i * 4 + 3);
                geo.faces.push(face);

                var ox = (cx + 0.05) / lettersPerSide;
                var oy = (cy + 0.05) / lettersPerSide;
                var off = 0.9 / lettersPerSide;

                geo.faceVertexUvs[0].push([
                new THREE.Vector2(ox, oy + off),
                new THREE.Vector2(ox + off, oy + off),
                new THREE.Vector2(ox + off, oy)]);
                geo.faceVertexUvs[0].push([
                new THREE.Vector2(ox, oy + off),
                new THREE.Vector2(ox + off, oy),
                new THREE.Vector2(ox, oy)]);
                if (code == 10) {
                    ln--;
                    j = 0;
                } else {
                    j++;
                }
            }

            // i can only get this working with merge.
            // Building one giant geometry doesn't work for some reason
            master_geometry.merge(geo);

        }

        console.log(shaderMaterial);
        shaderMaterial.attributes.labelpos.needsUpdate = true;

        book = new THREE.Mesh(
        master_geometry,
        shaderMaterial);

        //book.doubleSided = true;
        scene.add(book);

    }


    var uniforms = {
        map: {
            type: "t",
            value: createGlpyhSheet()
        }
    };

    var attributes = {
        labelpos: {
            type: 'v3',
            value: []
        }
    };

    shaderMaterial = new THREE.ShaderMaterial({
        attributes: attributes,
        uniforms: uniforms,
        vertexShader: document.querySelector('#vertex').textContent,
        fragmentShader: document.querySelector('#fragment').textContent
    });
    shaderMaterial.transparent = true;
    shaderMaterial.depthTest = false;


    strings = [];
    vectors = [];
    var sizeOfWorld = 100;
    var halfSize = sizeOfWorld * 0.5;

    for (var i = 0; i < 500; i++) {

        strings.push('test' + i);
        var vector = new THREE.Vector3();
        vector.x = Math.random() * sizeOfWorld - halfSize;
        vector.y = Math.random() * sizeOfWorld - halfSize;
        vector.z = Math.random() * sizeOfWorld - halfSize;
        vectors.push(vector);

    }

    console.log('creating labels');
    createLabels(strings, vectors);

    function animate() {
        controls.update();
        renderer.render(scene, camera);
        requestAnimationFrame(animate, renderer.domElement);
    }

    animate();
html {
            background-color: #ffffff;
        }
        * {
            margin: 0;
            padding: 0;
        }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/69/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/4862f5f1111346a957ac3e0cb0858be1568d0e03/examples/js/controls/OrbitControls.js"></script>
<script id="vertex" type="text/x-glsl-vert">
    varying vec2 vUv;
    attribute vec3 labelpos;

    void main() {
        vUv = uv;


        gl_Position = projectionMatrix * 
                      (modelViewMatrix * vec4(labelpos, 1) +
                       vec4(position.xy, 0, 0));

    }
</script>
<script id="fragment" type="text/x-glsl-frag">
    varying vec2 vUv;
    uniform sampler2D map;
    void main() {
        vec4 diffuse = texture2D(map, vUv);
        vec4 letters = mix(diffuse, vec4(1.0, 1.0, 1.0, diffuse.a), 1.0);
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * letters;
    }
</script>

The code was created before THREE js transitioned away from allowing attributes in uniforms, and enforcing we now use buffered Geometry instead. After some digging I found you can easily create a buffered Geometry from a standard geometry using:

buffGeometry = new THREE.BufferGeometry().fromGeometry( <my old Geometry object> );

How cool is that! - works a treat, however I cannot work out how or where to pass the long list of attribute vec3's to the shader to tell it where my mid point for each label should be, to achieve the same effect as the older example given.

Has anyone got any ideas on how to solve this? The example posted is exactly what I am after, but I really don't want to be stuck using an old version of THREE for the rest of time...

Many thanks for any suggestions :)

FR


Solution

  • So after much experimentation I figured it out myself - go me.

    You convert the old Geometry object to a THREE.BufferGeometry() using the aforementioned fromGeometry() function, create an Float32Array array of the location of each labels x,y,z coordinates for each and every vertices and pass that array to the BufferGeometry via the addAttribute function, the shader knows both where to draw the labels and where to pivot when rotating the camera, re-creating the billboard effect using the latest version of THREE.js. 8) See working attached code example, hope someone else finds this useful! :)

    var scene;
    var book;
    var shaderMaterial;
    var stats;
    var container;
    
    container = document.createElement('div');
    document.body.appendChild(container);
    
    var renderer = new THREE.WebGLRenderer({
      antialias: true
    });
    
    renderer.setClearColor(0x000000);
    document.body.appendChild(renderer.domElement);
    
    var camera = new THREE.PerspectiveCamera(55, 1, 0.1, 40000);
    
    window.onresize = function() {
      renderer.setSize(window.innerWidth, window.innerHeight);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    };
    
    window.onresize();
    
    scene = new THREE.Scene();
    
    camera.position.z = 25;
    camera.position.y = 15;
    scene.add(camera);
    
    var labelPosArray = [];
    
    var grid = new THREE.GridHelper(100, 10);
    scene.add(grid);
    
    stats = new Stats();
    container.appendChild(stats.dom);
    container.appendChild(renderer.domElement);
    
    var controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.damping = 0.2;
    var lettersPerSide = 16;
    
    function createGlpyhSheet() {
    
      var fontSize = 64;
    
      var c = document.createElement('canvas');
      c.width = c.height = fontSize * lettersPerSide;
      var ctx = c.getContext('2d');
      ctx.font = fontSize + 'px Monospace';
      var i = 0;
    
      for (var y = 0; y < lettersPerSide; y++) {
        for (var x = 0; x < lettersPerSide; x++, i++) {
          var ch = String.fromCharCode(i);
          ctx.fillText(ch, x * fontSize, -(8 / 32) * fontSize + (y + 1) * fontSize);
        }
      }
    
      var tex = new THREE.Texture(c);
      tex.flipY = false;
      tex.needsUpdate = true;
    
      return tex;
    
    }
    
    function createLabels(textArrays, positions) {
    
      var master_geometry = new THREE.Geometry();
    
      for (var k = 0; k < textArrays.length; k++) {
    
        var geo = new THREE.Geometry();
        geo.dynamic = true;
    
        var str = textArrays[k];
        var vec = positions[k];
    
        var j = 0,
          ln = 0;
    
        for (i = 0; i < str.length; i++) {
    
          var code = str.charCodeAt(i);
          var cx = code % lettersPerSide;
          var cy = Math.floor(code / lettersPerSide);
          var oneDotOne = .55;
    
          geo.vertices.push(
            new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 0.05, 0),
            new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 0.05, 0),
            new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 1.05, 0),
            new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 1.05, 0));
    
          labelPosArray.push(vec);
          labelPosArray.push(vec);
          labelPosArray.push(vec);
          labelPosArray.push(vec);
          labelPosArray.push(vec);
          labelPosArray.push(vec);
    
          var face = new THREE.Face3(i * 4 + 0, i * 4 + 1, i * 4 + 2);
          geo.faces.push(face);
          face = new THREE.Face3(i * 4 + 0, i * 4 + 2, i * 4 + 3);
          geo.faces.push(face);
    
          var ox = (cx + 0.05) / lettersPerSide;
          var oy = (cy + 0.05) / lettersPerSide;
          var off = 0.9 / lettersPerSide;
    
          geo.faceVertexUvs[0].push([
            new THREE.Vector2(ox, oy + off),
            new THREE.Vector2(ox + off, oy + off),
            new THREE.Vector2(ox + off, oy)
          ]);
          geo.faceVertexUvs[0].push([
            new THREE.Vector2(ox, oy + off),
            new THREE.Vector2(ox + off, oy),
            new THREE.Vector2(ox, oy)
          ]);
          if (code == 10) {
            ln--;
            j = 0;
          } else {
            j++;
          }
        }
    
        master_geometry.merge(geo);
    
      }
    
      var lps = new Float32Array(labelPosArray.length * 3);
      var cnt = 0;
    
      for (i = 0; i < labelPosArray.length; i++) {
    
        lps[cnt++] = labelPosArray[i].x;
        lps[cnt++] = labelPosArray[i].y;
        lps[cnt++] = labelPosArray[i].z;
    
      } // for
    
      buffGeometry = new THREE.BufferGeometry().fromGeometry(master_geometry);
    
      buffGeometry.addAttribute('labelpos', new THREE.BufferAttribute(lps, 3));
    
      book = new THREE.Mesh(
        buffGeometry,
        shaderMaterial);
    
      scene.add(book);
    
    }
    
    var uniforms = {
      map: {
        type: "t",
        value: createGlpyhSheet()
      }
    };
    
    shaderMaterial = new THREE.ShaderMaterial({
      uniforms: uniforms,
      vertexShader: document.querySelector('#vertex').textContent,
      fragmentShader: document.querySelector('#fragment').textContent
    });
    
    shaderMaterial.transparent = true;
    shaderMaterial.depthTest = false;
    
    strings = [];
    vectors = [];
    var sizeOfWorld = 100;
    var halfSize = sizeOfWorld * 0.5;
    
    for (var i = 0; i < 500; i++) {
    
      strings.push('label ' + i);
      var vector = new THREE.Vector3();
      vector.x = Math.random() * sizeOfWorld - halfSize;
      vector.y = Math.random() * sizeOfWorld - halfSize;
      vector.z = Math.random() * sizeOfWorld - halfSize;
      vectors.push(vector);
    
    }
    
    //console.log('creating labels');
    createLabels(strings, vectors);
    
    function animate() {
    
      controls.update();
      renderer.render(scene, camera);
      requestAnimationFrame(animate, renderer.domElement);
      stats.update();
    
    }
    
    animate();
    html {
      background-color: #ffffff;
    }
    
    * {
      margin: 0;
      padding: 0;
    }
    <script src="https://raw.githack.com/mrdoob/three.js/r124/build/three.js"></script>
    <script src="https://raw.githack.com/mrdoob/three.js/r124/examples/js/controls/OrbitControls.js"></script>
    
      <script src="https://raw.githack.com/mrdoob/three.js/r124/examples/js/libs/stats.min.js"></script>
    
    <script id="vertex" type="text/x-glsl-vert">
      varying vec2 vUv; attribute vec3 labelpos; void main() { vUv = uv; gl_Position = projectionMatrix * (modelViewMatrix * vec4(labelpos, 1) + vec4(position.xy, 0, 0)); }
    </script>
    <script id="fragment" type="text/x-glsl-frag">
      varying vec2 vUv; uniform sampler2D map; void main() { vec4 diffuse = texture2D(map, vUv); vec4 letters = mix(diffuse, vec4(1.0, 1.0, 1.0, diffuse.a), 1.0); gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * letters; }
    </script>