WebGL — 1. Introduction to shaders

Sushindhran Harikrishnan
Neosavvy Labs
Published in
5 min readMar 9, 2017

My interest in WebGL was recently reignited by my desire for a refresher on my Computer Graphics course at NYU under Professor Ken Perlin. My class was the first batch to do this course in WebGL, the previous batches were doing it in Java. I figured that blogging about it would be a good way to keep my motivation levels up, as I relearn/refresh some of those concepts. One blog post for one concept along with some working code is my target. As I always tend to do, I’m starting from the basics.

So What is WebGL? What are Vertex and Fragment Shaders? WebGL is a Javascript API that is based on OpenGL ES, to render content on HTML canvas. WebGL programs run both vertex shader and fragment shader code that is executed on the Graphics card’s GPU, enabling the CPU to do other work.

1. Vertex Shader

Consider rendering any primitive 3D shape like a cube that has vertices. The vertex shader takes each one of the vertices and does what it wants with them. But it eventually has to set something called gl_Position, a 4D floating point vector, which is the final position of the vertex on the screen. We are essentially taking a 3D position with x, y, z coordinates and projecting it onto a 2D surface. The code for a simple vertex shader would look something like this.

<script id="vs" type="other">
attribute vec3 aPosition;
varying vec3 vPosition;
void main() {
gl_Position = vec4(aPosition, 1.0);
vPosition = aPosition;
}
</script>

2. Fragment Shader

Now that we have all the vertices for the object, how do we add lighting, colors and textures? That’s where the fragment shader comes in. The fragment shader can have some logic for all of this, but it eventually has to set something called gl_FragColor, another 4D floating point vector, which is the final color of our fragment. Think of fragments as something akin to a vertex that is passed to a vertex shader. A primitive is broken down into discrete elements called fragments in the rasterization process. So if you are drawing a square, a fragment is the data provided by those three vertices to color each pixel within that square. In other words, fragments receive interpolated values from those three vertices. If one vertex is colored blue, and its adjacent vertex is green we would see the color values for the fragments interpolate from green to cyan to blue. We are just assigning the vertex position coordinates to the color vector. Our fragment shader code will look something like this.

<script id="fs" type="x-shader/x-fragment">
precision mediump float;
varying vec3 vPosition; // vertex position
void main() {
float x = vPosition.x;
float y = vPosition.y;
float z = vPosition.z;
vec3 color = vec3(x, y, z);
// Send final color to output, and add opacity
gl_FragColor = vec4(color, 1.0);
}
</script>

The above snippet would produce the same result as this simplified snippet below

<script id="fs" type="x-shader/x-fragment">
precision mediump float;
varying vec3 vPosition; // vertex position
void main() {
vec3 color = vPosition;
// Send final color to output, and add opacity
gl_FragColor = vec4(color, 1.0);
}
</script>

You will see a nice gradient on your WebGL canvas when you run the above code.

Gradient for the fragment shader

To get this working, I had to write some straightforward boilerplate code that I will go over briefly.

3. Creating the program and compiling your shaders

The program is created by calling gl.createProgram(). The shaders are created by calling gl.createShader(type). The type of shader is either gl.VERTEX_SHADER or gl.FRAGMENT_SHADER. This function creates the shaders, compiles them and attaches them to the program.

function addshader(gl, program, type, src) {
var shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw "Cannot compile shader:\n\n" + gl.getShaderInfoLog(shader);
}
gl.attachShader(program, shader);
}

4. Using the program and initializing the buffer

After creating the program, adding shaders and linking it, we tell WebGL to use it in subsequent render calls to use the program using gl.userProgram(program). Now we want to draw a square on the screen, consisting of two triangles, which serves as the canvas for our fragment shader above.

// Create a square as a strip of two triangles.
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
-1,1,
0,1,
1,0,
-1,-1,
0,1,
-1,0]),
gl.STATIC_DRAW
);

5. Passing the vertex data from the buffer to the shaders

To render whatever is in the buffer on the screen, we need to tell WebGL to provide the vertex shader with the position data from the buffer we have created above. We do that as follows.

gl.aPosition = gl.getAttribLocation(program, "aPosition");
gl.enableVertexAttribArray(gl.aPosition);
gl.vertexAttribPointer(gl.aPosition, 3, gl.FLOAT, false, 0, 0);

I mushed the last few steps together into a function called gl_init.

function gl_init(gl, vertexShader, fragmentShader) {
var program = gl.createProgram();
var buffer = gl.createBuffer();
addshader(gl, program, gl.VERTEX_SHADER, vertexShader);
addshader(gl, program, gl.FRAGMENT_SHADER, fragmentShader);
gl.linkProgram(program);
if (! gl.getProgramParameter(program, gl.LINK_STATUS))
throw "Could not link the shader program!";
gl.useProgram(program);
// Create a square as a strip of two triangles.
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
-1,1,
0,1,
1,0,
-1,-1,
0,1,
-1,0]),
gl.STATIC_DRAW
);
gl.aPosition = gl.getAttribLocation(program, "aPosition");
gl.enableVertexAttribArray(gl.aPosition);
gl.vertexAttribPointer(gl.aPosition, 3, gl.FLOAT, false, 0, 0);
gl.uTime = gl.getUniformLocation(program, "uTime");
}

6. Drawing on the screen

Now we get to the important part. We need to draw these vertices on the screen. gl.drawArrays() does that for us.

7. Animation

We use requestAnimationFrame to call our render function. The number of callbacks is usually 60 times a second. This is a cross-browser way of doing that.

requestAnimFrame = (function() {
return requestAnimationFrame
|| webkitRequestAnimationFrame
|| mozRequestAnimationFrame
|| oRequestAnimationFrame
|| msRequestAnimationFrame
|| function(callback) {
setTimeout(callback, 1000 / 60);
}; })();

Our render function would be something like this.

function gl_update(gl) {
gl.uniform1f(gl.uTime, (new Date()).getTime() / 1000 - time0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Start the next frame
requestAnimFrame(function() { gl_update(gl); });
}

8. Firing up our program

The gl variable we have been passing around until now is nothing but our html canvas element.

function start_gl(canvas_id, vertexShader, fragmentShader) {
// Make sure the browser supports WebGL.
try {
var canvas = document.getElementById(canvas_id);
var gl = canvas.getContext("experimental-webgl");
} catch (e) {
throw "Sorry, your browser does not support WebGL.";
}
// Initialize gl. Then start the frame loop.
gl_init(gl, vertexShader, fragmentShader);
gl_update(gl);
}

Finally, I wrote this small utility function to recurse through the DOM tree and pull out the vertex and fragment shader code from the <script></script> tags, which we pass as parameters to gl_start().

function getStringFromDOMElement(id) {
var node = document.getElementById(id);
// Recurse and get all text in the node
var recurseThroughDOMNode = function recurseThroughDOMNode(childNode, textContext) {
if (childNode) {
if (childNode.nodeType === 3) {
textContext += childNode.textContent;
}
return recurseThroughDOMNode(childNode.nextSibling, textContext);
} else {
return textContext;
}
};
return recurseThroughDOMNode(node.firstChild, '');
}

9. Using time

You may have noticed that the gl_init() function has this line.

gl.uTime = gl.getUniformLocation(program, "uTime");

I’ve modified my fragment shader to use that and produce some crazy animations.

<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform float uTime;
varying vec3 vPosition;
void main() {
float x = vPosition.x;
float y = vPosition.y;
float z = vPosition.z;
float xTime = uTime * x/sin(uTime);
float yTime = uTime * y/sin(uTime);
float zTime = uTime * z/sin(uTime);
vec3 color = vec3(sin(xTime), sin(yTime), sin(zTime));
gl_FragColor = vec4(color, 1.0);
}
</script>

Click here to see a running version of this. You can check out and download the code from our github repository here.

--

--