Billboards

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.

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;

We can move the position of the labels to fix.

// 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;

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

const trunkRadius = .2;
const trunkHeight = 1;
const trunkRadialSegments = 12;
const trunkGeometry = new THREE.CylinderGeometry(
    trunkRadius, trunkRadius, trunkHeight, trunkRadialSegments);
 
const topRadius = trunkRadius * 4;
const topHeight = trunkHeight * 2;
const topSegments = 12;
const topGeometry = new THREE.ConeGeometry(
    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.

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.

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

// add ground
{
  const size = 400;
  const geometry = new THREE.PlaneGeometry(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

const scene = new THREE.Scene();
scene.background = new THREE.Color('white');
scene.background = new THREE.Color('lightblue');

There are 11×11 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.

function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
  const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
  const halfFovY = THREE.MathUtils.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,
  };
}

Leave a comment

Your email address will not be published. Required fields are marked *