GLSL shaders

From Wikiid
Revision as of 18:43, 23 February 2008 by SteveBaker (Talk | contribs) (New page: This guide covers the very basics of writing a GLSL shader that can do fairly standard OpenGL lighting: == What can a shader do? == Shaders generally go around in pairs - one shader (the ...)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

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

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.

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.