Table of Contents

Lab 9 - Writing a Shader

In-Class Excercises

Before You Start

Writing a Shader

  1. The simplest shader:
    Make sure you can run shadertest1.py and that you can see a filled green disk on the screen. The color green is being passed into the shader through the tint constant. Try different color values for this constant. Then open up shader1.sha and notice how the value is used in the vertex shader to assign a color to a vertex. Play with the color values inside the vertex shader and see how it affects the final render.
  2. Diffuse and Specular Lighting (per vertex):
    Open and run shadertest2.py and make sure that you can see a shaded sphere this time. Ambient, diffuse and specular colors are being passed to the shader as constants. You can play with these values. Also notice that the light node is passed to the shader as well. Open up shader2.sha and examine how these values are being used. The constant mpos_light is the model space position of the light that is being passed in. This position is used to calculate the Light Vector which is then used to find the angle between the surface and the light, which gives rise to a diffuse color attenuation value. The specular attenuation value is missing. Just like you needed the light position passed in to calculate the Light Vector, you'll need to pass in the camera position to calculate the View Vector, which will be needed to find the specular attenuation value. Once you've gotten the View Vector, you need to calculate the Light Reflection Vector with this formula: R=norm(2N(N.L)-L) and then you can calculate the specular attenuation value as the dot product of R and V raised to some power like 6 (pow(x,y) raises x to the power of y). Try to make this specular lighting work in your shader.
  3. Diffuse and Specular Lighting (per pixel):
    In your shadertest2.py, load shader3.sha instead of shader2.sha. This shader has moved the calculation of the diffuse color attenuation into the fragment shader by passing the Light Vector and the Normal Vector over there from the vertex shader. Finish adding the specular attenuation value to the light calculation in this shader using the fragment shader to calculate a per-pixel Light Reflection Vector. Once you get this working, notice the drastic difference in quality between the per-vertex specular lighting and the per-pixel specular lighting.
  4. Texture:
    Let's add a texture to your sphere. Add the following lines to your python code (after you load the sphere model):
    diffuse_map = loader.loadTexture("./Models/Textures/bricks.png")
    ts0 = TextureStage( 'level0' )
    sphere.setTexture(ts0, diffuse_map)

    You now have access to this texture inside your shader as the tex_0 color sampler. A texture is nothing but a lookup table for color values. Now multiply your out color (that already includes your light calculations) in the fragment shader with the color value found in tex_0 at the texture coordinates that are passed into the fragment shader in l_texcoord0. The lookup function is tex2D(texture, texturecoordinates). You should now see a nicely textured and shaded sphere.

  5. Normal Map:
    To add lighting detail with a normal map, a new coordinate space, called the tangent space, needs to be created inside the vertex shader. This space is local to each vertex and is defined by the axes formed by the vertex normal, binormal and tangent. Therefore the following inputs need to be added to the vertex shader:
    float3 vtx_binormal0   : BINORMAL,
    float3 vtx_tangent0    : TANGENT,

    Now a transformation matrix that is capable of mapping other vectors into this local tangent space can be created like this:

    float3x3 mat_tangent;
    mat_tangent[0] = vtx_tangent0;
    mat_tangent[1] = vtx_binormal0;
    mat_tangent[2] = vtx_normal;  

    Once this new transformation matrix has been created inside the vertex shader, both the View Vector and the Light Vector should be projected into tangent space through a simple multiplication with this matrix. Once these vectors arrive inside the fragment shader, they should already be in tangent space and the third important component in our lighting calculations, namely the normal, should now be looked up in the normal map accessed through tex_0_n (note however that the texture stores components in the 0-1 range but we want them in the -1-1 range, so you need to correct the value when you look it up like this tex2D(normalmap, texturecoordinate)*2-1). Since the normal that we're reading from the normal map is already in tangent space (that's how they're stored in normal maps - and that explains why we needed to transform the other vectors), we can now proceed with the lighting calculations, just like we did in model space. See if you can make this work.