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:
<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>
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.
<!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>