OpenGL and CG Shaders – introduction
I wanted to write a little introduction on how to setup and use the nVidia CG Toolkit and start creating some shaders for first and most OpenGL, who cares about D3D anyway?!?
Getting the toolkit
Head over to nVidias CG site and fetch the toolkit from there. Remember to get latest version!
What else do you need? You need, or preferrable want to update your graphics drivers to latest version in case you’re running on some old shit that doesn’t support different OpenGL extensions.
Getting started
Well, first I’m gonna give you an address to a beginners tutorial to CG, just so you have something to look up in, in case you were to, not that I’m saying you ever are, get stuck somewhere.
It’s a good place to start, maybe not go through all the chapters straight away, but have a look at the first chapters and you get a general idea. That is pretty much what I’m gonna go through here too.
I already asume you are familiar with how to add library to your Visual Studio, cause we do need some of those cg librarys loaded. I will also use function oriented programming to demonstrate how this works, but theres no reason why you cant access it FROM objects so don’t be scred if you’re a OOP guru.
First, lets have a look at the CG shaders we’re gonna use. This is the simplest of the simplest ones, just to get you started. Lets have a look at the vertex shader:
-
void main( in float4 position : POSITION,
-
in float4 color : COLOR,
-
-
out float4 colorOUT : COLOR,
-
out float4 positionOUT : POSITION,
-
-
const uniform float4×4 ModelViewProj // The concatenated modelview and projection matrix
-
)
-
{
-
// Transform the current vertex from object space to clip space, since OpenGL isn't doing it for us
-
// as long we're using a vertex shader
-
positionOUT = mul(ModelViewProj, position);
-
-
colorOUT = color;
-
}
This is fairly simple and straight forward. What we do is pretty much just passing the color of the vertex on to the fragment shader(pixel shader). As you can see, we also need to transform the vertex position from object space to clip space, since we’re using a custom shader, OpenGL doesnt do this for us.
Now on to the fragment shader(pixel shader):
-
void main( in float4 colorIN : COLOR,
-
out float4 colorOUT : COLOR
-
)
-
{
-
colorOUT = float4(1.0f, .0f, .0f, 1.f);
-
}
Now, what happens here? The only thing we do is set the current pixel to the color red(RGBA). If we wanted to make a shader that just passed the original color on, we would just go colorOUT = colorIN;. But then you wouldnt see the result of the shader.
Init
Header definitions of functions we need, and global vars used:
-
CGprofile cgVertexShaderProfile; // Vertex Shader Profile
-
CGprofile cgPixelShaderProfile; // Pixel Shader Profile
-
CGcontext cgContext = NULL; // A Container for cG Programs
-
CGparameter modelViewProjMatrix; // Global for Resize
-
CGprogram cgCurVertex, cgCurPixel, cgVertexShader,cgPixelShader;
-
-
CGprogram &CGgetVertexShader();
-
CGprogram &CGgetFragmentShader();
-
CGprofile &CGgetVertexProfile();
-
CGprofile &CGgetFragmentProfile();
-
void CGsetCurrentVertexShader(CGprogram vShader);
-
void CGsetCurrentFragmentShader(CGprogram fShader);
-
void CGdisableProfile(CGprofile &profile);
-
int CGdestroy();
-
CGprogram CGcompileVertexShader(char* fileName = NULL);
-
CGprogram CGcompilePixelShader(char* fileName = NULL);
This is all in the source code provided, and some are explained later on, but most of them are just helper-functions to set and get current shaders. This was used in an OOP environment earlier, and its not really useful when used in such a small application like this, since the global vars are easy accessible.
First we need to initialize the cg stuff. Put this code after the opengl init block you’ve got:
-
cgContext = cgCreateContext();
-
if (cgContext == NULL)
-
{
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error init", MB_OK);
-
return -1;
-
}
-
-
cgVertexShaderProfile = cgGLGetLatestProfile(CG_GL_VERTEX);
-
if (cgVertexShaderProfile == CG_PROFILE_UNKNOWN)
-
{
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error loading profiles", MB_OK);
-
return -2;
-
}
-
-
cgPixelShaderProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
-
if (cgPixelShaderProfile == CG_PROFILE_UNKNOWN)
-
{
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error loading profiles", MB_OK);
-
return -2;
-
}
-
-
cgGLSetOptimalOptions(cgVertexShaderProfile);
-
if (cgGetError())
-
{
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error loading optimisations", MB_OK);
-
return -3;
-
}
-
-
cgGLSetOptimalOptions(cgPixelShaderProfile);
-
if (cgGetError())
-
{
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error loading optimisations", MB_OK);
-
return -3;
-
}
-
-
cgVertexShader = CGcompileVertexShader("vShader.cg");
-
cgPixelShader = CGcompilePixelShader("pShader.cg");
-
-
cgGLLoadProgram(cgVertexShader);
-
if(cgGetError()){
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error Loading", MB_OK);
-
return false;
-
}
-
-
cgGLLoadProgram(cgPixelShader);
-
if(cgGetError()){
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error Loading", MB_OK);
-
return false;
-
}
-
-
CGsetCurrentVertexShader(cgVertexShader);
-
CGsetCurrentFragmentShader(cgPixelShader);
First we create a context, like we usually do with OpenGL as well, just in different sense here. The next part is more interesting. This is where we set the profile used for the shaders. Why is this important? I’ll tell you, the profile you chose determine what features are available to you. It’s also important to remember that this depends on what graphics card you’ve got. You cant chose an nVidia profile for a Intel-based graphics card. And some profiles are used for DirectX and some for OpenGL. You can read more about profiles in the CG Turoial chapter 10. The basic you need to know is this:
arbvp1/arbfp1 – Basic multivendor vertex programmability (corresponding to ARB_vertex_program functionality)
vp20 – Basic NVIDIA vertex programmability (corresponding to NV_vertex_program functionality)
vp30 – Advanced NVIDIA vertex programmability (corresponding to NV_vertex_program2 functionality)
What the cgVertexShaderProfile = cgGLGetLatestProfile(CG_GL_VERTEX);/cgPixelShaderProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT); methods do for you is chose the appropriate profile for you, so you don’t have to think about finding it out yourself, and makes it easier to use the application on different hardware.
Next we have the cgGLSetOptimalOptions which sets the best options available by card, driver and selected profile.
CGcompileVertexShader/CGcompilePixelShader creates a cg program from file and are defined here:
-
CGprogram CGcompileVertexShader(char* fileName)
-
{
-
return cgCreateProgramFromFile(cgContext, CG_SOURCE, fileName, cgVertexShaderProfile, "main", 0);
-
}
-
CGprogram CGcompilePixelShader(char* fileName)
-
{
-
return cgCreateProgramFromFile(cgContext, CG_SOURCE, fileName, cgPixelShaderProfile, "main", 0);
-
}
The final CG method you need to run is the cgGLLoadProgram, which loads the program into the OpenGL pipeline.
Draw
Then we need to implement the actual draw methods:
-
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-
glLoadIdentity();
-
-
gluLookAt(-5.0, 1.0, 4.0, 0.0, 0.0, -7.0, 0.0, 1.0, 0.0);
-
-
cgGLEnableProfile(CGgetVertexProfile());
-
if(cgGetError()){
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error", MB_OK);
-
return false;
-
}
-
-
cgGLBindProgram(cgVertexShader);
-
if(cgGetError()){
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error", MB_OK);
-
return false;
-
}
-
-
cgGLEnableProfile(CGgetFragmentProfile());
-
if(cgGetError()){
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error", MB_OK);
-
return false;
-
}
-
-
cgGLBindProgram(cgPixelShader);
-
if(cgGetError()){
-
MessageBox(NULL, (LPCWSTR)cgGetErrorString(cgGetError()), L"Error", MB_OK);
-
return false;
-
}
-
-
modelViewProjMatrix = cgGetNamedParameter(CGgetVertexShader(), "ModelViewProj");
-
cgGLSetStateMatrixParameter(modelViewProjMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);
-
-
glPushMatrix();
-
-
glTranslatef(-1.5f, 0.0f, -7.0f);
-
glRotatef(rtri, 1.0f, 0.0f, 0.0f);
-
cgGLSetStateMatrixParameter(modelViewProjMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);
-
-
glBegin(GL_TRIANGLES);
-
glColor3f(1.0f, 0.0f, 0.0f);
-
glVertex3f(0.0f, 1.0f, 0.0f);
-
glColor3f(0.0f, 1.0f, 0.0f);
-
glVertex3f(-1.0f, -1.0f, 0.0f);
-
glColor3f(0.0f, 0.0f, 1.0f);
-
glVertex3f(1.0f, -1.0f, 0.0f);
-
glEnd();
-
glPopMatrix();
-
-
-
CGdisableProfile(CGgetFragmentProfile());
-
CGdisableProfile(CGgetVertexProfile());
-
-
-
glPushMatrix();
-
glTranslatef(1.5f, 0.0f, -7.0f);
-
glRotatef(rquad, 1.0f, 0.0f, 0.0f);
-
glBegin(GL_QUADS);
-
glColor3f(1.0f, 0.0f, 0.0f);
-
glVertex3f( 1.0f, 1.0f,-1.0f);
-
glVertex3f(-1.0f, 1.0f,-1.0f);
-
glVertex3f(-1.0f, 1.0f, 1.0f);
-
glVertex3f( 1.0f, 1.0f, 1.0f);
-
-
glColor3f(0.0f, 1.0f, 0.0f);
-
glVertex3f( 1.0f, -1.0f, 1.0f);
-
glVertex3f(-1.0f, -1.0f, 1.0f);
-
glVertex3f(-1.0f, -1.0f,-1.0f);
-
glVertex3f( 1.0f, -1.0f,-1.0f);
-
-
glColor3f(0.0f, 0.0f, 1.0f);
-
glVertex3f( 1.0f, 1.0f, 1.0f);
-
glVertex3f(-1.0f, 1.0f, 1.0f);
-
glVertex3f(-1.0f, -1.0f, 1.0f);
-
glVertex3f( 1.0f, -1.0f, 1.0f);
-
-
glColor3f(1.0f, 1.0f, 0.0f);
-
glVertex3f( 1.0f, -1.0f,-1.0f);
-
glVertex3f(-1.0f, -1.0f,-1.0f);
-
glVertex3f(-1.0f, 1.0f,-1.0f);
-
glVertex3f( 1.0f, 1.0f,-1.0f);
-
-
glColor3f(1.0f, 0.0f, 1.0f);
-
glVertex3f(-1.0f, 1.0f, 1.0f);
-
glVertex3f(-1.0f, 1.0f,-1.0f);
-
glVertex3f(-1.0f, -1.0f,-1.0f);
-
glVertex3f(-1.0f, -1.0f, 1.0f);
-
-
glColor3f(0.0f, 1.0f, 1.0f);
-
glVertex3f( 1.0f, 1.0f,-1.0f);
-
glVertex3f( 1.0f, 1.0f, 1.0f);
-
glVertex3f( 1.0f, -1.0f, 1.0f);
-
glVertex3f( 1.0f, -1.0f,-1.0f);
-
glEnd();
-
glPopMatrix();
-
-
rtri += 0.2f;
-
rquad -= 0.75f;
-
-
return true;
This is more, more and more code, but hang on, this is not too bad! Lets start from the beginning here(I’ll skipe the OpenGL stuff, I assume that if you read this, you already know some OpenGL!).
The first thing we do is now to enable and bind the cg profiles we created in the init seciton. This tells OpenGL that, hey – we want to use these shaders instead of the filthy ones you’re using at the moment! As long as these shaders are active, OpenGL uses them in the render pipeline and doesnt apply any of its own shaders. Also note that we need to specific bind both the vertex and the fragment shader.
The next thing we do is claim the variable called “ModelViewProj” and put it in our CGparameter modelViewProjMatrix variable. We will use this to put the concatenated modelview and projection matrices(CG_GL_MODELVIEW_PROJECTION_MATRIX). Why do we need this? I’ll tell you, we need it in the vertex shader to convert from object space to clip space – since OpenGL has withdrawn from every responsibility after you created your shaders. That is what the call to cgGLSetStateMatrixParameter does. See in our vertex shader that we have the following line:
-
const uniform float4×4 ModelViewProj
This variable is what we’re setting with the cgGLSetStateMatrixParameter.
After we have done some translation and rotation, we call the cgGLSetStateMatrixParameter again to update the matrix, if not the new matrix of the triangle isn’t update(e.g. no rotation and moving of triangle).
The last thing we do(last CG related thing) is call the CGdisableProfile(), which disables the current CG profiles and falls back to the OpenGL shaders. Now our CG shaders doesn’t do anything to the rendering pipeline – all the control is back to OpenGL.
Result
When compiled, that the shaders only apply to the triangle, not the box. This is only to show that you can use multiple shader in your application without any problems. This is really handy when you use shaders for bump-mapping or different lighting effects.
Go on to nVidias page and read the tutorial and start playing around with the shaders and see how much fun you can have with it. This is a good way to see how you can affect the rendering pipeline in OpenGL.
Download the Visual Studio source code here(include the cg binaries and libraries).