The goal of this lab is to give you an example of how 3D programs using programmable GPUs and shaders are different from those using traditional fixed function hardware, and a brief introduction to the development process for shaders.
This lab will only run on machines with a reasonably modern programmable GPU graphics card/chip, such as an nVidia 6000 series or later. The machines in N224 have these, the undergrad labs do not.
Download the C version or Java version of the GPU shader sample programs and extract it.
Inside is the source code for two programs (Phong and Brick) and some utility code for handling commmon tasks when writing simple shaders (GPU) that the two programs share. These are written in C or Java. You will also see some files with extension .glsl, which are shaders written in GLSlang, the GL Shading Language.
Compile and run the Phong program. You should see a coarsely tessellated purple cylinder. Pressing the N key will switch the shape to a pyramid. These shapes have been deliberately selected to exaggerate OpenGL lighting problems. The cylinder doesn't have enough tessellation, so it looks flat sided with highlights along the edges of each face. The pyramid has a shiny material but again the highlights appear only along the edges.
Open the Phong source in your favourite editor. The important sections are initPhong, which loads two programmable shaders and creates a GPU program object; and display, which now enables or disables the GPU program.
The two shaders are loaded from source code on disk. Open them in your text editor.
These shaders implement the standard OpenGL lighting with one directional light source and no texture mapping. In this Gouraud shading, the lighting calculation is done per vertex and interpolated across each fragment (pixel). Run the program again and use the G key to switch between standard OpenGL and GPU shading. There should be no difference.
Using shaders to emulate what OpenGL already does is not very interesting, but it does demonstrate that you don't lose any capabilities or performance by using a GPU.
Shaders make it easy for real time 3D programs to separate the modelling of 3D geometry from the final shading. In std_frag.glsl, change the (only) line to
gl_FragColor = gl_Color * 2.0;
Skim through the code in GPU: do you need to recompile your C/Java code each time you change the shader?
Run the program again and see what happens.
Shaders can use any attributes of geometry in any way you want. In std_vert.glsl, remove the lighting calculation code (save a copy somewhere!) and use the surface normal for the vertex color instead:
gl_FrontColor = vec4(gl_Normal.x, gl_Normal.y, gl_Normal.z, 1.0);
Run the program and see what happens.
Optional - put back the original lighting calculation, but try using the normalized vertex coordinates as the base color instead of the material.
The lighting problems in this program occur because the calculation is being done per-vertex and interpolated. It is more accurate but more computationally expensive to perform the lighting calculation at every fragment.
Undo any changes you make to the std_ shaders.
Make copies of the two std_ shaders, this time named phong_vert.glsl and phong_frag.glsl. Change phong.c or Phong.java to load these new shaders instead.
There are two steps needed to perform Phong shading.
First, the vertex
shader will still transform the vertex coordinate and calculate the eye
space surface normal, but the rest of the calculation is delayed to the
fragment shader. Cut the vertex shader code from the line
// Direction to light source
to the line
gl_FrontColor = ambient + ...
and paste it into the fragment shader. Change the final color calculation
from gl_FrontColor, which
is a value generated by a vertex shader, to gl_FragColor.
Second, the fragment shader needs the surface normals calculated by the vertex shader. The standard way to do this is for the vertex shader to generate extra texture coordinates as parameters for the fragment shader. When OpenGL rasterizes the polygon into fragments, it will interpolate these parameters and pass them to the fragment shader.
In the vertex shader, assign the eye space surface normal to the first set of texture coords with
gl_TexCoord[0] = vec4(normal.x, normal.y, normal.z, 1.0);
In the fragment shader, assign these texture coords to the normal local variable with
normal = gl_TexCoord[0].xyz;Normalize it again.
(Error messages from the GLSlang compiler include line numbers and the general rule is, it works like C/C++. If you really get stuck, ask your tutor for help.)
The program should now run with optional Phong shading. When you switch to the GPU program, the cylinder should be smoothly shaded across the surface (although the edges remain faceted) and the faces of the pyramid should show a specular highlight.
The lab code includes a second program, brick.c or Brick.java, and corresponding vertex and fragment shaders. If you are using C, edit the Makefile to build the brick program instead of phong; if you are using Java, just compile and run Brick.java.
This program displays three different geometries (cube, torus, teapot) with a tiled texture. What is different is that it is a procedural texture map: the "bricks" are generated at runtime by the fragment shader instead of being stored as an image.
Open the brick vertex and fragment shaders in your text editor. All the real work is done in the fragment code. The bricks are generated using the original geometry coordinates to calculate how many bricks high and wide the surface is, then subdividing that space between bricks and mortar.
At the start of the fragment shader are two vec2 constants with the brick and mortar dimensions. Try changing these values up or down.
The fragment shader uses objVertex.xz to calculate the brick coords. Try changing it to use xy instead.
The fragment shader code isn't complete, because all the bricks are lined up in vertical columns instead of being offset. Change the code to add 0.5 to the horizontal coordinate for every other row. (One way to determine whether the row is odd or even is to use the mod(x, y) function with major.y)
Optional - the brick texture is 2D so is not properly applied to some sides of the 3D geometries. Extend the brick calculations into 3D.
Optional - the brick texture is not shaded, as can be seen by switching off the GPU program with the G key. Add Phong shading from the previous section to the brick vertex and fragment shaders: