Buffers

On the GPU a mesh is not an object — it's a flat run of packed numbers. Typed arrays hold them off the JS heap, attributes tell the shader how to slice the byte stream, an index buffer deduplicates shared corners, and the whole thing is uploaded once and drawn forever.

Core Idea

The bolt above and the twelve numbers beside it are the same thing. On the GPU a mesh is not an object with vertices inside — it’s a buffer: a flat, gapless run of numbers in graphics memory. Drag a corner and watch its two cells rewrite; drag a cell and watch the shape deform. And until you touch it, the cells tick on their own — because animation is nothing but rewriting this array, sixty times a second.

Typed, Packed, Off the Heap

A plain JS array ([0.25, 1.1, …]) stores boxed values with per-element overhead, and the garbage collector may shuffle them at will. The GPU accepts none of that — it wants contiguous, strictly-typed bytes:

const positions = new Float32Array([0.25, 1.1, -0.65, 0.05, ...])

A Float32Array packs each number as an IEEE-754 float in exactly 4 bytes, back-to-back, in native memory outside the JS heap — the garbage collector never scans or moves it, so a 50 MB mesh can’t cause a mid-frame stutter. The byte math is always this direct: the demo’s 6 vertices × 2 floats × 4 bytes = 48 bytes, no more, no less.

To the GPU that buffer is a blind conveyor belt of bytes until one call declares its shape — “3 floats per vertex, tightly packed”:

gl.vertexAttribPointer(loc, 3, gl.FLOAT, false, /*stride*/ 0, /*offset*/ 0)

Each such stream is an attribute — position is one, and normals and UVs ride along as parallel streams of the same length (or interleaved in one stream, with stride and offset doing the slicing).

The Index Trick

Neighbouring triangles share corners — storing every triangle’s corners privately duplicates them, and worse, only looks connected:

An index buffer (Uint16Array — 2 bytes reach 65,535 vertices) stores each unique vertex once and spells the triangles out as pointers into that list. Less memory, a warm vertex cache — and genuine connectivity: welded corners cannot crack. A cube is the classic ratio: 8 unique corners serving 36 triangle-corner slots.

Three Ways Data Reaches a Shader

Route Scope Example
attribute per vertex, read from a buffer position, normal, uv
uniform per draw call, same for all vertices the MVP matrix — 16 floats moving a million vertices
varying per fragment, written by the vertex shader anything interpolated across the triangle

Upload Once, Draw Forever

The buffer’s economy is a one-time toll:

gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)   // pay once

After this copy into GPU memory, every frame draws with zero geometry traffic — only small uniforms cross per frame. Re-uploading a large buffer every frame is the classic self-inflicted CPU-bound wound; even particle systems that must update positions write into one persistent Float32Array and flag it (attribute.needsUpdate = true in Three.js) rather than allocating anew.

Connections

What the numbers mean — why triangles, how corners and faces organize — is the meshes story; this page is only about their bytes. The attribute streams feed the vertex shader at the front of the WebGL pipeline, BufferGeometry in Three.js is a thin wrapper over exactly these buffers, and the varying route exists because interpolation hands every fragment its blended share of vertex data. When buffers arrive as binary files from a network, byte order becomes real — endianness covers that trap.