attributes holds named BufferAttributes, typed arrays with an itemSize and a vertex count, bound to shader inputs of the same name (Vertex Attributes). An optional index lists triangles, groups split it by material, drawRange limits what is drawn, and boundingSphere drives frustum culling. A book's box, in Node:
import * as THREE from 'three';
const box = new THREE.BoxGeometry(0.42, 0.98, 0.12);
for (const [name, attr] of Object.entries(box.attributes)) {
console.log(name, attr.array.constructor.name, attr.count, 'x', attr.itemSize);
}
const { index, groups } = box;
console.log('index', index.array.constructor.name, index.count, 'groups', groups.length);
box.computeBoundingSphere();
console.log('radius', box.boundingSphere.radius.toFixed(3), 'drawRange', box.drawRange);Output
position Float32Array 24 x 3
normal Float32Array 24 x 3
uv Float32Array 24 x 2
index Uint16Array 36 groups 6
radius 0.536 drawRange { start: 0, count: Infinity }A box needs 24 vertices, not 8: each corner sits on three faces with three normals. The six groups let each face wear its own material. After editing an attribute's array, set attribute.needsUpdate = true.
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
.row { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap; }
canvas { display: block; }
pre { margin: 8px; font-size: 12px; background: #fff; padding: 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="row" id="row"><pre id="out"></pre></div>
<script type="module">
import * as THREE from 'three';
import { VertexNormalsHelper } from 'three/addons/helpers/VertexNormalsHelper.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(320, 300);
document.getElementById('row').prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 320 / 300, 0.1, 50);
camera.position.set(1.4, 1.1, 2.2);
camera.lookAt(0, 0, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.5));
const sun = new THREE.DirectionalLight('#ffffff', 2);
sun.position.set(2, 3, 4);
scene.add(sun);
// A book-sized box: one group per face, so an array of six materials colors each face
const box = new THREE.BoxGeometry(0.42, 0.98, 0.12);
const faceColors = ['#b5452f', '#e09a10', '#3f7d3a', '#2a9d8f', '#1f5f8b', '#5b3f99'];
const book = new THREE.Mesh(box, faceColors.map((color) =>
new THREE.MeshStandardMaterial({ color, transparent: true, opacity: 0.85 })));
scene.add(book);
// Every vertex as a dot, and every vertex normal as a line: three per corner
const dots = new THREE.Points(box, new THREE.PointsMaterial({ color: '#263238', size: 6, sizeAttenuation: false }));
book.add(dots);
const normals = new VertexNormalsHelper(book, 0.15, '#263238');
scene.add(normals);
const lines = [];
for (const [name, attr] of Object.entries(box.attributes)) {
lines.push(`${name.padEnd(8)} ${attr.array.constructor.name} ${attr.count} × ${attr.itemSize}`);
}
lines.push(`index ${box.index.array.constructor.name} ${box.index.count}`);
lines.push(`groups ${box.groups.length}: ` + box.groups.map((g) => g.materialIndex).join(' '));
box.computeBoundingSphere();
lines.push(`boundingSphere r = ${box.boundingSphere.radius.toFixed(3)}`);
lines.push(`drawRange ${JSON.stringify(box.drawRange)}`);
lines.push('', '24 vertices, not 8: each corner', 'sits on three faces with', 'three different normals.');
document.getElementById('out').textContent = lines.join('\n');
renderer.setAnimationLoop((time) => {
book.rotation.y = time / 2500;
normals.update();
renderer.render(scene, camera);
});
</script>