Learning Shaders

19 December 2024

·

5 min read

I’ve been learning shaders this week, and I wanted to share my impressions as a beginner — you might find it useful too.

A shader is a program that can transform data from an input object, like the coordinates of its vertices, to a rasterized image that can be displayed on the screen. It is one of the components that makes the graphics pipeline that allows you to see stuff on your computer screen.

The GLSL programming language

Shaders are written in a language which is very similar to C, and are compiled by the GPU libraries. It includes built-in types to do linear algebra, like vector and matrices, as well as functions that are useful in the context of graphics, like smoothstep.

varying vec2 vUv;
uniform float uTime;

void main() {
	float value = 0.0;
	value += sin(uTime / 100. + vUv.x*100.);
	value += 5.*sin(uTime / 1000. + vUv.y*300.);
	gl_FragColor = vec4(value, value, value, 1.);
}

When writing a shader, the inputs and outputs are defined in the global scope. The shader compiler checks if the global are available or not. In the previous example, vUv and uTime are inputs to the shader, and it sets the gl_FragColor “global”. Each shader runs the void main() function, but you can write your own functions like in C.

Vertex and fragment shaders

There are two different types of shaders that will be run by our graphics library:

  • Vertex shaders maps the coordinates of our object’s geometry, depending on the camera, perspective, or any other transformation.
  • Fragment shaders calculate the color of each pixel in the triangle of the geometry.

Diagram

First, we start with some 3d model described by triangles and its vertices. The vertex shader first maps each vertex, to a location on the projection. The simplest vertex shader for the library I’m using would look like this:

// main.vert
void main() {
	gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

With the vertex shader, we can affect the 3d model we are working with. For example, to apply some “ripple” effect to the surface of our object. Note that the vertex shader is run for each vertex.

The fragment shader is more interesting though. After placing or vertices with the vertex shader, our graphics library fills each triangle with pixels, and runs the fragment shader for each pixel. This is an easy task to do with a GPU, that can do many operations in parallel.

For example, we can write a shader that sets any pixel to red:

// main.frag
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 0.0);
}

This is not very interesting though, but in the next section we will do fancier stuff with varying.

Varying variables

What if we could set the color of a pixel, depending on its coordinates? Fragment shaders don’t have access to the location of our pixel — at least not in the library I’m using — but the tool of GLSL to use here is varying. A varying variable can be set from a vertex shader, and read from a fragment shader. We can adjust our previous example like so:

// main.vert
varying vec4 vPosition; 
void main() {
  vPosition = position;
	gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

// main.frag
varying vec4 vPosition;
void main() {
  gl_FragColor = vec4(vPosition.x, vPosition.y, 0.0, 1.0);
}

Picture

As you can see, we are coloring each pixel in our cube, depending on its location — greener areas come from a greater y coordinate and the red comes from a greater x.

There is one detail though, which I think is pretty neat: our vPosition is being set by the vertex shader. The vertex shader is only run for the 8 vertices of the cube. How do we get all the in-between values for each pixel that the fragment shader processes? That’s what varying does: it interpolates the value of the varying, depending on the 3 vertices of the triangle that we are drawing:

Diagram

In the previous diagram, only the 3 vertices of the triangle set the value of the varying float myV. For any other point in the triangle, the value of myV is interpolated depending on the value at the 3 vertices. I think this is very neat.

Keep learning!

That’s all for now. I don’t want to make a very long post, as I am still learning how GLSL works. We could also talk how you can pass external variables to the shader with the uniform qualifier, or how you can use UV’s to map a 2D texture into a 3D model, but that’s for another day.

If you want to start writing shaders, I recommend the following tutorials:

You can actually render shaders on your browser thanks to WebGL, and these pages give you a nice editor, for you to fiddle around: