Shaders 102 – Code Integration

Overview

Today I am going to talk about how to integrate shaders into your code.  Starting with the layout of a shader program and how to integrate it in your code.

Basic Shader Code

Here is a vertex shader:

//vertex shader from chapter 3 of the Cg textbook

struct C3E2v_Output {
float4 position : POSITION;
float3 color : COLOR;
float2 texCoord : TEXCOORD0;
};

C3E2v_Output C3E2v_varying(
float2 position : POSITION,
float4 color : COLOR,
float2 texCoord : TEXCOORD0)
{
C3E2v_Output OUT;

OUT.position = float4(position,0,1);
OUT.color = color;
OUT.texCoord = texCoord;

return OUT;
}

Breakdown

The program first begins with an output structure as follows:

struct C3E2v_Output {
float4 position : POSITION;
float3 color : COLOR;
float2 texCoord : TEXCOORD0;
};

Since we know that this is the vertex shader, and it has to pass values to the rest of the graphics pipeline, this is structure is just some of the values that our shader will be using.  By defining an output structure we can manipulate the items inside it.  Basically this is like a variable declaration for a function where we would be outputting position, color and texture coordinates.

Last week we talked about how Cg has vectors and matrices integrated into their variable declaration.

  • float4 position : is essentially =[x,y,z,w] where w=1.  If this was written in C++ it would be float position[4]={x,y,z,w}
  • float3 color : is similar to the vector above, but this is used to represent our colour channels =[r,g,b]
  • float2 texCoord : is our U and V coordinates in our texture = [u,v]
While this program does not showcase the matrix declaration of Cg, here are some examples
  • float4x4 matrix1 : this is a four by four matrix with 16 elements
  • half3x2 matrix2 : this is a three by two matrix with 6 elements
  • fixed2x4 matrix3 : this is a two by four matrix with 8 elements
If you wanted to declare a matrix with some values, you would do it like this:
float2x3 = {1.0, 2.0,
            3.0, 4.0,
           5.0, 6.0}

 


Next we have our entry function:

C3E2v_Output C3E2v_varying(
float2 position : POSITION,
float4 color : COLOR,
float2 texCoord : TEXCOORD0)

This is what defines our fragment or vertex program.  This is similar to the main function in C/C++.  What this is telling us, is that our shader is taking in position, colour and texture coordinates.  We know that our structure above has the same parameters as our input, so we know that we are going to be manipulating these parameters and then outputting them.

Last we have our function body:

C3E2v_Output OUT;

OUT.position = float4(position,0,1);
OUT.color = color;
OUT.texCoord = texCoord;

return OUT;

First we start of by creating our structure object called OUT.  This code really doesn’t do much but set values in the structure equal to the inputs and output them to the next stage in the pipeline.  The interesting piece of code is the OUT.position = float4(position,0,1) part.  This takes the incoming position with only two incoming parameters (x,y) and converts it into a float4 by giving the last two variables a 0 and 1 value to get (x,y,0,1).

3D Graphics Application Integration

Creating Variables

So knowing how that code works is great, however implementing Cg code in your C++ code is where I normally spend most of my time working with shaders.  The actual shader code is fairly easy to work with, but integrating it in your Graphics Application is where the real pain is.  The Cg book doesn’t really cover this explicitly, however it does have examples in the API in your /Program Files/NVIDIA Corporation/Cg/examples/ folder.

To start there are a number of things you have to declare, I normally do these as global variables:

static CGcontext myCgContext;
static CGprofile myCgVertexProfile,
                 myCgFragmentProfile;
static CGprogram myCgVertexProgram,
                 myCgFragmentProgram;
static CGparameter myCgVertexParam_constantColor;

static const char *myProgramName = "Varying Parameter",
*myVertexProgramFileName = "C3E2v_varying.cg",
*myVertexProgramName = "C3E2v_varying",
*myFragmentProgramFileName = "C2E2f_passthru.cg",
*myFragmentProgramName = "C2E2f_passthru";

Obviously the names of these do not have to be the same as what I have written, but the idea is to teach you what each one is.

The first part of creating the CGcontext is the part I know the least about.  I believe it is the part where you initialise the shader program.  So just be sure to ALWAYS do this.

The next part is creating your vertex and fragment profile.  This is another thing to always do.

The next two parts are where you are given a lot of freedom.  The CGparameter will vary from program to program.  These are essentially parameters that you take in your graphics application and send to your shader.  constantColor is just a variable that we can send to our shader to replace the colour of every pixel or vertex.  Later on I will post on how we can send in parameters like diffuse light color, light position, attenuation parameters and much more.

The last part is the program names.  This is where you define your main function for each shader and the name of their file name.  Common names for each are fragment_passthru or vertex_passthru.

Initialise Shaders

The next step is where you physically create the shader program.  Somewhere in your glut loop you should create a initCg() void function where you place all your initialisations.  The book places everything in main, which I find to be stupid, so don’t do that.  It creates a lot of hard to read clutter.

myCgContext = cgCreateContext();
checkForCgError("creating context");
cgGLSetDebugMode(CG_FALSE);
cgSetParameterSettingMode(myCgContext, CG_DEFERRED_PARAMETER_SETTING);

myCgVertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX);
cgGLSetOptimalOptions(myCgVertexProfile);
checkForCgError("selecting vertex profile");

myCgVertexProgram =
cgCreateProgramFromFile(
myCgContext,              // Cg runtime context
CG_SOURCE,                // Program in human-readable form
myVertexProgramFileName,  // Name of file containing program
myCgVertexProfile,        // Profile: OpenGL ARB vertex program
myVertexProgramName,      // Entry function name
NULL);                    // No extra compiler options;
checkForCgError("creating vertex program from file");
cgGLLoadProgram(myCgVertexProgram);
checkForCgError("loading verex program");

This code is fairly simple.  The beginning part creates the CgContext.  Then we create our vertex profile.  Lastly we tell the program where our Cg file is and the name of our entry function.  You would do something similar for the fragment shader.

The checkForCgError help your debug.  If anything goes wrong in the shader at that point, your cgError function will output an error code.

The other thing you can place in your initCg function is a GET_PARAM statement where you can pass variables to your shader program.

#define GET_PARAM(name) \
myCgVertexParam_##name = \
cgGetNamedParameter(myCgVertexProgram, #name); \
checkForCgError("could not get " #name " parameter");

GET_PARAM(modelViewProj);
GET_PARAM(globalAmbient);
GET_PARAM(eyePosition);

#define GET_PARAM2(varname, cgname) \
myCgVertexParam_##varname = \
cgGetNamedParameter(myCgVertexProgram, cgname); \
checkForCgError("could not get " cgname " parameter");

GET_PARAM2(material_Ke, "material.Ke");
GET_PARAM2(material_Ka, "material.Ka");
GET_PARAM2(material_Kd, "material.Kd");
GET_PARAM2(material_Ks, "material.Ks");
GET_PARAM2(material_shininess, "material.shininess");

The is an example of sending parameters directly to your entry function and to other structures you can make.  The first batch of code sends the modelViewProj matrix, GlobalAmbient colour and the eyePosition of the camera.  These would be items you list in your entry function input parameters.

The other batch of code is an example of you sending parameters to a structure called Material.  Material has emmissive, ambient, diffuse and specular lighting along with a shininess parameter.  This is helpful for creating virtual objects that reflect light differently so things can look like rubber or plastic.

 Enabling and Disabling

The last and simplest thing to do is to enable and disable your shaders during your OpenGL draw loop.

When you are drawing your objects, you need to bind your fragment and vertex shaders by:

//Enable Shaders
cgGLBindProgram(myCgVertexProgram);
checkForCgError("binding vertex program");
cgGLEnableProfile(myCgVertexProfile);
checkForCgError("enabling vertex profile");

cgGLBindProgram(myCgFragmentProgram);
checkForCgError("binding fragment program");
cgGLEnableProfile(myCgFragmentProfile);
checkForCgError("enabling fragment profile");

Once that is done you would go on with the rest of your draw calls and at the end of your draw loop you would disable them by:

// Disable Shaders
cgGLDisableProfile(myCgVertexProfile);
checkForCgError("disabling vertex profile");

cgGLDisableProfile(myCgFragmentProfile);
checkForCgError("disabling fragment profile");

Summary

That is pretty much a very basic description of how shaders are integrated into your C++ graphics application.

Thank you for reading,
– Moose

Shaders 101 – Intro

Hello!

Overview

I have read up to chapter 5 in the CG textbook (almost halfway done) and I thought it would be good to do a general summary of what I have learned so far.  Granted I might be misinformed or have fragmented knowledge about some aspects, but I hope I will be corrected in the comments.

What are Shaders and How Do They Work?

First of we are talking about the programming language Cg created by NVIDIA.  The point of shaders and the Cg language is to help you communicate via code with your graphics card to control the shape, appearance and motion of objects drawn.  Essentially it allows you to control the graphics pipeline, like a boss.  Cg programs control how vertices and fragments (potential pixels) are processed.  This means that our Cg programs are executed inside of our graphics cards.

Shaders are a powerful rendering tool for developers because they allows us to utilise our graphics cards.  Since our CPU’s are more suited toward general purpose operating system and application needs, it is better to use the GPU that is tailor built for graphics rendering.  GPU’s are built to effectively process and rasterize millions of vertices and billions of fragments per second.  The great thing about CG is that it gives you the advantages of a high level language (readability and ease of use) while giving you the performance of a low level assembly code.  Cg does not provide pointers and memory allocation tools.  However it supports vectors and matrices and many other math operations that make graphics calculations easier.

Cg is not meant to be used as a full fledged programming language.  We still need to build our 3D applications in C++ (or any language) then use our shader language (CG, HLSL, GLSL, RenderMan etc.) to optimise our graphics using the GPU.

The Graphics Pipeline

Graphics Pipeline: From the CG Textbook

In order to understand how shaders work, we have to have a general understanding on how the graphics pipeline (stages operating in parallel) work.  First your 3D application sends several geometric primitives (polygons, lines and points) to your GPU.  Each vertex has a position in 3D space along with its colour, texture coordinate and a normal vector.

Vertex Transformation

This is the first processing stage of the pipeline.  First several mathematical operations are performed on each vertex.  These operations can be:

  • Transformations (vertex space to screen space) for the rasterizer
  • Generating texture coordinates for texturing and lighting to determine its colour

Primitive Assembly and Rasterization

Once the vertices are processed, they are sent to this stage of the pipeline.  First the primitive assembly step assembles each vertex into geometric primitives.  This will result in a sequence of triangles, lines or points.

Geometric Primitives

After assembling the primitives will need to be clipped.  Since we are limited to a screen we cannot view the entire screen.  So according to our view frustum we clip and discard polygons (culling).  Once our screen is clipped our next step is to rasterize.  This is the process of determining what pixels are covered by a geometric primitive.

Rasterisation

The last important item in this process is for the user to understand the difference between pixels and fragments.  A pixel represents a location on the frame buffer that has colour, depth and other information.  A fragment is the data needed to generate and update those pixels.

Fragment Texturing and Coluring

Now that we have all our fragments the next set of operations determine its final colour.  This stage performs texturing and other math operations that influence the final colour of each fragment.

Raster Operations

This is the last stage of the graphics pipeline.  It is also one of the more complex stages.  Once the completed fragments come out of the previous stage the graphics API perform several operations on the incoming data.  Some of them are:

  • Pixel ownership test
  • Scissor test
  • Alpha test
  • Stencil test
  • Depth test
  • Blending
  • Dithering
  • Logic operations
This is pretty much the above process in a nutshell.
Pipeline In a Nutshell

Programmable Graphics Pipeline

So what was the point of talking about the pipeline?  Now that we know how the fixed pipeline works and how normal graphics API’s send information to be processed we can see where are shaders are executed and what they do.

Programmable Graphics Pipeline

The two important parts of this diagram are the programmable vertex processor that runs our Cg vertex programs and the programmable fragment processor that runs our Cg fragment programs.  The biggest difference between each one is the fact the the fragment processor allows for texturing.

Summary

Now that we know how the graphics pipeline works we can create programs to manipulate the pipeline however we want.  Next week we shall take a look at how to make programs for Cg and how they work in your 3D application.

Thank you for reading,
-Moose