Basic GLSL structures

From the basic syntax, GLSL resembles C or C++, but it actually is not. In addition to the standard flow and branch control structures, it has a large set of special purpose functions designed to do vector and texture operations, and it comes with the associated special data types. Let's have a look at some simple examples.

A minimal shader set

The equivalent of 'Hello World!' in GLSL is the following set of a vertex shader

#version 120

void main()
{
    gl_Position = ftransform();
}

and a fragment shader.

#version 120

void main()
{
    gl_FragColor = vec4 (1.0, 1.0, 1.0, 1.0);
}

Let's take a closer look. We start with telling the driver what GLSL version we want to use (it's only polite - and we can only use features the graphics card driver supplies and which we request by asking for a versioning). In the event, we request 1.2 - if you want GLSL 4.0 where the syntax is somewhat different, try #version 400 instead. This also has to match with what your application on the C++ side provides. In the following we'll always assume you want to run the older version.

Like in C++, there's a requirement to have a main function. This function is required to output pre-defined structures, gl_Position and gl_FragColor respectively (the existence of these is hard-coded into OpenGL). ftransform() is basically an instruction to pass the vertex through unmodified and just use the relevant transformation matrix to correctly position it in the scene.

gl_FragColor is the color the pixel will have on the screen. This is an GLSL specific data type, a 4-vector, and the line shows how a 4-vector is initialized with constant components. In the event, the color assigned is white (1.0, 1.0, 1.0) with full opacity (1.0) in the last component. Whatever colors the mesh rendered by the shader may have been assigned - this is what will appear on the screen.

Data types

Like in C++, variables need to be declared before they're used. This declaration can initialize the variable to a value, but it doesn't have to. So

int i;
int j = 1;

float g;
float h = 1.0;

bool test = false;

are all valid declarations. Note that graphics cards usually compute on floating point precision, so unless you're using a high version of GLSL (above 4.0) you can't declare any double precision data structures, and if unless you have a modern card there's no real hardware support to crunch them - so get used to floating point precision when writing rendering code!

The vector data type

Rendering deals regularly with vectors - both as coordinates and as colors. For coordinates, a vector is simply the pointer from the origin of a coordinate system to the location of the described point along the coordinate unit vectors - so (1,0,0) represents a point one unit away from the origin along the x-axis (we'll come to where the coordinate origin is and how the unit vectors point later).

Colors are (generally) represented in computer graphics as (rgb) triplets, where GLSL has the convention that the entries range from zero to unity, i.e. (0,0.0,) is black, (1,0,0) is red and (1,1,1) is white. The transparency of a surface, also called the alpha channel, is appended as a fourth component, making a color vector (rgba).

Interestingly, also coordinate vectors are 4-component vectors in GLSL. While physics (Special Relativity to be precise) knows 4-vectors as well, it's not the same thing. GLSL 4-vector coordinates can be (a bit simplified) understood as making the distinction between positions and directions. Both are pointers, but if you shift the coordinate system origin (or apply a translation transformation), positions change, but directions do not. Thus, if you have a coordinate vector like (x,y,z) and write a 1 into the last component, it is supposed to represent a position and will change with translations, if you write a 0 to the last component it represents a direction and will not be affected by translations. Which means that whatever we return as gl_Position should have a 1 in the last component (ftransform() takes care of that automatically).

Because GLSL practically lives of vector operations, they have their own data types and a very convenient syntax. Vectors can be 2,3 or 4-dimensional, and are introduced as

vec4 eyePosition;
vec3 color = vec3 (1.0, 0.5, 0.0);
vec2 textureCoord;

Access to the individual coordinates is provided by simply appending the coordinate you want, e.g. gl_Position.x is the x-coordinate of gl_Position. You can also ask for two coordinates at once, for instance vec2 pos = gl_Vertex.xy is a perfectly fine operation.

In fact, you can even make pretty weird assignments (that's called 'swizzling') like

vec3 base = vec3 (1.0, 2.0, 3.0);
vec3 test1 = base.xxz;
vec2 test2 = base.zz;

and will get test1 = (1.0, 1.0, 3.0) and test2 = (3.0, 3.0) - you get the picture. If you need the fourth component of a vector, that'd be vector.w by the way.

Equally valid is a syntax to reference vector coordinates by (rgba), so color.r picks the red component of a color vector - or the x-component of a position vector. Syntactically it doesn't matter, and being mathematically minded, when I started to learn GLSL I wrote all operation in xyz convention even when they had to do with colors - just to be yelled at that this is not how things are done.

GLSL supports all standard operations of vector algebra. If you find yourself unsure of vector operations and their meaning, etc., reading through some vector algebra might not be a bad idea at this point. For instance

vec3 test1 = vec3 (1.0, 0.0, 0.0);
vec3 test2 = vec3 (0.0, 1.0, 0.0);

float l = length(test1 + test2);
float d = dot (test1, test2);
vec3 crossVec = cross (test1, test2);
vec3 scaledVec = 2.0 * test1;

all produce valid results.

(note that mathematically there's no such thing as a cross product for 2-vectors or 4-vectors, but dot products and scalar products work in other dimension as long as both vectors have the same dimension)

In addition there is the the multiplication operator for vectors, for instance you can do

vec3 test1 = vec3 (1.0, 1.0, 0.0);
vec3 test2 = vec3 (0.0, 1.0, 0.0);

vec3 out = test1 * test2;

This gives the components for the new vector as the product of components of the old vector. Mathematically, that is not a part of vector algebra, but it is useful for color blending. After all, doing a cross product of two color vectors is not a very useful operation either - though there can be some merit in doing a dot product of color vectors to check whether their hue is similar!

Retrieving uniform values

Let's close this section with a slightly more involved example - suppose we want to change the minimal shader to return a runtime-configurable color rather than the default white. We would make the color channels uniforms, and we have to make sure these uniforms are assigned values when creating the effect outside GLSL. To pick them up in the fragment shader, we use the keyword uniform ahead of the variable declaration - this tells the shader to assign the value to whatever is stored for the uniform of that name. Then we need to combine then into a vector, and assign them to gl_FragColor. The end result might look like this

uniform float color_r;
uniform float color_g;
uniform float color_b;

void main()
{
vec3 color = vec3 (color_r, color_g, color_b);
gl_FragColor = vec4 (color, 1.0);
}

Dependent on how we decided to set things up in the C++ side, we could of course also have used

uniform vec4 color;

void main()
{
gl_FragColor = vec4 (color);
}

Continue with Coordinate systems.


Back to main index     Back to rendering     Back to GLSL

Created by Thorsten Renk 2016 - see the disclaimer and contact information.