9 10 11 12 13 14 15

More vertex attributes

25

The only vertex attribute which we used in the previous lessons was a vertex position. However in real life you need more data associated with each vertex — a color, texture coordinates, a normal vector, etc. So in this lesson we will learn how to use multiple vertex attributes.

Once again, let's remember how did our vertex shader look before we added a uniform and rotation there:

#version 300 es
precision highp float;

in vec2 position;

void main(void)
{
    gl_Position = vec4(position, 0.0, 1.0);
}

Now, lets add a color to vertex attributes:

#version 300 es
precision highp float;

in vec2 position;

// That's another vertex attribute
in vec3 color;

// An additional output value - oh, that's something new!
out vec3 v_color;

void main(void)
{
    v_color = color; // Put color attribute into additional output parameter
    gl_Position = vec4(position, 0.0, 1.0);
}

Also we need to update our fragment shader. Let's remember how's it look:

#version 300 es
precision highp float;

out vec4 frag_color;

void main(void)
{
    frag_color = vec4(1.0, 0.5, 0.25, 1.0);
}

Now we need to connect the vertex shader output color parameter to fragment shader input parameter. To do that we need to update our fragment shader like that:

#version 300 es
precision highp float;

// A vertex shader ouput parameter is an input parameter of a fragment shader
in vec3 v_color;

out vec4 frag_color;

void main(void)
{
    // Utilize v_color parameter to set the current pixel's color
    frag_color = vec4(v_color, 1.0);
}

v_color is an additional vertex shader output parameter. We will discuss it in the next lesson. Now, let's just focus on the additional vertex attributes. So, we added an input attribute into the vertex shader. Now we should update our GPU buffer by putting there a proper data. After that we should tell WebGL what sort of data is stored there.

Previously, our vertex data looked like that:

const vertices =
[
   -0.5,     -h / 3,
    0.0,  2 * h / 3,
    0.5,     -h / 3
];

Three vertices, two coordinates each. Now, let's add a color to each vertex. To do that we should add RGB values into this array. Each of RGB values is just a number from [0; 1] interval:

const vertices =
[
   // x           y    r    g    b
   -0.5,     -h / 3,   0, 0.5,   1,

   // x           y    r    g    b
    0.0,  2 * h / 3,   1,   0, 0.5,

   // x           y    r    g    b
    0.5,     -h / 3, 0.5,   1,   0
];

Notice that we put vertex attributes in an interleaved way. Next, we put that data into GPU buffer as before, nothing changed there:

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

However now we should tell WebGL that there are two different attributes for each of the vertices:

const position = gl.getAttribLocation(program, "position");
const color = gl.getAttribLocation(program, "color");
gl.enableVertexAttribArray(position);
gl.enableVertexAttribArray(color);

const stride = 5 * Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, stride, 0);
gl.vertexAttribPointer(color, 3, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT);

Let's see what's going on here. Now we have two vertex attributes — position and color. Like before, we need to find their locations, enable them and bind a data to them. Finding and enabling are easy. Let's take a closer look into vertexAttribPointer part:

gl.vertexAttribPointer(position, 2, gl.FLOAT, false, stride, 0);
gl.vertexAttribPointer(color, 3, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT);

The first parameter is an attribute location — that's easy. The second parameter is a number of components per attribute. position is a 2D vector, so it has two components: x and y. Color is presented as a 3D vector because we need three values per color: red, green and blue components. The third parameter is a type of each component and that's also an easy part. The next boolean parameter is ignored for FLOAT values (we will discuss its meaning some later).

The last two parameters are stride and offset. stride is a distance between two vertices in GPU buffer. In our case each of the vertex attributes have a total of 5 components — 2 for position and 3 for color. That's why the stride is 5. Also, a stride should be specified in bytes. That's why we also multiply it by Float32Array.BYTES_PER_ELEMENT.

offset is a relative byte offset of an attribute inside each of the vertex attributes group. We keep our attributes in an interleaved way, so the color attribute goes right after position attribute which size is 2 components. So we should skip position to get to color. That's why color offset is 2, and since it's a byte offset, it is also multiplied by Float32Array.BYTES_PER_ELEMENT.

So, two lines above configure how the data is presented in the vertex buffer. This data:

const vertices =
[
   // x           y    r    g    b
   -0.5,     -h / 3,   0, 0.5,   1,

   // x           y    r    g    b
    0.0,  2 * h / 3,   1,   0, 0.5,

   // x           y    r    g    b
    0.5,     -h / 3, 0.5,   1,   0
];

Now everything is ready and you should see a colorized triangle:

WebGL colorized triangle

How does it work and why the colors are so nicely flow into each other? We will learn it in the next lesson!

Lesson 11
Share this page:

Learning plan

Now we need to create a GPU buffer and transfer a geometry data into it
Everything is ready to draw our first triangle
11. Uniforms
How to use draw call level parameters to control the shading process
12. More vertex attributes
Let's add more attributes to vertices and use them to enhance our triangle
How to evaluate a vertex-specified data and interpolate it between vertices
14. Textures
An introduction on how to load texture, initialize it properly with WebGL and pass it into GLSL program
15. glMatrix
How to install and use glMatrix library which provides vector and matrix arithmetics and helper functions