Simple shader tutorial for GLSL in OpenGL using Threejs

Any advanced user looking at this probably thinks it simple, and in a lot of ways it is, but the implementation is as important as the theory and I wish to cover both.

Simple shader tutorial for GLSL in OpenGL using Threejs

The tutorial for after the 'basics' tutorial

If you don't know my story I have been working hard to move from a standard front end developer into the creative space;  A part of landing the jobs that look interesting to me is an advanced understanding of ThreeJs, more specifically, writing custom shaders.

I don't have a computer graphics degree and I don't have any formal training in linear algebra but luckily there is a huge amount of content to help get you started, here is a list of what I used:

  1. This is a great free course about computer graphics made by Autodesk
  2. This is a fantastic introduction to writing shaders by CJGammon
  3. This series by 3Blue1Brown really helped me get my head around linear algebra concepts
  4. 'TheBookOfShaders' is also a decent resource to help base understanding

A warning though, this stuff isn't easy. At least it wasn't for me but there are a lot of great resources to get you started.

But that was the problem, the majority of the resources were just to get you started. Its also easy enough to find advanced examples of shader for the pro's at places like: shadertoy but bridging that gap has been a real uphill (and still ongoing) battle for me.

Going forward with this I'm going to assume you understand the very basics of the vertex and the fragment shader, as that content is readily available and my tutorial is for people wanting to take the next step after that.

One of the many great graphics from the book of shaders

Documentation is fragmented and all over the place

Its hard to find whats actually available for you to play with in the shader. I'm certain the documentation for OpenGL or GLSL has it but when you are new a lot of the terminology used just muddies the water and you come out more confused than when you went in.

For example, one think I would consider a basic when you're learning GLSL is the uniforms that are actually available to you in each shader.

This is the list I have compiled but if I have missed any please don't hesitate to contact me and I will add them to this list (with a bit of digging and a little more knowledge of where to look I found them in the threejs docs here)

// In the vertex shader
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat3 normalMatrix;
uniform vec3 cameraPosition;
uniform vec3 normal;
uniform vec3 position;
uniform vec2 uv;

// in the fragment shader
uniform mat4 viewMatrix
uniform vec3 cameraPosition

and we will use them in this tutorial.

A simple scene to work with

Here I have provided a simple scene that uses the kind of thing you have seen in CJGammons video, it works out the gl_Position in the standard way without any vertex transformation and it exports the gl_Fragcolor as vec4(1,1,1,1) which is the same as rgba(255,255,255,1) or plain white with no transparency. The reason I have a background image is because its easier to see if the transparency is working rather than using a plain colour.

If you're new to shaders in Threejs it might be beneficial to comb through this simple scene and figure out how it all works or even use it as a baseline for your own learning.

I also included some orbit controls so that we can test that the shader effect is the same across all sides of the shape

Lets breakdown the effect

Any advanced user looking at this probably thinks it simple, and in a lot of ways it is, but the implementation is as important as the theory and I wish to cover both. But before we look at the code lets have a look and a think about whats going on. Here is what we are building:

Shield effect, the sphere should look like this from all angles

A crucial thing to consider is that in the vertex shader we have access to the face normal, this tells you what direction that vertex is facing in world space.

The key to this effect is we want it so that the more the vertex faces the camera, the more transparent it appears.

so for this technique we need to know a few things:

  • The vector of the vertex normal
  • The vector from the vertex position towards the camera
  • The angle between those two vectors

We have the vertex normal in world space, provided by the vertex shader as 'normal', next we need the vector from the vertex to the camera. We have the camera position and the vertex position provided, the vector between two points is calculated by :

vector between two points = destination point - start point

The dot product

Now the final part was the angle between the two vectors we have spoken about before. Fortunately there is a mathematical calculation that can already be used to see to what extent two vectors point in the same direction. The dot product! Also fortunately for us this operation can be done right inside the vertex shader with the dot() function. This is a quick diagram to show in 2d what results you can expect from the dot product:

So a result between -1 and 1. -1 being when the vectors are in the opposite direction and 1 when they are the same

Now conveniently for us we want to use the result from this calculation to affect the transparency (or alpha) in the fragment shader, which is a number between 0 and 1.

We want the result to be 0 if the vertex is facing the camera and 1 when its perpendicular, which is the opposite of what is output by the dot product, do we will need to do:

desired alpha = 1 - dot( [vector from normal to camera] - [vertex normal] );

Putting it into practice

You will see below a working example with commented code that does exactly what was described above. Lets just go over the unmentioned parts.

To pass information between a vertex and fragment shader you use the 'varying' attribute and it must be declared in an identically for both shaders. As you see here I decided to calculate the alpha in the vertex shader then pass it to the fragment using the varying output.

Another gotcha with this is that after you figure out the vector between the vertex and the camera, to do the dot product calculation properly the vector should be normalized (made to have the same direction but a length of 1). You will see this in the vertex shader using the normalize() function.

Feel free to have a play and if you dont understand message me on twitter and I will talk you through it.