Custom Geometry

Custom Geometry from Typed Arrays

Fill a Float32Array and wrap it in new THREE.BufferAttribute(array, itemSize). Without an index, every three positions form a triangle, counterclockwise from the front (Winding Order). BookNest's bookmark is three triangles, plus a color attribute the material multiplies in when vertexColors is on:

A bookmark built from nine vertices, shared by six meshesHTML
<script src="three-map.js"></script>
<script type="module">
import * as THREE from 'three';
import { BOOKS, createStage, renderFrames } from './bookshelf.js';
const { renderer, scene, camera } = createStage({ width: 640, height: 110 });
camera.position.set(0, 0, 1.3);
camera.lookAt(0, 0, 0);
const positions = new Float32Array([-0.06, 0.3, 0, -0.06, -0.3, 0, 0, -0.22, 0,   // left
  -0.06, 0.3, 0, 0, -0.22, 0, 0.06, 0.3, 0, 0.06, 0.3, 0, 0, -0.22, 0, 0.06, -0.3, 0]);
const colors = new Float32Array(positions.length);    // brighter toward the top
for (let i = 0; i < colors.length; i += 3) colors.fill(0.7 + positions[i + 1], i, i + 3);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.computeVertexNormals();                      // Section 5.3.4
BOOKS.forEach((book, i) => {
  const mark = new THREE.Mesh(geometry,
    new THREE.MeshStandardMaterial({ color: book.color, vertexColors: true }));
  mark.position.x = (i - 2.5) * 0.24;
  scene.add(mark);
});
renderFrames(renderer, scene, camera);
</script>
Browser output of Listing 5.8
Browser output of 8

Six meshes share one geometry, so the GPU stores nine vertices once. A Uint8Array with normalized set (the third BufferAttribute argument) stores colors in a quarter of the bytes.

BookNest bookmarks built from a Float32Array of positions and a vertex color attributeHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
  .ui { padding: 6px 8px; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div class="ui"><label><input type="checkbox" id="vc" checked> vertexColors</label>
  <label><input type="checkbox" id="wire"> wireframe (shows the three triangles)</label></div>
<script type="module">
import * as THREE from 'three';

const colors6 = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 240);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 600 / 240, 0.1, 50);
camera.position.set(0, 0, 1.2);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.5));
const sun = new THREE.DirectionalLight('#ffffff', 1.5);
sun.position.set(0, 1, 3);
scene.add(sun);

// Three triangles, counterclockwise seen from the front: a ribbon with a notched tail
const positions = new Float32Array([
  -0.06, 0.3, 0,   -0.06, -0.3, 0,   0, -0.22, 0,      // left
  -0.06, 0.3, 0,    0, -0.22, 0,     0.06, 0.3, 0,     // middle
   0.06, 0.3, 0,    0, -0.22, 0,     0.06, -0.3, 0,    // right
]);
// One RGB triple per vertex: brighter toward the top
const colors = new Float32Array(positions.length);
for (let i = 0; i < colors.length; i += 3) colors.fill(0.7 + positions[i + 1], i, i + 3);

const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.computeVertexNormals();

// Six meshes share the one geometry: nine vertices stored once on the GPU
const marks = colors6.map((color, i) => {
  const mark = new THREE.Mesh(geometry,
    new THREE.MeshStandardMaterial({ color, vertexColors: true, side: THREE.DoubleSide }));
  mark.position.x = (i - 2.5) * 0.24;
  scene.add(mark);
  return mark;
});

const setAll = (fn) => marks.forEach((m) => { fn(m.material); m.material.needsUpdate = true; });
document.getElementById('vc').addEventListener('change', (e) => setAll((m) => { m.vertexColors = e.target.checked; }));
document.getElementById('wire').addEventListener('change', (e) => setAll((m) => { m.wireframe = e.target.checked; }));

renderer.setAnimationLoop((time) => {
  marks.forEach((m, i) => { m.rotation.y = Math.sin(time / 800 + i) * 0.5; });
  renderer.render(scene, camera);
});
</script>