What is BufferGeometry?
BufferGeometry is the foundation of every shape in Three.js. Whether you use a built-in BoxGeometry or build something entirely custom, it all comes down to BufferGeometry under the hood. It stores mesh data as positions, normals, UVs, colors that are then sent as flat typed arrays that get sent directly to the GPU. You know, magic.
Understanding BufferGeometry gives you the power to create any shape that you can think of, optimize your scene's performance, and manipulate vertices at runtime. It's a lot of power that is worth learning.
What makes up BufferGeometry?
A BufferGeometry is made up of attributes. Each attribute is an array of numbers that describes something about each vertex:
position: Where each vertex is in 3D space (x, y, z)normal: Which direction each vertex faces (for lighting)uv: Texture coordinates (how textures wrap onto the surface)color: Per-vertex color (r, g, b)
Every three consecutive vertices define a triangle which, as you probably remember from our "Why Three.js" introduction, is the basic building block of all 3D geometry in Three.js.
So let's create some triangles!
The simplest custom geometry is a single triangle. Not hard to visualize or to understand. You define three vertices (three points in space) and pass them as a position attribute:
// Create a new empty geometry
const geometry = new THREE.BufferGeometry();
// Define 3 vertices (x, y, z for each)
const vertices = new Float32Array([
-1.0, -1.0, 0.0, // vertex 0 (bottom-left)
1.0, -1.0, 0.0, // vertex 1 (bottom-right)
0.0, 1.0, 0.0 // vertex 2 (top-center)
]);
// Create a BufferAttribute and assign it as the position attribute
// The second argument (3) means each vertex uses 3 values (x, y, z)
geometry.setAttribute(
'position',
new THREE.BufferAttribute(vertices, 3)
);
const material = new THREE.MeshBasicMaterial({
color: 0x44aaff,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);Building a Vase from Triangles
Real 3D shapes are built by combining hundreds or thousands of triangles into something more complex and meaningful. A common technique is the surface of revolution (or lathe). You define a 2D profile curve and spin it around an axis to generate a 3D shape. Here's a vase built this way, entirely from raw vertices and indices:
// Define the vase profile (radius, height) pairs
const profile = [
[0.0, 0.0], [0.8, 0.0], [1.1, 0.2], [1.3, 0.5],
[1.2, 1.0], [0.7, 1.5], [0.6, 1.8], [0.75, 2.2],
[1.0, 2.6], [0.9, 2.9], [0.75, 3.0]
];
const segments = 24; // slices around the Y axis
const vertices = [];
const indices = [];
// Generate vertices by rotating the profile around Y
for (let s = 0; s <= segments; s++) {
const angle = (s / segments) * Math.PI * 2;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
for (const [r, y] of profile) {
vertices.push(cos * r, y, sin * r);
}
}
// Connect adjacent rings with triangles
const ringSize = profile.length;
for (let s = 0; s < segments; s++) {
for (let p = 0; p < ringSize - 1; p++) {
const a = s * ringSize + p;
const b = a + ringSize;
indices.push(a, b, a + 1);
indices.push(a + 1, b, b + 1);
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position',
new THREE.BufferAttribute(new Float32Array(vertices), 3));
geometry.setIndex(indices);
geometry.computeVertexNormals();Indexed Geometry
When vertices are shared between triangles (like in a grid or cube), you're wasting memory by duplicating them. Indexed geometry solves this by defining vertices once and then referencing them by index:
const geometry = new THREE.BufferGeometry();
// Define 4 unique vertices for a quad (two triangles sharing an edge)
const vertices = new Float32Array([
-1, -1, 0, // 0: bottom-left
1, -1, 0, // 1: bottom-right
1, 1, 0, // 2: top-right
-1, 1, 0 // 3: top-left
]);
// Define which vertices form each triangle
// Two triangles: (0,1,2) and (0,2,3)
const indices = [
0, 1, 2, // first triangle
0, 2, 3 // second triangle
];
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(indices);
geometry.computeVertexNormals(); // Auto-calculate normals for lightingIndexed geometry is more memory efficient and is what Three.js uses internally for built-in geometries like BoxGeometry and SphereGeometry.
Vertex Colors
You can assign a color to each vertex. The GPU interpolates between them across each triangle face, creating smooth gradients:
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
-1, -1, 0,
1, -1, 0,
0, 1, 0
]);
// RGB color for each vertex (values 0-1)
const colors = new Float32Array([
1, 0, 0, // red (bottom-left)
0, 1, 0, // green (bottom-right)
0, 0, 1 // blue (top)
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// Tell the material to use vertex colors
const material = new THREE.MeshBasicMaterial({
vertexColors: true,
side: THREE.DoubleSide
});Modifying Geometry at Runtime
You can update vertex data every frame for dynamic effects like waves or terrain deformation. The key is setting needsUpdate = true on the attribute after changing it:
// Create a flat grid of vertices
const size = 10;
const segments = 50;
const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
// In the animation loop, modify the Z values to create waves
function animate(time) {
const positions = geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const y = positions[i + 1];
positions[i + 2] = Math.sin(x * 2 + time) * 0.3
+ Math.cos(y * 2 + time) * 0.3;
}
// Tell Three.js the data changed
geometry.attributes.position.needsUpdate = true;
// Recalculate normals so lighting updates too
geometry.computeVertexNormals();
}Computing Normals and Bounding Volumes
When you create custom geometry, Three.js doesn't automatically know how light should reflect off it or how big it is. You need to compute these yourself:
// Compute vertex normals from triangle faces
// Required for any material that responds to light
geometry.computeVertexNormals();
// Compute bounding box (axis-aligned box enclosing the geometry)
geometry.computeBoundingBox();
console.log(geometry.boundingBox.min, geometry.boundingBox.max);
// Compute bounding sphere (smallest sphere enclosing the geometry)
// Used by the frustum culler to skip off-screen objects
geometry.computeBoundingSphere();
console.log(geometry.boundingSphere.center, geometry.boundingSphere.radius);