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.

JavaScript (CPU) GPU (VRAM) BufferGeometry attributes.position → Float32Array([x,y,z, x,y,z, ...]) → VBO (GL_ARRAY_BUFFER) attributes.normal → Float32Array([nx,ny,nz, ...]) → VBO attributes.uv → Float32Array([u,v, u,v, ...]) → VBO index → Uint16Array([0,1,2, 1,3,2, ...]) → IBO (GL_ELEMENT_ARRAY_BUFFER) All VBOs + attribute pointers are wrapped in a VAO (Vertex Array Object) so the GPU can recall the whole layout with a single bind.
BufferGeometry Structure BufferGeometry.js
PropertyLineDescription
indexL101BufferAttribute (Uint16/Uint32Array) — triangle connectivity. Each trio of values is one triangle's vertex indices.
attributesL131Object mapping name → BufferAttribute. Well-known names: position, normal, uv, color, tangent, skinIndex, skinWeight.
morphAttributesL141Object mapping name → BufferAttribute[] — delta values for each morph target (blend shapes).
drawRangeObject {start, count} — default {start:0, count:Infinity}. Renders only a subset of vertices/indices, useful for streaming or partial update.
groupsArray of {start, count, materialIndex} — enables multi-material meshes. Each group is rendered with its own material.
boundingBoxBox3 — axis-aligned bounding box, used for frustum culling when computed.
boundingSphereSphere — 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:

x₀pos
y₀pos
z₀pos
x₁pos
y₁pos
z₁pos
x₂pos
y₂pos
z₂pos
nx₀normal
ny₀normal
nz₀normal
nx₁normal
ny₁normal
nz₁normal
nx₂normal
ny₂normal
nz₂normal
u₀uv
v₀uv
u₁uv
v₁uv
u₂uv
v₂uv

Key Properties

PropertyDescription
arrayThe raw typed array (Float32Array, Int16Array, Uint8Array, etc.)
itemSizeComponents per vertex: 1=float, 2=vec2, 3=vec3, 4=vec4
countNumber of vertices: array.length / itemSize
normalizedIf true, integer values are mapped to [0,1] or [-1,1] range on GPU
needsUpdateSet to true to re-upload this buffer to GPU on next render
usageGL 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:

Non-indexed: 6 vertices for a quad (2 triangles) positions: [v0, v1, v2, v1, v3, v2] ← v1, v2 duplicated Indexed: 4 vertices + 6 indices positions: [v0, v1, v2, v3] ← 4 unique vertices index: [0,1,2, 1,3,2] ← two triangles sharing v1 and v2 Savings: fewer vertices = less memory, less vertex shader executions GPU caches recently-processed vertices (post-T&L cache) using the index

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.

renderBufferDirect() ↓ bindingStates.setup( object, material, program, geometry, index ) ↓ ├─ Lookup or create a VAO (Vertex Array Object) for this combination │ ├─ For each attribute the shader needs: │ ├─ WebGLAttributes.update( attribute ) ← create/update GPU buffer │ │ ├─ gl.createBuffer() │ │ ├─ gl.bindBuffer( GL_ARRAY_BUFFER, buffer ) │ │ └─ gl.bufferData( GL_ARRAY_BUFFER, attribute.array, usage ) │ │ │ └─ gl.vertexAttribPointer( location, itemSize, type, normalized, stride, offset ) │ ↑ tells the GPU: "attribute at location X starts at this offset, has this size" │ └─ If geometry has an index: WebGLAttributes.update( index ) gl.bindBuffer( GL_ELEMENT_ARRAY_BUFFER, indexBuffer ) On subsequent renders: just gl.bindVertexArray( vao ) — one call
needsUpdate: Setting 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