Browse Source

add canvas texture article

Gregg Tavares 6 years ago
parent
commit
61883e15f2

+ 51 - 0
threejs/canvas-random-dots.html

@@ -0,0 +1,51 @@
+<!-- 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>Canvas Random Dots</title>
+  </head>
+  <body>
+  </body>
+<script>
+'use strict';
+
+function main() {
+  const ctx = document.createElement('canvas').getContext('2d');
+  document.body.appendChild(ctx.canvas);
+  ctx.canvas.width = 256;
+  ctx.canvas.height = 256;
+  ctx.fillStyle = '#FFF';
+  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+  function randInt(min, max) {
+    if (max === undefined) {
+      max = min;
+      min = 0;
+    }
+    return Math.random() * (max - min) + min | 0;
+  }
+
+  function drawRandomDot() {
+    ctx.fillStyle = `#${randInt(0x1000000).toString(16).padStart(6, '0')}`;
+    ctx.beginPath();
+
+    const x = randInt(256);
+    const y = randInt(256);
+    const radius = randInt(10, 64);
+    ctx.arc(x, y, radius, 0, Math.PI * 2);
+    ctx.fill();
+  }
+
+  function render() {
+    drawRandomDot();
+    requestAnimationFrame(render);
+  }
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 371 - 0
threejs/lessons/threejs-canvas-textures.md

@@ -0,0 +1,371 @@
+Title: Three.js Canvas Textures
+Description: How to use a canvas as a texture in Three.js
+
+This article continues from [the article on textures](threejs-textures.html).
+If you haven't read that yet you should probably start there.
+
+In [the previous article on textures](threejs-textures.html) we mostly used
+image files for textures. Sometimes though we want to generate a texture
+at runtime. One way to do this is to use a `CanvasTexture`.
+
+A canvas texture takes a `<canvas>` as its input. If you don't know how to
+draw with the 2D canvas API on a canvas [there's a good tutorial on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial).
+
+Let's make a simple canvas program. Here's one that draws dots at random places in random colors.
+
+```js
+const ctx = document.createElement('canvas').getContext('2d');
+document.body.appendChild(ctx.canvas);
+ctx.canvas.width = 256;
+ctx.canvas.height = 256;
+ctx.fillStyle = '#FFF';
+ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+function randInt(min, max) {
+  if (max === undefined) {
+    max = min;
+    min = 0;
+  }
+  return Math.random() * (max - min) + min | 0;
+}
+
+function drawRandomDot() {
+  ctx.fillStyle = `#${randInt(0x1000000).toString(16).padStart(6, '0')}`;
+  ctx.beginPath();
+
+  const x = randInt(256);
+  const y = randInt(256);
+  const radius = randInt(10, 64);
+  ctx.arc(x, y, radius, 0, Math.PI * 2);
+  ctx.fill();
+}
+
+function render() {
+  drawRandomDot();
+  requestAnimationFrame(render);
+}
+requestAnimationFrame(render);
+```
+
+it's pretty straight forward.
+
+{{{example url="../canvas-random-dots.html" }}}
+
+Now let's use it to texture something. We'll start with the example of texturing
+a cube from [the previous article](threejs-textures.html).
+We'll remove the code that loads an image and instead use
+our canvas by creating a `CanvasTexture` and passing it the canvas we created.
+
+```js
+const cubes = [];  // just an array we can use to rotate the cubes
+-const loader = new THREE.TextureLoader();
+-
++const ctx = document.createElement('canvas').getContext('2d');
++ctx.canvas.width = 256;
++ctx.canvas.height = 256;
++ctx.fillStyle = '#FFF';
++ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
++const texture = new THREE.CanvasTexture(ctx.canvas);
+
+const material = new THREE.MeshBasicMaterial({
+-  map: loader.load('resources/images/wall.jpg'),
++  map: texture,
+});
+const cube = new THREE.Mesh(geometry, material);
+scene.add(cube);
+cubes.push(cube);  // add to our list of cubes to rotate
+```
+
+And then call the code to draw a random dot in our render loop
+
+```js
+function render(time) {
+  time *= 0.001;
+
+  if (resizeRendererToDisplaySize(renderer)) {
+    const canvas = renderer.domElement;
+    camera.aspect = canvas.clientWidth / canvas.clientHeight;
+    camera.updateProjectionMatrix();
+  }
+
++  drawRandomDot();
++  texture.needsUpdate = true;
+
+  cubes.forEach((cube, ndx) => {
+    const speed = .2 + ndx * .1;
+    const rot = time * speed;
+    cube.rotation.x = rot;
+    cube.rotation.y = rot;
+  });
+
+  renderer.render(scene, camera);
+
+  requestAnimationFrame(render);
+}
+```
+
+The only extra thing we need to do is set the `needsUpdate` property
+of the `CanvasTexture` to tell three.js to update the texture with
+the latest contents of the canvas.
+
+And with that we have a canvas textured cube
+
+{{{example url="../threejs-canvas-textured-cube.html" }}}
+
+Note that if you want to use three.js to draw into the canvas you're
+better off using a `RenderTarget` which is covered in [this article](threejs-rendertargets.html).
+
+A common use case for canvas textures is to provide text in a scene.
+For example if you wanted to put a person's name on their character's
+badge you might use a canvas texture to texture the badge.
+
+Let's make a scene with 3 people and give each person a badge
+or label.
+
+Let's take the example above and remove all the cube related
+stuff. Then let's set the background to white and add two [lights](threejs-lights.html).
+
+```js
+const scene = new THREE.Scene();
++scene.background = new THREE.Color('white');
++
++function addLight(position) {
++  const color = 0xFFFFFF;
++  const intensity = 1;
++  const light = new THREE.DirectionalLight(color, intensity);
++  light.position.set(...position);
++  scene.add(light);
++  scene.add(light.target);
++}
++addLight([-3, 1, 1]);
++addLight([ 2, 1, .5]);
+```
+
+Let's make some code to make a label using canvas 2D
+
+```js
++function makeLabelCanvas(size, name) {
++  const borderSize = 2;
++  const ctx = document.createElement('canvas').getContext('2d');
++  const font =  `${size}px bold sans-serif`;
++  ctx.font = font;
++  // measure how long the name will be
++  const doubleBorderSize = borderSize * 2;
++  const width = ctx.measureText(name).width + doubleBorderSize;
++  const height = size + doubleBorderSize;
++  ctx.canvas.width = width;
++  ctx.canvas.height = height;
++
++  // need to set font again after resizing canvas
++  ctx.font = font;
++  ctx.textBaseline = 'top';
++
++  ctx.fillStyle = 'blue';
++  ctx.fillRect(0, 0, width, height);
++  ctx.fillStyle = 'white';
++  ctx.fillText(name, borderSize, borderSize);
++
++  return ctx.canvas;
++}
+```
+
+Then we'll make simple people from a cylinder for the body, a sphere
+for the head, and a plane for the label.
+
+First let's make the shared geometry.
+
+```js
++const bodyRadiusTop = .4;
++const bodyRadiusBottom = .2;
++const bodyHeight = 2;
++const bodyRadialSegments = 6;
++const bodyGeometry = new THREE.CylinderBufferGeometry(
++    bodyRadiusTop, bodyRadiusBottom, bodyHeight, bodyRadialSegments);
++
++const headRadius = bodyRadiusTop * 0.8;
++const headLonSegments = 12;
++const headLatSegments = 5;
++const headGeometry = new THREE.SphereBufferGeometry(
++    headRadius, headLonSegments, headLatSegments);
++
++const labelGeometry = new THREE.PlaneBufferGeometry(1, 1);
+```
+
+Then let's make a function to build a person from these
+parts.
+
+```js
++function makePerson(x, size, name, color) {
++  const canvas = makeLabelCanvas(size, name);
++  const texture = new THREE.CanvasTexture(canvas);
++  // because our canvas is likely not a power of 2
++  // in both dimensions set the filtering appropriately.
++  texture.minFilter = THREE.LinearFilter;
++  texture.wrapS = THREE.ClampToEdgeWrapping;
++  texture.wrapT = THREE.ClampToEdgeWrapping;
++
++  const labelMaterial = new THREE.MeshBasicMaterial({
++    map: texture,
++    side: THREE.DoubleSide,
++    transparent: true,
++  });
++  const bodyMaterial = new THREE.MeshPhongMaterial({
++    color,
++    flatShading: true,
++  });
++
++  const root = new THREE.Object3D();
++  root.position.x = x;
++
++  const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
++  root.add(body);
++  body.position.y = bodyHeight / 2;
++
++  const head = new THREE.Mesh(headGeometry, bodyMaterial);
++  root.add(head);
++  head.position.y = bodyHeight + headRadius * 1.1;
++
++  const label = new THREE.Mesh(labelGeometry, labelMaterial);
++  root.add(label);
++  label.position.y = bodyHeight * 4 / 5;
++  label.position.z = bodyRadiusTop * 1.01;
++
++  // if units are meters then 0.01 here makes size
++  // of the label into centimeters.
++  const labelBaseScale = 0.01;
++  label.scale.x = canvas.width  * labelBaseScale;
++  label.scale.y = canvas.height * labelBaseScale;
++
++  scene.add(root);
++  return root;
++}
+```
+
+You can see above we put the body, head, and label on a root
+`Object3D` and adjust their positions. This would let us move the
+root object if we wanted to move the people. The body is 2 units
+high. If 1 unit equals 1 meter then the code above tries to
+make the label in centimeters so they will be size centimeters
+tall and however wide is needed to fit the text.
+
+We can then make people with labels
+
+```js
++makePerson(-3, 32, 'Purple People Eater', 'purple');
++makePerson(-0, 32, 'Green Machine', 'green');
++makePerson(+3, 32, 'Red Menace', 'red');
+```
+
+What's left is to add some `OrbitControls` so we can move
+the camera.
+
+```html
+<script src="resources/threejs/r102/three.min.js"></script>
++<script src="resources/threejs/r102/js/controls/OrbitControls.js"></script>
+```
+
+```js
+const fov = 75;
+const aspect = 2;  // the canvas default
+const near = 0.1;
+-const far = 5;
++const far = 50;
+const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+-camera.position.z = 2;
++camera.position.set(0, 2, 5);
+
++const controls = new THREE.OrbitControls(camera, canvas);
++controls.target.set(0, 2, 0);
++controls.update();
+```
+
+and we get simple labels.
+
+{{{example url="../threejs-canvas-textured-labels.html" }}}
+
+Some things to notice.
+
+* If you zoom in the labels get pretty low-res.
+
+There is no easy solution. There are more complex font
+rendering techniques but I know of no plugin solutions.
+Plus they will require the user download font data which
+would be slow.
+
+One solution is to increase the resolution of the labels.
+Try setting the size passed into to double what it is now
+and setting `labelBaseScale` to half what it currently is.
+
+* The labels get longer the longer the name.
+
+If you wanted to fix this you'd instead choose a fixed sized
+label and then squish the text.
+
+This is pretty easy. Pass in a base width and scale the text to fit that
+width like this
+
+```js
+-function makeLabelCanvas(size, name) {
++function makeLabelCanvas(baseWidth, size, name) {
+  const borderSize = 2;
+  const ctx = document.createElement('canvas').getContext('2d');
+  const font =  `${size}px bold sans-serif`;
+  ctx.font = font;
+  // measure how long the name will be
++  const textWidth = ctx.measureText(name).width;
+
+  const doubleBorderSize = borderSize * 2;
+-  const width = ctx.measureText(name).width + doubleBorderSize;
++  const width = baseWidth + doubleBorderSize;
+  const height = size + doubleBorderSize;
+  ctx.canvas.width = width;
+  ctx.canvas.height = height;
+
+  // need to set font again after resizing canvas
+  ctx.font = font;
+-  ctx.textBaseline = 'top';
++  ctx.textBaseline = 'middle';
++  ctx.textAlign = 'center';
+
+  ctx.fillStyle = 'blue';
+  ctx.fillRect(0, 0, width, height);
+
++  // scale to fit but don't stretch
++  const scaleFactor = Math.min(1, baseWidth / textWidth);
++  ctx.translate(width / 2, height / 2);
++  ctx.scale(scaleFactor, 1);
+  ctx.fillStyle = 'white';
+  ctx.fillText(name, borderSize, borderSize);
+
+  return ctx.canvas;
+}
+```
+
+Then we can pass in a width for the labels
+
+```js
+-function makePerson(x, size, name, color) {
+-  const canvas = makeLabelCanvas(size, name);
++function makePerson(x, labelWidth, size, name, color) {
++  const canvas = makeLabelCanvas(labelWidth, size, name);
+
+...
+
+}
+
+-makePerson(-3, 32, 'Purple People Eater', 'purple');
+-makePerson(-0, 32, 'Green Machine', 'green');
+-makePerson(+3, 32, 'Red Menace', 'red');
++makePerson(-3, 150, 32, 'Purple People Eater', 'purple');
++makePerson(-0, 150, 32, 'Green Machine', 'green');
++makePerson(+3, 150, 32, 'Red Menace', 'red');
+```
+
+and we get labels where the text is centered and scaled to fit
+
+{{{example url="../threejs-canvas-textured-labels-scale-to-fit.html" }}}
+
+Another issue is that the labels don't always face the camera. If you're using 
+labels as badges that's probably a good thing. If you're using labels to put
+names over players in a 3D game maybe you want the labels to always face the camera.
+We'll cover how to do that in [an article on billboards](threejs-billboards.html).

+ 1 - 0
threejs/lessons/toc.html

@@ -18,6 +18,7 @@
     <li><a href="/threejs/lessons/threejs-shadertoy.html">Using Shadertoy shaders</a></li>    
     <li><a href="/threejs/lessons/threejs-align-html-elements-to-3d.html">Aligning HTML Elements to 3D</a></li>
     <li><a href="/threejs/lessons/threejs-indexed-textures.html">Using Indexed Textures for Picking and Color</a></li>
+    <li><a href="/threejs/lessons/threejs-canvas-textures.html">Using A Canvas for Dynamic Textures</a></li>
   </ul>
   <li>Optimization</li>
   <ul>

+ 121 - 0
threejs/threejs-canvas-textured-cube.html

@@ -0,0 +1,121 @@
+<!-- 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 - Canvas Textured Cube</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r102/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  const cubes = [];  // just an array we can use to rotate the cubes
+  const ctx = document.createElement('canvas').getContext('2d');
+  ctx.canvas.width = 256;
+  ctx.canvas.height = 256;
+  ctx.fillStyle = '#FFF';
+  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+  const texture = new THREE.CanvasTexture(ctx.canvas);
+
+  const material = new THREE.MeshBasicMaterial({
+    map: texture,
+  });
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+  cubes.push(cube);  // add to our list of cubes to rotate
+
+  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;
+  }
+
+  function randInt(min, max) {
+    if (max === undefined) {
+      max = min;
+      min = 0;
+    }
+    return Math.random() * (max - min) + min | 0;
+  }
+
+  function drawRandomDot() {
+    ctx.fillStyle = `#${randInt(0x1000000).toString(16).padStart(6, '0')}`;
+    ctx.beginPath();
+
+    const x = randInt(256);
+    const y = randInt(256);
+    const radius = randInt(10, 64);
+    ctx.arc(x, y, radius, 0, Math.PI * 2);
+    ctx.fill();
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    drawRandomDot();
+    texture.needsUpdate = true;
+
+    cubes.forEach((cube, ndx) => {
+      const speed = .2 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 184 - 0
threejs/threejs-canvas-textured-labels-scale-to-fit.html

@@ -0,0 +1,184 @@
+<!-- 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 - Canvas Textured Labels Scale to Fit</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r102/three.min.js"></script>
+<script src="resources/threejs/r102/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 = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 50;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.set(0, 2, 5);
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.target.set(0, 2, 0);
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('white');
+
+  function addLight(position) {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(...position);
+    scene.add(light);
+    scene.add(light.target);
+  }
+  addLight([-3, 1, 1]);
+  addLight([ 2, 1, .5]);
+
+  const bodyRadiusTop = .4;
+  const bodyRadiusBottom = .2;
+  const bodyHeight = 2;
+  const bodyRadialSegments = 6;
+  const bodyGeometry = new THREE.CylinderBufferGeometry(
+      bodyRadiusTop, bodyRadiusBottom, bodyHeight, bodyRadialSegments);
+
+  const headRadius = bodyRadiusTop * 0.8;
+  const headLonSegments = 12;
+  const headLatSegments = 5;
+  const headGeometry = new THREE.SphereBufferGeometry(
+      headRadius, headLonSegments, headLatSegments);
+
+  const labelGeometry = new THREE.PlaneBufferGeometry(1, 1);
+
+  function makeLabelCanvas(baseWidth, size, name) {
+    const borderSize = 2;
+    const ctx = document.createElement('canvas').getContext('2d');
+    const font =  `${size}px bold sans-serif`;
+    ctx.font = font;
+    // measure how long the name will be
+    const textWidth = ctx.measureText(name).width;
+
+    const doubleBorderSize = borderSize * 2;
+    const width = baseWidth + doubleBorderSize;
+    const height = size + doubleBorderSize;
+    ctx.canvas.width = width;
+    ctx.canvas.height = height;
+
+    // need to set font again after resizing canvas
+    ctx.font = font;
+    ctx.textBaseline = 'middle';
+    ctx.textAlign = 'center';
+
+    ctx.fillStyle = 'blue';
+    ctx.fillRect(0, 0, width, height);
+
+    // scale to fit but don't stretch
+    const scaleFactor = Math.min(1, baseWidth / textWidth);
+    ctx.translate(width / 2, height / 2);
+    ctx.scale(scaleFactor, 1);
+    ctx.fillStyle = 'white';
+    ctx.fillText(name, 0, 0);
+
+    return ctx.canvas;
+  }
+
+  function makePerson(x, labelWidth, size, name, color) {
+    const canvas = makeLabelCanvas(labelWidth, size, name);
+    const texture = new THREE.CanvasTexture(canvas);
+    // because our canvas is likely not a power of 2
+    // in both dimensions set the filtering appropriately.
+    texture.minFilter = THREE.LinearFilter;
+    texture.wrapS = THREE.ClampToEdgeWrapping;
+    texture.wrapT = THREE.ClampToEdgeWrapping;
+
+    const labelMaterial = new THREE.MeshBasicMaterial({
+      map: texture,
+      side: THREE.DoubleSide,
+      transparent: true,
+    });
+    const bodyMaterial = new THREE.MeshPhongMaterial({
+      color,
+      flatShading: true,
+    });
+
+    const root = new THREE.Object3D();
+    root.position.x = x;
+
+    const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
+    root.add(body);
+    body.position.y = bodyHeight / 2;
+
+    const head = new THREE.Mesh(headGeometry, bodyMaterial);
+    root.add(head);
+    head.position.y = bodyHeight + headRadius * 1.1;
+
+    const label = new THREE.Mesh(labelGeometry, labelMaterial);
+    root.add(label);
+    label.position.y = bodyHeight * 4 / 5;
+    label.position.z = bodyRadiusTop * 1.01;
+
+    // if units are meters then 0.01 here makes size
+    // of the label into centimeters.
+    const labelBaseScale = 0.01;
+    label.scale.x = canvas.width  * labelBaseScale;
+    label.scale.y = canvas.height * labelBaseScale;
+
+    scene.add(root);
+    return root;
+  }
+
+  makePerson(-3, 150, 32, 'Purple People Eater', 'purple');
+  makePerson(-0, 150, 32, 'Green Machine', 'green');
+  makePerson(+3, 150, 32, 'Red Menace', 'red');
+
+  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;
+  }
+
+
+  function render() {
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 176 - 0
threejs/threejs-canvas-textured-labels.html

@@ -0,0 +1,176 @@
+<!-- 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 - Canvas Textured Labels</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r102/three.min.js"></script>
+<script src="resources/threejs/r102/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 = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 50;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.set(0, 2, 5);
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.target.set(0, 2, 0);
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('white');
+
+  function addLight(position) {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(...position);
+    scene.add(light);
+    scene.add(light.target);
+  }
+  addLight([-3, 1, 1]);
+  addLight([ 2, 1, .5]);
+
+  const bodyRadiusTop = .4;
+  const bodyRadiusBottom = .2;
+  const bodyHeight = 2;
+  const bodyRadialSegments = 6;
+  const bodyGeometry = new THREE.CylinderBufferGeometry(
+      bodyRadiusTop, bodyRadiusBottom, bodyHeight, bodyRadialSegments);
+
+  const headRadius = bodyRadiusTop * 0.8;
+  const headLonSegments = 12;
+  const headLatSegments = 5;
+  const headGeometry = new THREE.SphereBufferGeometry(
+      headRadius, headLonSegments, headLatSegments);
+
+  const labelGeometry = new THREE.PlaneBufferGeometry(1, 1);
+
+  function makeLabelCanvas(size, name) {
+    const borderSize = 2;
+    const ctx = document.createElement('canvas').getContext('2d');
+    const font =  `${size}px bold sans-serif`;
+    ctx.font = font;
+    // measure how long the name will be
+    const doubleBorderSize = borderSize * 2;
+    const width = ctx.measureText(name).width + doubleBorderSize;
+    const height = size + doubleBorderSize;
+    ctx.canvas.width = width;
+    ctx.canvas.height = height;
+
+    // need to set font again after resizing canvas
+    ctx.font = font;
+    ctx.textBaseline = 'top';
+
+    ctx.fillStyle = 'blue';
+    ctx.fillRect(0, 0, width, height);
+    ctx.fillStyle = 'white';
+    ctx.fillText(name, borderSize, borderSize);
+
+    return ctx.canvas;
+  }
+
+  function makePerson(x, size, name, color) {
+    const canvas = makeLabelCanvas(size, name);
+    const texture = new THREE.CanvasTexture(canvas);
+    // because our canvas is likely not a power of 2
+    // in both dimensions set the filtering appropriately.
+    texture.minFilter = THREE.LinearFilter;
+    texture.wrapS = THREE.ClampToEdgeWrapping;
+    texture.wrapT = THREE.ClampToEdgeWrapping;
+
+    const labelMaterial = new THREE.MeshBasicMaterial({
+      map: texture,
+      side: THREE.DoubleSide,
+      transparent: true,
+    });
+    const bodyMaterial = new THREE.MeshPhongMaterial({
+      color,
+      flatShading: true,
+    });
+
+    const root = new THREE.Object3D();
+    root.position.x = x;
+
+    const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
+    root.add(body);
+    body.position.y = bodyHeight / 2;
+
+    const head = new THREE.Mesh(headGeometry, bodyMaterial);
+    root.add(head);
+    head.position.y = bodyHeight + headRadius * 1.1;
+
+    const label = new THREE.Mesh(labelGeometry, labelMaterial);
+    root.add(label);
+    label.position.y = bodyHeight * 4 / 5;
+    label.position.z = bodyRadiusTop * 1.01;
+
+    // if units are meters then 0.01 here makes size
+    // of the label into centimeters.
+    const labelBaseScale = 0.01;
+    label.scale.x = canvas.width  * labelBaseScale;
+    label.scale.y = canvas.height * labelBaseScale;
+
+    scene.add(root);
+    return root;
+  }
+
+  makePerson(-3, 32, 'Purple People Eater', 'purple');
+  makePerson(-0, 32, 'Green Machine', 'green');
+  makePerson(+3, 32, 'Red Menace', 'red');
+
+  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;
+  }
+
+
+  function render() {
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+