CG Vertex ShaderUsing vertex and fragment (or pixel) shaders to do some rendering dirty work can have numerous benefits. The most obvious is the movement of some of the graphics related CPU load off the CPU and onto the GPU. Cg provides a (reasonably) simple language for writing very powerful shaders. This tutorial has multiple aims. The first is to present a simple vertex shader that actually does something, without introducing unnecessary lighting etc The second is to provide the basic mechanism for running the vertex shader with visible results using OpenGL. As such, it is aimed at the beginner interested in Cg who has a little experience in OpenGL. This tutorial is based on the Latest NeHeGL Basecode. For more information on Cg check out nVidias website (developer.nvidia.com) and www.cgshaders.org for some cool shaders. NOTE: This tutorial is not intended to teach you everything you need to know about writing vertex shaders using Cg. It is intended to teach you how to successfully load and run vertex shaders within OpenGL. Setup: The first step (if it hasnt been done already) is to download the Cg Compiler from nVidia. It is important that you download version 1.1, as nVidia appear to have made changes between version 1.0 and 1.1 (different variable naming, replaced functions, etc ), and code compiled for one may not necessarily work with the other. The next step is to setup the header files and library files for Cg to a place where Visual Studio can find them. Because I am inherently distrustful of installers working the way they are supposed to, I copy the library files... From: C:\Program Files\NVIDIA Corporation\Cg\lib To: C:\Program Files\Microsoft Visual Studio\VC98\Lib and the header files (Cg sub-directory and GLext.h into the GL sub-directory)... From: C:\Program Files\NVIDIA Corporation\Cg\include To: C:\Program Files\Microsoft Visual Studio\VC98\Include Were now ready to get on with the tutorial. Cg tutorial: The information regarding Cg contained in this tutorial was obtained mostly from the Cg Toolkit Users Manual. There are a few important points that you need to remember when dealing with vertex (and later fragment) programs. The first thing to remember is that a vertex program will execute in its entirety on EVERY vertex. The only way to run the vertex program on selected vertices is to either load/unload the vertex program for each individual vertex, or to batch vertices into a stream that are affected by the vertex program, and a stream that isnt. The output of a vertex program is passed to a fragment shader, regardless of whether you have implemented and activated a fragment shader. Finally, remember that a vertex program is executed on the vertices before primitive assembly, while a fragment program is executed after rasterization. On with the tutorial. First, we need to create a blank file (save as wave.cg). We then create a structure to contain the variables and information that we want available to our shader. This code is added to the wave.cg file. struct appdata { float4 position : POSITION; float4 color : COLOR0; float3 wave : COLOR1; }; Each of the 3 variables (position, color and wave) are bound to predefined names (POSITION, COLOR0 and COLOR1 respectively). These predefined names are referred to as the binding semantics. In OpenGL, these predefined names implicitly specify the mapping of the inputs to particular hardware registers. The main program must supply the data for each of these variables. The position variable is REQUIRED, as it is used for rasterization. It is the only variable that is required as an input to the vertex program. The next step is to create a structure to contain the output which will be passed on to the fragment processor after rasterization. struct vfconn { float4 HPos : POSITION; float4 Col0 : COLOR0; }; As with the inputs, each of the output variables is bound to a predefined name. Hpos represents the position transformed into homogenous clip-space. Col0 represents the color of the vertex after changes made to it by the vertex program. The only thing left to do is to write the actual vertex program, utilizing both of our newly defined structures. vfconn main(appdata IN, uniform float4x4 ModelViewProj) { vfconn OUT; // Variable To Handle Our Output From The Vertex // Shader (Goes To A Fragment Shader If Available) Much as in C, we define our function as having a return type (struct vfconn), a function name (main, but can be anything we want), and the parameters. In our example, we take our struct appdata as an input (containing the current position of the vertex, the color of the vertex, and a wave value for moving the sine wave across the mesh). We also pass in a uniform parameter, which is the current modelview matrix from OpenGL (in our main program). This value typically does not change as we manipulate our vertices, and is therefore referred to as uniform. This matrix is required to transform the vertex position into homogenous clip-space. We declare a variable to hold our modified values from the vertex shader. These values are returned at the end of the function, and are passed to the fragment shader (if it exists). We now need to perform our modifications to the vertex data. // Change The Y Position Of The Vertex Based On Sine Waves IN.position.y = ( sin(IN.wave.x + (IN.position.z / 4.0) ) + sin(IN.wave.x + (IN.position.x / 5.0) ) ) * 2.5f; We change the Y position of the vertex depending on the current X / Z position of the vertex. The X and Z positions of the vertex are divided by 4.0 and 5.0 respectively to make them smoother (to see what I mean, change both of these values to 1.0). Our IN.wave variable contains an ever-increasing value which causes the sine waves to move across our mesh. This variable is specified within our main program. Therefore, we calculate the Y position of the X / Y position of the mesh as sin of the wave value + the current X or Z position. Finally, we multiple the value by 2.5 to make the waves more noticeable (higher). We now perform the required operations to determine the values to output to the fragment program. // Transform The Vertex Position Into Homogenous Clip-Space (Required) OUT.HPos = mul(ModelViewProj, IN.position); // Set The Color To The Value Specified In IN.color OUT.Col0.xyz = IN.color.xyz; return OUT; } First we transform the new vertex position into homogenous clip-space. We then set our output color to the input color, which is specified in our main program. Finally, we return our values for use by a fragment shader (if we have one). Well now move on the main program which creates a triangle mesh and runs our shader on each vertex to produce a nice wave effect. OpenGL tutorial: The main sequence of steps for dealing with our Cg shader is to generate our mesh, load up and compile our Cg program and then run this program on each vertex as it is being drawn. First we must get some of the necessary setup details out of the way. We need to include the necessary header files to run Cg shaders with OpenGL. After our other #include statements, we need to include the Cg and CgGL headers. #include <cg\cg.h> // NEW: Cg Header #include <cg\cggl.h> // NEW: Cg OpenGL Specific Header Now we should be ready to setup our project and get to work. Before we start, we need to make sure that Visual Studio can find the correct libraries. The following code will do the trick! #pragma comment( lib, "cg.lib" ) // Search For Cg.lib While Linking #pragma comment( lib, "cggl.lib" ) // Search For CgGL.lib While Linking Next well create some global variables for our mesh and for toggling the CG program on / off. #define SIZE 64 // Defines The Size Of The X/Z Axis Of The Mesh bool cg_enable = TRUE, sp; // Toggle Cg Program On / Off, Space Pressed? GLfloat mesh[SIZE][SIZE][3]; // Our Static Mesh GLfloat wave_movement = 0.0f; // Our Variable To Move The Waves Across The Mesh We define the size as 64 points on each edge of our mesh (X and Z axes). We then create an array for each vertex of our mesh. The final variable is required to make the sine waves move across our mesh. We now need to define some Cg specific global variables. CGcontext cgContext; // A Context To Hold Our Cg Program(s) The first variable we need is a CGcontext. A CGcontext variable is a container for multiple Cg programs. In general, you require only one CGcontext variable regardless of the number of vertex and fragment programs you have. You can select different programs from the same CGcontext using the functions cgGetFirstProgram and cgGetNextProgram. We next define a CGprogram variable for our vertex program. CGprogram cgProgram; // Our Cg Vertex Program Our CGprogram variable is used to store our vertex program. A CGprogram is essentially a handle to our vertex (or fragment) program. This is added to our CGcontext. We next need to have a variable to store our vertex profile. CGprofile cgVertexProfile; // The Profile To Use For Our Vertex Shader Our CGprofile defines the most suitable profile. We next need variables that provide a connection between variables in our main program and variables in our shader. CGparameter position, color, modelViewMatrix, wave; // The Parameters Needed For Our Shader Each CGparameter is essentially a handle to the corresponding parameter in our shader. Now that weve taken care of our global variables, its time to get to work on setting up our mesh and vertex program. In our Initialize function, before we call return TRUE;, we need to add our custom code. glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Draw Our Mesh In Wireframe Mode // Create Our Mesh for (int x = 0; x < SIZE; x++) { for (int z = 0; z < SIZE; z++) { mesh[x][z][0] = (float) (SIZE / 2) - x; // We Want To Center Our Mesh Around The Origin mesh[x][z][1] = 0.0f; // Set The Y Values For All Points To 0 mesh[x][z][2] = (float) (SIZE / 2) - z; // We Want To Center Our Mesh Around The Origin } } We first call glPolygonMode to change the display to wireframe (flatshaded looks awful without correct lighting). We then traverse through our mesh, setting the X and Z values around the origin. The Y value for each point is set to 0.0f. It is interesting to note that the values generated in this step at no point change during executing. With our mesh initialization out of the way, were now ready to initialize our Cg stuff. // Setup Cg cgContext = cgCreateContext(); // Create A New Context For Our Cg Program(s) // Validate Our Context Generation Was Successful if (cgContext == NULL) { MessageBox(NULL, "Failed To Create Cg Context", "Error", MB_OK); return FALSE; // We Cannot Continue } We first try to create a new CGcontext to store our Cg programs. If our return value is NULL, then our context creation fails. This will usually only fail due to memory allocation errors. cgVertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX); // Get The Latest GL Vertex Profile // Validate Our Profile Determination Was Successful if (cgVertexProfile == CG_PROFILE_UNKNOWN) { MessageBox(NULL, "Invalid profile type", "Error", MB_OK); return FALSE; // We Cannot Continue } cgGLSetOptimalOptions(cgVertexProfile); // Set The Current Profile We now determine the last vertex profile for use. To determine the latest fragment profile, we call cgGLGetLatestProfile with the CG_GL_FRAGMENT profile type. If our return value is CG_PROFILE_UNKNOWN, there was no appropriate profile available. With a valid profile, we can set call cgGLSetOptimalOptions. This function sets compiler arguments based on available compiler arguments, GPU and driver. These functions are used each time a new Cg program is compiler. (Essentially optimizes the compilation of the shader dependent on the current graphics hardware and drivers). // Load And Compile The Vertex Shader From File cgProgram = cgCreateProgramFromFile(cgContext, CG_SOURCE, "CG/Wave.cg", cgVertexProfile, "main", 0); // Validate Success if (cgProgram == NULL) { // We Need To Determine What Went Wrong CGerror Error = cgGetError(); // Show A Message Box Explaining What Went Wrong MessageBox(NULL, cgGetErrorString(Error), "Error", MB_OK); return FALSE; // We Cannot Continue } We now attempt to create our program from our source file. We call cgCreateProgramFromFile, which will load and compile our Cg program from the specified file. The first parameter defines which CGcontext variable our program will be attached to. The second parameter define whether our Cg code is assumed to be in a file that contains source code (CG_SOURCE), or a file which contains the object code from a pre-compiled Cg program (CG_OBJECT). The third parameter is the name of the file containing our Cg program. The fourth parameter is the latest profile for the particular type of program (use a vertex profile for vertex programs, fragment profiles for fragment programs). The fifth parameter determines the entry function of our Cg program. This function can be arbitrarily specified, and often should be something other than main. The last parameter provides for additional arguments to be passed to the Cg compiler. This is often left as NULL. If cgCreateProgramFromFile fails for any reason, we retrieve the last error by calling cgGetError. We can then get a human-readable string of the error contained in our CGerror variable by calling cgGetErrorString. Were almost finished our initialization. // Load The Program cgGLLoadProgram(cgProgram); The next step to do is to actually load our program, and prepare it for binding. All programs must be loaded before they can be bound to the current state. // Get Handles To Each Of Our Parameters So That // We Can Change Them At Will Within Our Code position = cgGetNamedParameter(cgProgram, "IN.position"); color = cgGetNamedParameter(cgProgram, "IN.color"); wave = cgGetNamedParameter(cgProgram, "IN.wave"); modelViewMatrix = cgGetNamedParameter(cgProgram, "ModelViewProj"); return TRUE; // Return TRUE (Initialization Successful) The final step of initialization requires our program to get handles to the variables which we wish to manipulate in our Cg program. For each CGparameter we attempt to retrieve a handle to the corresponding Cg program parameter. If a parameter does not exist, cgGetNamedParameter will return NULL. If the parameters into the Cg program are unknown, cgGetFirstParameter and cgGetNextParameter can be used to traverse the parameters of a given CGprogram. Weve finally finished with the initialization of our Cg program, so now well quickly take care of cleaning up after ourselves, and then its on to the fun of drawing. In the function Deinitialize, we need clean up our Cg program(s). cgDestroyContext(cgContext); // Destroy Our Cg Context And All Programs Contained Within It We simply call cgDestroyContext for each of our CGcontext variables (we can have multiple, but theres usually only one). You can individually delete all of your CGprograms by calling cgDestoryProgram, however calling cgDestoryContext deletes all CGprograms contained by the CGcontext, and then deletes the CGcontext itself. Now we will add some code to our Update function. The following code checks to see if the spacebar is pressed and not held down. If space is press and not held down, we toggle cg_enable from true to false or from false to true. if (g_keys->keyDown [' '] && !sp) { sp=TRUE; cg_enable=!cg_enable; } The last bit of code checks to see if the spacebar has been released, and if so, it sets sp (space pressed?) to false. if (!g_keys->keyDown [' ']) sp=FALSE; Now that weve dealt with all of that, its time to get down to the fun of actually drawing our mesh and running our vertex program. The final function we need to modify is the Draw function. Were going to add our code after glLoadIdentity and before glFlush. // Position The Camera To Look At Our Mesh From A Distance gluLookAt(0.0f, 25.0f, -45.0f, 0.0f, 0.0f, 0.0f, 0, 1, 0); First, we want to move our viewpoint far enough away from the origin to view our mesh. We move the camera 25 units vertically, 45 units away from the screen, and center our focal point at the origin. // Set The Modelview Matrix Of Our Shader To Our OpenGL Modelview Matrix cgGLSetStateMatrixParameter(modelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY); The next thing we want to do is set the model view matrix of our vertex shader to the current OpenGL modelview matrix. This needs to be done, as the position which is changed in our vertex shaders needs to transform the new position into homogenous clip-space, which is done by multiplying the new position by our modelview matrix. if (cg_enable) { cgGLEnableProfile(cgVertexProfile); // Enable Our Vertex Shader Profile // Bind Our Vertex Program To The Current State cgGLBindProgram(cgProgram); We then have to enable our vertex profile. cgGLEnableProfile enables a given profile by making the relevant OpenGL calls. cgGLBindProgram binds our program to the current state. This essentially activates our program, and subsequently runs our program on each vertex passed to the GPU. The same program will be run on each vertex until we disable our profile. // Set The Drawing Color To Light Green (Can Be Changed By Shader, Etc...) cgGLSetParameter4f(color, 0.5f, 1.0f, 0.5f, 1.0f); } Next we set the drawing color for our mesh. This value can be dynamically changed while drawing the mesh to create cool color cycling effects. Notice the check to see if cg_enable is true? If it is not, we do not deal with any of the Cg commands above. This prevents the CG code from running. Were now ready to render our mesh! // Start Drawing Our Mesh for (int x = 0; x < SIZE - 1; x++) { // Draw A Triangle Strip For Each Column Of Our Mesh glBegin(GL_TRIANGLE_STRIP); for (int z = 0; z < SIZE - 1; z++) { // Set The Wave Parameter Of Our Shader To The Incremented Wave Value From Our Main Program cgGLSetParameter3f(wave, wave_movement, 1.0f, 1.0f); glVertex3f(mesh[x][z][0], mesh[x][z][1], mesh[x][z][2]); // Draw Vertex glVertex3f(mesh[x+1][z][0], mesh[x+1][z][1], mesh[x+1][z][2]); // Draw Vertex wave_movement += 0.00001f; // Increment Our Wave Movement if (wave_movement > TWO_PI) // Prevent Crashing wave_movement = 0.0f; } glEnd(); } To render our mesh, we simply loop along the Z axis for each X axis (essentially we work in columns from one side of our mesh to the other). For each column, we begin a new triangle strip. For each vertex we render, we dynamically pass the value of our wave parameter of our vertex program. Because this value is determined by the wave_movement variable in our main program, which is incremented continuously, our sine waves appear to move across and down our mesh. We then pass the vertices we are currently drawing to our GPU, while the GPU will handle automatically running the our vertex program on each vertex. We slowly increment our wave_movement variable so as to get slow and smooth movement. If the value of wave_movement gets to high, we reset it back to 0 to prevent crashing. TWO_PI is defined at the top of the program. if (cg_enable) cgGLDisableProfile(cgVertexProfile); // Disable Our Vertex Profile Once weve finished our rendering, we check to see if cg_enable is true and if so, we disable our vertex profile and continue to render anything else that we wish. * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Ingo Boller ) |