javascript - Three.js:远处的物体是微弱的

标签 javascript three.js html5-canvas textures sprite

我正在研究 three.js scene渲染一些纹理点 Sprite 。这些 Sprite 从单个 uniform 获取它们的纹理,而那个 uniform 是一个 2D Canvas ,我在上面绘制了字母表中的字母:

enter image description here

虽然所有的字母都在这个 Canvas 上呈现为黑色,但 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 Canvas 时声明一个较轻的字体粗细属性,这些字母作为统一发送到场景,但这也没有成功。

有谁知道如何在不增加每个字母“权重”的情况下增加远点/字母的不透明度?任何建议都会非常有帮助!

最佳答案

这是因为应用到 Sprite 纹理的 mipmapping 造成的。当字母被 mipmap 到较小的分辨率时,文本的黑色像素会混合成灰色。

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

tex.minFilter = THREE.LinearFilter;

我认为 only two options you have没有 mipmapping 的缩小过滤器是 LinearFilterNearestFilter。请记住,禁用 mipmapping 可能会使您的纹理看起来有锯齿。

或者,您可以在 Photoshop 中创建自己的不会褪色为灰色的 mipmap,并使用 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>

关于javascript - Three.js:远处的物体是微弱的,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56978199/

相关文章:

javascript - 当我绘制边界框时, Canvas 内渲染的图像消失了?

javascript - Angular 不会更新 DOM 但会将数据记录到控制台

javascript - 更改标志的标签

three.js - 在 three.js 中使用 PointsMaterial 加载图像

javascript - 如何在 3 个 js 中依次使用 2 个不同的动画对 2 个对象进行动画处理?

javascript - 随着时间的推移 trim HTML Canvas 中随机绘制线条的长度

javascript - HTTP 响应中数组的异常表示(数组 : 0x7f7298c057f0)

javascript - 如何检查 Handlebars 中的变量是否错误?

javascript - AR.JS 自定义标记

javascript - 通过php将dataURL图像保存到本地