|
|
In this lesson, we will take our first steps in actually programming a shader. This will also include a first introduction to the shader language GLSL, its datatypes, and constructs. Finally, you will create your first shader, which you can then check out right away. Let's go!
|
|
When the first shading processors where introduced, developers had to write their shaders in pure assembler. Nowadays, a couple of standard languages have been developed to make programming shaders clear and simple. Beside CG from Nvidia and HLSL from Microsoft, there's GLSL from the GL ARB (Architectures Review Board). It's closely related to C and a part of the OpenGL 2.0 standard. This has the big advantage that no external compiler is needed to compile the sourcecode, because it's included in the OpenGL implementation.
|
|
The following code snippet shows a typical GLSL vertex shader, which is saved into a file. Vertex shaders normally have the extension .vert, where pixel shader use .frag:
[flat_wave.vert]
uniform float time;
void main(void)
{
vec4 v = vec4(gl_Vertex);
v.z = sin(5.0 * v.x + time * 0.01) * 0.25;
gl_Position = gl_ModelViewProjectionMatrix * v;
}
This above listing shows the shader's close similarity to C. Every shader needs a function void main(void), which takes fragment texturing and coloring stage a parameter, nor returns one.
Inside the shader, all sorts of computations can be performed and variables read or written. In addition to the standard datatypes: bool, int, and float, there are types for vectors, matrices, and textures.
| Datatype | Description |
|---|---|
| vec2, vec3, vec4 | Floatvector with 2, 3 or 4 elements |
| ivec2, ivec3, ivec4 | Integervector with 2, 3 or 4 elements |
| bvec2, bvec3, bvec4 | Boolvector with 2, 3 or 4 elementes |
| mat2, mat3, mat4 | Floatmatrix with 2x2, 3x3 or 4x4 elements |
| sampler1D, sampler2D, sampler3D |
1D-, 2D-, 3D-Texture |
| samplerCube | Cubemap-Texture |
Working with these datatypes alleviates some of the restrictions of C and is very intuitive. For example, it's possible to multiply matrices with vectors or other matrices. Dealing with vectors, on a whole, has been simplified.
|
|
Vectors are initialized by their constructors. The constructors accept single values (scalars), as well as vectors or a combination of both. The vector elements are initialized in the order of the given scalars and/or vectors. If a vector is constructed using a single scalar, all its fields are assigned the same value.
The fields of a vector are either selected, using the traditional bracked operator [] or using the dot operator. The developer is free to choose any of the aliases .r, .g, .b or .a for colors (instead of the indicies 0, 1, 2, 3), .x, .y, .z, .w for space coordinates, or .s, .t, .p, .q for texture coordinates. It's even possible to access multiple fields at once, by using a sequence of the characters mentioned above. The following snippet shows what code taking advantage of these extended features looks like:
vec3 position = vec3(1.0, 2.0, 3.0);
vec4 color = vec4(position, 3.0);
vec4 white = vec4(1.0);
float xPos = position[0];
float yPos = position.y;
vec2 redalpha = color.ra;
vec2 doublePos = position.xyxy;
color.r = 0.75;
struct dirlight {
vec3 direction;
vec3 color;
};
dirlight d1;
dirlight d2 = dirlight(vec3(1.0,1.0,0.0), vec3(0.8,0.8,0.4));
|
|
Except for the switch statement, all common control structures, like if, and else, for, and while can be used.
On older graphic boards, attention must be paid to the while loop. The compiler needs to be able to unroll the loop, this means that the number of interations has to be determined at compile time. Therefore, dependencies on (non-constant) variables are not allowed.
|
|
GLSL provides a wide range of predefined functions. Besides the trigonometric functions, sin(float) and cos(float), there are functions like dot(float) from linear algebra.
A special function, which should be mentioned here, is ftransform(). It returns the result of the vertex transformation from the fixed functionality. It also uses the same optimizations as the fixed functionality.
|
|
Developers have several options to communicate with a shader. However, this communication is only one-way, because you can't call a shader and poll its results. Instead of returning values to the calling application, the shader writes into the color- and the depthbuffer.
As already mentioned in lesson #1, shaders are able to read OpenGL states. This makes sense when querying, for example, OpenGL light sources or the current fog color from your shader. Theoretically, you could use unused fields from light sources to store user data, but things would get really messy.
In fact, shaders are able to access texture memory. So, you might use texture data as a new way of communicating with a shader. On newer hardware, it is even possible to communicate between shaders using this method.
Luckily, GLSL also defines a proper way of communicating with a shader using the qualifier uniform and attribute. Shader variables using these qualifiers have to be accessed read-only inside the shader, but the application can change them at anytime, e.g. a time variable letting the shader animate something over time).
Uniform variables can be seen as global variables, which are constant throughout a primitive or a whole scene. They can be any of the datatypes supported by the shaders. Their values can be changed in the application at runtime and can be read by vertex shaders and pixel shaders alike. In contrast to attribute variables, uniform variables can only be set outside glBegin() and glEnd().
Attribute variables make it possible to define new attributes for a vertex, like heat or weight. Because they only apply to vertices, they can't be read by pixel shader. In the following lessons, we will only work with uniform variables but for the sake of completeness attribute variables are mentioned here.
|
|
Communication between OpenGL and a shader is done using a predefined set of global variables. Depending on the variable, they can be read or written by a shader.
| Variable name | Datatype | Description |
|---|---|---|
| Readable in vertex shaders (vertex attributes) | ||
| gl_Color | vec4 | First drawing color |
| gl_SecondaryColor | vec4 | Second drawing color |
| gl_Normal | vec3 | Normal vector of a vertex |
| gl_Vertex | vec4 | Orientation vector of a vertex |
| gl_MultiTexCoord[0-7] | vec4 | Texture coordinates of the eight texture units |
| Writeable in vertex shaders | ||
| gl_Position | vec4 | Final position of the vertex on the screen |
| gl_PointSize | float | Pixel size of the vertex |
| Readable in pixel shaders | ||
| gl_FragCoord | vec4 | Position of the fragment on the screen |
| gl_FrontFacing | bool | Is the primitive of the fragment facing forward? |
| Writeable in fragment shaders | ||
| gl_FragColor | vec4 | Final color value of the fragment |
| gl_FragDepth | float | Overwrites the calculated Z-value of the fragment |
The variables gl_Position and gl_FragColor play a central role , as they represent the "results" of the individual shaders. gl_Position has to be set (!) by every vertex shader, even if its value doesn't make sense. The same goes for gl_FragColor, which is the pixel shader's counterpart (although a fragment can also be dismissed, so it's simply not drawn).
Furthermore, a couple of built-in uniform variables exist (remember: the global ones for communication between application and shader). They give access to the different matrices of OpenGL and are shown in Table 3.
| Uniform name | Datatype | Description |
|---|---|---|
| gl_ModelViewMatrix | mat4 | Translation, rotation and scalling of a model |
| gl_ProjectionMatrix | mat4 | Transformation from world- to screen space |
| gl_ModelViewProjectionMatrix | mat4 | Result of the multiplication of these two matrices |
| gl_NormalMatrix | mat3 | Special transformation matrix for normal vectors |
|
|
Communication between the shaders is done using the qualifier varying. Variables, which are qualified varying, have to be declared globally, in both the vertex and pixel shader. The values, which are assigned inside the vertex shader, are interpolated automatically between the vertices of the primitive the vertex belongs to. The interpolated values are then given to the pixel shader. That this interpolation is neccesary may become clear, if you think about a simple triangle. It consists of three vertices, but when it is drawn solid on the screen, it may need thousands of fragments. In this case, three calls of the vertex shader are followed by thousands of calls of the pixel shader.
Besides the user defined varying variables, there are also a couple of predefined GLSL varyings you can use to communicate between the shaders:
| Varying name | Datatype | Description |
|---|---|---|
| Only inside the vertex shader (writing) | ||
| gl_FrontColor | vec4 | Front color of the vertex |
| gl_BackColor | vec4 | Back color of the vertex |
| gl_FrontSecondaryColor | vec4 | Second front color |
| gl_BackSecondaryColor | vec4 | Second back color |
| Only inside the pixel shader (reading) | ||
| gl_Color | vec4 | First draw color |
| gl_SecondaryColor | vec4 | Second draw color |
| Inside both shaders | ||
| gl_TexCoord[] | vec4 | Texturecoordinates of the respective texture unit |
Except for gl_TexCoord[] all of these variables can be used, without a prior declaration. gl_TexCoord need to be declared with the same number of elements in both shaders.
The varying variables gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor, and gl_BackSecondaryColor can only be read inside the pixel shader using the aliases gl_Color and gl_SecondaryColor respectively. Which value from the vertex shader is used in the pixel shader depends on whether the fragment belongs to a front or back facing primitive.
|
|
After we have learned the basics of shaders and the shading language GLSL in general, it's time to do some practicing!
|
|
Your task is to write a minimal shader, which does nothing more than filling a model with a constant color. To the user, the model will look like a solid shape version of the 3D model. For this, we need both, a vertex and a pixel shader. The vertex shader should only transform the incoming vertex data, while the pixel shader colors the incoming fragments with a constant color.

|
|