Browse Source

add optimizing lots of objects article.

Gregg Tavares 6 years ago
parent
commit
fc86920be2
24 changed files with 1448 additions and 0 deletions
  1. 89 0
      threejs/gpw-data-viewer.html
  2. BIN
      threejs/lessons/resources/images/bring-up-fps-meter.gif
  3. BIN
      threejs/lessons/resources/images/fps-meter.gif
  4. 113 0
      threejs/lessons/resources/threejs-lots-of-objects.js
  5. 537 0
      threejs/lessons/threejs-optimize-lots-of-objects.md
  6. 4 0
      threejs/lessons/toc.html
  7. 3 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014_2010_1_deg_asc_readme.txt
  8. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014bt_2010_cntm_1_deg.asc
  9. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014bt_2010_cntm_1_deg.prj
  10. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014bt_2010_dens_1_deg.asc
  11. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014bt_2010_dens_1_deg.prj
  12. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc
  13. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.prj
  14. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_dens_1_deg.asc
  15. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_dens_1_deg.prj
  16. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc
  17. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.prj
  18. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_dens_1_deg.asc
  19. 6 0
      threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_dens_1_deg.prj
  20. 2 0
      threejs/resources/editor.js
  21. BIN
      threejs/resources/images/world.jpg
  22. 228 0
      threejs/threejs-lots-of-objects-merged-vertexcolors.html
  23. 202 0
      threejs/threejs-lots-of-objects-merged.html
  24. 198 0
      threejs/threejs-lots-of-objects-slow.html

+ 89 - 0
threejs/gpw-data-viewer.html

@@ -0,0 +1,89 @@
+<!doctype html>
+<html>
+<meta charset="utf-8">
+<head>
+<title>gpw data viewer</title>
+</head>
+<body>
+<canvas></canvas>
+</body>
+<script>
+'use strict';
+
+async function loadFile(url) {
+  const req = await fetch(url);
+  return req.text();
+}
+
+function parseData(text) {
+  const data = [];
+  const settings = {data};
+  let max;
+  let min;
+  // split into lines
+  text.split('\n').forEach((line) => {
+    // split the line by whitespace
+    const parts = line.trim().split(/\s+/);
+    if (parts.length === 2) {
+      // only 2 parts, must be a key/value pair
+      settings[parts[0]] = parseFloat(parts[1]);
+    } else if (parts.length > 2) {
+      // more than 2 parts, must be data
+      const values = parts.map((v) => {
+        const value = parseFloat(v);
+        if (value === settings.NODATA_value) {
+          return undefined;
+        }
+        max = Math.max(max === undefined ? value : max, value);
+        min = Math.min(min === undefined ? value : min, value);
+        return value;
+      });
+      data.push(values);
+    }
+  });
+  return Object.assign(settings, {min, max});
+}
+
+function drawData(file) {
+  const {min, max, ncols, nrows, data} = file;
+  const range = max - min;
+  const ctx = document.querySelector('canvas').getContext('2d');
+  // make the canvas the same size as the data
+  ctx.canvas.width = ncols;
+  ctx.canvas.height = nrows;
+  // but display it double size so it's not too small
+  ctx.canvas.style.width = px(ncols * 2);
+  ctx.canvas.style.height = px(nrows * 2);
+  // fill the canvas to dark gray
+  ctx.fillStyle = '#444';
+  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+  // draw each data point
+  data.forEach((row, latNdx) => {
+    row.forEach((value, lonNdx) => {
+      if (value === undefined) {
+        return;
+      }
+      const amount = (value - min) / range;
+      const hue = 1;
+      const saturation = 1;
+      const lightness = amount;
+      ctx.fillStyle = hsl(hue, saturation, lightness);
+      ctx.fillRect(lonNdx, latNdx, 1, 1);
+    });
+  });
+}
+
+function px(v) {
+  return `${v | 0}px`;
+}
+
+function hsl(h, s, l) {
+  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
+}
+
+loadFile('resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+  .then(parseData)
+  .then(drawData);
+
+</script>
+</html>

BIN
threejs/lessons/resources/images/bring-up-fps-meter.gif


BIN
threejs/lessons/resources/images/fps-meter.gif


+ 113 - 0
threejs/lessons/resources/threejs-lots-of-objects.js

@@ -0,0 +1,113 @@
+'use strict';
+
+/* global THREE, dat, threejsLessonUtils */
+
+{
+  class DegRadHelper {
+    constructor(obj, prop) {
+      this.obj = obj;
+      this.prop = prop;
+    }
+    get value() {
+      return THREE.Math.radToDeg(this.obj[this.prop]);
+    }
+    set value(v) {
+      this.obj[this.prop] = THREE.Math.degToRad(v);
+    }
+  }
+
+  function scaleCube(zOffset) {
+    const root = new THREE.Object3D();
+
+    const size = 3;
+    const geometry = new THREE.BoxBufferGeometry(size, size, size);
+    geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0, zOffset * size));
+    const material = new THREE.MeshBasicMaterial({
+      color: 'red',
+    });
+    const cube = new THREE.Mesh(geometry, material);
+    root.add(cube);
+    cube.add(new THREE.LineSegments(
+        new THREE.EdgesGeometry(geometry),
+        new THREE.LineBasicMaterial({color: 'white'})));
+
+    [[0, 0], [1, 0], [0, 1]].forEach((rot) => {
+      const size = 10;
+      const divisions = 10;
+      const gridHelper = new THREE.GridHelper(size, divisions);
+      root.add(gridHelper);
+      gridHelper.rotation.x = rot[0] * Math.PI * .5;
+      gridHelper.rotation.z = rot[1] * Math.PI * .5;
+    });
+
+    return {
+      obj3D: root,
+      update: (time) => {
+        const s = THREE.Math.lerp(0.5, 2, Math.sin(time) * .5 + .5);
+        cube.scale.set(s, s, s);
+      },
+    };
+  }
+
+  threejsLessonUtils.addDiagrams({
+    scaleCenter: {
+      create() {
+        return scaleCube(0);
+      },
+    },
+    scalePositiveZ: {
+      create() {
+        return scaleCube(.5);
+      },
+    },
+    lonLatPos: {
+      create(info) {
+        const {scene, camera, renderInfo} = info;
+        const size = 10;
+        const divisions = 10;
+        const gridHelper = new THREE.GridHelper(size, divisions);
+        scene.add(gridHelper);
+
+        const geometry = new THREE.BoxBufferGeometry(1, 1, 1);
+        
+        const lonHelper = new THREE.Object3D();
+        scene.add(lonHelper);
+        const latHelper = new THREE.Object3D();
+        lonHelper.add(latHelper);
+        const positionHelper = new THREE.Object3D();
+        latHelper.add(positionHelper);
+
+        {
+          const lonMesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 'green'}));
+          lonMesh.scale.set(0.2, 1, 0.2);
+          lonHelper.add(lonMesh);
+        }
+        {
+          const latMesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 'blue'}));
+          latMesh.scale.set(1, 0.25, 0.25);
+          latHelper.add(latMesh);
+        }
+        {
+          const geometry = new THREE.SphereBufferGeometry(0.1, 24, 12);
+          const posMesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 'red'}));
+          posMesh.position.z = 1;
+          positionHelper.add(posMesh);
+        }
+
+        camera.position.set(1, 1.5, 1.5);
+        camera.lookAt(0, 0, 0);
+
+        const gui = new dat.GUI({autoPlace: false});
+        renderInfo.elem.appendChild(gui.domElement);
+        gui.add(new DegRadHelper(lonHelper.rotation, 'y'), 'value', -180, 180).name('lonHelper x rotation');
+        gui.add(new DegRadHelper(latHelper.rotation, 'x'), 'value', -90, 90).name('latHelper y rotation');
+
+        return {
+          trackball: false,
+        };
+      },
+    },
+  });
+
+}
+

+ 537 - 0
threejs/lessons/threejs-optimize-lots-of-objects.md

@@ -0,0 +1,537 @@
+Title: Three.js Optimize Lots of Objects
+Description: Optimize by merging Objects
+
+This article is part of a series of articles about three.js. The first article
+is [three.js fundamentals](threejs-fundamentals.html). If you haven't read that
+yet and you're new to three.js you might want to consider starting there. 
+
+There are many ways to optimize things for three.js. One way is often referred
+to as *merging geometry*. Every `Mesh` you create and three.js represents 1 or
+more requests by the system to draw something. Drawing 2 things has more
+overhead than drawing 1 even if the results are the same so one way to optimize
+is to merge meshes.
+
+Let's show an example of when this is a good solution for an issue. Let's
+re-create the [WebGL Globe](https://globe.chromeexperiments.com/).
+
+The first thing we need to do is get some data. The WebGL Globe said the data
+they use comes from [SEDAC](http://sedac.ciesin.columbia.edu/gpw/). Checking out
+the site I saw there was [demographic data in a grid
+format](http://sedac.ciesin.columbia.edu/data/set/gpw-v4-basic-demographic-characteristics-rev10).
+I downloaded the data at 60 minute resolution. Then I took a look at the data
+
+It looks like this
+
+```txt
+ ncols         360
+ nrows         145
+ xllcorner     -180
+ yllcorner     -60
+ cellsize      0.99999999999994
+ NODATA_value  -9999
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ 9.241768 8.790958 2.095345 -9999 0.05114867 -9999 -9999 -9999 -9999 -999...
+ 1.287993 0.4395509 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+```
+
+There's a few lines that are like key/value pairs followed by lines with a value
+per grid point, one line for each row of data points.
+
+To make sure we understand the data let's try to plot it in 2D.
+
+First some code to load the text file
+
+```js
+async function loadFile(url) {
+  const req = await fetch(url);
+  return req.text();
+}
+```
+
+The code above returns a `Promise` with the contents of the file at `url`;
+
+Then we need some code to parse the file
+
+```js
+function parseData(text) {
+  const data = [];
+  const settings = {data};
+  let max;
+  let min;
+  // split into lines
+  text.split('\n').forEach((line) => {
+    // split the line by whitespace
+    const parts = line.trim().split(/\s+/);
+    if (parts.length === 2) {
+      // only 2 parts, must be a key/value pair
+      settings[parts[0]] = parseFloat(parts[1]);
+    } else if (parts.length > 2) {
+      // more than 2 parts, must be data
+      const values = parts.map((v) => {
+        const value = parseFloat(v);
+        if (value === settings.NODATA_value) {
+          return undefined;
+        }
+        max = Math.max(max === undefined ? value : max, value);
+        min = Math.min(min === undefined ? value : min, value);
+        return value;
+      });
+      data.push(values);
+    }
+  });
+  return Object.assign(settings, {min, max});
+}
+```
+
+The code above returns an object with all the key/value pairs from the file as
+well as a `data` property with all the data in one large array and the `min` and
+`max` values found in the data.
+
+Then we need some code to draw that data
+
+```js
+function drawData(file) {
+  const {min, max, data} = file;
+  const range = max - min;
+  const ctx = document.querySelector('canvas').getContext('2d');
+  // make the canvas the same size as the data
+  ctx.canvas.width = ncols;
+  ctx.canvas.height = nrows;
+  // but display it double size so it's not too small
+  ctx.canvas.style.width = px(ncols * 2);
+  ctx.canvas.style.height = px(nrows * 2);
+  // fill the canvas to dark gray
+  ctx.fillStyle = '#444';
+  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+  // draw each data point
+  data.forEach((row, latNdx) => {
+    row.forEach((value, lonNdx) => {
+      if (value === undefined) {
+        return;
+      }
+      const amount = (value - min) / range;
+      const hue = 1;
+      const saturation = 1;
+      const lightness = amount;
+      ctx.fillStyle = hsl(hue, saturation, lightness);
+      ctx.fillRect(lonNdx, latNdx, 1, 1);
+    });
+  });
+}
+
+function px(v) {
+  return `${v | 0}px`;
+}
+
+function hsl(h, s, l) {
+  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
+}
+```
+
+And finally gluing it all together
+
+```js
+loadFile('resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+  .then(parseData)
+  .then(drawData);
+```
+
+Gives us this result
+
+{{{example url="../gpw-data-viewer.html" }}}
+
+So that seems to work. 
+
+Let's try it in 3D. Starting with the code from [rendering on
+demand](threejs-rendering-on-demand.html) We'll make one box per data in the
+file.
+
+First let's make a simple sphere with a texture of the world. Here's the texture
+
+<div class="threejs_center"><img src="../resources/images/world.jpg" style="width: 600px"></div>
+
+And the code to set it up.
+
+```js
+{
+  const loader = new THREE.TextureLoader();
+  const texture = loader.load('resources/images/world.jpg', render);
+  const geometry = new THREE.SphereBufferGeometry(1, 64, 32);
+  const material = new THREE.MeshBasicMaterial({map: texture});
+  scene.add(new THREE.Mesh(geometry, material));
+}
+```
+
+Notice the call to `render` when the texture has finished loading. We need this
+because we're [rendering on demand](threejs-rendering-on-demand.html) instead of
+continuously so we need to render once when the texture is loaded.
+
+Then we need to change the code that drew a dot per data point above to instead
+make a box per data point.
+
+```js
+function addBoxes(file) {
+  const {min, max, data} = file;
+  const range = max - min;
+
+  // make one box geometry
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+  // make it so it scales away from the positive Z axis
+  geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
+
+  // these helpers will make it easy to position the boxes
+  // We can rotate the lon helper on its Y axis to the longitude
+  const lonHelper = new THREE.Object3D();
+  scene.add(lonHelper);
+  // We rotate the latHelper on its X axis to the latitude
+  const latHelper = new THREE.Object3D();
+  lonHelper.add(latHelper);
+  // The position helper moves the object to the edge of the sphere
+  const positionHelper = new THREE.Object3D();
+  positionHelper.position.z = 1;
+  latHelper.add(positionHelper);
+
+  const lonFudge = Math.PI * .5;
+  const latFudge = Math.PI * -0.135;
+  data.forEach((row, latNdx) => {
+    row.forEach((value, lonNdx) => {
+      if (value === undefined) {
+        return;
+      }
+      const amount = (value - min) / range;
+      const material = new THREE.MeshBasicMaterial();
+      const hue = THREE.Math.lerp(0.7, 0.3, amount);
+      const saturation = 1;
+      const lightness = THREE.Math.lerp(0.1, 1.0, amount);
+      material.color.setHSL(hue, saturation, lightness);
+      const mesh = new THREE.Mesh(geometry, material);
+      scene.add(mesh);
+
+      // adjust the helpers to point to the latitude and longitude
+      lonHelper.rotation.y = THREE.Math.degToRad(lonNdx + file.xllcorner) + lonFudge;
+      latHelper.rotation.x = THREE.Math.degToRad(latNdx + file.yllcorner) + latFudge;
+
+      // use the world matrix of the position helper to
+      // position this mesh.
+      positionHelper.updateWorldMatrix(true, false);
+      mesh.applyMatrix(positionHelper.matrixWorld);
+
+      mesh.scale.set(0.005, 0.005, THREE.Math.lerp(0.01, 0.5, amount));
+    });
+  });
+}
+```
+
+The code is mostly straight forward from our test drawing code. 
+
+We make one box and adjust its center so it scales away from positive Z. If we
+didn't do this it would scale from the center but we want them to grow away from the origin.
+
+<div class="spread">
+  <div>
+    <div data-diagram="scaleCenter" style="height: 250px"></div>
+    <div class="code">default</div>
+  </div>
+  <div>
+    <div data-diagram="scalePositiveZ" style="height: 250px"></div>
+    <div class="code">adjusted</div>
+  </div>
+</div>
+
+Of course we could also solve that by parenting the box to more `THREE.Object3D`
+objects like we covered in [scene graphs](threejs-scenegraphs.html) but the more
+nodes we add to a scene graph the slower it gets.
+
+We also setup this small hierarchy of nodes of `lonHelper`, `latHelper`, and
+`positionHelper`. We use these objects to compute a position around the sphere
+were to place the box. 
+
+<div class="spread">
+  <div data-diagram="lonLatPos" style="width: 600px; height: 400px;"></div>
+</div>
+
+Above the <span style="color: green;">green bar</span> represents `lonHelper` and
+is used to rotate toward longitude on the equator. The <span style="color: blue;">
+blue bar</span> represents `latHelper` which is used to rotate to a
+latitude above or below the equator. The <span style="color: red;">red
+sphere</span> represents the offset that that `positionHelper` provides.
+
+We could do all of the math manually to figure out positions on the global but
+doing it this way leaves most of the math to the library itself so we don't need
+to deal with.
+
+For each data point we create a `MeshBasicMaterial` and a `Mesh` and then we ask
+for the world matrix of the `positionHelper` and apply that to the new `Mesh`.
+Finally we scale the mesh at its new position.
+
+Like above, we could also have created a `latHelper`, `lonHelper`, and
+`positionHelper` for every new box but that would be even slower.
+
+There are up to 360x145 boxes we're going to create. That's up to 52000 boxes.
+Because some data points are marked as "NO_DATA" the actual number of boxes
+we're going to create is around 19000. If we added 3 extra helper objects per
+box that would be nearly 80000 scene graph nodes that THREE.js would have to
+compute positions for. By instead using one set of helpers to just position the
+meshes we save around 60000 operations.
+
+A note about `lonFudge` and `latFudge`. `lonFudge` is π/2 which is a quarter of a turn.
+That makes sense. It just means the texture or texture coordinates start at a
+different offset around the globe. `latFudge` on the other hand I have no idea
+why it needs to be π * -0.135, that's just an amount that made the boxes line up
+with the texture.
+
+The last thing we need to do is call our loader
+
+```
+loadFile('resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+  .then(parseData)
+-  .then(drawData)
++  .then(addBoxes)
++  .then(render);
+```
+
+Once the data has finished loading and parsing then we need to render at least
+once since we're [rendering on demand](threejs-rendering-on-demand.html).
+
+{{{example url="../threejs-lots-of-objects-slow.html" }}}
+
+If you try to rotate the example above by dragging on the sample you'll likely
+notice it's slow.
+
+We can check the framerate by [opening the
+devtools](threejs-debugging-javascript.html) and turning on the browser's frame
+rate meter.
+
+<div class="threejs_center"><img src="resources/images/bring-up-fps-meter.gif"></div>
+
+On my machine I see a framerate under 20fps.
+
+<div class="threejs_center"><img src="resources/images/fps-meter.gif"></div>
+
+That doesn't feel very good to me and I suspect many people have slower machines
+which would make it even worse. We'd better look into optimizing.
+
+For this particular problem we can merge all the boxes into a single geometry.
+We're currently drawing around 19000 boxes. By merging them into a single
+geometry we'd remove 18999 operations.
+
+Here's the new code to merge the boxes into a single geometry.
+
+```js
+function addBoxes(file) {
+  const {min, max, data} = file;
+  const range = max - min;
+
+-  // make one box geometry
+-  const boxWidth = 1;
+-  const boxHeight = 1;
+-  const boxDepth = 1;
+-  const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+-  // make it so it scales away from the positive Z axis
+-  geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
+
+  // these helpers will make it easy to position the boxes
+  // We can rotate the lon helper on its Y axis to the longitude
+  const lonHelper = new THREE.Object3D();
+  scene.add(lonHelper);
+  // We rotate the latHelper on its X axis to the latitude
+  const latHelper = new THREE.Object3D();
+  lonHelper.add(latHelper);
+  // The position helper moves the object to the edge of the sphere
+  const positionHelper = new THREE.Object3D();
+  positionHelper.position.z = 1;
+  latHelper.add(positionHelper);
++  // Used to move the center of the box so it scales from the position Z axis
++  const originHelper = new THREE.Object3D();
++  originHelper.position.z = 0.5;
++  positionHelper.add(originHelper);
+
+  const lonFudge = Math.PI * .5;
+  const latFudge = Math.PI * -0.135;
++  const geometries = [];
+  data.forEach((row, latNdx) => {
+    row.forEach((value, lonNdx) => {
+      if (value === undefined) {
+        return;
+      }
+      const amount = (value - min) / range;
+
+-      const material = new THREE.MeshBasicMaterial();
+-      const hue = THREE.Math.lerp(0.7, 0.3, amount);
+-      const saturation = 1;
+-      const lightness = THREE.Math.lerp(0.1, 1.0, amount);
+-      material.color.setHSL(hue, saturation, lightness);
+-      const mesh = new THREE.Mesh(geometry, material);
+-      scene.add(mesh);
+
++      const boxWidth = 1;
++      const boxHeight = 1;
++      const boxDepth = 1;
++      const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+
+      // adjust the helpers to point to the latitude and longitude
+      lonHelper.rotation.y = THREE.Math.degToRad(lonNdx + file.xllcorner) + lonFudge;
+      latHelper.rotation.x = THREE.Math.degToRad(latNdx + file.yllcorner) + latFudge;
+
+-      // use the world matrix of the position helper to
+-      // position this mesh.
+-      positionHelper.updateWorldMatrix(true, false);
+-      mesh.applyMatrix(positionHelper.matrixWorld);
+-
+-      mesh.scale.set(0.005, 0.005, THREE.Math.lerp(0.01, 0.5, amount));
+
++      // use the world matrix of the origin helper to
++      // position this geometry
++      positionHelper.scale.set(0.005, 0.005, THREE.Math.lerp(0.01, 0.5, amount));
++      originHelper.updateWorldMatrix(true, false);
++      geometry.applyMatrix(originHelper.matrixWorld);
++
++      geometries.push(geometry);
+    });
+  });
+
++  const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(
++      geometries, false);
++  const material = new THREE.MeshBasicMaterial({color:'red'});
++  const mesh = new THREE.Mesh(mergedGeometry, material);
++  scene.add(mesh);
+
+}
+```
+
+Above we removed the code that was changing the box geometry's center point and
+are instead doing it by adding an `originHelper`. Before we were using the same
+geometry 19000 times. This time we are creating new geometry for every single
+box and since we are going to use `applyMatrix` to move the vertices of each box
+geometry we might as well do it once instead of twice.
+
+At the end we pass an array of all the geometries to
+`THREE.BufferGeometryUtils.mergeBufferGeometries` which will combined all of
+them into a single mesh.
+
+We also need to include the `BufferGeometryUtils`
+
+```html
+<script src="resources/threejs/r98/js/utils/BufferGeometryUtils.js"></script>
+```
+
+And now, at least on my machine, I get 60 frames per second
+
+{{{example url="../threejs-lots-of-objects-merged.html" }}}
+
+So that worked but because it's one mesh we only get one material which means we
+only get one color where as before we had a different color on each box. We can
+fix that by using vertex colors.
+
+Vertex colors add a color per vertex. By setting all the colors of each vertex
+of each box to specific colors every box will have a different color.
+
+```js
++const color = new THREE.Color();
+
+const lonFudge = Math.PI * .5;
+const latFudge = Math.PI * -0.135;
+const geometries = [];
+data.forEach((row, latNdx) => {
+  row.forEach((value, lonNdx) => {
+    if (value === undefined) {
+      return;
+    }
+    const amount = (value - min) / range;
+
+    const boxWidth = 1;
+    const boxHeight = 1;
+    const boxDepth = 1;
+    const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+
+    // adjust the helpers to point to the latitude and longitude
+    lonHelper.rotation.y = THREE.Math.degToRad(lonNdx + file.xllcorner) + lonFudge;
+    latHelper.rotation.x = THREE.Math.degToRad(latNdx + file.yllcorner) + latFudge;
+
+    // use the world matrix of the origin helper to
+    // position this geometry
+    positionHelper.scale.set(0.005, 0.005, THREE.Math.lerp(0.01, 0.5, amount));
+    originHelper.updateWorldMatrix(true, false);
+    geometry.applyMatrix(originHelper.matrixWorld);
+
++    // compute a color
++    const hue = THREE.Math.lerp(0.7, 0.3, amount);
++    const saturation = 1;
++    const lightness = THREE.Math.lerp(0.1, 1.0, amount);
++    color.setHSL(hue, saturation, lightness);
++    // get the colors as an array of values from 0 to 255
++    const rgb = color.toArray().map(v => v * 255);
++
++    // make an array to store colors for each vertex
++    const numVerts = geometry.getAttribute('position').count;
++    const itemSize = 3;  // r, g, b
++    const colors = new Uint8Array(itemSize * numVerts);
++
++    // copy the color into the colors array for each vertex
++    colors.forEach((v, ndx) => {
++      colors[ndx] = rgb[ndx % 3];
++    });
++
++    const normalized = true;
++    const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
++    geometry.addAttribute('color', colorAttrib);
+
+    geometries.push(geometry);
+  });
+});
+```
+
+The code above looks up the number or vertices needed by getting the `position`
+attribute from the geometry. We then create a `Uint8Array` to put the colors in.
+It then adds that as an attribute by calling `geometry.addAttribute`.
+
+Lastly we need to tell three.js to use the vertex colors. 
+
+```
+const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(
+    geometries, false);
+-const material = new THREE.MeshBasicMaterial({color:'red'});
++const material = new THREE.MeshBasicMaterial({
++  vertexColors: THREE.VertexColors,
++});
+const mesh = new THREE.Mesh(mergedGeometry, material);
+scene.add(mesh);
+```
+
+And with that we get our colors back
+
+{{{example url="../threejs-lots-of-objects-merged-vertexcolors.html" }}}
+
+Merging geometry is a common optimization technique. For example rather than
+100 trees you might merge the trees into 1 geometry, a pile of individual rocks
+into a single geometry of rocks, a picket fence from individual pickets into
+one fence mesh. Another example in Minecraft it doesn't likely draw each cube
+individually but rather creates groups of merged cubes and also selectively removing 
+faces that are never visible.
+
+The problem with making everything one mesh though is it's no longer easy
+to move any part that was previously separate. Depending on our use case
+though there are creative solutions. We'll explore one in another article.
+
+<canvas id="c"></canvas>
+<script src="../resources/threejs/r98/three.min.js"></script>
+<script src="../resources/threejs/r98/js/controls/TrackballControls.js"></script>
+<script src="../../3rdparty/dat.gui.min.js"></script>
+<script src="resources/threejs-lesson-utils.js"></script>
+<script src="resources/threejs-lots-of-objects.js"></script>

+ 4 - 0
threejs/lessons/toc.html

@@ -17,6 +17,10 @@
     <li><a href="/threejs/lessons/threejs-post-processing-3dlut.html">Applying a LUT File for effects</a></li>    
     <li><a href="/threejs/lessons/threejs-shadertoy.html">Using Shadertoy shaders</a></li>    
   </ul>
+  <li>Optimization</li>
+  <ul>
+    <li><a href="/threejs/lessons/threejs-optimize-lots-of-objects.html">Optimizing Lots of Objects</a></li>
+  </ul>
   <li>Tips</li>
   <ul>
     <li><a href="/threejs/lessons/threejs-debugging-javascript.html">Debugging JavaScript</a></li>

+ 3 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014_2010_1_deg_asc_readme.txt

@@ -0,0 +1,3 @@
+documentation: http://sedac.ciesin.columbia.edu/downloads/docs/gpw-v4/gpw-v4-documentation.pdf
+
+website: http://sedac.ciesin.columbia.edu/data/collection/gpw-v4/sets/browse

File diff suppressed because it is too large
+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014bt_2010_cntm_1_deg.asc


+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014bt_2010_cntm_1_deg.prj

@@ -0,0 +1,6 @@
+Projection    GEOGRAPHIC
+Datum         WGS84
+Spheroid      WGS84
+Units         DD
+Zunits        NO
+Parameters    

File diff suppressed because it is too large
+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014bt_2010_dens_1_deg.asc


+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014bt_2010_dens_1_deg.prj

@@ -0,0 +1,6 @@
+Projection    GEOGRAPHIC
+Datum         WGS84
+Spheroid      WGS84
+Units         DD
+Zunits        NO
+Parameters    

File diff suppressed because it is too large
+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc


+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.prj

@@ -0,0 +1,6 @@
+Projection    GEOGRAPHIC
+Datum         WGS84
+Spheroid      WGS84
+Units         DD
+Zunits        NO
+Parameters    

File diff suppressed because it is too large
+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_dens_1_deg.asc


+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_dens_1_deg.prj

@@ -0,0 +1,6 @@
+Projection    GEOGRAPHIC
+Datum         WGS84
+Spheroid      WGS84
+Units         DD
+Zunits        NO
+Parameters    

File diff suppressed because it is too large
+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc


+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.prj

@@ -0,0 +1,6 @@
+Projection    GEOGRAPHIC
+Datum         WGS84
+Spheroid      WGS84
+Units         DD
+Zunits        NO
+Parameters    

File diff suppressed because it is too large
+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_dens_1_deg.asc


+ 6 - 0
threejs/resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_dens_1_deg.prj

@@ -0,0 +1,6 @@
+Projection    GEOGRAPHIC
+Datum         WGS84
+Spheroid      WGS84
+Units         DD
+Zunits        NO
+Parameters    

+ 2 - 0
threejs/resources/editor.js

@@ -60,6 +60,7 @@ function fixSourceLinks(url, source) {
   const imageSrcRE = /((?:image|img)\.src = )"(.*?)"/g;
   const loaderLoadRE = /(loader\.load[a-z]*\s*\(\s*)('|")(.*?)('|")/ig;
   const loaderArrayLoadRE = /(loader\.load[a-z]*\(\[)([\s\S]*?)(\])/ig;
+  const loadFileRE = /(loadFile\s*\(\s*)('|")(.*?)('|")/ig;
   const arrayLineRE = /^(\s*["|'])([\s\S]*?)(["|']*$)/;
   const urlPropRE = /(url:\s*)('|")(.*?)('|")/g;
   const prefix = getPrefix(url);
@@ -87,6 +88,7 @@ function fixSourceLinks(url, source) {
   source = source.replace(linkRE, makeLinkFQed);
   source = source.replace(imageSrcRE, makeLinkFQed);
   source = source.replace(urlPropRE, makeLinkFDedQuotes);
+  source = source.replace(loadFileRE, makeLinkFDedQuotes);
   source = source.replace(loaderLoadRE, makeLinkFDedQuotes);
   source = source.replace(loaderArrayLoadRE, makeArrayLinksFDed);
 

BIN
threejs/resources/images/world.jpg


+ 228 - 0
threejs/threejs-lots-of-objects-merged-vertexcolors.html

@@ -0,0 +1,228 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Lots of Objects - Merged w/vertex colors</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r98/three.js"></script>
+<script src="resources/threejs/r98/js/utils/BufferGeometryUtils.js"></script>
+<script src="resources/threejs/r98/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 60;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 10;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2.5;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.enableDamping = true;
+  controls.dampingFactor = 0.05;
+  controls.rotateSpeed = 0.1;
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('black');
+
+  {
+    const loader = new THREE.TextureLoader();
+    const texture = loader.load('resources/images/world.jpg', render);
+    const geometry = new THREE.SphereBufferGeometry(1, 64, 32);
+    const material = new THREE.MeshBasicMaterial({map: texture});
+    scene.add(new THREE.Mesh(geometry, material));
+  }
+
+  async function loadFile(url) {
+    const req = await fetch(url);
+    return req.text();
+  }
+
+  function parseData(text) {
+    const data = [];
+    const settings = {data};
+    let max;
+    let min;
+    // split into lines
+    text.split('\n').forEach((line) => {
+      // split the line by whitespace
+      const parts = line.trim().split(/\s+/);
+      if (parts.length === 2) {
+        // only 2 parts, must be a key/value pair
+        settings[parts[0]] = parseFloat(parts[1]);
+      } else if (parts.length > 2) {
+        // more than 2 parts, must be data
+        const values = parts.map((v) => {
+          const value = parseFloat(v);
+          if (value === settings.NODATA_value) {
+            return undefined;
+          }
+          max = Math.max(max === undefined ? value : max, value);
+          min = Math.min(min === undefined ? value : min, value);
+          return value;
+        });
+        data.push(values);
+      }
+    });
+    return Object.assign(settings, {min, max});
+  }
+
+  function addBoxes(file) {
+    const {min, max, data} = file;
+    const range = max - min;
+
+    // these helpers will make it easy to position the boxes
+    // We can rotate the lon helper on its Y axis to the longitude
+    const lonHelper = new THREE.Object3D();
+    scene.add(lonHelper);
+    // We rotate the latHelper on its X axis to the latitude
+    const latHelper = new THREE.Object3D();
+    lonHelper.add(latHelper);
+    // The position helper moves the object to the edge of the sphere
+    const positionHelper = new THREE.Object3D();
+    positionHelper.position.z = 1;
+    latHelper.add(positionHelper);
+    // Used to move the center of the cube so it scales from the position Z axis
+    const originHelper = new THREE.Object3D();
+    originHelper.position.z = 0.5;
+    positionHelper.add(originHelper);
+
+    const color = new THREE.Color();
+
+    const lonFudge = Math.PI * .5;
+    const latFudge = Math.PI * -0.135;
+    const geometries = [];
+    data.forEach((row, latNdx) => {
+      row.forEach((value, lonNdx) => {
+        if (value === undefined) {
+          return;
+        }
+        const amount = (value - min) / range;
+
+        const boxWidth = 1;
+        const boxHeight = 1;
+        const boxDepth = 1;
+        const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+
+        // adjust the helpers to point to the latitude and longitude
+        lonHelper.rotation.y = THREE.Math.degToRad(lonNdx + file.xllcorner) + lonFudge;
+        latHelper.rotation.x = THREE.Math.degToRad(latNdx + file.yllcorner) + latFudge;
+
+        // use the world matrix of the origin helper to
+        // position this geometry
+        positionHelper.scale.set(0.005, 0.005, THREE.Math.lerp(0.01, 0.5, amount));
+        originHelper.updateWorldMatrix(true, false);
+        geometry.applyMatrix(originHelper.matrixWorld);
+
+        // compute a color
+        const hue = THREE.Math.lerp(0.7, 0.3, amount);
+        const saturation = 1;
+        const lightness = THREE.Math.lerp(0.1, 1.0, amount);
+        color.setHSL(hue, saturation, lightness);
+        // get the colors as an array of values from 0 to 255
+        const rgb = color.toArray().map(v => v * 255);
+
+        // make an array to store colors for each vertex
+        const numVerts = geometry.getAttribute('position').count;
+        const itemSize = 3;  // r, g, b
+        const colors = new Uint8Array(itemSize * numVerts);
+
+        // copy the color into the colors array for each vertex
+        colors.forEach((v, ndx) => {
+          colors[ndx] = rgb[ndx % 3];
+        });
+
+        const normalized = true;
+        const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
+        geometry.addAttribute('color', colorAttrib);
+
+        geometries.push(geometry);
+      });
+    });
+
+    const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(
+        geometries, false);
+    const material = new THREE.MeshBasicMaterial({
+      vertexColors: THREE.VertexColors,
+    });
+    const mesh = new THREE.Mesh(mergedGeometry, material);
+    scene.add(mesh);
+  }
+
+  loadFile('resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+    .then(parseData)
+    .then(addBoxes)
+    .then(render);
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  let requestId;
+
+  function render() {
+    requestId = undefined;
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    controls.update();
+    renderer.render(scene, camera);
+  }
+  render();
+
+  controls.addEventListener('change', () => {
+    if (!requestId) {
+      requestId = requestAnimationFrame(render);
+    }
+  });
+
+  window.addEventListener('resize', render);
+
+  // note: this is a workaround for an OrbitControls issue
+  // in an iframe. Will remove once the issue is fixed in
+  // three.js
+  window.addEventListener('mousedown', (e) => {
+    e.preventDefault();
+    window.focus();
+  });
+  window.addEventListener('keydown', (e) => {
+    e.preventDefault();
+  });
+}
+
+main();
+</script>
+</html>
+

+ 202 - 0
threejs/threejs-lots-of-objects-merged.html

@@ -0,0 +1,202 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Lots of Objects - Merged</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r98/three.js"></script>
+<script src="resources/threejs/r98/js/utils/BufferGeometryUtils.js"></script>
+<script src="resources/threejs/r98/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 60;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 10;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2.5;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.enableDamping = true;
+  controls.dampingFactor = 0.05;
+  controls.rotateSpeed = 0.1;
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('black');
+
+  {
+    const loader = new THREE.TextureLoader();
+    const texture = loader.load('resources/images/world.jpg', render);
+    const geometry = new THREE.SphereBufferGeometry(1, 64, 32);
+    const material = new THREE.MeshBasicMaterial({map: texture});
+    scene.add(new THREE.Mesh(geometry, material));
+  }
+
+  async function loadFile(url) {
+    const req = await fetch(url);
+    return req.text();
+  }
+
+  function parseData(text) {
+    const data = [];
+    const settings = {data};
+    let max;
+    let min;
+    // split into lines
+    text.split('\n').forEach((line) => {
+      // split the line by whitespace
+      const parts = line.trim().split(/\s+/);
+      if (parts.length === 2) {
+        // only 2 parts, must be a key/value pair
+        settings[parts[0]] = parseFloat(parts[1]);
+      } else if (parts.length > 2) {
+        // more than 2 parts, must be data
+        const values = parts.map((v) => {
+          const value = parseFloat(v);
+          if (value === settings.NODATA_value) {
+            return undefined;
+          }
+          max = Math.max(max === undefined ? value : max, value);
+          min = Math.min(min === undefined ? value : min, value);
+          return value;
+        });
+        data.push(values);
+      }
+    });
+    return Object.assign(settings, {min, max});
+  }
+
+  function addBoxes(file) {
+    const {min, max, data} = file;
+    const range = max - min;
+
+    // these helpers will make it easy to position the boxes
+    // We can rotate the lon helper on its Y axis to the longitude
+    const lonHelper = new THREE.Object3D();
+    scene.add(lonHelper);
+    // We rotate the latHelper on its X axis to the latitude
+    const latHelper = new THREE.Object3D();
+    lonHelper.add(latHelper);
+    // The position helper moves the object to the edge of the sphere
+    const positionHelper = new THREE.Object3D();
+    positionHelper.position.z = 1;
+    latHelper.add(positionHelper);
+    // Used to move the center of the cube so it scales from the position Z axis
+    const originHelper = new THREE.Object3D();
+    originHelper.position.z = 0.5;
+    positionHelper.add(originHelper);
+
+    const lonFudge = Math.PI * .5;
+    const latFudge = Math.PI * -0.135;
+    const geometries = [];
+    data.forEach((row, latNdx) => {
+      row.forEach((value, lonNdx) => {
+        if (value === undefined) {
+          return;
+        }
+        const amount = (value - min) / range;
+
+        const boxWidth = 1;
+        const boxHeight = 1;
+        const boxDepth = 1;
+        const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+
+        // adjust the helpers to point to the latitude and longitude
+        lonHelper.rotation.y = THREE.Math.degToRad(lonNdx + file.xllcorner) + lonFudge;
+        latHelper.rotation.x = THREE.Math.degToRad(latNdx + file.yllcorner) + latFudge;
+
+        // use the world matrix of the origin helper to
+        // position this geometry
+        positionHelper.scale.set(0.005, 0.005, THREE.Math.lerp(0.01, 0.5, amount));
+        originHelper.updateWorldMatrix(true, false);
+        geometry.applyMatrix(originHelper.matrixWorld);
+
+        geometries.push(geometry);
+      });
+    });
+
+    const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(
+        geometries, false);
+    const material = new THREE.MeshBasicMaterial({color:'red'});
+    const mesh = new THREE.Mesh(mergedGeometry, material);
+    scene.add(mesh);
+  }
+
+  loadFile('resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+    .then(parseData)
+    .then(addBoxes)
+    .then(render);
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  let requestId;
+
+  function render() {
+    requestId = undefined;
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    controls.update();
+    renderer.render(scene, camera);
+  }
+  render();
+
+  controls.addEventListener('change', () => {
+    if (!requestId) {
+      requestId = requestAnimationFrame(render);
+    }
+  });
+
+  window.addEventListener('resize', render);
+
+  // note: this is a workaround for an OrbitControls issue
+  // in an iframe. Will remove once the issue is fixed in
+  // three.js
+  window.addEventListener('mousedown', (e) => {
+    e.preventDefault();
+    window.focus();
+  });
+  window.addEventListener('keydown', (e) => {
+    e.preventDefault();
+  });
+}
+
+main();
+</script>
+</html>
+

+ 198 - 0
threejs/threejs-lots-of-objects-slow.html

@@ -0,0 +1,198 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Lots of Objects - Slow</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r98/three.js"></script>
+<script src="resources/threejs/r98/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 60;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 10;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2.5;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.enableDamping = true;
+  controls.dampingFactor = 0.05;
+  controls.rotateSpeed = 0.1;
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('black');
+
+  {
+    const loader = new THREE.TextureLoader();
+    const texture = loader.load('resources/images/world.jpg', render);
+    const geometry = new THREE.SphereBufferGeometry(1, 64, 32);
+    const material = new THREE.MeshBasicMaterial({map: texture});
+    scene.add(new THREE.Mesh(geometry, material));
+  }
+
+  async function loadFile(url) {
+    const req = await fetch(url);
+    return req.text();
+  }
+
+  function parseData(text) {
+    const data = [];
+    const settings = {data};
+    let max;
+    let min;
+    // split into lines
+    text.split('\n').forEach((line) => {
+      // split the line by whitespace
+      const parts = line.trim().split(/\s+/);
+      if (parts.length === 2) {
+        // only 2 parts, must be a key/value pair
+        settings[parts[0]] = parseFloat(parts[1]);
+      } else if (parts.length > 2) {
+        // more than 2 parts, must be data
+        const values = parts.map((v) => {
+          const value = parseFloat(v);
+          if (value === settings.NODATA_value) {
+            return undefined;
+          }
+          max = Math.max(max === undefined ? value : max, value);
+          min = Math.min(min === undefined ? value : min, value);
+          return value;
+        });
+        data.push(values);
+      }
+    });
+    return Object.assign(settings, {min, max});
+  }
+
+  function addBoxes(file) {
+    const {min, max, data} = file;
+    const range = max - min;
+
+    // make one box geometry
+    const boxWidth = 1;
+    const boxHeight = 1;
+    const boxDepth = 1;
+    const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+    // make it so it scales away from the positive Z axis
+    geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
+
+    // these helpers will make it easy to position the boxes
+    // We can rotate the lon helper on its Y axis to the longitude
+    const lonHelper = new THREE.Object3D();
+    scene.add(lonHelper);
+    // We rotate the latHelper on its X axis to the latitude
+    const latHelper = new THREE.Object3D();
+    lonHelper.add(latHelper);
+    // The position helper moves the object to the edge of the sphere
+    const positionHelper = new THREE.Object3D();
+    positionHelper.position.z = 1;
+    latHelper.add(positionHelper);
+
+    const lonFudge = Math.PI * .5;
+    const latFudge = Math.PI * -0.135;
+    data.forEach((row, latNdx) => {
+      row.forEach((value, lonNdx) => {
+        if (value === undefined) {
+          return;
+        }
+        const amount = (value - min) / range;
+        const material = new THREE.MeshBasicMaterial();
+        const hue = THREE.Math.lerp(0.7, 0.3, amount);
+        const saturation = 1;
+        const lightness = THREE.Math.lerp(0.1, 1.0, amount);
+        material.color.setHSL(hue, saturation, lightness);
+        const mesh = new THREE.Mesh(geometry, material);
+        scene.add(mesh);
+
+        // adjust the helpers to point to the latitude and longitude
+        lonHelper.rotation.y = THREE.Math.degToRad(lonNdx + file.xllcorner) + lonFudge;
+        latHelper.rotation.x = THREE.Math.degToRad(latNdx + file.yllcorner) + latFudge;
+
+        // use the world matrix of the position helper to
+        // position this mesh.
+        positionHelper.updateWorldMatrix(true, false);
+        mesh.applyMatrix(positionHelper.matrixWorld);
+
+        mesh.scale.set(0.005, 0.005, THREE.Math.lerp(0.01, 0.5, amount));
+      });
+    });
+  }
+
+  loadFile('resources/data/gpw/gpw-v4-basic-demographic-characteristics-rev10_a000_014_2010_1_deg_asc/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+    .then(parseData)
+    .then(addBoxes)
+    .then(render);
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  let requestId;
+
+  function render() {
+    requestId = undefined;
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    controls.update();
+    renderer.render(scene, camera);
+  }
+  render();
+
+  controls.addEventListener('change', () => {
+    if (!requestId) {
+      requestId = requestAnimationFrame(render);
+    }
+  });
+  window.addEventListener('resize', render);
+
+  // note: this is a workaround for an OrbitControls issue
+  // in an iframe. Will remove once the issue is fixed in
+  // three.js
+  window.addEventListener('mousedown', (e) => {
+    e.preventDefault();
+    window.focus();
+  });
+  window.addEventListener('keydown', (e) => {
+    e.preventDefault();
+  });
+}
+
+main();
+</script>
+</html>
+

Some files were not shown because too many files changed in this diff