Skip to content Skip to sidebar Skip to footer

Three.js: Distant Objects Are Faint

I'm working on a three.js scene that renders some textured point sprites. Those sprites get their textures from a single uniform, and that uniform is a 2D canvas on which I've draw

Solution 1:

This is happening because of the mipmapping that's being applied to your sprite texture. When the letters are mipmapped to smaller resolutions, the black pixels of your text are getting blended to grey.

You could avoid using the mipmapped texture by changing the .minFilter property of your texture after declaring it, which is what I did in the code snippet below:

tex.minFilter = THREE.LinearFilter;

I think the only two options you have for minification filters without mipmapping are LinearFilter and NearestFilter. Keep in mind that disabling mipmapping may give your textures an aliased look.

Alternatively, you could create your own mipmaps that don't fade to grey in Photoshop, and define them with texture.mipmaps.

// aliasesvarBA = THREE.BufferAttribute,
    IBA = THREE.InstancedBufferAttribute,
    ARR = Float32Array;


functionWordmap() {
  // configthis.wordScalar = 0.0003; // sizes up wordsthis.heightScalar = 0.002; // controls mountain heightthis.sep = 0.9; // separation between charactersthis.maxWords = 1000000; // max number of words to drawthis.background = '#fff'; // background colorthis.color = '#000'; // text color// staticthis.size = 64; // size of each character on canvas// statethis.state = {
    layout: 'grid', // name of the currently active layoutflying: false, // bool indicating whether we're flying cameraclock: null, // clock to measure how long we've been flying cameratransitioning: false, // bool indicating whether layout is transitioningtransitionQueued: false, // bool indicating whether to run another layout transition
  }
  // datathis.data = {
    input: null,
    words: [],
    layouts: {},
    heightmap: {},
    characters: {},
  }
  // initializethis.init();
}


/**
* Scene
**/Wordmap.prototype.createScene = function() {
  // generate a scene objectvar scene = newTHREE.Scene();

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

  // generate a renderervar renderer = newTHREE.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 controlsvar controls = newTHREE.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 orientationif (window.location.search.includes('axes=true')) {
    var axesHelper = newTHREE.AxesHelper(5);
    scene.add(axesHelper);
  }

  // store objects on instancethis.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 canvasvar tex = newTHREE.Texture(canvas);
  tex.flipY = false;
  tex.minFilter = THREE.LinearFilter;
  tex.needsUpdate = true;
  // store the character map on the instancethis.data.characters = {
    map: charToCoords,
    tex: tex,
  }
}


/**
* Heightmap canvas
**/Wordmap.prototype.getHeightmap = function(cb) {
  var img = newImage();
  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 = newTHREE.InstancedBufferGeometry();
  geometry.addAttribute('uv', newBA(newARR([0,0]), 2, true, 1));
  geometry.addAttribute('position', newBA(newARR([0,0,0]), 3, true, 1));
  geometry.addAttribute('translation', newIBA(attrs.translations, 3, true, 1));
  geometry.addAttribute('target', newIBA(attrs.translations, 3, true, 1));
  geometry.addAttribute('texOffset', newIBA(attrs.texOffsets, 2, true, 1));
  // build the meshthis.setShaderMaterial();
  var mesh = newTHREE.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 attributesvar attrs = {
    translations: newFloat32Array(n * 3),
    texOffsets: newFloat32Array(n * 2),
  }
  var iters = {
    translationIter: 0,
    texOffsetIter: 0,
  }
  // assume each word has x y coords assignedfor (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 = newTHREE.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 maptex:        { 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() {
  returnthis.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 layoutif (i == 0 && !this.state.layout) this.state.layout = name;
  }
}


// center an array of vertex positions -1:1 on each axisWordmap.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 dimensionfor (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;
  returnObject.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 coordinatesvar 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 = newTHREE.PerspectiveCamera(75, aspectRatio, 0.001, 10),
      _controls = newTHREE.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 = newTHREE.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 = newTHREE.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) {
  returnthis.data.layouts[this.state.layout].wordToCoords[word];
}

/**
* Typeahaed
**/functionTypeahead() {
  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 dupesfor (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);
      }
    }
  })

  functionclearTypeahead(e) {
    typeahead.innerHTML = '';
  }

  functionsubmit() {
    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();
    elseclearTypeahead();
  });
}


/**
* Main
**/functionget(url, onSuccess, onErr, onProgress) {
  var xmlhttp = newXMLHttpRequest();
  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 guiwindow.onload = function() {
  wm = newWordmap();
  typeahead = newTypeahead();

  // 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: 7px10px;
  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: 8px18px;
  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;
}

#typeaheaddiv {
  margin: 2px10px;
  cursor: pointer;
  white-space: nowrap;
}

#typeaheaddiv: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;
}
<divid='no-results'>No Results!</div><divid='search-container'><div><inputid='search'value='pythons'></input><buttonid='search-button'>SEARCH</button></div><divid='typeahead'></div></div><scriptid='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;

voidmain() {
  // 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><scriptid='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;

voidmain() {
  #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 circularif (length(gl_PointCoord - vec2(0.5)) > 0.5) discard;
    gl_FragColor = vec4(0.7, 0.7, 0.8, 0.5);
  #endif
}
</script><scriptsrc='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/three.min.js'></script><scriptsrc='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/trackball-controls.min.js'></script><scriptsrc='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/tweenlite.min.js'></script><scriptsrc='https://duhaime.s3.amazonaws.com/blog/visualizations/wordmap/dat.gui.min.js'></script>

Post a Comment for "Three.js: Distant Objects Are Faint"