# 广告牌(Billboards)

在 上一篇文章 我们使用了一个 `CanvasTexture` 在人物上创作标签(Labels)和徽标(Badges)。有时我们想制作一些总是面对相机的东西。Three.js提供了 `Sprite` 和 `SpriteMaterial` 来实现这个功能。

我们修改这个徽标的例子 使用Canvas作为纹理， 应用 `Sprite` and `SpriteMaterial`

    function makePerson(x, labelWidth, size, name, color) {
      const canvas = makeLabelCanvas(labelWidth, size, name);
      const texture = new THREE.CanvasTexture(canvas);
      // 因为我们的Canvas的尺寸可能不是2的N次方
      // 在两个维度上适当地设置filter属性
      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;

现在标签始终是面向相机了。

点击在新窗口打开

一个问题是，从某些角度来看的话，标签与人物重合了。

我们可以通过移动标签的位置来解决此问题。

    +// 如果单位是米，这里就用0.01
    +// 也就是以厘米作为标签的单位
    +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;

    -// 如果单位是米，这里就用0.01
    -// 也就是以厘米作为标签的单位
    -const labelBaseScale = 0.01;
    label.scale.x = canvas.width  * labelBaseScale;
    label.scale.y = canvas.height * labelBaseScale;

点击在新窗口打开

我们可以用Billboard做的另一件事是绘制立面(Facades)。

我们不绘制 3D 对象，而是使用图片绘制 2D 平面化的 3D 对象，这通常比绘制 3D 对象要快。

例如，我们用树木网络制作一个场景，我们让每一棵树的底部是圆柱体，顶部是圆锥体。

第一步，我们创建圆锥体和圆柱体的Geometry和Material，所有的树都会复用这些。

    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'});

然后我们创建一个函数，对每一棵树的树干和树顶创建一个 `Mesh` ，并把它们都加入到一个 `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;
    }

然后我们会创建一个循环，生成树网络。

    for (let z = -50; z <= 50; z += 10) {
      for (let x = -50; x <= 50; x += 10) {
        makeTree(x, z);
      }
    }

让我们再增加一个地平面。

    // 添加地面
    {
      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);
    }

然后把背景调整为浅蓝(lightblue)

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

我们得到了一个树木网络

点击在新窗口打开

这里有11x11或者121棵树，每棵树由12个多边形组成的锥体 + 48个多边形组成的树干组成，所以每棵树包含60个多边形。121*60的结果是7260，这并不是很多，当然更精细的3D树可能有1000-3000个多边形构成。如果3000个多边形构成的树，那么121棵树将包含363000个多边形。

使用Facades，可以降低这个数字。

我们可以在一些绘图应用中手动创建一个Facade，现在让我们编写一些代码来手动生成一个。

现在写一些代码把对象绘制到纹理中，使用一个 `RenderTarget`，我们提到过使用 `RenderTarget` 来渲染，具体在这篇 渲染目标 文章里。

    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;

      // 为视锥体选择合适的near和far值
      // 可以把盒模型包裹进来
      camera.near = boxSize / 100;
      camera.far = boxSize * 100;

      camera.updateProjectionMatrix();
    }

    function makeSpriteTexture(textureSize, obj) {
      const rt = new THREE.WebGLRenderTarget(textureSize, textureSize);

      const aspect = 1;  // 因为Render Target是正方形
      const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

      scene.add(obj);

      // 计算对象的盒模型
      const box = new THREE.Box3().setFromObject(obj);

      const boxSize = box.getSize(new THREE.Vector3());
      const boxCenter = box.getCenter(new THREE.Vector3());

      // 设置相机去构建盒模型
      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,
      };
    }

关于上面代码的一些注意事项：

我们使用之前代码定义好的视图的 (`fov`) 属性，

我们计算一个包含树的盒模型，这和 加载.obj的文件 中提到的方式一致，有一点微小的改变。

我们再次调用 `frameArea`，稍微改写了一下加载.obj的文件。 在这种情况下，我们计算相机需要离物体多远，从而让它的视野以包含对象。然后我们将相机的-z值设置为从对象盒模型的中心到此的距离。

我们将想要适应的大小乘以1.1倍(`fudge`) 来确保树完全在渲染目标中。这是因为我们用来计算对象是否适合相机的视口的尺寸，没有考虑到对象的边缘可能会超出我们的可视区域之外。我们可以计算出如何让盒子100%合适，但这会浪费很多空间，所以我们就是蒙混 (fudge) 一下。

然后我们渲染到RenderTarget中，然后从场景中移除此对象。

重点是需要场景中的灯光，但我们需要确保场景中没有其他东西。

我们也不能给场景设置背景色。

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

最后我们返回了纹理，位置和缩放比例，我们需要创建Facade，让它看起来在同一个地方。

然后我们制作一棵树，调用此代码：

    // 创建Billboard纹理
    const tree = makeTree(0, 0);
    const facadeSize = 64;
    const treeSpriteInfo = makeSpriteTexture(facadeSize, tree);

然后我们可以制作一个Facade网络，而不是树网络。

    +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);
      }
    }

在上面的代码中，我们应用了定位Facade所需的偏移量和缩放比例，因此他会出现在和原树同一个地方。

现在我们已经完成了Facade纹理的制作，我们可以再次设置背景。

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

现在我们得到了一个全是树Facades的场景。

点击在新窗口打开

与上面的树模型相比，它们看起来非常相似。我们使用了低分辨率纹理，只有64x64像素，所以Facade是块状的，你当然可以提高分辨率。通常Facade只会用在非常远处的物体，因为当它们非常小的时候，低分辨率纹理就足够了。它节省了绘制远处只有几个像素的精致树模型时间。

另一个问题是我们只能从一侧查看树。这往往是通过渲染更多的Facade来解决，比如绘制对象周围的8个方向，然后根据实际相机的方向来设置要展示的Facade。

是否使用Facade由你决定，如果你决定去使用它们，希望这篇文章给了你一些想法和解决方案。