Browse Source

add billboards article

Gregg Tavares 6 years ago
parent
commit
a19a7e6ce3

BIN
threejs/lessons/resources/images/billboard-label-z-issue.png


+ 322 - 1
threejs/lessons/threejs-billboards.md

@@ -1,6 +1,327 @@
 Title: Three.js Billboards
 Title: Three.js Billboards
 Description: How to make things always face the camera.
 Description: How to make things always face the camera.
 
 
-TBD
+In [a previous article](threejs-canvas-textures.html) we used a `CanvasTexture`
+to make labels / badges on characters. Sometimes we'd like to make labels or
+other things that always face the camera. Three.js provides the `Sprite` and
+`SpriteMaterial` to make this happen.
 
 
+Let's change the badge example from [the article on canvas textures](threejs-canvas-textures.html)
+to use `Sprite` and `SpriteMaterial`
 
 
+```js
+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({
++  const labelMaterial = new THREE.SpriteMaterial({
+    map: texture,
+-    side: THREE.DoubleSide,
+    transparent: 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);
++  const label = new THREE.Sprite(labelMaterial);
+  root.add(label);
+  label.position.y = bodyHeight * 4 / 5;
+  label.position.z = bodyRadiusTop * 1.01;
+
+```
+
+and the labels now always face the camera
+
+{{{example url="../threejs-billboard-labels-w-sprites.html" }}}
+
+One problem is from certain angles the labels now intersect the
+characters. 
+
+<div class="threejs_center"><img src="resources/images/billboard-label-z-issue.png" style="width: 455px;"></div>
+
+We can move the position of the labels to fix.
+
+```js
++// if units are meters then 0.01 here makes size
++// of the label into centimeters.
++const labelBaseScale = 0.01;
+const label = new THREE.Sprite(labelMaterial);
+root.add(label);
+-label.position.y = bodyHeight * 4 / 5;
+-label.position.z = bodyRadiusTop * 1.01;
++label.position.y = head.position.y + headRadius + size * labelBaseScale;
+
+-// 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;
+```
+
+{{{example url="../threejs-billboard-labels-w-sprites-adjust-height.html" }}}
+
+Another thing we can do with billboards is draw facades.
+
+Instead of drawing 3D objects we draw 2D planes with an image
+of 3D objects. This is often faster than drawing 3D objects.
+
+For example let's make a scene with grid of trees. We'll make each
+tree from a cylinder for the base and a cone for the top.
+
+First we make the cone and cylinder geometry and materials that
+all the trees will share
+
+```js
+const trunkRadius = .2;
+const trunkHeight = 1;
+const trunkRadialSegments = 12;
+const trunkGeometry = new THREE.CylinderBufferGeometry(
+    trunkRadius, trunkRadius, trunkHeight, trunkRadialSegments);
+
+const topRadius = trunkRadius * 4;
+const topHeight = trunkHeight * 2;
+const topSegments = 12;
+const topGeometry = new THREE.ConeBufferGeometry(
+    topRadius, topHeight, topSegments);
+
+const trunkMaterial = new THREE.MeshPhongMaterial({color: 'brown'});
+const topMaterial = new THREE.MeshPhongMaterial({color: 'green'});
+```
+
+Then we'll make a function that makes a `Mesh` each for the trunk and top
+of a tree and parents both to an `Object3D`.
+
+```js
+function makeTree(x, z) {
+  const root = new THREE.Object3D();
+  const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
+  trunk.position.y = trunkHeight / 2;
+  root.add(trunk);
+
+  const top = new THREE.Mesh(topGeometry, topMaterial);
+  top.position.y = trunkHeight + topHeight / 2;
+  root.add(top);
+
+  root.position.set(x, 0, z);
+  scene.add(root);
+
+  return root;
+}
+```
+
+Then we'll make a loop to place a grid of trees.
+
+```js
+for (let z = -50; z <= 50; z += 10) {
+  for (let x = -50; x <= 50; x += 10) {
+    makeTree(x, z);
+  }
+}
+```
+
+Let's also add a ground plane while we're at it
+
+```js
+// add ground
+{
+  const size = 400;
+  const geometry = new THREE.PlaneBufferGeometry(size, size);
+  const material = new THREE.MeshPhongMaterial({color: 'gray'});
+  const mesh = new THREE.Mesh(geometry, material);
+  mesh.rotation.x = Math.PI * -0.5;
+  scene.add(mesh);
+}
+```
+
+and change the background to light blue
+
+```js
+const scene = new THREE.Scene();
+-scene.background = new THREE.Color('white');
++scene.background = new THREE.Color('lightblue');
+```
+
+and we get a grid of trees
+
+{{{example url="../threejs-billboard-trees-no-billboards.html" }}}
+
+There are 11x11 or 121 trees. Each tree is made from a 12 polygon
+cone and a 48 polygon trunk so each tree is 60 polygons. 121 * 60
+is 7260 polygons. That's not that many but of course a more detailed
+3D tree might be 1000-3000 polygons. If they were 3000 polygons each
+then 121 trees would be 363000 polygons to draw.
+
+Using facades we can bring that number down.
+
+We could manually create a facade in some painting program but let's write 
+some code to try to generate one.
+
+Let's write some code to render an object to a texture
+using a `RenderTarget`. We covered rendering to a `RenderTarget`
+in [the article on render targets](threejs-rendertargets.html).
+
+```js
+function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
+  const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+  const halfFovY = THREE.Math.degToRad(camera.fov * .5);
+  const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
+
+  camera.position.copy(boxCenter);
+  camera.position.z += distance;
+
+  // pick some near and far values for the frustum that
+  // will contain the box.
+  camera.near = boxSize / 100;
+  camera.far = boxSize * 100;
+
+  camera.updateProjectionMatrix();
+}
+
+function makeSpriteTexture(textureSize, obj) {
+  const rt = new THREE.WebGLRenderTarget(textureSize, textureSize);
+
+  const aspect = 1;  // because the render target is square
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+
+  scene.add(obj);
+
+  // compute the box that contains obj
+  const box = new THREE.Box3().setFromObject(obj);
+
+  const boxSize = box.getSize(new THREE.Vector3());
+  const boxCenter = box.getCenter(new THREE.Vector3());
+
+  // set the camera to frame the box
+  const fudge = 1.1;
+  const size = Math.max(...boxSize.toArray()) * fudge;
+  frameArea(size, size, boxCenter, camera);
+
+  renderer.autoClear = false;
+  renderer.setRenderTarget(rt);
+  renderer.render(scene, camera);
+  renderer.setRenderTarget(null);
+  renderer.autoClear = true;
+
+  scene.remove(obj);
+
+  return {
+    position: boxCenter.multiplyScalar(fudge),
+    scale: size,
+    texture: rt.texture,
+  };
+}
+```
+
+Some things to note about the code above:
+
+We're using the field of view (`fov`) defined above this code.
+
+We're computing a box that contains the tree the same way
+we did in [the article on loading a .obj file](threejs-load-obj.html)
+with a few minor changes.
+
+We call `frameArea` again adapted [the article on loading a .obj file](threejs-load-obj.html).
+In this case we compute how far the camera needs to be away from the object
+given its field of view to contain the object. We then position the camera -z that distance
+from the center of the box that contains the object.
+
+We multiply the size we want to fit by 1.1 (`fudge`) to make sure the tree fits
+completely in the render target. The issue here is the size we're using to
+calculate if the object fits in the camera's view is not taking into account
+that the very edges of the object will end up dipping outside area we
+calculated. We could compute how to make 100% of the box fit but that would
+waste space as well so instead we just *fugde* it.
+
+Then we render to the render target and remove the object from
+the scene. 
+
+It's important to note we need the lights in the scene but we
+need to make sure nothing else is in the scene.
+
+We also need to not set a background color on the scene
+
+```js
+const scene = new THREE.Scene();
+-scene.background = new THREE.Color('lightblue');
+```
+
+Finally we've made the texture we return it and the position and scale we
+need to make the facade so that it will appear to be in the same place.
+
+We then make a tree and call this code and pass it in
+
+```js
+// make billboard texture
+const tree = makeTree(0, 0);
+const facadeSize = 64;
+const treeSpriteInfo = makeSpriteTexture(facadeSize, tree);
+```
+
+We can then make a grid of facades instead of a grid of tree models
+
+```js
++function makeSprite(spriteInfo, x, z) {
++  const {texture, offset, scale} = spriteInfo;
++  const mat = new THREE.SpriteMaterial({
++    map: texture,
++    transparent: true,
++  });
++  const sprite = new THREE.Sprite(mat);
++  scene.add(sprite);
++  sprite.position.set(
++      offset.x + x,
++      offset.y,
++      offset.z + z);
++  sprite.scale.set(scale, scale, scale);
++}
+
+for (let z = -50; z <= 50; z += 10) {
+  for (let x = -50; x <= 50; x += 10) {
+-    makeTree(x, z);
++    makeSprite(treeSpriteInfo, x, z);
+  }
+}
+```
+
+In the code above we apply the offset and scale needed to position the facade so it
+appears the same place the original tree would have appeared.
+
+Now that we're done making the tree facade texture we can set the background again
+
+```js
+scene.background = new THREE.Color('lightblue');
+```
+
+and now we get a scene of tree facades
+
+{{{example url="../threejs-billboard-trees-static-billboards.html" }}}
+
+Compare to the trees models above and you can see it looks fairly similar.
+We used a low-res texture, just 64x64 pixels so the facades are blocky.
+You could increase the resolution. Often facades are used only in the far
+distance when they are fairly small so a low-res texture is enough and
+it saves on drawing detailed trees that are only a few pixels big when
+far away.
+
+Another issue is we are only viewing the tree from one side. This is often
+solved by rendering more facades, say from 8 directions around the object
+and then setting which facade to show based on which direction the camera
+is looking at the facade.
+
+Whether or not you use facades is up to you but hopefully this article
+gave you some ideas and suggested some solutions if you decide to use them.

+ 1 - 0
threejs/lessons/toc.html

@@ -19,6 +19,7 @@
     <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-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-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>
     <li><a href="/threejs/lessons/threejs-canvas-textures.html">Using A Canvas for Dynamic Textures</a></li>
+    <li><a href="/threejs/lessons/threejs-billboards.html">Billboards and Facades</a></li>
   </ul>
   </ul>
   <li>Optimization</li>
   <li>Optimization</li>
   <ul>
   <ul>

+ 179 - 0
threejs/threejs-billboard-labels-w-sprites-adjust-height.html

@@ -0,0 +1,179 @@
+<!-- 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 - Billboard Labels w/Sprites height adjusted</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script src="resources/threejs/r103/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);
+
+  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.SpriteMaterial({
+      map: texture,
+      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;
+
+    // if units are meters then 0.01 here makes size
+    // of the label into centimeters.
+    const labelBaseScale = 0.01;
+    const label = new THREE.Sprite(labelMaterial);
+    root.add(label);
+    label.position.y = head.position.y + headRadius + size * labelBaseScale;
+
+    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>
+

+ 180 - 0
threejs/threejs-billboard-labels-w-sprites.html

@@ -0,0 +1,180 @@
+<!-- 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 - Billboard Labels w/Sprites</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script src="resources/threejs/r103/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);
+
+  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.SpriteMaterial({
+      map: texture,
+      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.Sprite(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>
+

+ 136 - 0
threejs/threejs-billboard-trees-no-billboards.html

@@ -0,0 +1,136 @@
+<!-- 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 - Billboard Trees No Billboards</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script src="resources/threejs/r103/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 = 1000;
+  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.minPolarAngle = 0;
+  controls.maxPolarAngle = Math.PI / 2;
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('lightblue');
+
+  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 trunkRadius = .2;
+  const trunkHeight = 1;
+  const trunkRadialSegments = 12;
+  const trunkGeometry = new THREE.CylinderBufferGeometry(
+      trunkRadius, trunkRadius, trunkHeight, trunkRadialSegments);
+
+  const topRadius = trunkRadius * 4;
+  const topHeight = trunkHeight * 2;
+  const topSegments = 12;
+  const topGeometry = new THREE.ConeBufferGeometry(
+      topRadius, topHeight, topSegments);
+
+  const trunkMaterial = new THREE.MeshPhongMaterial({color: 'brown'});
+  const topMaterial = new THREE.MeshPhongMaterial({color: 'green'});
+
+  function makeTree(x, z) {
+    const root = new THREE.Object3D();
+    const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
+    trunk.position.y = trunkHeight / 2;
+    root.add(trunk);
+
+    const top = new THREE.Mesh(topGeometry, topMaterial);
+    top.position.y = trunkHeight + topHeight / 2;
+    root.add(top);
+
+    root.position.set(x, 0, z);
+    scene.add(root);
+
+    return root;
+  }
+
+  for (let z = -50; z <= 50; z += 10) {
+    for (let x = -50; x <= 50; x += 10) {
+      makeTree(x, z);
+    }
+  }
+
+  // add ground
+  {
+    const size = 400;
+    const geometry = new THREE.PlaneBufferGeometry(size, size);
+    const material = new THREE.MeshPhongMaterial({color: 'gray'});
+    const mesh = new THREE.Mesh(geometry, material);
+    mesh.rotation.x = Math.PI * -0.5;
+    scene.add(mesh);
+  }
+
+  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>
+

+ 206 - 0
threejs/threejs-billboard-trees-static-billboards.html

@@ -0,0 +1,206 @@
+<!-- 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 - Billboard Trees Static Billboards</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script src="resources/threejs/r103/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 = 1000;
+  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.minPolarAngle = 0;
+  controls.maxPolarAngle = Math.PI / 2;
+  controls.update();
+
+  const scene = new THREE.Scene();
+
+  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 trunkRadius = .2;
+  const trunkHeight = 1;
+  const trunkRadialSegments = 12;
+  const trunkGeometry = new THREE.CylinderBufferGeometry(
+      trunkRadius, trunkRadius, trunkHeight, trunkRadialSegments);
+
+  const topRadius = trunkRadius * 4;
+  const topHeight = trunkHeight * 2;
+  const topSegments = 12;
+  const topGeometry = new THREE.ConeBufferGeometry(
+      topRadius, topHeight, topSegments);
+
+  const trunkMaterial = new THREE.MeshPhongMaterial({color: 'brown'});
+  const topMaterial = new THREE.MeshPhongMaterial({color: 'green'});
+
+  function makeTree(x, z) {
+    const root = new THREE.Object3D();
+    const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
+    trunk.position.y = trunkHeight / 2;
+    root.add(trunk);
+
+    const top = new THREE.Mesh(topGeometry, topMaterial);
+    top.position.y = trunkHeight + topHeight / 2;
+    root.add(top);
+
+    root.position.set(x, 0, z);
+    scene.add(root);
+
+    return root;
+  }
+
+  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
+    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+    const halfFovY = THREE.Math.degToRad(camera.fov * .5);
+    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
+
+    camera.position.copy(boxCenter);
+    camera.position.z += distance;
+
+    // pick some near and far values for the frustum that
+    // will contain the box.
+    camera.near = boxSize / 100;
+    camera.far = boxSize * 100;
+
+    camera.updateProjectionMatrix();
+  }
+
+  function makeSpriteTexture(textureSize, obj) {
+    const rt = new THREE.WebGLRenderTarget(textureSize, textureSize);
+
+    const aspect = 1;  // because the render target is square
+    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+
+    scene.add(obj);
+
+    // compute the box that contains obj
+    const box = new THREE.Box3().setFromObject(obj);
+
+    const boxSize = box.getSize(new THREE.Vector3());
+    const boxCenter = box.getCenter(new THREE.Vector3());
+
+    // set the camera to frame the box
+    const fudge = 1.1;
+    const size = Math.max(...boxSize.toArray()) * fudge;
+    frameArea(size, size, boxCenter, camera);
+
+    renderer.autoClear = false;
+    renderer.setRenderTarget(rt);
+    renderer.render(scene, camera);
+    renderer.setRenderTarget(null);
+    renderer.autoClear = true;
+
+    scene.remove(obj);
+
+    return {
+      offset: boxCenter.multiplyScalar(fudge),
+      scale: size,
+      texture: rt.texture,
+    };
+  }
+
+  // make billboard texture
+  const tree = makeTree(0, 0);
+  const facadeSize = 64;
+  const treeSpriteInfo = makeSpriteTexture(facadeSize, tree);
+
+  function makeSprite(spriteInfo, x, z) {
+    const {texture, offset, scale} = spriteInfo;
+    const mat = new THREE.SpriteMaterial({
+      map: texture,
+      transparent: true,
+    });
+    const sprite = new THREE.Sprite(mat);
+    scene.add(sprite);
+    sprite.position.set(
+        offset.x + x,
+        offset.y,
+        offset.z + z);
+    sprite.scale.set(scale, scale, scale);
+  }
+
+  for (let z = -50; z <= 50; z += 10) {
+    for (let x = -50; x <= 50; x += 10) {
+      makeSprite(treeSpriteInfo, x, z);
+    }
+  }
+
+  scene.background = new THREE.Color('lightblue');
+
+  {
+    const size = 400;
+    const geometry = new THREE.PlaneBufferGeometry(size, size);
+    const material = new THREE.MeshPhongMaterial({color: 'gray'});
+    const mesh = new THREE.Mesh(geometry, material);
+    mesh.rotation.x = Math.PI * -0.5;
+    scene.add(mesh);
+  }
+
+  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>
+