GLSL shaders

From Wikiid
Jump to: navigation, search

This guide covers the very basics of writing a GLSL shader that can do fairly standard OpenGL lighting:

Contents

What can a shader do?

Shaders generally go around in pairs - one shader (the "Vertex shader") is a short program that takes in one vertex from the main CPU and produces one vertex that is passed on to the GPU rasterizer which uses the vertices to create triangles - which it then chops up into individual pixel-sized fragments. The other shader (the "Fragment shader" - also known (incorrectly) as the "Pixel shader") take one pixel from the rasterizer and generates one pixel to write or blend into the frame buffer.

The vertex shader runs from start to end for each and every vertex that's passed into the graphics card - the fragment process does the same thing at the pixel level. In most scenes there are a heck of a lot more pixel fragments than there are vertices - so the performance of the fragment shader is vastly more important and any work we can do in the vertex shader, we probably should.

Shader Programming Languages

There are three programming languages for shaders - and they are all very similar:

  • Cg - (C for Graphics) - the nVidia shader language that started the whole business of high level shader languages. Cg works with OpenGL as well as Direct-X.
  • HLSL - (High Level Shader Language) - the official Direct-X shader language - which is basically just Cg with some Microsoft involvement.
  • GLSL - (GL Shader Language) - the official OpenGL shader language - quite similar to Cg and HLSL in many ways - but with some annoying quirks and some notable improvements.

There is one really significant difference between Cg/HLSL and GLSL and that is that with the former, the compiler is a separate program that compiles to some kind of intermediate binary representation. GLSL has the compiler integrated into the graphics API so that the intermediate binary representation is not visible at any stage in the process.

GLSL has explicit ties to the OpenGL API - to the extent that much of the OpenGL 'state' (eg which light sources are bound, what material properties are currently set up) is presented as pre-defined global variables in GLSL. In Cg/HLSL, they must be passed explicitly from CPU to shader.

This document is about GLSL.

Shader variables

There are a confusing number of modifiers that can be applied to a 'global' variable declaration in GLSL:

  • uniform - A Uniform variable is passed from the application program in the CPU into either (or both) vertex and fragment shaders. The values of all uniforms must be set before drawing any triangles with that shader so their values stay the same throughout the drawing of a triangle mesh (they are 'uniform'). Most application programs will set some uniforms for the entire frame and others uniquely to one particular shader-pair.
  • varying - A Varying variable is produced inside the vertex shader and passed through to an identically named varying in the fragment shader. Because the vertex shader only sets the value of the varying variables at each vertex, the rasterizer interpolates those values (in a perspectively-correct manner) to generate per-fragment values to pass into the fragment shader. These variables 'vary' across each triangle.
  • attribute - An attribute is a part of the description of a vertex passed from the application program in the CPU to the vertex shader alone. Unlike a 'uniform', each attributes value is set for each vertex in turn allowing each vertex to have a different value.
  • const - Constants that are compiled into the shader (probably) and which never change.
  • Variables without modifiers are just ordinary globals that are private to each shader.

Shader variable types

The GPU hardware is inherently a four-way parallel processor. It can do four arithmetic instructions in parallel. Hence, it is very convenient to have specialised arrays of 2, 3 or 4 numbers that the hardware can crunch in parallel. So when declaring variables, one may use a simple 'float' - but also a 'vec2', 'vec3' or 'vec4' - being a 2, 3 or 4 element "array" of floats. You may also declare 'int' and 'ivec2', 'ivec3' or 'ivec4' or 'bool', 'bvec2', 'bvec3' or 'bvec4'.

This choice of names for the array types is horrible - most people much prefer the Cg/HLSL versions: 'float', 'float2', 'float3', 'float4', 'int', 'int2', 'int3', 'int4', 'bool', 'bool2', 'bool3', 'bool4'. Hence it is common to see header files containing:

   #define float2 vec2
   #define float3 vec3
   ...
   #define bool3  bvec3

You can also declare matrices ('mat2', 'mat3' or 'mat4' for 2x2, 3x3 or 4x4 floating point matrices) and also small arrays and structures - just like in C.

Another type of variable is called a 'sampler' - which is actually an integer that the C++ application sets equal to the 'slot' that a particular texture is bound to. But on the shader side of things, it's not accessible as a number - it has this special variable type - and it must always be 'uniform'.

  • sampler1D, sampler2D, sampler3D -- Textures of 1, 2 or 3 dimensions
  • sampler1Dshadow, sampler2Dshadow -- Depth-component texture
  • samplerCube -- Cube Maps

Arithmetic

All of the usual C arithmetic operators are there - but you can use them with vec2/3/4 types as well as with simple variables:

  vec4 x, y ;
  x = vec4 ( 1, 2, 3, 4 ) ;
  y = vec4 ( 5, 6, 7, 8 ) ;
  x += y ;

...leaves x set to ( 6, 8, 10, 12 ). Numbers are widened as needed:

  vec3 x ;
  x = vec3 ( 1, 2, 3, 4 ) + 5 ;

...leaves x set to ( 6, 7, 8, 9 ).

Builtin functions.

There are a lot of built-in functions in GLSL:

A Typical Vertex Shader

A typical vertex shader looks like this:

 varying vec3 normal, lightDir, halfVector ;
 void main()
 {
   gl_TexCoord[0] = gl_MultiTexCoord0 ;
   normal     = normalize ( gl_NormalMatrix * gl_Normal      ) ;
   lightDir   = normalize ( gl_LightSource[0].position.xyz   ) ;
   halfVector = normalize ( gl_LightSource[0].halfVector.xyz ) ;
   gl_Position = ftransform () ;
 }

The first line declares a number of 'varying' variables - these are variables that are passed from the vertex shader, through the rasterizer, to the fragment shader. The rasterizer takes the 'varying' values passed to it by the vertex shader and interpolates them across the triangle before passing them on to the fragment shader.

The function 'void main()' is called afresh for each vertex in the 3D object model. It first copies the inmcoming texture coordinate ("gl_MultiTexCoord0") into the outgoing texture coordinate ("gl_TexCoord[0]"), then it rotates the per-vertex normal data into the correct coordinate system and normalises all of the lighting vectors that come from the CPU. The results being passed into the 'varying' variables to be passes onto the interpolator. Finally we call 'ftransform' to perform the standard matrix transforms on the standard vertex position coordinate and places the result into the standard "gl_Position" variable - which is needed by the rasterizer.

A Typical Fragment Shader

A typical fragment shader looks like this:

 uniform sampler2D texture ;
 varying vec3 normal, lightDir, halfVector ;
 void main()
 {
   vec3  dl = gl_LightSource[0].diffuse .rgb * gl_FrontMaterial.diffuse.rgb ;
   vec3  al = gl_LightSource[0].ambient .rgb * gl_FrontMaterial.ambient.rgb +
                                               gl_FrontMaterial.emission.rgb ;
   vec3  sl = gl_LightSource[0].specular.rgb * gl_FrontMaterial.specular.rgb ;
   vec3  tx = texture2D ( texture, gl_TexCoord[0].st ).rgb ;
   float sh = gl_FrontMaterial.shininess ;
   vec3 n = normalize ( normal ) ;
   vec3 d = tx * ( dl * max ( dot ( n, lightDir                 ), 0.0 ) + al ) ;
   vec3 s = sl *  pow ( max ( dot ( n, normalize ( halfVector ) ), 0.0 ), sh ) ;
   gl_FragColor = vec4 ( min ( d + s, 1.0) , 1.0 ) ;
 }

The word 'uniform' in line one denotes a variable that's sent from the application software on the CPU. The variable is of type 'sampler2D' - which is GLSL-speak for a texture map.

Notice that the list of 'varying' parameters on the second line matches exactly the list in the Vertex Shader above...this is (of course) no accident. Note though that the numerical values output into these variables in the Vertex shader are set at the vertices of the triangle - the values are then interpolated between the vertices to get the value at each pixel which is sent to the fragment shader.

The 'void main()' function is executed for each pixel in turn.

In this shader, the values 'dl', 'al' and 'sl' are the lighting terms (dl=diffuse lighting, al=ambient+emission lighting and sl=specular lighting). They are calculated by multiplying the light source properties by the corresponding material property colour from the polygon.

'tx' is the texture color that is obtained from calling 'texture2D' with the uniform sampler2D parameter and the texture coordinate from the Vertex shader.

Next, we calculate the total diffuse lighting from 'dl', the light direction the normal direction and the 'al' term. Then the specular lighting term - from the dot product of the 'halfVector' and the normal raised to the power of the 'shininess'.

And finally - we add the diffuse and specular terms, clamped so they don't get bigger than 1.0 and with an 'alpha' component of 1. This is written into 'gl_FragColor' - which is what is ultimately written or blended into the frame buffer.

Personal tools