Attributes

You may need to read the article about program structure before this one.


Since shaders run on the GPU, any data that they need to access must be passed to the GPU. One way to do so is with attributes, which are a type of variable that reads data from arrays of binary data called buffers and are used as inputs to vertex shaders. They are used to pass vertex-specific data to shader programs.

Since attributes are inputs to vertex shaders, they are declared using the in keyword. They cannot be declared within fragment shaders. For example:

#version 300 es

in vec4 a_position;

void main() {
	gl_Position = a_position;
}

In order to manipulate an attribute from JavaScript, the WebGL API must first be queried for its location, which is a pointer to a variable in a shader program. This can be done with getAttribLocation. For example:

const location: number = gl.getAttribLocation(program, "a_position");

Buffers

In order to manipulate a buffer with the WebGL API, it first needs to be pointed at with a type of pointer called a binding point. Each binding point has a specific purpose. For example, the array buffer binding point is used to specify a buffer that contains vertex attributes.

A buffer can be created with createBuffer, bound to a binding point with bindBuffer, and filled with data with bufferData. For example:

const buffer: WebGLBuffer = gl.createBuffer();

gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

const data: BufferSource = new Float32Array([0, 0, 0, 0.5, 0.7, 0]);

gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

Notice that buffer is not passed to bufferData. This is because it is first bound to ARRAY_BUFFER, which is then passed in its stead.

Float32Array is a type of TypedArray that contains 32-bit floating-point values as binary data. Although JavaScript is aware of the type of data, this context is lost when stored in the buffer.

Vertex Array Objects

Since binary data requires extra context in order to be interpreted correctly, WebGL needs to be passed some information when assigning a buffer to an attribute:

  • The number of components that should be read for each vertex.
  • The type of data in the buffer.
  • Whether the data in the buffer should be normalized.
  • The stride, which is the offset in bytes between the beginnings of consecutive vertex attributes.
  • The offset in bytes of the first element that should be read.

A vertex array object (VAO) is a collection of information that tells attributes which buffers they should read data from and how they should do it. A VAO can be thought of as representing a type of shape. For example, every cube has the same vertex positions and can therefore use the same VAO.

In order to allow an attribute to accept data from buffers, it must be enabled with enableVertexAttribArray. Once an attribute is enabled, it can be told to read a certain buffer by a VAO. A WebGLVertexArrayObject can be created with createVertexArray. Like buffers, VAOs must be bound with bindVertexArray in order to be manipulated. After that, they can be given data with vertexAttribPointer. For example:

const vao: WebGLVertexArrayObject = gl.createVertexArray();

gl.bindVertexArray(vao);

gl.enableVertexAttribArray(location);

gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

const size = 2;
const type: number = gl.FLOAT;
const normalized = false;
const stride = 0;
const offset = 0;
gl.vertexAttribPointer(location, size, type, normalized, stride, offset);

Notice that neither buffer nor ARRAY_BUFFER are passed to vertexAttribPointer. This is because the array buffer is automatically used, since the array buffer is the buffer that contains vertex data. Likewise, vao is never passed to vertexAttribPointer because the currently-bound VAO is automatically used.

Since size (the number of components that should be read for each vertex) is set to 2, each vertex will get its x and y values from the buffer, and its z and w values will be set to their default values of 0 and 1, respectively. Since the buffer contains six values that are read two at a time, there are three vertices' worth of data stored in the buffer.

The type is set to FLOAT because the data in the buffer came from a Float32Array.

The stride being set to 0 tells WebGL that the data is tightly-packed, meaning that the number of bytes from the start of one element to the next is the same as the byte size of the element.

Rasterizing

The data stored in a VAO can be rasterized by using the correct shader program with useProgram, binding the correct VAO, then executing one of a few rasterization functions, such as drawArrays. For example:

gl.useProgram(program);
gl.bindVertexArray(vao);

const primitiveType: number = gl.TRIANGLES;
const skippedVertexCount = 0;
const vertexCount = 3;
gl.drawArrays(primitiveType, skippedVertexCount, vertexCount);

Indices

WebGL allows vertex data to be reused by specifying indices in the element array buffer.

Imagine that you want to rasterize a rectangle (four vertices). Rectangles are not primitives, so they must be rasterized using two triangles (six vertices). However, since two pairs of those six vertices overlap, they can be defined using only four vertices if they are given indices.

Indices can be added to a VAO by filling the element array buffer with unsigned integer data. For example:

const data = new Float32Array([
	0,
	0.5, // Vertex 0 at (0, 0.5).
	0,
	0, // Vertex 1 at (0, 0).
	0.7,
	0, // Vertex 2 at (0.7, 0).
	0.7,
	0.5 // Vertex 3 at (0.7, 0.5).
]);

const indices = new Uint8Array([
	0,
	1,
	2, // Triangle 0 with vertices 0, 1, and 2.
	0,
	2,
	3 // Triangle 1 with vertices 0, 2, and 3.
]);

const buffer: WebGLBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

const vao: WebGLVertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(location);
gl.vertexAttribPointer(location, 2, gl.FLOAT, false, 0, 0);

const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

Notice that vertexAttribPointer is not called for the element array buffer. This is because the element array buffer is a property of the currently-bound VAO rather than a global property (like the array buffer), so calling bindBuffer binds the element array buffer to the currently-bound VAO.

When rasterizing a VAO with indices, drawElements should be called instead of drawArrays. For example:

const primitive: number = gl.TRIANGLES;
const indexCount = 6;
const indexType: number = gl.UNSIGNED_BYTE;
const skippedIndexCount = 0;
gl.drawElements(primitive, indexCount, indexType, skippedIndexCount);

Note that the type of data stored in the element array buffer is never passed to the VAO, so it must be passed to drawElements. This is set to UNSIGNED_BYTE because the data is from a Uint8Array.


The next article is about uniforms.