Three.js:遥远的对象微弱

时间:2019-07-10 20:30:15

标签: javascript three.js html5-canvas textures sprite

我正在研究一个Three.js scene,它可以渲染一些纹理点精灵。这些精灵从一个单独的uniform中获取纹理,而该制服是2D画布,在上面绘制了字母:

enter image description here

虽然所有字母都在此画布上以黑色显示,但是Three.js场景中距离相机远的点在我的场景中显得很隐蔽:

// aliases
var BA = THREE.BufferAttribute,
    IBA = THREE.InstancedBufferAttribute,
    ARR = Float32Array;


function Wordmap() {
  // config
  this.wordScalar = 0.0003; // sizes up words
  this.heightScalar = 0.002; // controls mountain height
  this.sep = 0.9; // separation between characters
  this.maxWords = 1000000; // max number of words to draw
  this.background = '#fff'; // background color
  this.color = '#000'; // text color
  // static
  this.size = 64; // size of each character on canvas
  // state
  this.state = {
    layout: 'grid', // name of the currently active layout
    flying: false, // bool indicating whether we're flying camera
    clock: null, // clock to measure how long we've been flying camera
    transitioning: false, // bool indicating whether layout is transitioning
    transitionQueued: false, // bool indicating whether to run another layout transition
  }
  // data
  this.data = {
    input: null,
    words: [],
    layouts: {},
    heightmap: {},
    characters: {},
  }
  // initialize
  this.init();
}


/**
* Scene
**/


Wordmap.prototype.createScene = function() {
  // generate a scene object
  var scene = new THREE.Scene();

  // generate a camera
  var aspectRatio = window.innerWidth / window.innerHeight;
  var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.001, 10);

  // generate a renderer
  var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
  renderer.sortObjects = false; // make scene.add order draw order
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.domElement.id = 'gl-scene';
  document.body.appendChild(renderer.domElement);

  // generate controls
  var controls = new THREE.TrackballControls(camera, renderer.domElement);
  controls.zoomSpeed = 0.05;
  controls.panSpeed = 0.1;

  // position the camera
  camera.position.set(0.03, -0.80, 1.3);
  camera.up.set(0.00, 0.32, 0.94);
  camera.quaternion.set({_w: 0.81, _x: 0.58, _y: 0.01, _z: 0.00})
  controls.target.set(0.01, 1.00, 0.24);
  controls.update();

  // add ?axes=true to url to see axis helpers for global orientation
  if (window.location.search.includes('axes=true')) {
    var axesHelper = new THREE.AxesHelper(5);
    scene.add(axesHelper);
  }

  // store objects on instance
  this.scene = scene;
  this.camera = camera;
  this.controls = controls;
  this.renderer = renderer;
}


Wordmap.prototype.render = function() {
  requestAnimationFrame(this.render.bind(this));
  this.renderer.render(this.scene, this.camera);
  this.controls.update();
  if (this.state.transitionQueued) {
    this.state.transitionQueued = false;
    this.updateLayout();
  }
}


Wordmap.prototype.onWindowResize = function() {
  this.camera.aspect = window.innerWidth / window.innerHeight;
  this.camera.updateProjectionMatrix();
  this.renderer.setSize(window.innerWidth, window.innerHeight);
  this.setPointScale();
}


/**
* Character canvas
**/

Wordmap.prototype.setCharacters = function() {
  var canvas = document.createElement('canvas'),
      ctx = canvas.getContext('2d'),
      charToCoords = {},
      yOffset = -0.25, // offset to draw full letters w/ baselines...
      xOffset = 0.05; // offset to draw full letter widths
  canvas.width = this.size * 16; // * 16 because we want 16**2 = 256 letters
  canvas.height = this.size * 16; // must set size before setting font size
  canvas.id = 'letter-canvas';
  ctx.font = this.size + 'px Monospace';
  // draw the letters on the canvas
  ctx.fillStyle = this.color;
  for (var x=0; x<16; x++) {
    for (var y=0; y<16; y++) {
      var char = String.fromCharCode((x*16) + y);
      charToCoords[char] = {x: x, y: y};
      ctx.fillText(char, (x+xOffset)*this.size, yOffset*this.size+(y+1)*this.size);
    }
  }
  // build a three canvas with the canvas
  var tex = new THREE.Texture(canvas);
  tex.flipY = false;
  tex.needsUpdate = true;
  // store the character map on the instance
  this.data.characters = {
    map: charToCoords,
    tex: tex,
  }
}


/**
* Heightmap canvas
**/

Wordmap.prototype.getHeightmap = function(cb) {
  var img = new Image();
  img.crossOrigin = 'Anonymous';
  img.onload = function() {
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d');
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);
    cb(ctx.getImageData(0,0, img.width, img.height));
  }
  img.src = 'https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/heightmap.jpg';
}


/**
* Geometry
**/

Wordmap.prototype.addWords = function() {
  var attrs = this.getWordAttrs(),
      geometry = new THREE.InstancedBufferGeometry();
  geometry.addAttribute('uv', new BA(new ARR([0,0]), 2, true, 1));
  geometry.addAttribute('position', new BA(new ARR([0,0,0]), 3, true, 1));
  geometry.addAttribute('translation', new IBA(attrs.translations, 3, true, 1));
  geometry.addAttribute('target', new IBA(attrs.translations, 3, true, 1));
  geometry.addAttribute('texOffset', new IBA(attrs.texOffsets, 2, true, 1));
  // build the mesh
  this.setShaderMaterial();
  var mesh = new THREE.Points(geometry, this.material);
  mesh.frustumCulled = false;
  mesh.name = 'words';
  this.mesh = mesh;
  this.scene.add(mesh);
}


Wordmap.prototype.getWordAttrs = function() {
  var n = 0, // total number of characters among all words
      layout = this.data.layouts[this.state.layout],
      words = layout.words,
      positions = layout.positions;
  for (var i=0; i<words.length; i++) n += words[i].length;
  // build up word attributes
  var attrs = {
    translations: new Float32Array(n * 3),
    texOffsets: new Float32Array(n * 2),
  }
  var iters = {
    translationIter: 0,
    texOffsetIter: 0,
  }
  // assume each word has x y coords assigned
  for (var i=0; i<words.length; i++) {
    var word = words[i],
        x = positions[i][0],
        y = positions[i][1],
        z = positions[i][2] || this.getHeightAt(x, y);
    for (var c=0; c<word.length; c++) {
      var offsets = this.data.characters.map[word[c]] || this.data.characters.map['?'];
      attrs.translations[iters.translationIter++] = x + (this.wordScalar * this.sep * c);
      attrs.translations[iters.translationIter++] = y;
      attrs.translations[iters.translationIter++] = z;
      attrs.texOffsets[iters.texOffsetIter++] = offsets.x;
      attrs.texOffsets[iters.texOffsetIter++] = offsets.y;
    }
  }
  return attrs;
}


Wordmap.prototype.setShaderMaterial = function() {
  this.material = new THREE.RawShaderMaterial({
    vertexShader: document.getElementById('vertex-shader').textContent,
    fragmentShader: document.getElementById('fragment-shader').textContent,
    uniforms: {
      pointScale: { type: 'f', value: 0.0, },
      cellSize:   { type: 'f', value: this.size / (this.size * 16), }, // letter size in map
      tex:        { type: 't', value: this.data.characters.tex, },
      color:      { type: 'f', value: this.getColorUniform() },
      transition: { type: 'f', value: 0.0, },
    },
    //transparent: true,
    defines: {
      WORDS: true,
    }
  });
  this.setPointScale();
}


Wordmap.prototype.getColorUniform = function() {
  return this.color === '#fff' ? 1.0 : 0.0;
}


Wordmap.prototype.getHeightAt = function(x, y) {
  // because x and y axes are scaled -1:1, rescale 0:1
  x = (x+1)/2;
  y = (y+1)/2;
  var row = Math.floor(y * this.data.heightmap.height),
      col = Math.floor(x * this.data.heightmap.width),
      idx = (row * this.data.heightmap.width * 4) + (col * 4),
      z = (this.data.heightmap.data[idx] + Math.random()) * this.heightScalar;
  return z;
}


Wordmap.prototype.init = function() {
  this.setCharacters();
  this.setBackgroundColor();
  this.getHeightmap(function(heightMapData) {
    this.data.heightmap = heightMapData;
    get('https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/wordmap-layouts.json', function(data) {
      this.data.input = data;
      this.parseLayouts();
      this.createScene();
      this.addWords();
      this.render();
      setTimeout(this.flyInCamera.bind(this), 500);
      window.addEventListener('resize', this.onWindowResize.bind(this));
    }.bind(this))
  }.bind(this))
}


Wordmap.prototype.parseLayouts = function() {
  for (var i=0; i<this.data.input.length; i++) {
    var l = this.data.input[i],
        name = l.name || i,
        words = l.words,
        positions = this.center(l.positions),
        wordToCoords = {};
    for (var j=0; j<words.length; j++) {wordToCoords[words[j]] = positions[j];}
    this.data.layouts[name] = {
      words: words,
      positions: positions,
      wordToCoords: wordToCoords,
    }
    // activate the first layout
    if (i == 0 && !this.state.layout) this.state.layout = name;
  }
}


// center an array of vertex positions -1:1 on each axis
Wordmap.prototype.center = function(arr) {
  var max = Number.POSITIVE_INFINITY,
      min = Number.NEGATIVE_INFINITY,
      domX = {min: max, max: min},
      domY = {min: max, max: min},
      domZ = {min: max, max: min};
  // find the min, max of each dimension
  for (var i=0; i<arr.length; i++) {
    var x = arr[i][0],
        y = arr[i][1],
        z = arr[i][2] || 0;
    if (x < domX.min) domX.min = x;
    if (x > domX.max) domX.max = x;
    if (y < domY.min) domY.min = y;
    if (y > domY.max) domY.max = y;
    if (z < domZ.min) domZ.min = z;
    if (z > domZ.max) domZ.max = z;
  }
  var centered = [];
  for (var i=0; i<arr.length; i++) {
    var cx = (((arr[i][0]-domX.min)/(domX.max-domX.min))*2)-1,
        cy = (((arr[i][1]-domY.min)/(domY.max-domY.min))*2)-1,
        cz = (((arr[i][2]-domZ.min)/(domZ.max-domZ.min))*2)-1 || null;
    if (arr[i].length == 3) centered.push([cx, cy, cz]);
    else centered.push([cx, cy]);
  }
  return centered;
}


Wordmap.prototype.queryWords = function(s) {
  var map = this.data.layouts[this.state.layout].wordToCoords;
  return Object.keys(map).filter(function(w) {
    return w.toLowerCase().indexOf(s.toLowerCase()) > -1;
  });
}


Wordmap.prototype.updateLayout = function() {
  if (this.state.transitioning) {
    this.state.transitionQueued = true;
    return;
  }
  this.state.transitioning = true;
  this.setPointScale();
  var attrs = this.getWordAttrs();
  this.mesh.geometry.attributes.target.array = attrs.translations;
  this.mesh.geometry.attributes.target.needsUpdate = true;
  TweenLite.to(this.mesh.material.uniforms.transition, 1, {
    value: 1,
    ease: Power4.easeInOut,
    onComplete: function() {
      requestAnimationFrame(function() {
        this.mesh.geometry.attributes.translation.array = attrs.translations;
        this.mesh.geometry.attributes.translation.needsUpdate = true;
        this.mesh.material.uniforms.transition = {type: 'f', value: 0};
        this.state.transitioning = false;
      }.bind(this))
    }.bind(this)
  })
}


/**
* User callbacks
**/

Wordmap.prototype.setBackgroundColor = function() {
  document.querySelector('body').style.background = this.background;
}


Wordmap.prototype.setTextColor = function() {
  this.setCharacters();
  this.mesh.material.uniforms.tex.value = this.data.characters.tex;
  this.mesh.material.uniforms.color.value = this.getColorUniform();
}


Wordmap.prototype.setPointScale = function() {
  var val = window.devicePixelRatio * window.innerHeight * this.wordScalar;
  this.material.uniforms.pointScale.value = val;
  this.material.uniforms.pointScale.needsUpdate = true;
  this.renderer.setPixelRatio(window.devicePixelRatio);
}


Wordmap.prototype.flyTo = function(coords) {
  if (this.state.flying) return;
  this.state.flying = true;
  // pull out target coordinates
  var self = this,
      x = coords[0],
      y = coords[1],
      z = coords[2] || self.getHeightAt(coords[0], coords[1]),
      z = z + 0.015,
      // specify animation duration
      duration = 3,
      // create objects to use during flight
      aspectRatio = window.innerWidth / window.innerHeight,
      _camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.001, 10),
      _controls = new THREE.TrackballControls(_camera, self.renderer.domElement),
      q0 = self.camera.quaternion.clone(),
      _up = self.camera.up;
  _camera.position.set(x, y, z);
  _controls.target.set(x, y, z);
  _controls.update();
  TweenLite.to(self.camera.position, duration, {
    x: x,
    y: y,
    z: z,
    onStart: function() {
      self.state.clock = new THREE.Clock();
      self.state.clock.start();
    },
    onUpdate: function() {
      var deg = self.state.clock.getElapsedTime() / duration;
      THREE.Quaternion.slerp(q0, _camera.quaternion, self.camera.quaternion, deg);
    },
    onComplete: function() {
      var q = _camera.quaternion,
          p = _camera.position,
          u = _camera.up,
          c = _controls.target;
      self.camera.position.set(p.x, p.y, p.z);
      self.camera.up.set(u.x, u.y, u.z);
      self.camera.quaternion.set(q.x, q.y, q.z, q.w);
      self.controls.target = new THREE.Vector3(c.x, c.y, c.z-1.0);
      self.controls.update();
      self.state.flying = false;
    },
    ease: Power4.easeInOut,
  });
}


Wordmap.prototype.flyInCamera = function() {
  TweenLite.to(this.camera.position, 3.5, {
    z: 0.56,
    ease: Power4.easeInOut,
  });
}


Wordmap.prototype.getWordCoords = function(word) {
  return this.data.layouts[this.state.layout].wordToCoords[word];
}

/**
* Typeahaed
**/

function Typeahead() {
  var input = document.querySelector('#search'), // query box
      typeahead = document.querySelector('#typeahead'), // typeahead options
      button = document.querySelector('#search-button'); // submit button

  input.addEventListener('keyup', function(e) {
    clearTypeahead();
    if (e.keyCode == 13 || e.target.value.length < 2) return;
    var matches = wm.queryWords(e.target.value),
        rendered = {}; // store the rendered objects to prevent cased dupes
    for (var i=0; i<Math.min(50, matches.length); i++) {
      if (!(matches[i].toLowerCase().trim() in rendered)) {
        rendered[ matches[i].toLowerCase().trim() ] = true;
        var elem = document.createElement('div');
        elem.textContent = matches[i];
        elem.onclick = function(str, e) {
          input.value = str;
          submit();
        }.bind(this, matches[i]);
        document.querySelector('#typeahead').appendChild(elem);
      }
    }
  })

  function clearTypeahead(e) {
    typeahead.innerHTML = '';
  }

  function submit() {
    if (!input.value) return;
    var coords = wm.getWordCoords(input.value);
    if (!coords) {
      var elem = document.querySelector('#no-results');
      elem.style.transform = 'translate(0, 75px)';
      setTimeout(function() {
        elem.style.transform = 'translate(0, 24px)';
      }, 1500);
      return;
    }
    wm.flyTo(coords);
    clearTypeahead();
  }

  button.addEventListener('click', submit);
  window.addEventListener('click', clearTypeahead);
  input.addEventListener('keydown', function(e) {
    if (e.keyCode == 13) submit();
    else clearTypeahead();
  });
}


/**
* Main
**/

function get(url, onSuccess, onErr, onProgress) {
  var xmlhttp = new XMLHttpRequest();
  xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == XMLHttpRequest.DONE) {
      if (xmlhttp.status === 200) {
        if (onSuccess) onSuccess(JSON.parse(xmlhttp.responseText));
      } else {
        if (onErr) onErr(xmlhttp)
      }
    };
  };
  xmlhttp.onprogress = function(e) {
    if (onProgress) onProgress(e);
  };
  xmlhttp.open('GET', url, true);
  xmlhttp.send();
};



// create the gui
window.onload = function() {
  wm = new Wordmap();
  typeahead = new Typeahead();

  // build the gui
  gui = new dat.GUI({hideable: false})

  gui.add(wm.state, 'layout', ['grid', 'tsne'])
    .name('layout')
    .onFinishChange(wm.updateLayout.bind(wm))

  gui.add(wm, 'wordScalar', 0.0, 0.001)
    .name('font size')
    .onFinishChange(wm.updateLayout.bind(wm))

  gui.add(wm, 'heightScalar', 0.0, 0.003)
    .name('mountain')
    .onFinishChange(wm.updateLayout.bind(wm))

  gui.addColor(wm, 'background')
    .name('background')
    .onChange(wm.setBackgroundColor.bind(wm))

  gui.add(wm, 'color', ['#fff', '#000'])
    .name('color')
    .onChange(wm.setTextColor.bind(wm))
};
html,
body {
  width: 100%;
  height: 100%;
}

body {
  margin: 0;
  overflow: hidden;
}

body::after {
  content: '';
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2));
}

canvas {
  position: relative;
  z-index: 10;
}

body .dg.ac {
  z-index: 100;
}

#letter-canvas {
  position: fixed;
  top: 0;
  left: 0;
}

#search-container {
  position: absolute;
  top: 23px;
  left: 50%;
  width: 360px;
  margin-left: -180px;
  font-family: courier, monospace;
  z-index: 100;
}

#search,
#search-button {
  padding: 7px 10px;
  font-size: 16px;
  line-height: 16px;
  box-sizing: border-box;
}

#search,
#search-button,
#search-button::before {
  border-radius: 3px;
}

#search {
  border: 1px solid #aaa;
}

#search-button {
  position: relative;
  opacity: 0.7;
  border: 1px solid #797979;
}

#search-button::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: #fff;
  z-index: -1;
}

#search,
#typeahead {
  width: 240px;
  font-family: inherit;
}

#search {
  z-index: 10;
}

#search-button {
  background: #b4cdde;
  color: #485661;
  padding: 8px 18px;
  font-weight: 600;
  letter-spacing: 0.05em;
  font-family: sans-serif;
  cursor: pointer;
}

#typeahead {
  background: #fff;
  max-height: 100px;
  overflow: auto;
  box-sizing: border-box;
  border-bottom-left-radius: 3px;
  border-bottom-right-radius: 3px;
}

.hidden {
  display: none;
}

.displayed {
  display: inline-block;
}

#typeahead div {
  margin: 2px 10px;
  cursor: pointer;
  white-space: nowrap;
}

#typeahead div:hover {
  background: #efefef;
}

#no-results {
  padding: 6px;
  background: firebrick;
  color: #fff;
  font-size: 1em;
  transform: translate(0, 24px);
  display: block;
  margin: 0 auto;
  width: 107px;
  text-align: center;
  position: absolute;
  left: 50%;
  margin-left: -180px;
  z-index: 90;
  font-family: courier;
  border-radius: 3px;
  transition: transform 0.3s;
}
<div id='no-results'>No Results!</div>
<div id='search-container'>
  <div>
    <input id='search' value='pythons'></input>
    <button id='search-button'>SEARCH</button>
  </div>
  <div id='typeahead'></div>
</div>

<script id='vertex-shader' type='x-shader/x-vertex'>
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform vec3 cameraPosition;
uniform float pointScale;
uniform float transition;

attribute vec3 position;
attribute vec3 translation;
attribute vec3 target;
attribute vec2 texOffset;

varying vec2 vTexOffset;

void main() {
  // project this particle
  vec3 raw0 = position + translation;
  vec3 raw1 = position + target;
  vec3 raw = mix(raw0, raw1, clamp(transition, 0.0, 1.0));
  vec4 mvPosition = modelViewMatrix * vec4(raw, 1.0);
  gl_Position = projectionMatrix * mvPosition;

  // make distant points small
  vec4 cam4 = vec4(cameraPosition, 1.0);
  gl_PointSize = (pointScale / -mvPosition.z);

  vTexOffset = texOffset;
}
</script>

<script id='fragment-shader' type='x-shader/x-fragment'>
precision mediump float;

uniform sampler2D tex;
uniform vec3 fogColor;
uniform float cellSize;
uniform float fogNear;
uniform float fogFar;
uniform float color;

varying vec2 vTexOffset;

void main() {
  #ifdef WORDS
    vec2 uv = vTexOffset + vec2(gl_PointCoord.x, gl_PointCoord.y);
    vec2 scaledUv = uv * vec2(cellSize, cellSize);
    gl_FragColor = texture2D(tex, scaledUv);
    if (gl_FragColor.a < 0.01) discard; // discard non-letter pixels
  #else
    // make point circular
    if (length(gl_PointCoord - vec2(0.5)) > 0.5) discard;
    gl_FragColor = vec4(0.7, 0.7, 0.8, 0.5);
  #endif
}
</script>

<script src='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/three.min.js'></script>
<script src='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/trackball-controls.min.js'></script>
<script src='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/tweenlite.min.js'></script>
<script src='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/dat.gui.min.js'></script>

我以为我可以通过将以下内容添加为片段着色器的最后一行来将片段着色器中每个像素的alpha属性设置为1:

gl_FragColor.a = 1.0;

这样做会使显示的字体非常粗大且像素化,我更希望使字体以上面的可视化方式显示。

我还尝试使用上面的方法,并在创建充满字母的2d画布时声明了一个较轻的font-weight属性,这些字母被作为制服发送到场景,但这也没有解决。

有人知道我如何在不增加每个字母“权重”的情况下增加远点/字母的不透明度吗?任何建议都将大有帮助!

1 个答案:

答案 0 :(得分:4)

之所以发生这种情况,是因为将mipmapping应用于您的sprite纹理。当字母被映射为较小的分辨率时,文本的黑色像素将变为灰色。

您可以通过在声明纹理后更改其.minFilter属性来避免使用mipmapped纹理,这是我在以下代码段中所做的:

tex.minFilter = THREE.LinearFilter;

我认为没有进行映射的缩小过滤器的only two options you haveLinearFilterNearestFilter。请记住,禁用mipmapping可能会使您的纹理具有锯齿状外观。

或者,您可以创建自己的mipmap,使它们在Photoshop中不会褪色为灰色,并使用texture.mipmaps进行定义。

// aliases
var BA = THREE.BufferAttribute,
    IBA = THREE.InstancedBufferAttribute,
    ARR = Float32Array;


function Wordmap() {
  // config
  this.wordScalar = 0.0003; // sizes up words
  this.heightScalar = 0.002; // controls mountain height
  this.sep = 0.9; // separation between characters
  this.maxWords = 1000000; // max number of words to draw
  this.background = '#fff'; // background color
  this.color = '#000'; // text color
  // static
  this.size = 64; // size of each character on canvas
  // state
  this.state = {
    layout: 'grid', // name of the currently active layout
    flying: false, // bool indicating whether we're flying camera
    clock: null, // clock to measure how long we've been flying camera
    transitioning: false, // bool indicating whether layout is transitioning
    transitionQueued: false, // bool indicating whether to run another layout transition
  }
  // data
  this.data = {
    input: null,
    words: [],
    layouts: {},
    heightmap: {},
    characters: {},
  }
  // initialize
  this.init();
}


/**
* Scene
**/


Wordmap.prototype.createScene = function() {
  // generate a scene object
  var scene = new THREE.Scene();

  // generate a camera
  var aspectRatio = window.innerWidth / window.innerHeight;
  var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.001, 10);

  // generate a renderer
  var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
  renderer.sortObjects = false; // make scene.add order draw order
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.domElement.id = 'gl-scene';
  document.body.appendChild(renderer.domElement);

  // generate controls
  var controls = new THREE.TrackballControls(camera, renderer.domElement);
  controls.zoomSpeed = 0.05;
  controls.panSpeed = 0.1;

  // position the camera
  camera.position.set(0.03, -0.80, 1.3);
  camera.up.set(0.00, 0.32, 0.94);
  camera.quaternion.set({_w: 0.81, _x: 0.58, _y: 0.01, _z: 0.00})
  controls.target.set(0.01, 1.00, 0.24);
  controls.update();

  // add ?axes=true to url to see axis helpers for global orientation
  if (window.location.search.includes('axes=true')) {
    var axesHelper = new THREE.AxesHelper(5);
    scene.add(axesHelper);
  }

  // store objects on instance
  this.scene = scene;
  this.camera = camera;
  this.controls = controls;
  this.renderer = renderer;
}


Wordmap.prototype.render = function() {
  requestAnimationFrame(this.render.bind(this));
  this.renderer.render(this.scene, this.camera);
  this.controls.update();
  if (this.state.transitionQueued) {
    this.state.transitionQueued = false;
    this.updateLayout();
  }
}


Wordmap.prototype.onWindowResize = function() {
  this.camera.aspect = window.innerWidth / window.innerHeight;
  this.camera.updateProjectionMatrix();
  this.renderer.setSize(window.innerWidth, window.innerHeight);
  this.setPointScale();
}


/**
* Character canvas
**/

Wordmap.prototype.setCharacters = function() {
  var canvas = document.createElement('canvas'),
      ctx = canvas.getContext('2d'),
      charToCoords = {},
      yOffset = -0.25, // offset to draw full letters w/ baselines...
      xOffset = 0.05; // offset to draw full letter widths
  canvas.width = this.size * 16; // * 16 because we want 16**2 = 256 letters
  canvas.height = this.size * 16; // must set size before setting font size
  canvas.id = 'letter-canvas';
  ctx.font = this.size + 'px Monospace';
  // draw the letters on the canvas
  ctx.fillStyle = this.color;
  for (var x=0; x<16; x++) {
    for (var y=0; y<16; y++) {
      var char = String.fromCharCode((x*16) + y);
      charToCoords[char] = {x: x, y: y};
      ctx.fillText(char, (x+xOffset)*this.size, yOffset*this.size+(y+1)*this.size);
    }
  }
  // build a three canvas with the canvas
  var tex = new THREE.Texture(canvas);
  tex.flipY = false;
  tex.minFilter = THREE.LinearFilter;
  tex.needsUpdate = true;
  // store the character map on the instance
  this.data.characters = {
    map: charToCoords,
    tex: tex,
  }
}


/**
* Heightmap canvas
**/

Wordmap.prototype.getHeightmap = function(cb) {
  var img = new Image();
  img.crossOrigin = 'Anonymous';
  img.onload = function() {
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d');
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);
    cb(ctx.getImageData(0,0, img.width, img.height));
  }
  img.src = 'https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/heightmap.jpg';
}


/**
* Geometry
**/

Wordmap.prototype.addWords = function() {
  var attrs = this.getWordAttrs(),
      geometry = new THREE.InstancedBufferGeometry();
  geometry.addAttribute('uv', new BA(new ARR([0,0]), 2, true, 1));
  geometry.addAttribute('position', new BA(new ARR([0,0,0]), 3, true, 1));
  geometry.addAttribute('translation', new IBA(attrs.translations, 3, true, 1));
  geometry.addAttribute('target', new IBA(attrs.translations, 3, true, 1));
  geometry.addAttribute('texOffset', new IBA(attrs.texOffsets, 2, true, 1));
  // build the mesh
  this.setShaderMaterial();
  var mesh = new THREE.Points(geometry, this.material);
  mesh.frustumCulled = false;
  mesh.name = 'words';
  this.mesh = mesh;
  this.scene.add(mesh);
}


Wordmap.prototype.getWordAttrs = function() {
  var n = 0, // total number of characters among all words
      layout = this.data.layouts[this.state.layout],
      words = layout.words,
      positions = layout.positions;
  for (var i=0; i<words.length; i++) n += words[i].length;
  // build up word attributes
  var attrs = {
    translations: new Float32Array(n * 3),
    texOffsets: new Float32Array(n * 2),
  }
  var iters = {
    translationIter: 0,
    texOffsetIter: 0,
  }
  // assume each word has x y coords assigned
  for (var i=0; i<words.length; i++) {
    var word = words[i],
        x = positions[i][0],
        y = positions[i][1],
        z = positions[i][2] || this.getHeightAt(x, y);
    for (var c=0; c<word.length; c++) {
      var offsets = this.data.characters.map[word[c]] || this.data.characters.map['?'];
      attrs.translations[iters.translationIter++] = x + (this.wordScalar * this.sep * c);
      attrs.translations[iters.translationIter++] = y;
      attrs.translations[iters.translationIter++] = z;
      attrs.texOffsets[iters.texOffsetIter++] = offsets.x;
      attrs.texOffsets[iters.texOffsetIter++] = offsets.y;
    }
  }
  return attrs;
}


Wordmap.prototype.setShaderMaterial = function() {
  this.material = new THREE.RawShaderMaterial({
    vertexShader: document.getElementById('vertex-shader').textContent,
    fragmentShader: document.getElementById('fragment-shader').textContent,
    uniforms: {
      pointScale: { type: 'f', value: 0.0, },
      cellSize:   { type: 'f', value: this.size / (this.size * 16), }, // letter size in map
      tex:        { type: 't', value: this.data.characters.tex, },
      color:      { type: 'f', value: this.getColorUniform() },
      transition: { type: 'f', value: 0.0, },
    },
    //transparent: true,
    defines: {
      WORDS: true,
    }
  });
  this.setPointScale();
}


Wordmap.prototype.getColorUniform = function() {
  return this.color === '#fff' ? 1.0 : 0.0;
}


Wordmap.prototype.getHeightAt = function(x, y) {
  // because x and y axes are scaled -1:1, rescale 0:1
  x = (x+1)/2;
  y = (y+1)/2;
  var row = Math.floor(y * this.data.heightmap.height),
      col = Math.floor(x * this.data.heightmap.width),
      idx = (row * this.data.heightmap.width * 4) + (col * 4),
      z = (this.data.heightmap.data[idx] + Math.random()) * this.heightScalar;
  return z;
}


Wordmap.prototype.init = function() {
  this.setCharacters();
  this.setBackgroundColor();
  this.getHeightmap(function(heightMapData) {
    this.data.heightmap = heightMapData;
    get('https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/wordmap-layouts.json', function(data) {
      this.data.input = data;
      this.parseLayouts();
      this.createScene();
      this.addWords();
      this.render();
      setTimeout(this.flyInCamera.bind(this), 500);
      window.addEventListener('resize', this.onWindowResize.bind(this));
    }.bind(this))
  }.bind(this))
}


Wordmap.prototype.parseLayouts = function() {
  for (var i=0; i<this.data.input.length; i++) {
    var l = this.data.input[i],
        name = l.name || i,
        words = l.words,
        positions = this.center(l.positions),
        wordToCoords = {};
    for (var j=0; j<words.length; j++) {wordToCoords[words[j]] = positions[j];}
    this.data.layouts[name] = {
      words: words,
      positions: positions,
      wordToCoords: wordToCoords,
    }
    // activate the first layout
    if (i == 0 && !this.state.layout) this.state.layout = name;
  }
}


// center an array of vertex positions -1:1 on each axis
Wordmap.prototype.center = function(arr) {
  var max = Number.POSITIVE_INFINITY,
      min = Number.NEGATIVE_INFINITY,
      domX = {min: max, max: min},
      domY = {min: max, max: min},
      domZ = {min: max, max: min};
  // find the min, max of each dimension
  for (var i=0; i<arr.length; i++) {
    var x = arr[i][0],
        y = arr[i][1],
        z = arr[i][2] || 0;
    if (x < domX.min) domX.min = x;
    if (x > domX.max) domX.max = x;
    if (y < domY.min) domY.min = y;
    if (y > domY.max) domY.max = y;
    if (z < domZ.min) domZ.min = z;
    if (z > domZ.max) domZ.max = z;
  }
  var centered = [];
  for (var i=0; i<arr.length; i++) {
    var cx = (((arr[i][0]-domX.min)/(domX.max-domX.min))*2)-1,
        cy = (((arr[i][1]-domY.min)/(domY.max-domY.min))*2)-1,
        cz = (((arr[i][2]-domZ.min)/(domZ.max-domZ.min))*2)-1 || null;
    if (arr[i].length == 3) centered.push([cx, cy, cz]);
    else centered.push([cx, cy]);
  }
  return centered;
}


Wordmap.prototype.queryWords = function(s) {
  var map = this.data.layouts[this.state.layout].wordToCoords;
  return Object.keys(map).filter(function(w) {
    return w.toLowerCase().indexOf(s.toLowerCase()) > -1;
  });
}


Wordmap.prototype.updateLayout = function() {
  if (this.state.transitioning) {
    this.state.transitionQueued = true;
    return;
  }
  this.state.transitioning = true;
  this.setPointScale();
  var attrs = this.getWordAttrs();
  this.mesh.geometry.attributes.target.array = attrs.translations;
  this.mesh.geometry.attributes.target.needsUpdate = true;
  TweenLite.to(this.mesh.material.uniforms.transition, 1, {
    value: 1,
    ease: Power4.easeInOut,
    onComplete: function() {
      requestAnimationFrame(function() {
        this.mesh.geometry.attributes.translation.array = attrs.translations;
        this.mesh.geometry.attributes.translation.needsUpdate = true;
        this.mesh.material.uniforms.transition = {type: 'f', value: 0};
        this.state.transitioning = false;
      }.bind(this))
    }.bind(this)
  })
}


/**
* User callbacks
**/

Wordmap.prototype.setBackgroundColor = function() {
  document.querySelector('body').style.background = this.background;
}


Wordmap.prototype.setTextColor = function() {
  this.setCharacters();
  this.mesh.material.uniforms.tex.value = this.data.characters.tex;
  this.mesh.material.uniforms.color.value = this.getColorUniform();
}


Wordmap.prototype.setPointScale = function() {
  var val = window.devicePixelRatio * window.innerHeight * this.wordScalar;
  this.material.uniforms.pointScale.value = val;
  this.material.uniforms.pointScale.needsUpdate = true;
  this.renderer.setPixelRatio(window.devicePixelRatio);
}


Wordmap.prototype.flyTo = function(coords) {
  if (this.state.flying) return;
  this.state.flying = true;
  // pull out target coordinates
  var self = this,
      x = coords[0],
      y = coords[1],
      z = coords[2] || self.getHeightAt(coords[0], coords[1]),
      z = z + 0.015,
      // specify animation duration
      duration = 3,
      // create objects to use during flight
      aspectRatio = window.innerWidth / window.innerHeight,
      _camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.001, 10),
      _controls = new THREE.TrackballControls(_camera, self.renderer.domElement),
      q0 = self.camera.quaternion.clone(),
      _up = self.camera.up;
  _camera.position.set(x, y, z);
  _controls.target.set(x, y, z);
  _controls.update();
  TweenLite.to(self.camera.position, duration, {
    x: x,
    y: y,
    z: z,
    onStart: function() {
      self.state.clock = new THREE.Clock();
      self.state.clock.start();
    },
    onUpdate: function() {
      var deg = self.state.clock.getElapsedTime() / duration;
      THREE.Quaternion.slerp(q0, _camera.quaternion, self.camera.quaternion, deg);
    },
    onComplete: function() {
      var q = _camera.quaternion,
          p = _camera.position,
          u = _camera.up,
          c = _controls.target;
      self.camera.position.set(p.x, p.y, p.z);
      self.camera.up.set(u.x, u.y, u.z);
      self.camera.quaternion.set(q.x, q.y, q.z, q.w);
      self.controls.target = new THREE.Vector3(c.x, c.y, c.z-1.0);
      self.controls.update();
      self.state.flying = false;
    },
    ease: Power4.easeInOut,
  });
}


Wordmap.prototype.flyInCamera = function() {
  TweenLite.to(this.camera.position, 3.5, {
    z: 0.56,
    ease: Power4.easeInOut,
  });
}


Wordmap.prototype.getWordCoords = function(word) {
  return this.data.layouts[this.state.layout].wordToCoords[word];
}

/**
* Typeahaed
**/

function Typeahead() {
  var input = document.querySelector('#search'), // query box
      typeahead = document.querySelector('#typeahead'), // typeahead options
      button = document.querySelector('#search-button'); // submit button

  input.addEventListener('keyup', function(e) {
    clearTypeahead();
    if (e.keyCode == 13 || e.target.value.length < 2) return;
    var matches = wm.queryWords(e.target.value),
        rendered = {}; // store the rendered objects to prevent cased dupes
    for (var i=0; i<Math.min(50, matches.length); i++) {
      if (!(matches[i].toLowerCase().trim() in rendered)) {
        rendered[ matches[i].toLowerCase().trim() ] = true;
        var elem = document.createElement('div');
        elem.textContent = matches[i];
        elem.onclick = function(str, e) {
          input.value = str;
          submit();
        }.bind(this, matches[i]);
        document.querySelector('#typeahead').appendChild(elem);
      }
    }
  })

  function clearTypeahead(e) {
    typeahead.innerHTML = '';
  }

  function submit() {
    if (!input.value) return;
    var coords = wm.getWordCoords(input.value);
    if (!coords) {
      var elem = document.querySelector('#no-results');
      elem.style.transform = 'translate(0, 75px)';
      setTimeout(function() {
        elem.style.transform = 'translate(0, 24px)';
      }, 1500);
      return;
    }
    wm.flyTo(coords);
    clearTypeahead();
  }

  button.addEventListener('click', submit);
  window.addEventListener('click', clearTypeahead);
  input.addEventListener('keydown', function(e) {
    if (e.keyCode == 13) submit();
    else clearTypeahead();
  });
}


/**
* Main
**/

function get(url, onSuccess, onErr, onProgress) {
  var xmlhttp = new XMLHttpRequest();
  xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == XMLHttpRequest.DONE) {
      if (xmlhttp.status === 200) {
        if (onSuccess) onSuccess(JSON.parse(xmlhttp.responseText));
      } else {
        if (onErr) onErr(xmlhttp)
      }
    };
  };
  xmlhttp.onprogress = function(e) {
    if (onProgress) onProgress(e);
  };
  xmlhttp.open('GET', url, true);
  xmlhttp.send();
};



// create the gui
window.onload = function() {
  wm = new Wordmap();
  typeahead = new Typeahead();

  // build the gui
  gui = new dat.GUI({hideable: false})

  gui.add(wm.state, 'layout', ['grid', 'tsne'])
    .name('layout')
    .onFinishChange(wm.updateLayout.bind(wm))

  gui.add(wm, 'wordScalar', 0.0, 0.001)
    .name('font size')
    .onFinishChange(wm.updateLayout.bind(wm))

  gui.add(wm, 'heightScalar', 0.0, 0.003)
    .name('mountain')
    .onFinishChange(wm.updateLayout.bind(wm))

  gui.addColor(wm, 'background')
    .name('background')
    .onChange(wm.setBackgroundColor.bind(wm))

  gui.add(wm, 'color', ['#fff', '#000'])
    .name('color')
    .onChange(wm.setTextColor.bind(wm))
};
html,
body {
  width: 100%;
  height: 100%;
}

body {
  margin: 0;
  overflow: hidden;
}

body::after {
  content: '';
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2));
}

canvas {
  position: relative;
  z-index: 10;
}

body .dg.ac {
  z-index: 100;
}

#letter-canvas {
  position: fixed;
  top: 0;
  left: 0;
}

#search-container {
  position: absolute;
  top: 23px;
  left: 50%;
  width: 360px;
  margin-left: -180px;
  font-family: courier, monospace;
  z-index: 100;
}

#search,
#search-button {
  padding: 7px 10px;
  font-size: 16px;
  line-height: 16px;
  box-sizing: border-box;
}

#search,
#search-button,
#search-button::before {
  border-radius: 3px;
}

#search {
  border: 1px solid #aaa;
}

#search-button {
  position: relative;
  opacity: 0.7;
  border: 1px solid #797979;
}

#search-button::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: #fff;
  z-index: -1;
}

#search,
#typeahead {
  width: 240px;
  font-family: inherit;
}

#search {
  z-index: 10;
}

#search-button {
  background: #b4cdde;
  color: #485661;
  padding: 8px 18px;
  font-weight: 600;
  letter-spacing: 0.05em;
  font-family: sans-serif;
  cursor: pointer;
}

#typeahead {
  background: #fff;
  max-height: 100px;
  overflow: auto;
  box-sizing: border-box;
  border-bottom-left-radius: 3px;
  border-bottom-right-radius: 3px;
}

.hidden {
  display: none;
}

.displayed {
  display: inline-block;
}

#typeahead div {
  margin: 2px 10px;
  cursor: pointer;
  white-space: nowrap;
}

#typeahead div:hover {
  background: #efefef;
}

#no-results {
  padding: 6px;
  background: firebrick;
  color: #fff;
  font-size: 1em;
  transform: translate(0, 24px);
  display: block;
  margin: 0 auto;
  width: 107px;
  text-align: center;
  position: absolute;
  left: 50%;
  margin-left: -180px;
  z-index: 90;
  font-family: courier;
  border-radius: 3px;
  transition: transform 0.3s;
}
<div id='no-results'>No Results!</div>
<div id='search-container'>
  <div>
    <input id='search' value='pythons'></input>
    <button id='search-button'>SEARCH</button>
  </div>
  <div id='typeahead'></div>
</div>

<script id='vertex-shader' type='x-shader/x-vertex'>
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform vec3 cameraPosition;
uniform float pointScale;
uniform float transition;

attribute vec3 position;
attribute vec3 translation;
attribute vec3 target;
attribute vec2 texOffset;

varying vec2 vTexOffset;

void main() {
  // project this particle
  vec3 raw0 = position + translation;
  vec3 raw1 = position + target;
  vec3 raw = mix(raw0, raw1, clamp(transition, 0.0, 1.0));
  vec4 mvPosition = modelViewMatrix * vec4(raw, 1.0);
  gl_Position = projectionMatrix * mvPosition;

  // make distant points small
  vec4 cam4 = vec4(cameraPosition, 1.0);
  gl_PointSize = (pointScale / -mvPosition.z);

  vTexOffset = texOffset;
}
</script>

<script id='fragment-shader' type='x-shader/x-fragment'>
precision mediump float;

uniform sampler2D tex;
uniform vec3 fogColor;
uniform float cellSize;
uniform float fogNear;
uniform float fogFar;
uniform float color;

varying vec2 vTexOffset;

void main() {
  #ifdef WORDS
    vec2 uv = vTexOffset + vec2(gl_PointCoord.x, gl_PointCoord.y);
    vec2 scaledUv = uv * vec2(cellSize, cellSize);
    gl_FragColor = texture2D(tex, scaledUv);
    if (gl_FragColor.a < 0.01) discard; // discard non-letter pixels
  #else
    // make point circular
    if (length(gl_PointCoord - vec2(0.5)) > 0.5) discard;
    gl_FragColor = vec4(0.7, 0.7, 0.8, 0.5);
  #endif
}
</script>

<script src='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/three.min.js'></script>
<script src='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/trackball-controls.min.js'></script>
<script src='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/tweenlite.min.js'></script>
<script src='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/dat.gui.min.js'></script>