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.