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:
- 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; }
- 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 yourMaterials
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.
- 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!
- 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.
- 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! - 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!
- 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.