(click anywhere to close)
OPEN MENU

[Three.js] GLSL Advanced Tutorial

category: Games | course: Threejs | difficulty:

In the previous tutorial, you learned all about setting up GLSL and how it works (the fundamentals). Now, I'll show you how you can do special things with it!

Define variables

At the top of a shader, above the main function, we define the different variable types I mentioned earlier (varying and uniform). If you define a variable in one shader, you have to define it in the exact same way in the other shader as well! When creating a new variable in GLSL outside of the main function you have to state its GLSL-type and its normal/usual type. This sounds weird, but that basically means: uniform/varying/attribute + float/int/vec3/etc. Like this:

//This bit is the same in vertex and fragment shader
varying vec3 vNormal;
uniform vec3 uLightDirection;

void Main() {
    //blabla..
}

It's common practice, to put a little 'v' or 'u' in front of varyings and uniforms respectively, so you know what type something is by looking at the name. If you then define variables inside the Main function, it works the same way, but you just leave out the first bit (varying/uniform).

NOTE: If you create afloat variable, it wants all its input in float numbers! So don't use a 1 or 0, use 1.0 and 0.0

Do something with 'em!

I'm just going to show you how this works with an example. This example creates some 'fake' shading on an object. We first pass the normal for every vertex to the fragment shader (this works because interpolation of normals occurs between the two), and then use that and a custom light we set to create shading.

Vertex Shader:

//This variable will contain the normal
varying vec3 vNormal;

void main() {
  // 'normal' is a standard value provided by Three.js for every vertex
  // just as 'position'
  vNormal = normal;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

Fragment Shader:

//Declared in exactly the same way as Vertex Shader
varying vec3 vNormal;

void main() {
  //Create a vector to determine where light comes from 
  // (similar to directional light in this case)
  vec3 light = vec3(0.5, 0.2, 1.0);

  //Normalize it 
  light = normalize(light);

  //Calculate 'dot product'
  // and clamp 0->1 instead of -1->1
  float dProd = max(0.0,
                    dot(vNormal, light));

  //And output this color.
  gl_FragColor = vec4(dProd, dProd, dProd, 1.0);  //RGBA

}

What's this dot product? The dot product, is an often used (and therefore built-in) function that calculates the cosine between two vectors. If the normal and light vector are equal (point in same direction), this returns 1 (fully lit). If they are completely opposite, this returns -1 (which we make 0.0, and is completely dark).

What's thenormalize for? Normalize shrinks all three values of the vector down to a value between 0 and 1. This must be done to calculate the dot product. If one of the vectors is not normalized, then the equation does not work properly. Now you should have a nicely shaded 3D object!

Passing through attributes

Now we've seen some uniforms and varyings in action, but where are those attributes? Well, those are usually passed in from the HTML page - you can also input uniforms from there. First you create a JS Object that will contain all the attributes, then inside that you create another new object for every attribute that holds the type and value. Then, you pass it into the Shader. Like this:

//Attributes
//Every element of the array corresponds with the vertex of the same index
var attributes = {
  displacement: {
    type: 'f', // a float
    value: [] // an empty array
  }
};

//Uniforms
var uniforms = {
  amplitude: {
    type: 'f', // a float
    value: 0
  }
};

//Create final ShaderMaterial
var shaderMaterial =
    new THREE.MeshShaderMaterial({
      uniforms:       uniforms,
      attributes:     attributes,
      vertexShader:   vShader,
      fragmentShader: fShader
    });

Then of course, you also need to define these variables at the top of the shader(s) so that they are properly received, and then within the Main function you can use them for whatever you want!

Data Types!

GLSL supports a few basic data types, and a few specifically useful ones when working with 3D graphics:

  • int integer (0,1,10,-5,etc.)
  • float floating point number (0.2, -0.5, 15.6, 4/3, etc.)
  • bool boolean (true or false, 0 or 1)
  • vec2, vec3, vec4: vectors of length 2,3,4 respectively
  • mat2, mat3, mat4: matrices of 2x2, 3x3, 4x4 respectively

GLSL is optimized for throwing vectors together, so for example a vec4 can be created by saying vec4(someVec3, 4thvalue). Also, you can multiply a vec3 with a single float variable (scalar) without the program throwing all sorts of errors. This will simply multiply each component of the vector seperately.

How could there ever be a 4-dimensional vector?

Well, any vec4 in GLSL has the properties RGBA or XYZW you can get/set, depending on your interpretation.

RGBA works for colors: a red, green and blue channel, and analpha (opacity) channel).

XYZW works for actual points and vectors: the x, y, z coordinate, and the fourth W value decides what type it is (because something with an x, y and z could be both a vector and a point). Vectors have 0, points have 1. And then you might think 'why didn't they just create different functions for vectors and points?' Because using this fourth value helps a lot when it comes to using matrices (which is what the computer does a lot)..

Built-in methods

All the usual methods are built into GLSL: if/else statements, for loops, do while loops, etc. However, I recommend you try to avoid if/else statements as much as possible. They are (relatively) heavy on the GPU, and as seen that for now performance/steady FPS is the major bottleneck in most 3D web apps, you'll want to optimize performance as much as possible. It also has the other standard (math) functions:

  • sin, cos, tan, atan
  • pow, exp, log, exp2, log2, sqrt, inversesqrt
  • abs, floor, ceil, mod, sign, min, max, clamp

(if you don't know some of these, look them up, they can really help you out sometimes.)

Next to that, there are GLSL-specific (vector) functions. You've already seennormalize anddot product, and here's the rest:

  • Length (length()): Returns the length of the vector that's put into it.
  • Distance (distance()): Returns the distance between two points put into it.
  • Cross product (cross()): Returns the axis of rotation between two vectors, i.e. the vector that is perpendicular to both vectors.
  • Reflect (reflect()): Reflects a vector (needs the incoming vector, and a vector representing the normal of the surface).
  • Refract (refract()): Refracts a vector (needs the incoming vector, normal of surface, and ratio or refraction).

Textures!

To add textures to your shader, are multiple steps are required. First. you must use Three.js to load a texture to a variable. Then you pass this data as a uniform variable to the shader. Then within the shader you must pass the UV coordinates of every vertex to the fragment shader, so that the latter can use both variables (texture and UV) to access the correct part of the texture to display. Sounds complicated? Not really, justlook at this code:

For the main page/javascript:

var tex = THREE.ImageUtils.loadTexture('/path/to/texture.png');
//optionally set some settings for it
//tex.magFilter = THREE.NearestFilter;

//Create the material, pass in the texture as a uniform with type 't'
var mat = new THREE.ShaderMaterial({
    uniforms: {
        theTexture: {type: 't', value: tex}
    },
    vertexShader:vShader,
    fragmentShader:fShader,
    //Set transparent to true if your texture has some regions with alpha=0
    transparent: true
});

Vertex Shader:

//A varying that gets the UV coordinates and gives them to the FS
varying vec2 vUv;

void main() {
    //Get UV coordinates
    vUv = uv;
    //As always, keep position as is
    gl_Position = projectionMatrix *
                  modelViewMatrix * vec4(position, 1.0 );
}

Fragment Shader:

//Same varying to retrieve UV coordinates 
varying vec2 vUv;
//A uniform of type 'sampler2D' with the same name as was used in Three.js
uniform sampler2D theTexture;
 
void main() {
    //Get the color from the texture by using texture2D()
    //And as always, set it to the fragment's color
    gl_FragColor = texture2D(theTexture, vUv);
}

It's that simple! Well, for simple textures. If you want to go crazy, I suggest you pick up a good book on the math behind 3D computer graphics.

And now?

Now you've learned the basics of GLSL. But of course, there's much more! You could animate the uniforms you put into the shader, add normal and bump maps, create awesome materials, anything you like!

However, I must leave you on your own here. Experiment yourself and create inspiring 3D graphics and shaders!

CONTINUE WITH THIS COURSE
Do you like my tutorials?
To keep this site running, donate some motivational food!
Crisps
(€2.00)
Chocolate Milk
(€3.50)
Pizza
(€5.00)