Skip to content

WebGL - Geometry

To render any object (or primitive) in WebGL, you first need to define its geometry. This typically involves specifying the positions of its vertices and, optionally, how those vertices connect to form shapes (using indices). Other per-vertex data like colors or texture coordinates can also be included.

This geometric data originates in your JavaScript code, often stored in Typed Arrays, and must be transferred to the GPU’s memory for the shaders to access. This transfer is done using WebGL Buffer Objects.

You define the vertices (and optionally indices) for your shapes using JavaScript Typed Arrays, which provide raw binary data views essential for WebGL.

Vertices define the corner points. For a triangle with coordinates (0.0, 0.5), (-0.5, -0.5), and (0.5, -0.5):

const triangleVertices = new Float32Array([
0.0, 0.5, // Vertex 0 (Top)
-0.5, -0.5, // Vertex 1 (Bottom Left)
0.5, -0.5 // Vertex 2 (Bottom Right)
]);

If drawing this triangle directly with gl.drawArrays, you don’t strictly need indices. However, if using gl.drawElements or defining more complex shapes, indices are used.

A square can be made from two triangles sharing vertices. Vertices: 0=(-0.5, 0.5), 1=(0.5, 0.5), 2=(0.5, -0.5), 3=(-0.5, -0.5).

const squareVertices = new Float32Array([
-0.5, 0.5, // Vertex 0
0.5, 0.5, // Vertex 1
0.5, -0.5, // Vertex 2
-0.5, -0.5 // Vertex 3
]);
// Indices define two triangles: (0, 3, 1) and (1, 3, 2)
const squareIndices = new Uint16Array([
0, 3, 1,
1, 3, 2
]);

Using indices (Uint16Array or Uint32Array) allows reusing vertex data, saving memory and potentially boosting performance.

WebGL buffer objects are memory allocations on the GPU used to store rendering data. They are essential for efficiently transferring your geometry (and other data) from the CPU (JavaScript) to the GPU (Shaders).

The two primary types for geometry are:

  • Vertex Buffer Object (VBO): Stores per-vertex data like coordinates, colors, texture coordinates, normals. Bound to the gl.ARRAY_BUFFER target.
  • Index Buffer Object (IBO): Stores vertex indices for indexed drawing (gl.drawElements). Bound to the gl.ELEMENT_ARRAY_BUFFER target.

Workflow for Using Buffers:

  1. Create Buffer: Use gl.createBuffer() to get an empty buffer object reference.
  2. Bind Buffer: Use gl.bindBuffer(target, buffer) to make a buffer active on a specific target (gl.ARRAY_BUFFER for VBOs, gl.ELEMENT_ARRAY_BUFFER for IBOs). All subsequent operations on that target affect the bound buffer.
  3. Upload Data: Use gl.bufferData(target, data, usage) to allocate memory and copy data from your TypedArray (data) into the currently bound buffer. The usage hint (e.g., gl.STATIC_DRAW, gl.DYNAMIC_DRAW) tells WebGL how you intend to use the data, potentially allowing for optimizations.
  4. Unbind Buffer (Optional but Recommended): Use gl.bindBuffer(target, null) to unbind the buffer from the target. This prevents accidental modification.
  5. Configure Attributes (for VBOs): Later, you’ll bind the VBO again and tell WebGL how vertex attributes in your shader should read data from it using gl.vertexAttribPointer.

The usage parameter in gl.bufferData helps the WebGL driver optimize memory placement and access:

  • gl.STATIC_DRAW: Data is set once and used many times (e.g., static model geometry). Most common.
  • gl.DYNAMIC_DRAW: Data is updated frequently and used many times (e.g., vertices for an animated character).
  • gl.STREAM_DRAW: Data is set once and used only a few times (e.g., particle effects data).

These are hints; the implementation might treat them similarly, but providing the correct hint is good practice.

// Assume 'gl' is the WebGLRenderingContext
// Assume 'triangleVertices' and 'squareIndices' TypedArrays exist
// --- Vertex Buffer (VBO) ---
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, triangleVertices, gl.STATIC_DRAW);
// gl.bindBuffer(gl.ARRAY_BUFFER, null); // Optional unbind
// --- Index Buffer (IBO) ---
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, squareIndices, gl.STATIC_DRAW);
// gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); // Optional unbind

Setting up vertex attributes involves binding the correct VBO and calling gl.vertexAttribPointer and gl.enableVertexAttribArray for each attribute. This setup can be complex, especially for models with multiple attributes.

A Vertex Array Object (VAO) stores all this configuration state: which VBOs are bound, how each attribute reads from its VBO, and which attributes are enabled.

Workflow with VAOs:

  1. Create VAO: const vao = gl.createVertexArray(); (Note: In WebGL 1, requires OES_vertex_array_object extension: gl.createVertexArrayOES())
  2. Bind VAO: gl.bindVertexArray(vao);
  3. Setup Buffers and Attributes: Perform all gl.bindBuffer, gl.bufferData, gl.vertexAttribPointer, gl.enableVertexAttribArray calls needed for your object. Also bind the IBO (gl.ELEMENT_ARRAY_BUFFER) if using one.
  4. Unbind VAO: gl.bindVertexArray(null); (to prevent accidental changes)
  5. To Draw: Simply bind the VAO (gl.bindVertexArray(vao);) and then make your draw call (gl.drawArrays or gl.drawElements). All associated buffer bindings and attribute configurations are restored automatically.

VAOs significantly simplify rendering loops and often improve performance by reducing the number of WebGL state changes per frame. They are standard and required in WebGL 2.

Here’s a summary of key buffer-related methods:

MethodDescription
gl.createBuffer()Creates a new, empty buffer object.
gl.deleteBuffer(buffer)Deletes a previously created buffer object, freeing GPU memory.
gl.bindBuffer(target, buffer)Binds a buffer object to a specific target (gl.ARRAY_BUFFER or gl.ELEMENT_ARRAY_BUFFER). Pass null to unbind.
gl.bufferData(target, dataOrSize, usage)Allocates buffer memory and optionally uploads data from a TypedArray or ArrayBuffer (dataOrSize). If dataOrSize is a number, it allocates that many bytes without initializing.
gl.bufferSubData(target, offset, data)Updates a subset of an existing buffer’s data without reallocating memory. offset is the byte offset to start writing.
gl.isBuffer(buffer)Returns true if the provided object is a valid WebGL buffer.
gl.getBufferParameter(target, pname)Retrieves information about the buffer currently bound to target. pname can be gl.BUFFER_SIZE or gl.BUFFER_USAGE.