This is a continuation from WebGL Fundamentals. Before we continue I think we need to discuss at a basic level what WebGL and your GPU actually do. There are basically 2 parts to this GPU thing. The first part processes vertices (or streams of data) into clip space vertices. The second part draws pixels based on the first part.
When you call
gl.drawArrays(gl.TRIANGLES, 0, 9);
The 9 there means "process 9 vertices" so here are 9 vertices being processed.
On the left is the data you provide. The vertex shader is a function you
write in GLSL. It gets called once for each vertex.
You do some math and set the special variable gl_Position
with a clip space value
for the current vertex. The GPU takes that value and stores it internally.
Assuming you're drawing TRIANGLES
, every time this first part generates 3
vertices the GPU uses them to make a triangle. It figures out which
pixels the 3 points of the triangle correspond to, and then rasterizes the
triangle which is a fancy word for “draws it with pixels”. For each
pixel it will call your fragment shader asking you what color to make that
pixel. Your fragment shader has output a vec4
with the color it wants for that pixel.
That’s all very interesting but as you can see in our examples up to this point the fragment shader has very little info per pixel. Fortunately we can pass it more info. We define “varyings” for each value we want to pass from the vertex shader to the fragment shader.
As a simple example, let's just pass the clip space coordinates we computed directly from the vertex shader to the fragment shader.
We'll draw with a simple triangle. Continuing from our previous example let's change our rectangle to a triangle.
// Fill the buffer with the values that define a triangle.
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, -100,
150, 125,
-175, 100]),
gl.STATIC_DRAW);
}
And we have to only draw 3 vertices.
// Draw the scene.
function drawScene() {
...
// Draw the geometry.
* gl.drawArrays(gl.TRIANGLES, 0, 3);
}
Then in our vertex shader we declare a varying by making an out
to pass data to the
fragment shader.
out vec4 v_color;
...
void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
// Convert from clip space to color space.
// Clip space goes -1.0 to +1.0
// Color space goes from 0.0 to 1.0
* v_color = gl_Position * 0.5 + 0.5;
}
And then we declare the same varying as an in
in the fragment shader.
#version 300 es
precision highp float;
in vec4 v_color;
out vec4 outColor;
void main() {
* outColor = v_color;
}
WebGL will connect the varying in the vertex shader to the varying of the same name and type in the fragment shader.
Here's the working version.
Move, scale and rotate the triangle. Notice that since the colors are computed from clip space they don't move with the triangle. They are relative to the background.
Now think about it. We only compute 3 vertices. Our vertex shader only gets called 3 times therefore it's only computing 3 colors yet our triangle is many colors. This is why it's called a varying.
WebGL takes the 3 values we computed for each vertex and as it rasterizes the triangle it interpolates between the values we computed for the vertices. For each pixel it calls our fragment shader with the interpolated value for that pixel.
In the example above we start out with the 3 vertices
Vertices | |
---|---|
0 | -100 |
150 | 125 |
-175 | 100 |
Our vertex shader applies a matrix to translate, rotate, scale and convert to clip space. The defaults for translation, rotation and scale are translation = 200, 150, rotation = 0, scale = 1,1 so that's really only translation. Given our backbuffer is 400x300 our vertex shader applies the matrix and then computes the following 3 clip space vertices.
values written to gl_Position | ||
---|---|---|
0.000 | 0.660 | |
0.750 | -0.830 | |
-0.875 | -0.660 |
It also converts those to color space and writes them to the varying v_color that we declared.
values written to v_color | ||
---|---|---|
0.5000 | 0.830 | 0.5 |
0.8750 | 0.086 | 0.5 |
0.0625 | 0.170 | 0.5 |
Those 3 values written to v_color are then interpolated and passed to the fragment shader for each pixel.
We can also pass in more data to the vertex shader which we can then pass on to the fragment shader. So for example let's draw a rectangle, that consists of 2 triangles, in 2 colors. To do this we'll add another attribute to the vertex shader so we can pass it more data and we'll pass that data directly to the fragment shader.
in vec2 a_position;
+in vec4 a_color;
...
out vec4 v_color;
void main() {
...
// Copy the color from the attribute to the varying.
* v_color = a_color;
}
We now have to supply colors for WebGL to use.
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
+ var colorLocation = gl.getAttribLocation(program, "a_color");
...
+ // Create a buffer for the colors.
+ var buffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+
+ // Set the colors.
+ setColors(gl);
// setup attributes
...
+ // tell the color attribute how to pull data out of the current ARRAY_BUFFER
+ gl.enableVertexAttribArray(colorLocation);
+ var size = 4;
+ var type = gl.FLOAT;
+ var normalize = false;
+ var stride = 0;
+ var offset = 0;
+ gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset);
...
+// Fill the buffer with colors for the 2 triangles
+// that make the rectangle.
+function setColors(gl) {
+ // Pick 2 random colors.
+ var r1 = Math.random();
+ var b1 = Math.random();
+ var g1 = Math.random();
+
+ var r2 = Math.random();
+ var b2 = Math.random();
+ var g2 = Math.random();
+
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array(
+ [ r1, b1, g1, 1,
+ r1, b1, g1, 1,
+ r1, b1, g1, 1,
+ r2, b2, g2, 1,
+ r2, b2, g2, 1,
+ r2, b2, g2, 1]),
+ gl.STATIC_DRAW);
+}
And here's the result.
Notice that we have 2 solid color triangles. Yet we're passing the values in a varying so they are being varied or interpolated across the triangle. It's just that we used the same color on each of the 3 vertices of each triangle. If we make each color different we'll see the interpolation.
// Fill the buffer with colors for the 2 triangles
// that make the rectangle.
function setColors(gl) {
// Make every vertex a different color.
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(
* [ Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1]),
gl.STATIC_DRAW);
}
And now we see the interpolated varying.
Not very exciting I suppose but it does demonstrate using more than one attribute and passing data from a vertex shader to a fragment shader. If you check out the image processing examples you'll see they also use an extra attribute to pass in texture coordinates.
Buffers are the way of getting vertex and other per vertex data onto the
GPU. gl.createBuffer
creates a buffer.
gl.bindBuffer
sets that buffer as the buffer to be worked on.
gl.bufferData
copies data into the current buffer.
Once the data is in the buffer we need to tell WebGL how to get data out of it and provide it to the vertex shader's attributes.
To do this, first we ask WebGL what locations it assigned to the attributes. For example in the code above we have
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
var colorLocation = gl.getAttribLocation(program, "a_color");
Once we know the location of the attribute we then issue 2 commands.
gl.enableVertexAttribArray(location);
This command tells WebGL we want to supply data from a buffer.
gl.vertexAttribPointer(
location,
numComponents,
typeOfData,
normalizeFlag,
strideToNextPieceOfData,
offsetIntoBuffer);
And this command tells WebGL to get data from the buffer that was last
bound with gl.bindBuffer, how many components per vertex (1 - 4), what the
type of data is (BYTE
, FLOAT
, INT
, UNSIGNED_SHORT
, etc...), the stride
which means how many bytes to skip to get from one piece of data to the
next piece of data, and an offset for how far into the buffer our data is.
Number of components is always 1 to 4.
If you are using 1 buffer per type of data then both stride and offset can always be 0. 0 for stride means "use a stride that matches the type and size". 0 for offset means start at the beginning of the buffer. Setting them to values other than 0 is more complicated and though it might have some benefits in terms of performance it's not worth the complication unless you are trying to push WebGL to its absolute limits.
I hope that clears up buffers and attributes.
You might want to take a look at this interactive state diagram for another way of understanding how WebGL works.
Next let's go over shaders and GLSL.