An Introduction to Shaders - Part 1
Introduction
Iāve previously given you an introduction to Three.js. If youāve not read that you might want to as itās the foundation on which I will be building during this article.
What I want to do is discuss shaders. WebGL is brilliant, and as Iāve said before Three.js (and other libraries) do a fantastic job of abstracting away the difficulties for you. But there will be times you want to achieve a specific effect, or you will want to dig a little deeper into how that amazing stuff appeared on your screen, and shaders will almost certainly be a part of that equation. Also if youāre like me you may well want to go from the basic stuff in the last tutorial to something a little more tricky. Iāll work on the basis that youāre using Three.js, since it does a lot of the donkey work for us in terms of getting the shader going.
Iāll say up front as well that quite a lot of this will be me explaining the context for shaders, and that there will be a second part of this guide where we will get into slightly more advanced territory. The reason for this is that shaders are unusual at first sight and take a bit of explaining.
Our Two Shaders
WebGL does not offer the use of the Fixed Pipeline, which is a shorthand way of saying that it doesnāt give you any means of rendering your stuff out of the box. What it does offer, however, is the Programmable Pipeline, which is more powerful but also more difficult to understand and use. In short the Programmable Pipeline means as the programmer you take responsibility for getting the vertices and so forth rendered to the screen. Shaders are a part of this pipeline, and there are two types of them:
- Vertex shaders
- Fragment shaders
Both of which, Iām sure youāll agree, mean absolutely nothing by themselves. What you should know about them is that they both run entirely on your graphics cardās GPU. This means that we want to offload all that we can to them, leaving our CPU to do other work. A modern GPU is heavily optimised for the functions that shaders require so itās great to be able to use it.
Vertex Shaders
Take a standard primitive shape, like a sphere. Itās made up of vertices,
right? A vertex shader is given every single one of these vertices in turn and
can mess around with them. Itās up to the vertex shader what it actually does
with each one, but it has one responsibility: it must at some point set
something called gl_Position
, a 4D float vector, which is the final
position of the vertex on screen. In and of itself thatās quite an interesting
process, because weāre actually talking about getting a 3D position (a vertex
with x,y,z) onto, or projected, to a 2D screen. Thankfully for us if weāre
using something like Three.js we will have a shorthand way of setting the
gl_Position
without things getting too heavy.
Fragment Shaders
So we have our object with its vertices, and weāve projected them to the 2D screen, but what about the colours we use? What about texturing and lighting? Thatās exactly what the fragment shader is there for.
Very much like the vertex shader, the fragment shader also only has one must-
do job: it must set or discard the gl_FragColor
variable, another 4D float
vector, which the final colour of our fragment. But what is a fragment? Think
of three vertices which make a triangle. Each pixel within that triangle needs
to be drawn out. A fragment is the data provided by those three vertices for
the purpose of drawing each pixel in that triangle. Because of this the
fragments receive interpolated values from their constituent vertices. If one
vertex is coloured red, and its neighbour is blue we would see the colour
values interpolate from red, through purple, to blue.
Shader Variables
When talking about variables there are three declarations you can make: Uniforms, Attributes and Varyings. When I first heard of those three I was very confused since they donāt match anything else Iād ever worked with. But hereās how you can think of them:
- Uniforms are sent to both vertex shaders and fragment shaders and contain values that stay the same across the entire frame being rendered. A good example of this might be a lightās position.
- Attributes are values that are applied to individual vertices. Attributes are only available to the vertex shader. This could be something like each vertex having a distinct colour. Attributes have a one-to-one relationship with vertices.
- Varyings are variables declared in the vertex shader that we want to share with the fragment shader. To do this we make sure we declare a varying variable of the same type and name in both the vertex shader and the fragment shader. A classic use of this would be a vertexās normal since this can be used in the lighting calculations.
In the second part of this article weāll use all three types so you can get a feel for how they are applied for real.
Now weāve talked about vertex shaders and fragment shaders and the types of variables they deal with, itās now worth looking at the simplest shaders we can create.
Bonjourno World
Here, then, is the Hello World of vertex shaders:
/**
* Multiply each vertex by the
* model-view matrix and the
* projection matrix (both provided
* by Three.js) to get a final
* vertex position
*/
void main() {
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
and hereās the same for the fragment shader:
/**
* Set the colour to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, // R
0.0, // G
1.0, // B
1.0); // A
}
Thatās really all there is to it. If you were to use that you would see an āunlitā pink shape on your screen. Not too complicated though, right?
In the vertex shader we are sent a couple of uniforms by Three.js. These two uniforms are 4D matrices, called the Model-View Matrix and the Projection Matrix. You donāt desperately need to know exactly how these work, although itās always best to understand how things do what they do if you can. The short version is that they are how the 3D position of the vertex is actually projected to the final 2D position on the screen.
Iāve actually left them out of the snippet above because Three.js adds them to the top of your shader code itself so you donāt need to worry about doing it. Truth be told it actually adds a lot more than that, such as light data, vertex colours and vertex normals. If you were doing this without Three.js you would have to create and set all those uniforms and attributes yourself. True story.
Using a MeshShaderMaterial
OK, so we have a shader set up, but how do we use it with Three.js? It turns out that itās terribly easy. Itās rather like this:
/**
* Assume we have jQuery to hand
* and pull out from the DOM the
* two snippets of text for
* each of our shaders
*/
var vShader = $('vertexshader');
var fShader = $('fragmentshader');
var shaderMaterial =
new THREE.ShaderMaterial({
vertexShader: vShader.text(),
fragmentShader: fShader.text()
});
From there Three.js will compile and run your shaders attached to the mesh to which you give that material. It doesnāt get much easier than that really. Well it probably does, but weāre talking about 3D running in your browser so I figure you expect a certain amount of complexity.
We can actually add two more properties to our MeshShaderMaterial: uniforms and attributes. They can both take vectors, integers or floats but as I mentioned before uniforms are the same for the whole frame, i.e. for all vertices, so they tend to be single values. Attributes, however, are per- vertex variables, so they are expected to be an array. There should be a one- to-one relationship between the number of values in the attributes array and the number of vertices in the mesh.
Conclusion
Iāll stop there for now as weāve actually covered a rather large amount, and yet in many ways weāve only just scratched the surface. In the next guide Iām going provide a more advanced shader to which I will be passing through some attributes and uniforms as well as doing a bit of fake lighting.
Iāve wrapped up the source code in this lab article so you have it as a reference. If youāve enjoyed this let me know via Twitter - it makes for a happy Paul.