GLSL Tutorial: Syntax

From LWJGL
Jump to: navigation, search

In this tutorial I hope to give you a lot more information on actually writing shaders (only vertex and fragment) than was present in the previous tutorial (Which is not mine).

GLSL was initially based around the C language and therefore shares much of its syntax and features, however even for hardened Java developers this won't be too much of a leap. Many features of C that differ from Java, such as pointers, have been removed and I don't believe the syntax is too different.

Basics

Primitive Types

int - This is exactly the same as Java.

float - You guessed it.

uint - A nice feature from C, means unsigned integer. (Essentially an integer that can only be positive).

bool - Just short for boolean, otherwise same as Java. Please note unlike in C, bool variables are assigned true and false values.


Modifiers

Without getting too complicated, there is one modifier for GLSL, const. This is the equivalent of the final keyword in Java.


Vectors

Vectors can be of type int, float or boolean and can have 2, 3 or 4 elements. Defined as:

floats -- vec2, vec3 or vec4.

ints -- ivec2, ivec3, ivec4.

booleans -- bvec2, bvec3, bvec4.


Matrices

Matrices only come as squares of floats, but can be 2x2, 3x3 or 4x4. Defined as:

mat2, mat3 or mat4.

There are also several other types related to textures. They are sampler1D, sampler2D, sampler3D, samplerCube, sampler1DShadow and sampler2DShadow. I will not be going into these in this tutorials but information is out there if you want to know, thats how I found out this stuff.


Declaring and Initializing

Declaring a variable in GLSL is very similar to Java and for primitive types is actually the same. Here is some example code declaring and initializing, and assigning values to a few primitive types.


int i = 52;  //Declare integer variable i and set value to 52.
float f;     //Values don't have to be set upon declaration. 
f = 18.2;    //Assign value of 18.2 to f.
bool b = true;  //Good old booleans, where would we be without them?

i = 0;     //set i to 0.
int j = i + 20;    //Lastly a bit of simple maths to show it is the same as Java. See not so hard.

Please note that this is just a snippet of code as will be most of the examples. It will not run on its own.

Now for matrices and vectors. GLSL makes great use of something that you should all be familiar with, CONSTRUCTORS. However this is one slight difference... You do not use the new keyword, which can be a little confusing at first. The other thing with vectors and matrices is that GLSL really isn't picky with what goes in the constructor as long as the number of arguments is right. Hence a constructor for a vec4 could be: 4 floats, 2 vec2s, 1 vec2 and 2 floats, 1 vec3 and 1 float etc. Matrices have one special constructor which requires only one float, which sets the diagonal of the matrix to that value, so using 1 would give the identity matrix. Other than this they are the same as vectors but with more arguments. Please note they are defined in column major order. If you don't know what that means look it up (But you'll probably have to look up a lot more before you know what to do with matrices) Ok I'll give a refresher, it means you read top to bottom and THEN left to right.

Here are some examples:


vec4 vector4 = vec4(1, 2, 3, 4);//Same as java minus new keyword: class name(Its not really a class), open brackets, argument list,   
                                      //close bracket. 
                                            //Initialize this with components 1, 2, 3, 4
vec2 vector2a = vec2(1, 2); //A vector length 2.
vec2 vector2b = vec2(3, 4); //Another vector length 2.
vector4 = vec4(vector2a, vector2b); //And 2 + 2 = 4, so vec2 + vec2 = vec4. Thats logic for you.
                                           //In Java this would be:
                                                //vector4 = vec4(vector2a[0], vector2a[1], vector2a[0], vector2a[1]);
vec3 vector3 = vec3(1, 2, 3, 4);  //Oh look another vector.
vector4 = vec4(vector3, 4);       //I think your getting the hang of it now.
vector4 = vec4(4, vector3);       //You can even do it backwards. The thrills just keep coming.

//Now onto matrices

mat2 matrix2 = mat2(1);     //Do you remember what I said? This gives an identity matrix of size 2.

vec2 column1 = vec2(1, 0);
vec2 column2 = vec2(0, 1);   //Can you guess what I'm going to do with these?

mat2 matrixFromVectors = mat2(column1, column2);   //Wow. This gives another identity matrix size 2.
matrixFromVectors = mat2(1, 0, column2);           //As with vectors you can use any combination of floats and vectors.

mat4 matrixFromFloats = mat4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); //If you've got time to spare you can use all floats.
                                                                                  //This is another identity but size 4.

This is all well and good but how do we access the values in these vectors I hear you ask. Well...


Accessing Values in Vectors and Matrices

This can be done exactly the same as in Java however the good people at Khronos have seen fit to give us a gift. With vectors, we may access particular values with x, y, z, w (By convention position or normal coordinates), r, g, b, a (You guessed it these are for colours) or s, t, p, q (These are for texture coordinates. p is used here instead of r since that is already used in colours) Although conventions dictate using these for their respective types (position, colour or texture), you do not need to and those really sad people can use square brackets with the old integer inside. A little note with Matrices. You cannot use the letters, those are for vectors. This is the same as java if you think of them as a 2D array. mat4[n] will give you the nth column as a vec4 and mat4[n][j] will give you the jth row on the nth column as a float.

More examples


vec4 colourBlue = vec4(0, 0, 1, 1); //This is the colour blue as rgba values.
float redComponent = colourBlue.r; //This is 0. Could also be colourBlue[0].
float blueComponent = colourBlue.b; //This is 1. Could be colourBlue[2]

mat4 matrix = mat4(1); //A normal matrix. Also a viewing matrix at (0, 0, 0) pointing along z axis.
vec4 column1 = mat4[0]; //the first column of the matrix would be 1, 0, 0, 0.
vec4 fowardDirection = vec4(matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2]); //Pretty much says it all.


Arrays

Arrays in C are quite different to those in Java in that they have a fixed length defined when they are declared or initialized. Using anything other than a constant value (which means no non final variables in Java talk) as a length would not compile. In C you could just use dynamic memory management instead but this does not exist in GLSL. There are ways around this which aren't greatly memory efficient but are not too complicated but I will go into these in the next tutorial. For now you must settle with fixed length arrays. Arrays can be declared and initialized in a number of ways which I will now demonstrate.


    int intArray[5];  // declares an integer array of length 5. After this dec you cannot change the size. 
    const int length = 8;    
    int intArray2[length];    //Since length is a constant it can be used to declare the array.
    
    float floatArray[4] = {1.1, 2.2, 3.3, 4.4};   // Float array, length 4 with given values.
    float floatArray2[4] = float[4]{1.1, 2.2, 3.3, 4.4};    //This is exactly the same as above.
    float floatArray3[4] = float[]{1.1, 2.2, 3.3, 4.4};     //Again, exactly the same.
    
    int a[3];
    int b[] = a;         //The compiler figures out the length of b from a. This only works at declaration.

    b = int[3]{2, 3, 4};    // Give b some values.

    int lengthOfB = b.length();    // A nice little method to get the length of an array.

    b[0] = 9;      // Set the first element in b to 9. Yes this is the same as Java.

    vec3 vectorArray[3] = {vec3(1, 1, 1), vec3(1, 1, 1), vec3(1, 1, 1)};  //Arrays can be of any type or structure (see below) 
                                                                          //But not other arrays. ie no multidimensional arrays.
                                                                          //int array2D[4][2] IS WRONG.

There seems to be quite a few problems with the implementations of arrays in various different drivers, even within the same company. I have seen a lot of forum posts complaining that "int a[4] = {1, 2, 3, 4};" generates an error on their system whereas "int a[4] = int[]{1, 2, 3, 4};" compiles fine, only to be replied with someone else saying that the original method works fine and it is the second one that generates an error on their hardware. Opengl being platform independant, this is not good. I find that there are two ways to ensure your code will compile. The first uses uniform variables (See next tutorial), the second involves declaring the array without setting values and then setting each component individually. Like this.


    const int length = 20;
    float a[length];      // Use a constant int to define size.
    for(int i = 0; i < length; i++) {
        a[i] = i;      //integers will be automatically cast to float in this instance.
    }
    // I find that a lot of the time, a for loop is required anyway so this is not too much extra work for you. 

    //Without the loop it would be:
    a[0] = 0;
    a[1] = 1;
    a[2] = 2;
    //...
    a[16] = 16;
    a[17] = 17;
    a[19] = 19;


Structures

In C (and so GLSL) there are no classes, so things can get a little complicated, however structures are a slightly similar feature that is implemented in C and GLSL is structures. You can think of these as being classes without methods, only fields. The syntax of a structure is:

   struct name {
       type1 field1; 
       type2 field2;
       etc.
   };

Notice the semi-colon after the last closing brace (This semi-colon has caused me agony). Structures, just like classes can be used to represent real world objects, for example a light source. You still access structure members with the dot operator but they do not have constructors, you must set each value individually. Here is an example of a structure definition and some code initializing one.


struct SpotLight {
    vec3 position;
    vec3 direction;
    vec3 colour;
};

// Then in a function later on.

SpotLight light1;
light1.position = vec3(0, 0, 0); // It is positioned at the origin
light1.direction = vec3(0, 0, 1); // It points along the positive z axis
light1.colour = vec3(1, 0, 0);  // It is red.

Simple isn't it, makes me long for a class.


Still Very Basic

Writing Shaders

Now having given you completely useless information so far (in that you can't use it on it's own), I will now show you how to use this in an actual shader. Unlike in Java, there are no classes (there are structures but they are slightly different and I wont be explaining them just yet. They are a regular feature of C if you want to look them up). No classes means that you go straight into your functions. (These are functions not methods as they are not contained within a class) Just like in Java, there is a main method which is the entry point for that shader. It must have a void return type, have no arguments and since there are no access modifiers (public, private etc), the declaration for a main method is just "void main()"

#version 110
//This is a preprocessor directive which specifies not surprisingly the version of GLSL you are using. 110 is 1.1 (the first 
//version of GLSL released). If you do not use this GLSL will revert to 1.1, so this particular time it is pretty useless and I'm 
//only including it because it is good practice. 

void main {
    //In here we put the main code. 
    //Other functions can also be defined afterwards and called from here
    //Just like a regular method.
}


Pre-Defined Vaiables

In order to actually change the way in which geometry is drawn, we need two things. The data provided by your main application through calls to glVertex3f, glNormalf, glDrawArrays etc. and a variable to output which will be passed onto the next stage of the rendering pipeline. GLSL gives us these through pre-defined variables. The major ones are:


In Both Vertex and Fragment Shaders

-INPUTS

- gl_NormalMatrix a mat4 which describes the transformation necessary for a normal.

- gl_ModelViewMatrix a mat4 which is the modelview matrix given by the main application.

- gl_ProjectionMatrix a mat4 which is the projection matrix given by the main application.

- gl_ModelViewProjectionMatrix a mat4 which is the the combination of the modelview and projection matrices given by the main application.


In a Vertex Shader

-INPUTS

- gl_Vertex a vec4 which is the position given by the main application.

- gl_Normal a vec3 which is the normal given by the main application.

- gl_Color a vec4 which is the colour given by the main application.


- OUTPUTS

- gl_Position a vec4 describing the vertex's final position that will be passed onto the fragment shader

- gl_FrontColor a vec4 describing the colour of any vertex for a forward facing fragment, that will be passed onto the fragment shader.

- gl_BackColor a vec4 describing the colour of any vertex for a backward facing fragment, that will be passed onto the fragment shader.


In a Fragment Shader

-INPUTS

- gl_Color a vec4 passed to the frag shader from the vertex shader (either gl_FrontColor or gl_BackColor)


-OUTPUTS

- gl_FragColor a vec4 that is the final colour this fragment will be rendered as.


Now, lets use these to make just about the simplest shaders possible. It will use the modelview and projection matrices to transform the position to the desired place, use the normal matrix to transform the normal of the surface to the correct value with after the other transforms and finally will set both back and front colours to the original colour.

The Vertex Shader which in this example does all the work.


void main {
    gl_Normal = gl_NormalMatrix * gl_Normal; //Note that you can perform operations on matrices and vectors as if they were 
                                                //primitive types. This is useful for simple, readable code like this. 
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //The order in which you times matrices and vertices is IMPORTANT.
    gl_FrontColor = gl_Color;   //These lines just pass on the colour value to the fragment shader.
    gl_BackColor = gl_Color;
}

The lazy fragment shader


void main {
    gl_FragColor = gl_Color;   //Very simply sets the fragment's colour to the value it is given.
}

Now this should work perfectly, however there is just one thing I want to change and we will end up with an improved version of the above.

The first point I have to make I am afraid I cannot explain. Due to the particular implementations of the GLSL language of various hardware vendors, the opengl specification cannot guarantee that the transformation of a vertex by the gl_ModelViewProjectionMatrix will be the same frame to frame even if nothing else changes. This we do not like, therefore we have been provided with a very nice function that does everything for us, better than we could. It's declaration is

vec4 ftransform()

It takes gl_Vertex, works it's magic and returns the transformed position. So implementing this into the previous example

The New Vertex Shader


void main {
    gl_Normal = gl_NormalMatrix * gl_Normal; 
    gl_Position = ftransform; //There doesen't that look nicer.
    gl_FrontColor = gl_Color;
    gl_BackColor = gl_Color;
}


Functions and Global Variables

If we imagine each shader as a class, it should be quite clear what global vaiables are. They are simply the fields of the class, and can be accessed anywhere, ie globally. They are defined anywhere after preprocessor annotation (See later) and outside of any functions. Like so:


const vec4 colour = vec4(1, 1, 1, 1);   // A constant global variable. (They do not need to be constant).

void main {
    gl_Position = ftransform();
    gl_FrontColor = colour;         //Sets the colour to that of the global variable.
}

Functions (or methods) are ever so slightly different to Java. In a regular C program, a function can only be called if it has already been defined in the file. Hence if you define function foo1(), then function foo2(), foo1() could be called from foo2() but not the other way around. To get around this, you can declare your functions at the top of the file without writing any code for them. This is done exactly as you would an abstract method in Java, except that you override the method in the same file and you do not use the abstract keyword. Example:


vec4 getColour(int i); // Returns a colour based on an integer argument. return type is vec4, name is getColour, requires 1 int
                            //argument. Actual function body comes later.
                                 //Note it is not necessary to give each argument a name, only a type. So this could be:
                                       //vec4 getColour(int); However this is generally seen as less clear as the original.

void main {
    gl_Position = ftransform();
    gl_FrontColor = getColour();         //The getColour function can be used even though it is defined after this one.
}

vec4 getColour(int i) {            //The same declaration of the function, just with the code body added on.
    if(i == 0) {
        return vec4(1, 0, 0, 1);
    } else if(i == 1) {
        return vec4(0, 1, 0, 1);
    } else if(i == 2) {
        return vec4(0, 0, 1, 1);
    } else {
        return vec4(1, 1, 1, 1);
    }
}    

Useful Functions

That's almost everything for this tutorial. I'm just going to finish off with a few other helpful functions in GLSL. In these declarations, vec can be any one of vec2, vec3 or vec4 unless otherwise stated and likewise with mat.

float radians(float f) converts degrees to radians

float degrees(float f) converts radians to degrees

float sin(float f) Sine function

float cos(float f) Cosine function

float tan(float f) Tangent function

float asin(float f) ArcSine function

float acos(float f) ArcCosine function

float atan(float f) ArcTangent function

NB. The hyperbolic versions of each of these trigonometric functions exists for the more maths able. Just add h to the end: sinh, cosh, tanh etc.

float pow(float f1, float f2) return the value of the first argument to the power of the second. ie f1^f2

float exp(float f1) return the value of e to the power of the first argument ie e^f1

float exp2(float f1) returns the value of 2 to the power of the first argument ie 2^f1

float sqrt(float f1) returns the value of the square root of the first argument ie f1^0.5

float length(vec v) returns the magnitude of the given vector.

float distance(vec v1, vec v2) returns the distance between the two points v1 and v2. ie magnitude of v2-v1

float dot(vec v1, vec v2) return the dot product of the two vectors.

vec3 cross(vec3 v1, vec3 v2) returns the cross product of the two vectors NB must have 3 components.

vec normalize(vec v) return v with unit length.

mat transpose(mat m) returns the transpose of m.

mat inverse(mat m) returns the inverse of m.

float determinant(mat m) returns the determinant of m.


Neither this nor the list of pre-defined variables is complete. There are many many more, I have just listed those I feel to be the most useful. If you feel something is missing, then it probably is. The internet is out there. For further reading on all sorts of computer graphics topics in an opengl context (there is a lot on glsl) I reccomend The Lighthouse3D. I think its truly an exceptional website where I learnt most of my GLSL knowledge.

My next tutorial will explain passing variables other than the standard ones into a shader.