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. They are used to pass vertex-specific data to vertex shaders.

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

#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 the attribute's location, which is a pointer to a variable in a shader program. For attributes, this is done with getAttribLocation.

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

Alternatively, the attribute's location can be set ahead of time with either bindAttribLocation or a layout qualifier.

layout(location = 2) in vec4 a_position;

Vertex Buffer Objects

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 that is bound to the ARRAY_BUFFER binding point is called a vertex buffer object (VBO).

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

const data = new Float32Array([0, 0, 0, 0.5, 0.7, 0]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
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. Although JavaScript is aware of the type of data in the Float32Array, this context is lost when the data is stored as raw binary data 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:

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.

const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(location);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(location, 2, gl.FLOAT, false, 0, 0);

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

Since size 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. The offset and stride values can be used in conjunction with each other to retrieve different types of vertex data from one buffer.

Rendering

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

gl.useProgram(program);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);

Element Buffer Objects

WebGL allows vertex data to be reused by specifying indices in the buffer that is bound to the ELEMENT_ARRAY_BUFFER binding point. A buffer that is bound to the ELEMENT_ARRAY_BUFFER binding point is called an element buffer object (EBO).

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.

An EBO can be added to a VAO by filling the element array buffer with unsigned integer data.

const data = new Float32Array([0, 0.5, 0, 0, 0.7, 0, 0.7, 0.5]);
const indices = new Uint8Array([0, 1, 2, 0, 2, 3]);

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

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

const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(location);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(location, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

Notice that vertexAttribPointer is not called for the EBO. This is because the EBO is unique among buffers in that it is a property of the currently-bound VAO rather than a global property, so calling bindBuffer binds the EBO to the currently-bound VAO.

When a VAO has an EBO, the drawElements method can be called instead of drawArrays in order to utilize indexed drawing.

gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);

Notice that the type of data stored in the element array buffer is never passed to the VAO, so it must be passed to drawElements instead.


The next article is about uniforms.