Geometry (BufferGeometry)
How vertex data lives in typed arrays, gets structured into named attributes, and is uploaded to GPU buffers for rendering.
The Core Idea
A BufferGeometry is a collection of named attributes — each attribute is a flat typed array (like Float32Array) that holds per-vertex data. The GPU reads these arrays directly from Video RAM.
BufferGeometry Structure BufferGeometry.js
| Property | Line | Description |
|---|---|---|
| index | L101 | BufferAttribute (Uint16/Uint32Array) — triangle connectivity. Each trio of values is one triangle's vertex indices. |
| attributes | L131 | Object mapping name → BufferAttribute. Well-known names: position, normal, uv, color, tangent, skinIndex, skinWeight. |
| morphAttributes | L141 | Object mapping name → BufferAttribute[] — delta values for each morph target (blend shapes). |
| drawRange | — | Object {start, count} — default {start:0, count:Infinity}. Renders only a subset of vertices/indices, useful for streaming or partial update. |
| groups | — | Array of {start, count, materialIndex} — enables multi-material meshes. Each group is rendered with its own material. |
| boundingBox | — | Box3 — axis-aligned bounding box, used for frustum culling when computed. |
| boundingSphere | — | Sphere — bounding sphere for culling. Auto-computed on first render if null. |
Core Methods
// Set an attribute (also triggers GPU upload on next render)
geometry.setAttribute( 'position',
new THREE.BufferAttribute( new Float32Array([...]), 3 ) // 3 = itemSize (x,y,z)
);
// Retrieve an attribute
const pos = geometry.getAttribute( 'position' );
// Set the index buffer
geometry.setIndex( new THREE.BufferAttribute(
new Uint16Array([ 0,1,2, 1,3,2 ]), 1
));
// Mark the position attribute as needing GPU re-upload
geometry.getAttribute('position').needsUpdate = true;
// Render only vertices 10–99
geometry.drawRange.start = 10;
geometry.drawRange.count = 90;
// Free CPU + GPU memory
geometry.dispose();
BufferAttribute — Per-Vertex Arrays BufferAttribute.js
A BufferAttribute wraps a typed array and tells three.js how to interpret it for the GPU.
// Triangle with 3 vertices, each having (x, y, z)
const positions = new Float32Array([
// vertex 0 vertex 1 vertex 2
0, 1, 0, -1, -1, 0, 1, -1, 0
]);
const attr = new THREE.BufferAttribute(
positions, // the typed array
3 // itemSize: how many values per vertex (3 = vec3)
);
// attr.count = 3 (positions.length / itemSize)
// attr.array = positions (the raw typed array)
// attr.itemSize = 3
Memory Layout
Data is interleaved or separate. Three.js uses separate arrays by default (one per attribute). The memory layout for 3 vertices looks like:
Key Properties
| Property | Description |
|---|---|
| array | The raw typed array (Float32Array, Int16Array, Uint8Array, etc.) |
| itemSize | Components per vertex: 1=float, 2=vec2, 3=vec3, 4=vec4 |
| count | Number of vertices: array.length / itemSize |
| normalized | If true, integer values are mapped to [0,1] or [-1,1] range on GPU |
| needsUpdate | Set to true to re-upload this buffer to GPU on next render |
| usage | GL hint: StaticDrawUsage (default), DynamicDrawUsage, StreamDrawUsage |
| updateRange | {offset, count} — partial buffer update. Only re-uploads a slice of the array. |
Indexed vs Non-Indexed Geometry
Without an index buffer, each triangle requires 3 unique vertices. With indexing, vertices can be shared between triangles:
For a sphere with 1000 segments, vertex sharing reduces vertex count by ~50% and enables the GPU's built-in vertex cache.
How Geometry Reaches the GPU
Three.js uploads geometry lazily — on the first render call for a given geometry+material+object combination.
attribute.needsUpdate = true triggers a new gl.bufferData() or gl.bufferSubData() on the next render. For partial updates set attribute.updateRange = {offset, count} first.BufferAttribute Usage Hints
// Static mesh (default) — uploaded once, rarely changed
geometry.getAttribute('position').usage = THREE.StaticDrawUsage;
// → gl.bufferData(..., gl.STATIC_DRAW)
// Animated/morphing mesh — re-uploaded frequently
geometry.getAttribute('position').usage = THREE.DynamicDrawUsage;
// → gl.bufferData(..., gl.DYNAMIC_DRAW)
// Per-frame streaming (particles, etc.)
geometry.getAttribute('position').usage = THREE.StreamDrawUsage;
// → gl.bufferData(..., gl.STREAM_DRAW)
These hints tell the GPU driver how to allocate VRAM (which memory pool to use), affecting upload speed and rendering throughput.
Multi-Material Geometry (Groups)
A geometry can be split into groups, each rendered with a different material. This is used for models with separate textures on different parts (e.g. a car body and its windows).
// Define groups (ranges of the index buffer)
geometry.addGroup( 0, 36, 0 ); // indices 0-35 → material[0]
geometry.addGroup( 36, 12, 1 ); // indices 36-47 → material[1]
// Mesh with multiple materials
const mesh = new THREE.Mesh( geometry, [
new THREE.MeshStandardMaterial({ color: 0xff0000 }), // index 0
new THREE.MeshStandardMaterial({ color: 0x0000ff }), // index 1
]);
The renderer calls renderBufferDirect() once per group, with the corresponding material. Each call becomes a separate draw call on the GPU.
Built-in Geometry Generators
Three.js includes generators that produce BufferGeometry with pre-computed attributes:
// BoxGeometry — generates position, normal, uv attributes + index
const box = new THREE.BoxGeometry( 1, 1, 1 );
// SphereGeometry — sphere with lat/lon segments
const sphere = new THREE.SphereGeometry( 0.5, 32, 16 );
// PlaneGeometry — flat plane in XZ
const plane = new THREE.PlaneGeometry( 10, 10, 100, 100 );
// Custom: build your own
const geom = new THREE.BufferGeometry();
geom.setAttribute( 'position', new THREE.BufferAttribute(
new Float32Array([ 0,1,0, -1,-1,0, 1,-1,0 ]), 3
));
// Normals are optional but required for lighting
geom.computeVertexNormals(); // auto-compute from faces