Table of Contents

LAB6: Programmable Shaders

This lab is based on a variety of sources, including the Ogre Shaders Wiki.

Discussion

Discussion thread for this lab is here: Lab 6 Discussion Thread

Goal

The goal of this lab is to understand how you can upload and execute simple shader programs on the GPU. Those programs will be associated with Ogre materials that you can assign to objects in a scene.

Preparation

From previous labs you should already have a Models folder inside the folder where you keep your application executables. To continue to organize your resources properly, you should now create another folder to hold your custom materials. Call this folder Materials and put it next to your models folder. In order for your Ogre applications to find the contents of these two folders make sure the following lines are in your resources_d.cfg:

FileSystem=Models
FileSystem=Materials

From now on we assume that all your custom materials and shader programs are stored in the Materials folder.

Lab Project

Follow these steps to complete the lab project:

  1. Create a New Project Create a new empty project called “Lab6” in the same way you have created new projects for other lab projects. Create the Lab6Main.cpp file that contains a minimal Ogre application that displays at least one ground plane and an Ogre model. You can use your own application, or you can use the following code:
    #include "OGRE/Ogre.h";
     
    class MyApplication {
    private:
    	Ogre::SceneManager*     _sceneManager;
    	Ogre::Root*	        _root;
    	Ogre::Entity*		_ogre;
    	Ogre::Entity*		_ground;
    	Ogre::Camera*		_camera;
     
    public:
     
    	MyApplication() {
    		_sceneManager = NULL;
    		_root = NULL;
    		_ogre = NULL;
    		_ground = NULL;
    	}
     
    	~MyApplication() {
    		delete _root;
    	}
     
    	void loadResources() {
    		Ogre::ConfigFile cf;
    		cf.load("resources_d.cfg");
    		Ogre::ConfigFile::SectionIterator sectionIter = cf.getSectionIterator();
    		Ogre::String sectionName, typeName, dataName;
    		while(sectionIter.hasMoreElements()) {
    			sectionName=sectionIter.peekNextKey();
    			Ogre::ConfigFile::SettingsMultiMap *settings =sectionIter.getNext();
    			Ogre::ConfigFile::SettingsMultiMap::iterator i;
    			for(i=settings->begin(); i!=settings->end(); ++i) {
    				typeName=i->first;
    				dataName=i->second;
    				Ogre::ResourceGroupManager::getSingleton().addResourceLocation(dataName, typeName, sectionName);	
    			}
    		}
    		Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
    	}
     
    	void createScene() {
    		_ogre =_sceneManager->createEntity("Sinbad.mesh");
    		_sceneManager->getRootSceneNode()->createChildSceneNode()->attachObject(_ogre);
     
    		Ogre::Plane plane(Ogre::Vector3::UNIT_Y,-5);
    		Ogre::MeshManager::getSingleton().createPlane("plane",Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,plane,1500,1500,200,200,true,1,5,5,Ogre::Vector3::UNIT_Z);
    		Ogre::Entity* _ground=_sceneManager->createEntity("LightPlaneEntity","plane");
    		_sceneManager->getRootSceneNode()->createChildSceneNode()->attachObject(_ground);
     
                    // HERE YOU SET THE MATERIALS FOR EACH OBJECT
                    // e.g. _ogre->setMaterialName(<put name here>);
                    // e.g. _ground->setMaterialName(<put name here>);
     
    		Ogre::Light* light = _sceneManager->createLight("Light1");
    		light->setType(Ogre::Light::LT_POINT);
    		light->setPosition(Ogre::Vector3(-10.0, 10.0, 5.0));
    	}
     
    	int startup() {
    		_root=new Ogre::Root("Plugins_d.cfg");
    		if(!_root->showConfigDialog()) {
    			return -1;
    		}	
     
    		Ogre::RenderWindow* window=_root->initialise(true,"Ogre3D Lab6");
    		_sceneManager=_root->createSceneManager(Ogre::ST_GENERIC);
     
    		_camera=_sceneManager->createCamera("Camera");
    		_camera->setPosition(Ogre::Vector3(0,0,50));
    		_camera->lookAt(Ogre::Vector3(0,0,0));
    		_camera->setNearClipDistance(5);
     
    		Ogre::Viewport* viewport=window->addViewport(_camera);
    		viewport->setBackgroundColour(Ogre::ColourValue(0.0,0.0,0.5));
    		_camera->setAspectRatio(Ogre::Real(viewport->getActualWidth())/Ogre::Real(viewport->getActualHeight()));
     
    		loadResources();
    		createScene();
     
    		_root->startRendering();
    		return 0;
     
    	}
    };
     
     
    int main(void) {
    	MyApplication app;
    	app.startup();
    	return 0;
    }
  2. Fixed Diffuse Color Fragment Shader The first shader program we create is a fragment shader that simply returns a fixed color for each fragment that gets processed. In Ogre you can write shader programs in any of the major high-level shading languages, but we will be using Cg. Create a new shader program file called diffuseshader.cg in your Materials folder and place the following code inside it:
    float4 main_orange_fp(in float3 TexelPos : TEXCOORD0) : COLOR {
        float4 oColor;
     
        oColor.r = 1.0;
        oColor.g = 0.8;
        oColor.b = 0.0;
        oColor.a = 0.0;
     
        return oColor;
    }

    Now that the shader program is ready, you have to instantiate it inside a material before you can apply it to objects in Ogre. Create a new material file called myshaders.material in the same folder and place the following material script inside it:

    fragment_program shader/orangeFP cg {
        source diffuseshader.cg
        entry_point main_orange_fp
        profiles ps_1_1 arbfp1 
        
    }
    
    material shader/orange {
        technique {
            pass {           
                fragment_program_ref shader/orangeFP {
                }       
                texture_unit {       
                }             
            }
        }
    }

    Essentially the new material shader/orange calls the shader/orangeFP program definition, which in turn calls the main_orange_fp method inside diffuseshader.cg. Since this is a fragment program, it gets called for every fragment processed with this material. Finally, you simply assign this material to the entities in your Ogre application:

    _ogre->setMaterialName("shader/orange");
    _ground->setMaterialName("shader/orange"); 

    Verify that both your ground and your ogre model show up orange on the screen.

  3. Parametric Diffuse Color Fragment ShaderYou can use the same shader program in many materials and simply let each material pass a parameter into the program to tell it how to paint a given surface. This makes more sense than writing a new shader program every time you want to paint an object in a different color for example. You can pass parameters into Cg shader programs through the so called uniform parameter. In the diffuseshader.cg file, add the following new shader program:
    float4 main_color_fp(in float3 TexelPos : TEXCOORD0, uniform float4 color) : COLOR {
        float4 oColor = color;
        return oColor;
    }

    This fragment shader expects a new parameter called color, and assigns that value to the color returned for this fragment. In the same material file as before, you now add the following shader program definition and material script:

    fragment_program shader/diffuseFP cg {
        source diffuseshader.cg
        entry_point main_color_fp
        profiles ps_1_1 arbfp1 
     
        default_params {
            param_named color float4 0.7 0.2 0.2 1.0
        }
    } 
     
    material shader/white {
        technique {
            pass {             
                fragment_program_ref shader/diffuseFP {
                    param_named color float4 0.8 0.8 0.8 1.0
                }    
                texture_unit {       
                }                      
            }
        }
    }

    Here you have created a new material shader/white that passes the color value <0.8,0.8,0.8,1.0> into the shader program called by the shader/diffuseFP program definition. Notice that if you skip specifying the color value in the material script, the program definition will pass its own default value into the shader program (in this case <0.7,0.2,0.2,1.0>). Now use this material for your ogre model:

    _ogre->setMaterialName("shader/white");

    You can of course create several new materials now, each with a different color!

  4. Custom Diffuse Color Fragment Shader You may also want to control some parameter in a shader program from within the application code. To do this, you indicate that a shader program parameter should be a custom parameter. You do not have to change your diffuse fragment shader program to do this (it is already expecting an external color parameter), but you need to create a new shader program definition and a new material in the materials file to do this:
    fragment_program shader/customFP cg {
        source diffuseshader.cg
        entry_point main_color_fp
        profiles ps_1_1 arbfp1 
        
        default_params {
            param_named_auto color custom 1
        }
    }
    
    material shader/custom {
        technique {
            pass {             
                fragment_program_ref shader/diffuseFP {
                    param_named_auto color custom 1
                }          
                texture_unit {       
                }                     
            }
        }
    }

    You are with this essentially telling Ogre that if this material is associated with an object, it should look for a custom parameter in an object's custom parameter slot number 1 and pass that on to the fragment program. So, when you assign this material to entities in an Ogre application, you have to remember to add this custom parameter as well. This is how you do that (setCustomParameter takes the parameter slot number as the first argument):

    _ogre->getSubEntity(0)->setCustomParameter(1, Ogre::Vector4(0.0, 0.0, 1.0, 1.0));
    _ogre->setMaterialName("shader/custom");

    You should now have a blue ogre model.

  5. Texture Vertex and Fragment ShadersPainting an object in a single color is not particularly interesting. More commonly we read diffuse color information from a texture as we process each fragment. We can do this if we supply each fragment with texture coordinates that are interpolated from texture coordinates stored at the nearest vertices. To do texturing, we should create two shader programs: (1) We should make sure that a vertex program provides texture coordinates and (2) we should use the interpolated texture coordinates in a fragment program that returns the right color value from a texture. You should now create a new shader program file called textureshader.cg and place the following code inside:
    void main_vp(
            // Per-vertex information
            float4 vtx_position         : POSITION,     // Vertex position in model space
            float2 vtx_texcoord0        : TEXCOORD0,    // Texture UV set 0
            // Provided parameters
            uniform float4x4  mat_modelproj,
            // Shader outputs
            out float4 l_position       : POSITION,     // Transformed vertex position
            out float2 l_texcoord0      : TEXCOORD0)    // UV0
     
    {
        // Calculate output position (a vertex shader is expected to at least do this!)
        l_position = mul(mat_modelproj, vtx_position);
        // Simply copy the input vertex UV to the output
        l_texcoord0 = vtx_texcoord0;
    }
     
    void main_fp(
            // Interpolated fragment values
            float2 l_texcoord0        : TEXCOORD0,    // UV interpolated for current pixel
            // Provided parameters and data
            uniform sampler2D texture,        // Texture we're going to use
            // Shader output
            out float4 o_color    : COLOR)    // Output color we want to write
    {
        // Just sample texture using supplied UV
        o_color = tex2D(texture, l_texcoord0);
    }

    Now that the texture shader programs are ready, we need to instantiate them in an ogre material. First we provide the shader program definitions in our materials file:

    vertex_program shader/textureVP cg {
        source textureshader.cg         
        entry_point main_vp    
        profiles vs_1_1 arbvp1    
    
        default_params {
            param_named_auto mat_modelproj worldviewproj_matrix   
        }
    }
    
    fragment_program shader/textureFP cg {
        source textureshader.cg
        entry_point main_fp
        profiles ps_1_1 arbfp1  
    }

    Notice that we are passing one parameter, mat_modelproj, into the vertex shader. But instead of specifying a value, we simply indicate the name worldviewproj_matrix. This name refers to a list of values that an ogre application can supply automatically to a shader program (that's why we use param_named_auto). We obviously won't know the model-to-view projection matrix when we create the material, so we want it supplied at run-time instead. Finally, create the material that uses these two new shader program definitions and additionally loads a texture into the available texture unit (copy the actual texture from \OgreSDK_vc10_v1-8-1\media\materials\textures\Water02.jpg into your Materials folder so that your material can find it for sure):

    material shader/texture {
        technique {
            pass {      
                vertex_program_ref shader/textureVP {
                }
                fragment_program_ref shader/textureFP {
                }
                texture_unit {
                    texture Water02.jpg 2d            
                }                      
            }
        }
    }

    Apply this material to your _ground entity and make sure you see the texture on the ground!

  6. Animated Vertex ShaderTo try to have a vertex shader to something a little more interesting, how about actually moving each vertex a little bit based on the time that passes? That way you can very cheaply animate vertices according to any formula you like! To do this, you need to pass a time parameter into the vertex shader. Luckily, time is one of the parameters that ogre applications can provide automatically to a shader program. Add the following alternate texture vertex shader to textureshader.cg:
    void main_time_vp(
            // Per-vertex information
            float4 vtx_position         : POSITION,     // Vertex position in model space
            float2 vtx_texcoord0        : TEXCOORD0,    // Texture UV set 0
            // Provided parameters
            uniform float4x4  mat_modelproj,
            uniform float t,                            // Expecting time here
            // Shader outputs
            out float4 l_position       : POSITION,     // Transformed vertex position
            out float2 l_texcoord0      : TEXCOORD0)    // UV0
     
    {
        // Displace the vertical coordinate based on x-location and time
        float4 temp = vtx_position;
        temp.y = temp.y+cos(temp.x+t);
     
        // Calculate output position
        l_position = mul(mat_modelproj, temp);
        // Simply copy the input vertex UV to the output
        l_texcoord0 = vtx_texcoord0;
    }

    Now all you have to do is to supply an automatic time value in the shader program definition in the materials file:

    vertex_program shader/timetextureVP cg {
        source textureshader.cg         
        entry_point main_time_vp    
        profiles vs_1_1 arbvp1    
    
        default_params {
            param_named_auto mat_modelproj worldviewproj_matrix   
            param_named_auto t time 
        }
    }

    Now create a new material that uses this vertex program definition instead of the regular texture vertex program definition and apply that material to the ground object in your application. You should see your ground move!

  7. Per Pixel Phong ShaderFinally, let's try calculating the color value of a fragment based on an actual lighting model such as the Phong lighting model. Since we will be calculating the lighting value inside each fragment, we call this per-pixel lighting. This basically means that instead of using interpolated color values from the nearby vertices, we use interpolated vector values (model space vertex position, normal, view direction and light direction) to calculate the color value inside the fragment program. Create a new shader program file called lightingshader.cg and place the following code inside:
    // Cg
    void main_vp(
      float4 vtx_position       : POSITION,
      float3 vtx_normal         : NORMAL,
      float2 vtx_texcoord0      : TEXCOORD0,
     
      uniform float4x4 mat_modelproj,
      uniform float4   mspos_light,
      uniform float4   mspos_camera,
     
      out float4 l_position  : POSITION,
      out float2 l_texcoord0 : TEXCOORD0,
      out float3 l_N    	 : TEXCOORD1,
      out float3 l_L     	 : TEXCOORD2,
      out float3 l_V    	 : TEXCOORD3,
      out float3 l_P         : TEXCOORD4
    )
    {
      l_position = mul(mat_modelproj, vtx_position);
      l_texcoord0 = vtx_texcoord0;
     
      // The principal vectors for our Phong lighting model calculation:
      // L = Light Vector, N = Vertex Normal, V = View Vector R = Light Reflection Vector 
      l_N = vtx_normal;  // The Normal of the vertex itself was passed in automatically
      // We passed in the light and camera NodePaths and get their model space coordinates
      // here through the "mspos_<name>" variable. Everything here should be done in model space.
      l_L = normalize(mspos_light.xyz - vtx_position.xyz);
      l_V = normalize(mspos_camera.xyz - vtx_position.xyz);
      l_P = vtx_position.xyz;
      // We can't calculate the R vector here because it won't interpolate correctly for each fragment
      // (it relies on a dot product which complicates things for it), so we'll calculate it inside the 
      // fragment shader. The other vectors will all get interpolated and passed to the fragments.
     
    }
     
    void main_fp(
      float2 l_texcoord0 	: TEXCOORD0, 
      float3 l_N		: TEXCOORD1, 
      float3 l_L    	: TEXCOORD2, 
      float3 l_V    	: TEXCOORD3,
      float3 l_P            : TEXCOORD4,
     
      uniform float4 k_ambientc,
      uniform float4 k_diffusec,
      uniform float4 k_specularc,
     
      out float4 o_color : COLOR)
    {
      // Inside the fragment shader, we get all the interpolated vectors
      // The Diffuse Attenuation follows under what angle the light shines on the fragment
      float diffuse_attn = saturate(dot(l_L,l_N));
     
      // The Specular Attenuation follows how close to the line of light reflection you are looking
      float3 R = normalize(2*l_N*dot(l_N,l_L)-l_L);
      float specular_attn = pow(saturate(dot(R,l_V)),6.0);
     
      // Here we return the color based on the full phong light model
      o_color = 0.2*k_ambientc + diffuse_attn*k_diffusec+specular_attn*k_specularc;
     
    }

    As you can see, we are expecting the application to pass in the location of both the camera and the light into the vertex shader (in model space coordinates!). Luckily, these are available in the list of automated values provided by Ogre. The shader program definition for the vertex shader is then:

    vertex_program shader/lightingVP cg {
        source lightingshader.cg
        entry_point main_vp
        profiles vs_1_1 arbvp1
        
        default_params {
            param_named_auto mat_modelproj worldviewproj_matrix
            param_named_auto mspos_light light_position_object_space 0
            param_named_auto mspos_camera camera_position_object_space
        }
    }

    Notice that the light position that is provided is indexed as light number 0. This refers to the closest light source to the object (which in this case is the point light if you used the application code provided in the first step of this lab). We are also expecting the colors in our lighting model to be passed into the fragment shader, which could be provided by each material using this shader. This is the fragment shader program definition:

    fragment_program shader/lightingFP cg {
        source lightingshader.cg
        entry_point main_fp
        profiles ps_2_0 arbfp1
      
        default_params {
            param_named k_ambientc float4 0.5 0.5 0.5 1.0
            param_named k_diffusec float4 0.8 0.1 0.1 1.0
            param_named k_specularc float4 0.6 0.6 0.6 1.0
        }
    }

    Now create a new material that uses these two shader program definitions and assign the material to the ogre model. You should see a properly shaded (albeit single colored) model!

When You Are Finished

Upload your commented source files into Lab6 in MySchool (zip them up if more than one). The lab projects will not be graded, but their completion counts towards your participation grade.