C/C++ Users Journal January, 2005
Modern programmable graphics cards allow custom vertex and fragment programs to replace the fixed-function vertex and fragment stages of the graphics pipeline. Vertex and fragment programs can be written in low-level assembly languages or a high-level shader language such as NVIDIA's Cg ("C for graphics") [1]. Cg uses a familiar C-like syntax and provides runtime libraries for integrating Cg code with programs written for OpenGL or DirectX. In addition to the OpenGL runtime library, Cg and OpenGL can be integrated by using Cg's compiler to translate Cg into assembly-language programs accessible through OpenGL extensions. In this article, I explain how to incorporate Cg into OpenGL programs using the runtime library and the relevant OpenGL extensions.
Before examining the mechanics of the Cg runtime library, I need to address some details about how OpenGL supports vertex and fragment programs written in low-level assembly language. OpenGL supports several assembly language dialects for vertex and fragment programs through its extensions mechanism. The GL_ARB_vertex_program and GL_ARB_fragment_program extensions are officially sanctioned by the Architectural Review Board (ARB), and are widely supported by hardware vendors such as ATI, NVIDIA, and others. NVIDIA also supplies a set of proprietary extensions for its own vertex and fragment program dialects, but these extensions are not typically supported by other vendors. ARB vertex and fragment programs are usually stored in ASCII text files that will be read into a character string by the application [2].
Listing 1 shows how to load a vertex and a fragment program, as well as how to set a local parameter in the fragment program. Programs are bound to GLuint variables, just like texture maps. A variety of functions let you set local parameters (specific to a particular program) and environment parameters (affecting all fragment or vertex programs). Although Listing 1 does not illustrate this, vertex programs receive vertex attributes such as position, normal, color, and texture coordinates through calls to functions such as glVertexAttrib3fARB(...) instead of the usual glVertex3f(...), glNormal3f(...), and so on. Each vertex attribute is assigned a number, so you are required to remember that the vertex color attribute is number 4, and so on. Needless to say, this mechanism is not particularly elegant or easy to use, but fortunately, the Cg OpenGL runtime library makes life much easier.
Although a complete discussion of the Cg language is beyond the scope of this article, a couple of Cg language features are relevant to the Cg OpenGL runtime library.
First, a single text file can contain several vertex and fragment programs. Cg does not dictate a standard name for a program entry point like the C/C++ main function. You are free to pick whatever name you like for your program entry points. You must, however, provide the Cg compiler with the name of your program entry point. Second, there are two basic types of data available to your Cg program. Uniform data is constant across multiple vertices or fragments, and varying data changes on a per-vertex or per-fragment basis. Uniform data includes texture maps (called "samplers" in Cg programs), transformation matrices, and constant vertex colors. Varying data includes vertex positions, normals, texture coordinates, and other interpolated quantities. Uniform and varying parameters are handled differently by the Cg OpenGL runtime library. Complete information about the Cg language and runtime libraries can be found in [3] and [4].
The Cg OpenGL runtime library provides runtime access to the Cg compiler, as well as providing richer functionality for managing parameters, attributes, and other data than the OpenGL ARB interfaces. The runtime library actually consists of two parts, a core library that is independent of OpenGL and an OpenGL-specific library. The core library handles access to the compiler and manages programs, and the OpenGL library handles parameters and other data specific to OpenGL. The header files cg.h and cgGL.h contain the function declarations for the core Cg runtime and the Cg OpenGL runtime library, respectively. Cg programs can only be loaded once a runtime context has been created with cgCreateContext():
CGcontext context = cgCreateContext();
A context can be destroyed with cgDestroyContext():
cgDestroyContext(context);
Again, there are several assembly-language flavors compatible with different graphics hardware. The Cg compiler supports a variety of assembly language targets through profiles. For example, the profile arbfp1 targets ARB_fragment_program. Although you can instruct the Cg runtime library to use a particular profile, Cg can select the most advanced profile supported by your hardware with:
CGprofile profile =
cgGLGetLatestProfile(CGGLenum
profileType)
where profileType is either CG_GL_VERTEX or CG_GL_FRAGMENT. Once a profile is selected, the Cg runtime can choose optimal options for the Cg compiler with:
cgGLSetOptimalOptions(profile);
Once a context is created and a profile is selected, a program can be created from a text file using:
CGprogram
cgCreateProgramFromFile(
CGcontext context,
CGenum programType,
const char *programFile,
CGprofile profile,
const char *entry,
const char **args);
The argument programType can be either CG_OBJECT or CG_SOURCE. If it is CG_SOURCE, then the file is assumed to be an ASCII text file containing Cg source code, and the compiler is run. If it is CG_OBJECT, then the file is assumed to be precompiled object code. Whereas the entry point to a C or C++ program must be called main, Cg lets you give program entry points arbitrary names. The entry point name is specified in the argument entry. Finally, any compiler options are passed in the last argument args. This should be NULL if you have set optimal compiler options. The last steps are to load the program with:
void cgGLLoadProgram(
CGprogram program);
and to bind it with:
void cgGLBindProgram(
CGprogram program);
Once a program has been created, compiled, and loaded, you can acquire handles to the program parameters, which let you correctly pass vertex attributes, texture maps, and other data to your programs.
The Cg runtime library uses a technique of parameter shadowing for uniform parameters. Whenever you set the value of a uniform program parameter with a Cg library call, the value is stored internally and the appropriate OpenGL functions are called. This lets you avoid having to reset uniform parameter values every time your Cg program is about to execute. However, varying data, such as vertex position and texture coordinates, must be set every time the program executes. Whereas the OpenGL ARB interface requires you to refer to parameters by attribute or register number, the Cg runtime lets you refer to parameters by name. For instance, a vertex program will typically operate on the vertex position. A parameter handle for vertex position is created by:
CGparameter position =
cgGLGetNamedParameter(
vertexProgram,
"position");
If the Cg program vertexProgram does not contain a parameter called "position", then an error is generated. Once you have a handle to the position parameter, you can pass vertex position data to the program in one of two ways. You can pass position information for a single vertex by calling a function such as:
cgGLSetParameter3f(position,
1.0, 5.0, 4.0);
This is analogous to setting vertex data in OpenGL immediate mode with glVertex3f(...), and in fact, the Cg runtime library will call the appropriate immediate mode function when varying parameters are set this way. Alternately, you can specify vertex array data by calling:
cgGLEnableClientState(position);
cgGLSetParameterPointer(
position, 3, GL_FLOAT, 0,
vertexPositions);
where vertexPositions is an array of type GLfloat containing the individual vertex coordinates. The geometry contained in the vertex array is drawn by calling the usual OpenGL function glDrawArrays(...). You can set up arrays of normals, colors, and texture coordinates in exactly the same manner.
You will pass most of the data to your vertex and fragment programs this way. The two exceptions are matrix data and texture maps. You create a handle to a matrix parameter just as you do for regular parameters. However, you can pass in an OpenGL state matrix by calling the function:
void cgGLSetStateMatrixParameter( CGparameter parameter, GLenum stateMatrixType, GLenum transform);
where stateMatrixType is CG_GL_MODELVIEW_MATRIX, CG_GL_PROJECTION_MATRIX, CG_GL_TEXTURE_MATRIX, or CG_GL_MODELVIEW_PROJECTION_MATRIX. The enum transform specifies a transformation to apply to the matrix before passing it to your program. It can be one of CG_GL_MATRIX_IDENTITY, CG_CG_MATRIX_TRANSPOSE, CG_GL_MATRIX_INVERSE, or CG_GL_MATRIX_INVERSE_TRANSPOSE. There are other functions for passing in arbitrary matrix data.
Setting up a texture map is quite easy. Once you create the texture map with the usual series of OpenGL calls and acquire a handle to the texture parameter, you simply call:
void cgGLSetTextureParameter(
CGparameter parameter,
GLuint textureId);
where the GLuint is the name of the texture object you created with glGenTextures(...). The Cg runtime library automatically determines the dimensions of the texture map and other texture parameters. Before a texture map can be used, you must call:
void cgGLEnableTextureParameter( CGparameter parameter);
which binds the texture.
Listing 2 shows the initialization and drawing routines from a complete example application, to follow. The Cg User's Manual [3] has a skeleton program that illustrates many of the aforementioned concepts.
The Cg runtime library provides some error-checking functionality. When an error occurs, the Cg runtime sets an error code, which can be decoded into a string:
CGerror error = cgGetError(); const char *errorStr = cgGetErrorString(error);
You can optionally provide an error callback function, which is called whenever an error occurs. The callback function has prototype void error_callback(void), and is set with cgSetErrorCallback(error_callback). You would typically want to report the error string from within the error callback function.
Cgc is the command-line compiler for Cg. A typical command-line invocation of Cgc is:
cgc -profile arbfp1 -entry FragmentProgram -o fragment.fp fragment.cg
where the -profile flag specifies the assembly-language profile, the -entry flag identifies the entry point function, and the -o flag specifies the output filename. Omitting the -o flag sends the output to stdout, which is useful for visually inspecting the generated assembly code. If you hand-compile your Cg programs, you will need to use OpenGL extensions to load the programs and set parameters.
I have provided an example application using Cg and OpenGL. The program loads and renders a mesh in a cartoon style [5]. Cartoon images usually have solid colors, sharp transitions from illuminated HASH(0x80bca8) to shadows, and black outlines around the silhouette and other features. Silhouette computation is a complex topic in itself, but fortunately, there are some simple OpenGL tricks for drawing reasonably accurate silhouettes. The silhouette algorithm I used first draws the backfacing triangles in the mesh in wire-frame mode, colored black. Then it enables the Cg toon shader and draws the frontfacing polygons.
The basic idea behind cartoon shading is simple. Each polygon has a diffuse color. This color is the basis of the illuminated, shadowed, and highlighted colors. I created a 1D texture with 32 texels. This texture map is basically a series of steps representing the transitions from shadow to illumination and from illumination to specular highlight. For each vertex, the dot product of the normal and the light direction is computed. This value is used to index into the 1D texture, and the value of the texture map scales the diffuse color of the material. If the dot product is less than zero, the vertex is backfacing and won't be drawn. If the dot product is 0, then the vertex normal is perpendicular to the direction of the light and is therefore in shadow. Values between 0 and 1 go from shadowed through illuminated to highlighted. Using GL_NEAREST texture filtering ensures that the shadow borders are sharp and distinct. Note that it is important to have a smooth mesh with surface normals specified per vertex rather than per triangle. My Mesh class preprocesses the mesh to ensure that surface normals are properly computed. The complete source code for this example is available at http://www.cuj.com/code/. See Listing 2 for excerpts from the initialization and drawing routines. Listing 3 shows the complete Cg vertex and fragment programs, which were inspired by examples in [4].
Rendered examples are shown in Figures 1 and 2. The airplane model in Figure 1 is a modification of a mesh available at [6], and the model in Figure 2 is my own design.
The Cg runtime library provides additional capabilities, beyond what I've discussed in this article. For example, there are functions to query the compile state of programs, cycle through programs and parameters, and query parameter values. There is also a comparable runtime library supporting Microsoft DirectX. In the OpenGL realm, the Cg runtime library provides a clean, intuitive mechanism for integrating vertex and fragment programs with OpenGL. Although the OpenGL shader landscape is likely to change with the widespread adoption of the OpenGL 2.0 Standard, Cg will remain a solid choice for OpenGL and cross-platform shader integration.
[1] http://developer.nvidia.com/object/cg_toolkit.html.
[2] Lengyel, E. The OpenGL Extensions Guide, Charles River Media, 2003.
[3] NVIDIA Corp. Cg Toolkit User's Manual (http://developer.nvidia.com/Cg).
[4] Fernando, R. and M. Kilgard. The Cg Tutorial, Addison-Wesley, 2003.
[5] Gooch, B. and A. Gooch. Non-Photorealistic Rendering, A.K. Peters, 2001.