Introduction to Cg Runtime with OpenGL

Cg Runtime - Example 2

Cg Runtime - Example 2

In this article I will provide a brief introduction to the Cg runtime and I will show a very simple example that uses the Cg shader language.

I will assume that the reader has some basic knowledge of OpenGL and how to create an application that uses OpenGL to render graphics to the screen. If you need a brief introduction to OpenGL, you can refer to my article [Introduction to OpenGL for Game Programmers]

Introduction

Cg is a C-like programming language developed by nVidia. The Cg language allows the developer to create vertex and fragment programs that can be used regardless of the graphics API or platform.

Cg shader programs can be used either as individual program files (one for the vertex program and one for the fragment program) in which case the extension of the files is usually “.cg” and it is up to the developer to determine which files stores the vertex program, and which stores the fragment program. Cg shader programs can also be stored in effect files (usually has the file extension “.cgfx“). Effect files are easier to load and easier to use in your program. Effect files can encapsulate both the vertex, fragment, and shader states that are required by the shader program to render the geometry correctly. Using effect files also requires less programming in the application to use them.

For this article and future articles on Cg, I will only use CgFX effect files.

Dependencies

The demo shown in this article uses several 3rd party libraries to simplify the development process.

  • The Cg Toolkit (Version 3): The Cg Toolkit provides the tools and API needed to integrate the Cg shader programs in your application.
  • Boost (1.46.1): Boost has some very useful libraries that I use throughout my demo applications. In this demo, I use the Signals, Filesystem, Function, and Bind boost libraries to provide a generic, platform independent functionality that simplifies some of the features used in this demo.
  • Simple DirectMedia Layer (1.2.14): Simple DirectMedia Layer (SDL) is a cross-platform multimedia library that I use to create the main application window, initialize OpenGL, and handle keyboard, mouse and joystick input.

Any dependencies used by the demo are included in the source distribution available at the bottom of the article so you can hopefully just unzip, compile and run the included samples.

The Cg Runtime

The Cg Runtime is a set of functions that you use in a C, or C++ program that associate a shader program to the geometry that is rendered in a particular graphics API. The Cg runtime consists of a core API and a graphics implementation specific API. Core API functions are prefixed with “cg” while the OpenGL specific API functions are prefixed with “cgGL“. Like wise the Direct3D specific API functions are prefixed with “cgD3D8“, “cgD3D9“, “cgD3D10“, or “cgD3D11“. Whenever possible, I will try to use the core API functions so that the program is not strictly bound to a specific graphics API like OpenGL or Direct3D. In some cases, the use of the graphics API specific functions are required (when setting texture parameters for example) and in this case I will use the OpenGL specific API function.

In this article, I will show how the Cg runtime is used to load a simple Cg shader program and then render a simple set of vertices using OpenGL. I will also show how to query and update a parameter that is defined in the shader program.

For this demo, I have created a framework that I will use to simplify the initialization of the Cg runtime, load and update effects, access and update shader programs, choose valid techniques and loop through passes. I will use this framework for future articles about Cg in order to focus more on the Cg language and only discuss the relevant parts of the runtime implementation.

The EffectManager Class

The EffectManager class is responsible for initializing the Cg runtime. It will also be used to load effect files from disk and it also provides a feature that allows the effect file to be edited while the program is running and update the effect on-the-fly so that changes to the effect file can be observed without restarting the application.

The EffectManager class is implemented as a singleton class which means that there is only one instance of the class and the class instance is accessed using static accessor functions defined in the class itself. The lifetime of the EffectManager is controlled using the EffectManager::Create and EffectManager::Destroy methods. Destroying the effect manager will also cause all effects that have been loaded to be destroyed.

To get a reference to the singleton instance of the EffectManager class, use the EffectManager::Get static method.

After an instance of the EffectManager has been created using the EffectManager::Create method, the EffectManager must be initialized using the EffectManager::Initialize method.

The EffectManager::Initialize Method

The EffectManager::Initialize method should only be called once for an instance of the EffectManager class. This method will create the Cg context and register the OpenGL states that are used by the runtime.

bool EffectManager::Initialize()
{
    if ( m_bIsInitialized ) return true;

    // Register the error handler
    cgSetErrorHandler( EffectManager::ErrorHandler, this );

    // Create the Cg context.
    m_cgContext = cgCreateContext();

    // Register the default state assignments for OpenGL.
    // See the documentation on CgFX States in the Cg Reference Manual.
    cgGLRegisterStates( m_cgContext );
    // This will allow the Cg runtime to manage texture binding.
    cgGLSetManageTextureParameters( m_cgContext, CG_TRUE );

    m_bIsInitialized = true;
    return true;
}

Whenever an error occurs in the Cg runtime, the EffectManager::ErrorHandler static method will be invoked. We can register the error handler for the Cg runtime using the cgSetErrorHandler method.

Almost everything done using the Cg runtime requires a context to be created. The context must be created using the cgCreateContext method before we can load effects and execute shaders.

The cgGLRegisterStates method will register state assignments for OpenGL states. The state assignments will not be discussed in too much detail in this article, but calling this method will allow the CgFX file to define which OpenGL states should be set before a vertex program or fragment program are executed.

The cgGLSetManageTextureParameters method will allow the Cg runtime to handle binding texture parameters used by samplers in the Cg fragment program. This will save us the trouble of invoking the OpenGL functions needed to bind the correct textures to the texture samplers in the fragment shader program.

The EffectManager::ErrorHandler Method

If an error occurs in the Cg runtime, our application will be notified by registering the error handler method using the cgSetErrorHandler method shown in the EffectManager::Initialize method.

void EffectManager::ErrorHandler( CGcontext context, CGerror error, void* appData )
{
    if ( error != CG_NO_ERROR )
    {
        const char* pStr = cgGetErrorString(error);
        std::string strError = ( pStr == NULL ) ? "" : pStr;
        std::cerr << "EffectManager::ErrorHandler: " << strError << std::endl;

        std::string strListing;
        if ( error == CG_COMPILER_ERROR )
        {
            pStr = cgGetLastListing( context );
            strListing = ( pStr == NULL ) ? "" : pStr;

            std::cerr << strListing << std::endl;
        }

        // Invoke the event to notify any subscribers.
        EffectManager* pEffectManager = static_cast<EffectManager*>( appData );
        RuntimeErrorEventArgs eventArgs( *pEffectManager, error, strError, strListing );
        pEffectManager->OnRuntimeError( eventArgs );
    }
}

In this implementation, the error string is simply written to the std::cerr output stream and an event is triggered.

I use the boost::signals and boost::function library to register events that can be triggered in the application. Any registered receiver will receive the event notification and can then perform application specific actions to handle the events. The event types are defined in the “Events.h” header file included in the source code.

The EffectManager::CreateEffectFromFile Method

In order to create and load effects, the EffectManager::CreateEffectFromFile method is used. The method takes two parameters:

  • std::string effectFile: The relative or absolute file path to the effect to be loaded.
  • std::string effectName: The unique name that is to be associated with the loaded effect.
Effect& EffectManager::CreateEffectFromFile( const std::string& effectFile, const std::string& effectName )
{
    assert( m_bIsInitialized );
    // first check if the file is valid
    if ( !fs::exists( effectFile ) || !fs::is_regular_file(effectFile) || fs::file_size(effectFile) == 0 )
    {
        std::cerr << "EffectManager::CreateEffectFromFile: Invalid file: \"" << effectFile << "\"" << std::endl;
        return Effect::InvalidEffect;
    }

    Effect* pEffect = NULL;

    // Look for the effect with the same name in the effect map if it already exists
    EffectMap::iterator effectIter = m_Effects.find( effectName );
    if ( effectIter == m_Effects.end() )
    {
        // Load a new effect.
        pEffect = new Effect( m_cgContext, effectFile, effectName );
        pEffect->EffectLoaded += boost::bind( &EffectManager::OnEffectLoaded, this, _1 );

        m_Effects.insert( EffectMap::value_type( effectName, pEffect ) );
    }
    else
    {
        pEffect = effectIter->second;
        if ( pEffect->m_EffectFile != effectFile )
        {
            // Replace an existing effect with a new one.
            std::cerr << "EffectManager::CreateEffectFromFile: An effect with name \"" << effectName << "\" was found in the effect map." << std::endl;
            std::cerr << "    This effect will be replaced with the new effect: \"" << effectFile << "\"" << std::endl;
            pEffect->LoadEffectFromFile( effectFile );
        }
    }
    
    EffectLoadedEventArgs effectLoadedEventArgs( *this, *pEffect );
    OnEffectLoaded( effectLoadedEventArgs );

    return *pEffect;
}

The EffectManager class stores the loaded effects in a map. Each effect is uniquely identified by a name. The same effect file can be loaded multiple times using different names. This is useful if you change the value of a constant parameter and want to recompile the same effect with the modified constant parameter. This will allow you to create a single shader (called an “Uber Shader”) whose functionality is determined by the value of specific constant parameters. Changing the constant parameters will require the shader to be recompiled but will result in an shader program with completely different code paths. I will show a more concrete example of this in a later article.

If an effect with the specific name didn’t already exist in the effect map, a new effect is created with the file and name supplied.

The Effect class is doing all the heavy lifting here. The only responsibility the EffectManager class has here is to create an instance of the Effect class, store that instance in the effect map, and pass a reference to the newly created Effect to the caller.

Again you see the use of the events on line 128. This event is used to notify the application that an effect has been loaded (or reloaded). In such a case, the application can set uniform effect parameters such as vector parameters or texture parameters that are used by the effect.

The Effect Class

The Effect class stores the EffectParameters and Technique instances that are used by the Effect.

A shader effect can define multiple vertex programs and fragment programs in a single file. An effect file can also define multiple Techniques and each technique can define one or more passes.

A Technique can be thought of as a compiled version of your effect program. On older graphics adapters, certain features may not be available and you will want to provide a technique that supports the older graphics hardware. For new hardware, you may want to provide a technique that takes advantages of more advanced features so you can provide more realistic effects.

A technique may consist of multiple passes. Each pass defines an entry point for either the vertex program, the fragment program, neither, or both. I will show an example of a technique and pass definition in the example Cg program.

An effect defines a single CgFX file that is loaded using the Effect::LoadEffectFromFile method. When an effect is created in the EffectManager, this method is implicitly called by the Effect constructor to load the effect. If the CgFX file on disk was modified, this method will also be used to reload the modified effect.

The Effect::LoadEffectFromFile Method

The Effect::LoadEffectFromFile method accepts a single string argument which defines the location of the CgFX file to load. The method will return true if the method successfully loaded the effect, or false if there was a problem loading the effect (like a compiler error for example).

bool Effect::LoadEffectFromFile( const std::string& effectFile )
{
    bool bSuccess = false;

    CGeffect effect = cgCreateEffectFromFile( m_cgContext, effectFile.c_str(), NULL );

    if ( effect != NULL )
    {
        Destroy();  // Destroy the current effect as we will be loading a new one.

        // Replace the existing effect.
        m_cgEffect = effect;

        // Get the parameters
        CGparameter parameter = cgGetFirstEffectParameter( m_cgEffect );
        while ( parameter != NULL )
        {
            EffectParameter* pParameter = new EffectParameter( m_cgContext, parameter );
            m_ParametersByName.insert( ParameterMap::value_type(pParameter->GetParameterName(), pParameter) );
            if ( pParameter->HasSemantic() )
            {
                m_ParametersBySemantic.insert( ParameterMap::value_type(pParameter->GetParameterSemantic(), pParameter) );
            }

            parameter = cgGetNextParameter( parameter );
        }

        // Get the techniques
        CGtechnique technique = cgGetFirstTechnique( m_cgEffect );
        while ( technique != NULL )
        {
            Technique* pTechnique = new Technique( m_cgContext, technique );
            m_Techniques.push_back(pTechnique);

            // Add the technique to our name map for fast lookup
            m_TechniquesByName.insert( TechniqueMap::value_type(pTechnique->GetTechniqueName(), pTechnique) );

            technique = cgGetNextTechnique(technique);
        }

        m_bIsValid = true;

        EffectLoadedEventArgs loadedArgs( *this, *this );
        OnEffectLoaded( loadedArgs );

        bSuccess = true;
    }

    // Store the filename so we can reload it later if necessary
    m_EffectFile = effectFile;

    // Store the current timestamp
    // Even if the effect fails to load because of a syntax error for example,
    // we should set the current timestamp so we don't continually try to reload
    // the broken effect until the file has been updated and saved.
    m_TimeStamp = time(NULL);

    return bSuccess;
}

On line 31, we use the Cg runtime method cgCreateEffectFromFile to load the effect and if we get a valid handle to an effect, then the effect was loaded correctly.

If the effect is valid, the Effect::Destroy method is called to cleanup the current effect.

Each effect may define multiple parameters and multiple techniques that are used in the shader program. To iterate over the effect parameters, we need to get a handle to the first effect parameter using the cgGetFirstEffectParameter shown on line 41. Each additional parameter in the effect file can be retrieved using the cgGetNextParameter function that takes the previous effect parameter as an argument.

Effect parameters will always have a name and optionally may have a semantic. A semantic is used to tell the Cg runtime how the input streams should be bound to the shader program.

In general, effect parameters have the following definition:

TYPE ParameterName : SEMANTIC

There are a few reserved semantics for input and output types, but we can also define semantics ourselves that we can use to automatically identify a parameter. For example, if we wanted to define a semantic for the camera’s view matrix, we may write something like this in the effect file:

float4x4 viewMatrix : VIEW

We can then search for parameters that use the “VIEW” semantic and have the effect framework automatically assign the view matrix to the value of this parameter by using shared parameters.

Once we have loaded and stored all the effect parameters, the effect’s techniques will be iterated in a similar way by using the cgGetFirstTechnique and cgGetNextTechnique methods of the Cg runtime.

Each effect file can define multiple techniques. The best practice is to first define the techniques that use the most recent profiles, then simpler techniques using older profiles will be defined after. Then, when you want to find a valid technique, you iterate through the list of techniques and use the first valid technique. The Effect class defines a method called GetFirstValidTechnique that provides this functionality.

At the end of the method on line 82, the the time-stamp for the current time is saved so that we can check periodically if the effect files’s time-stamp becomes greater than the effect instances’s load time, then we can try to reload the effect file from disk. The effect file can be reloaded using the Effect::CheckForReload method.

The Effect::CheckForReload Method

Using the boost::filesystem library, it is trivial to test to see if a file is valid and to check if the file’s modification time is greater than the last loaded time-stamp.

void Effect::CheckForReload()
{
    fs::path filePath = m_EffectFile;
    if ( fs::exists(filePath) && fs::is_regular_file(filePath) && fs::last_write_time(filePath) > m_TimeStamp )
    {
        LoadEffectFromFile( std::string(m_EffectFile) );
    }
}

The “fs” namespace is just an alias for the boost::filesystem namespace. I find “fs” easier to type than boost::filesystem. The boost::filesystem library defines some convenient functions for checking if a file is valid and to query the modification date of the file.

If the file’s modification becomes greater than the effect’s load time, then the effect will be reloaded using the Effect::LoadEffectFromFile method.

The EffectParameter Class

The EffectParameter class encapsulates all of the functionality required to get and set the values of shader parameters.

Shader parameters can be either scalar, vector or matrix types (bool, int, half, float, or double), samplers (sampler1D, sampler2D, sampler3D, samplerCUBE, or samplerRECT), arrays, or structs. In the case of structs and arrays, the EffectParameter can store nested EffectParameter(s).

The EffectParameter Constructor

The EffectParameter constructor will load the parameter types and values that are needed by the application to properly update the parameter at runtime.

EffectParameter::EffectParameter( CGcontext context, CGparameter parameter )
: m_cgContext( context )
, m_cgParameter( parameter )
, m_cgType( CG_UNKNOWN_TYPE )
, m_cgBaseType( CG_UNKNOWN_TYPE )
, m_cgVariability( CG_UNKNOWN )
, m_iNumRows( 0 )
, m_iNumCols( 0 )
, m_bIsValid( false )
, m_bIsDirty( false )
, m_bIsDefault( true )
{
    if ( m_cgParameter != NULL && cgIsParameter(m_cgParameter) )
    {
        m_bIsValid = true;

        const char* name = cgGetParameterName(m_cgParameter);
        if ( name != NULL )
        {
            m_ParameterName = name;
        }
        const char* semantic = cgGetParameterSemantic(m_cgParameter);
        if ( semantic != NULL )
        {
            m_ParameterSemantic = semantic;
        }

        m_cgParameterClass = cgGetParameterClass(m_cgParameter);
        m_cgType = cgGetParameterType(m_cgParameter);
        m_cgBaseType = cgGetParameterBaseType( m_cgParameter );
        m_cgVariability = cgGetParameterVariability(m_cgParameter);

        GetInternalValue();
    }
    else
    {
        m_ParameterName = "InvalidEffectParameter";
    }
}

The Cg runtime API has a few methods that we can use to query the state and value type of a parameter.

The cgIsParameter method will return CG_TRUE if the passed parameter is a valid parameter type.

The name of the parameter in the effect can be queried using the cgGetParameterName method and likewise the semantic of the parameter can be queried using the cgGetParameterSemantic if one is specified for the parameter.

We also need to know the type of the parameter. Parameters can have a class which will be one of CG_PARAMETERCLASS_SCALAR, CG_PARAMETERCLASS_VECTOR, CG_PARAMETERCLASS_MATRIX, CG_PARAMETERCLASS_STRUCT, CG_PARAMETERCLASS_ARRAY, CG_PARAMETERCLASS_SAMPLER, or CG_PARAMETERCLASS_OBJECT. The parameter class can be queried using the function cgGetParameterClass.

The type of the parameter can be queried using the cgGetParameterType method. This method will return a value that represents the exact type of the parameter. For example, a 4-component float vector will have a type CG_FLOAT4 and a texture sampler will have a type of CG_SAMPLER1D, CG_SAMPLER2D, CG_SAMPLER3D, CG_SAMPLERCUBE, CG_SAMPLERRECT, or any of the sampler types that store a shadow map. As you can imagine, the type of the parameter can become slightly unwieldy but we can use the base type to simplify getting and setting the parameter.

The cgGetParameterBaseType can be used to determine the base type of the parameter. For numerical types, the base type can be CG_BOOL, CG_INT, CG_HALF, CG_FLOAT, CG_DOUBLE, or any of the basic primitive types. For samplers, the base type is just CG_SAMPLER.

The variability of the parameter can be queried using the cgGetParameterVariability method which will be CG_VARYING which means that the value of the parameter is determined by the input stream and is different for every invocation of the program. These parameters cannot be set directly. CG_UNIFORM is a parameter whose value can be changed without needing to recompile the effect. CG_LITERAL means that changing it’s value will require the program to be recompiled because the parameter can be used to optimize code paths (completely removing code blocks for parts of a program if necessary). The variability of a parameter can be set using the cgSetParameterVariability method. This method is exposed in the EffectParameter::SetVariability method.

If the parameter is valid, the Effect::GetInternalValue method is used to read the default value of the parameter and store it in a local variable so the application can query the value of the parameter if necessary.

The EffectParameter::GetInternalValue Method

Depending on the parameter class and type, the parameter must be queried in a specific way. Let’s examine each parameter class.

First, we’ll take a look at the numerical parameter classes CG_PARAMETERCLASS_SCALAR, CG_PARAMETERCLASS_VECTOR, and CG_PARAMETERCLASS_MATRIX.

void EffectParameter::GetInternalValue()
{
    switch ( m_cgParameterClass )
    {
    case CG_PARAMETERCLASS_SCALAR:
    case CG_PARAMETERCLASS_VECTOR:
    case CG_PARAMETERCLASS_MATRIX:
        {
            m_iNumRows = cgGetParameterRows( m_cgParameter );
            m_iNumCols = cgGetParameterColumns( m_cgParameter );

            int iSize = ( m_iNumRows * m_iNumCols );
            assert( iSize > 0 );

            switch ( m_cgBaseType )
            {
            case CG_BOOL:
                {
                    // treat bool as int?
                    int* values = new int[iSize];
                    cgGetParameterValueic( m_cgParameter, iSize, values );
                    m_ParameterValue = values;
                }
                break;
            case CG_INT:
                {
                    int* values = new int[iSize];
                    cgGetParameterValueic( m_cgParameter, iSize, values );
                    m_ParameterValue = values;
                }
                break;
            case CG_FLOAT:
                {
                    float* values = new float[iSize];
                    cgGetParameterValuefc(m_cgParameter, iSize, values );
                    m_ParameterValue = values;
                }
                break;
            case CG_DOUBLE:
                {
                    double* values = new double[iSize];
                    cgGetParameterValuedc( m_cgParameter, iSize, values );
                    m_ParameterValue = values;
                }
                break;
            default:
                {
                    std::cerr << "EffectParameter::GetInternalValue: Unsupported parameter base type: " << cgGetTypeString(m_cgBaseType) << std::endl;
                }
                break;
            }
        }
        break;

All of these types can be handled in basically the same way using the cgGetParameterValue method. For vector and matrix types, we can query the size of the parameter using the cgGetParameterRows, and cgGetParameterColumns methods. For scalar types, these functions will always return 1, and 1 respectively so it will work for scalar types as well.

For parameters of class CG_PARAMETERCLASS_STRUCT, and CG_PARAMETERCLASS_ARRAY, we need to store the nested parameter types. For structs, we can iterate the internal struct parameters with cgGetFirstStructParameter and cgGetNextParameter.

For parameters of class CG_PARAMETERCLASS_ARRAY, we can use the cgGetArraySize method and for every index of the array, we use the cgGetArrayParameter method to get the handle to the parameter at that index.

    case CG_PARAMETERCLASS_STRUCT:
        {
            // Iterate the struct members, setting a named parameter for each one.
            CGparameter parameter = cgGetFirstStructParameter( m_cgParameter );
            while ( parameter != NULL )
            {
                // Create a new parameter and add it to the internal parameter list
                EffectParameter* pParameter = new EffectParameter( m_cgContext, parameter );
                m_NestedParameters.push_back( pParameter );
                m_NestedParametersByName.insert( ParameterMap::value_type( pParameter->GetParameterName(), pParameter ) );
                
                parameter = cgGetNextParameter(parameter);
            }
        }
        break;
    case CG_PARAMETERCLASS_ARRAY:
        {
            // Query the size of the array
            int arraySize = cgGetArraySize( m_cgParameter, 0 );
            for ( int i = 0; i < arraySize; ++i )
            {
                CGparameter parameter = cgGetArrayParameter( m_cgParameter, i );
                EffectParameter* pParameter = new EffectParameter( m_cgContext, parameter );
                m_NestedParameters.push_back( pParameter );
                m_NestedParametersByName.insert( ParameterMap::value_type( pParameter->GetParameterName(), pParameter) );
            }
        }
        break;

The struct and array parameters are stored in the m_NestedParameters vector.

This does not account for unsized arrays. The Cg shader language allows you to specify unsized arrays in the Cg program. You can then specify the size of the array using the cgSetArraySize method. If the size of an array changes, the effect will need to be recompiled since loops in program that are based on the size of an array parameter are usually un-rolled by the compiler since the array sizes are usually fixed at compile-time.

Setting the parameter is very similar to getting it. Setting the parameter values is done using the EffectParameter::Set method. I decided to use a deferred parameter setting method so that parameters are not updated to the GPU until the EffectParameter::UpdateParameter is explicitly called.

The EffectParameter::UpdateParameter Method

The EffectParameter::UpdateParameter method will update the value of the parameter in the Cg runtime so that it is used for the next render pass.

Let’s first take a look at how the numerical parameters are set.

bool EffectParameter::UpdateParameter()
{
    // Don't try to update invalid parameters
    if ( !m_bIsValid ) return false;

    bool bSuccess = true;

    try 
    {
        switch ( m_cgParameterClass )
        {
        case CG_PARAMETERCLASS_SCALAR:
        case CG_PARAMETERCLASS_VECTOR:
        case CG_PARAMETERCLASS_MATRIX:
            {
                int numElements = m_iNumRows * m_iNumCols;
                switch ( m_cgBaseType )
                {
                case CG_BOOL:
                    {
                        const int* values = boost::any_cast<int*>( m_ParameterValue );
                        cgSetParameterValueic( m_cgParameter, numElements, values );
                    }
                    break;
                case CG_INT:
                    {
                        const int* values = boost::any_cast<int*>( m_ParameterValue );
                        cgSetParameterValueic( m_cgParameter, numElements, values );                    
                    }
                    break;
                case CG_FLOAT:
                    {
                        const float* values = boost::any_cast<float*>( m_ParameterValue );
                        cgSetParameterValuefc( m_cgParameter, numElements, values );                    
                    }
                    break;
                case CG_DOUBLE:
                    {
                        const double* values = boost::any_cast<double*>( m_ParameterValue );
                        cgSetParameterValuedc( m_cgParameter, numElements, values );                    
                    }
                    break;
                }
            }
            break;

Using the boost::any type allows the parameter to be stored as a type-safe generic type that can be casted to the correct type at runtime.

To set the numerical types, we use the cgSetParameterValue method. By default, setting the Cg parameters with this method will immediately update the value in the Cg runtime. The parameter setting mode can be adjusted using the cgSetParameterSettingMode method.

To update struct and array types, we need to recursively call the nested parameter’s update method.

        case CG_PARAMETERCLASS_STRUCT:
        case CG_PARAMETERCLASS_ARRAY:
            {
                // Try to update all the nested parameters
                ParameterList::iterator iter = m_NestedParameters.begin();
                while ( iter != m_NestedParameters.end() )
                {
                    bSuccess |= (*iter)->UpdateParameter();
                    ++iter;
                }
            }
            break;

And for the sampler types, we use the cgGLSetTextureParameter method.

        case CG_PARAMETERCLASS_SAMPLER:
            {
                GLuint texID = boost::any_cast( m_ParameterValue );
                cgGLSetTextureParameter( m_cgParameter, texID );
            }
            break;

The Technique Class

Techniques are used in effects to encapsulate a particular effect rendering. Techniques can encapsulate program functionality that use particular features of the graphics API. A single effect file can define multiple techniques. To check if a technique is valid, use the cgValidateTechnique method. When a technique is loaded using the framework, the validity of the technique is checked and the user can use the Technique::IsValid method to see if the technique is valid.

The Effect class provides the method Effect::GetFirstValidTechnique to get a reference to the first valid technique that appears in the effect file.

The Technique Constructor

The main puropse of the Technique class is to store the passes that are defined in the technique. The passes for a technique are iterated using the cgGetFirstPass and cgGetNextPass method as shown in the code below.

Technique::Technique( CGcontext context, CGtechnique technique )
: m_cgContext( context )
, m_cgTechnique( technique )
{
    // Validate the technique
    m_bIsValid = ( m_cgTechnique != NULL ) && ( cgValidateTechnique(m_cgTechnique) == CG_TRUE );
    if ( m_cgTechnique != NULL )
    {
        const char* techniqueName = cgGetTechniqueName(m_cgTechnique);
        if ( techniqueName != NULL )
        {
            m_TechniquName = techniqueName;
        }

        // Get all the passes for this technique
        CGpass pass = cgGetFirstPass( m_cgTechnique );
        while ( pass != NULL )
        {
            Pass* pPass = new Pass( m_cgContext, pass );
            m_Passes.push_back(pPass);

            // Add the pass to the name map for faster lookup
            m_PassesByName.insert( PassMap::value_type( pPass->GetPassName(), pPass ) );

            pass = cgGetNextPass( pass );
        }
    }
}

Each pass defined in a technique is stored in the technique’s m_Passes vector and can be retrieved using the Technique::GetPasses method. The Technique class also provides a function to get a particular pass by name. The Technique::GetPassByName method will return a reference to a pass with the specified name if one exists, otherwise it will return an invalid pass.

The Pass Class

The Pass class defines a render pass for an effect. Each technique can consist of multiple passes. Before a pass is used, the program must call Pass::BeginPass and when it is finished, it must call Pass::EndPass.

The Pass::BeginPass and Pass::EndPass Methods

Before an effect is applied to rendered geometry, the Pass::BeginPass method must be called. This will ensure that the correct pass is used to render the geometry. All of the render states will also be updated to that of the pass. When the rendering is finished, the Pass::EndPass method must be called to unbind the pass and reset the render states defined in the pass.

bool Pass::BeginPass()
{
    if ( m_bIsValid )
    {
        cgSetPassState( m_cgPass );
    }
    return m_bIsValid;
}

void Pass::EndPass()
{
    if ( m_bIsValid )
    {
        cgResetPassState( m_cgPass );
    }
}

The pass is bound to the rendering API using the cgSetPassState method and unbound using the cgResetPassState method.

Sample Cg Application

Now that we’ve seen the primary components of the framework, let’s see how we can combine everything into a sample application.

For this demo, I have created an Application class that is similar to using GLUT in that it will create and initialize the application window and invoke callback methods to all the user to handle updates, rendering, and keyboard and mouse input. For this demo, I will not go into a lot of details about the application class but I will just show the implementation of the effect framework that can be used to render geometry using shaders.

I will show two simple scenes. The first scene simply renders a triangle that changes color as time progresses, and the second scene renders a textured triangle.

Headers and Forward Declarations

Our program starts with the header files that are used by our application.

#include "EffectTemplatePCH.h"
#include "Application.h"
#include "ElapsedTime.h"
#include "EffectManager.h"
#include "Effect.h"
#include "EffectParameter.h"
#include "Technique.h"
#include "Pass.h"

#include "Events.h"

To speed up compile times, I usually use pre-compiled headers in my programs. The “EffectTemplatePCH.h” header file includes all the 3rd party (static) header files that are used by the program.

The “Application.h” file defines the application class that I will use to create the render window, initialize OpenGL and register event callbacks.

The “ElapsedTime.h” header defines a simple class that can be used to query the time step between updates.

The “EffectManager.h”, “Effect.h”, “EffectParameter.h”, “Technique.h”, and “Pass.h” define the classes that were described earlier.

For the second scene, a static texture is defined into a GLubyte array. This example texture comes directly from the example code installed with the Cg toolkit.

static const GLubyte
myDemonTextureImage[3*(128*128)] = {
    /* RGB8 image data for a mipmapped 128x128 demon texture */
#include "demon_image.h"
};
GLuint g_DemonTextureID = 0;

The “demon_image.h” file defines the binary byte data that defines the texture. The g_DemonTextureID variable is used to store the OpenGL texture object ID for the demon texture.

The application class makes extensive use of events to callback methods defined in the main execution unit. Events are defined in the “Events.h” header file. I will not go in to too much detail about events but you are welcome to investigate the “Events.h” header file to see how they are being used.

Application g_App( "Effect Template", 1280, 720 );

void OnKeyPressed( KeyEventArgs& e );
void OnMouseButtonPressed( MouseButtonEventArgs& e );
void OnMouseMoved( MouseMotionEventArgs& e );
void OnResized( ResizeEventArgs& e );
void OnUpdate( UpdateEventArgs& e );
void OnRender( RenderEventArgs& e );

void OnInitialize( EventArgs& e );
void OnEffectLoaded( EffectLoadedEventArgs& e );
void OnTerminate( EventArgs& e );

The Application instance is created with the dimensions of the render window and several callback methods are declared that will be registered with the application.

This demo will demonstrate two simple scenes. We’ll forward declare the render methods for the two scenes as well as some global variables that will be used to keep track of the currently rendered scene.

static const int gs_iNumScenes = 2;
static int gs_iCurrentScene = 0;

void RenderScene0();
void RenderScene1();

The Main Method

In the main method, we will simply register the event callbacks and run the application.

int main( int argc, char* argv[] )
{

    // Register events
    g_App.KeyPressed += OnKeyPressed;
    g_App.MouseButtonPressed += OnMouseButtonPressed;
    g_App.MouseMoved += OnMouseMoved;
    g_App.Resize += OnResized;
    
    g_App.Render += OnRender;
    g_App.Update += OnUpdate;
    g_App.Initialized += OnInitialize;
    g_App.Terminated += OnTerminate;

    return g_App.Run();
}

The OnInitialize Method

The Application class will trigger the OnInitialize callback after the application is successfully initialized. We can use this method to initialize the effect manager and load the effects. We will also use this method to load any resources used by the application such as textures or geometry.

void OnInitialize( EventArgs& e )
{
    // Create an effect manager
    EffectManager::Create().Initialize();
    EffectManager& effectMgr = EffectManager::Get();

    effectMgr.EffectLoaded += OnEffectLoaded;

    // First load the resources that are used by the effects before the effects are loaded.
    glGenTextures( 1, &g_DemonTextureID );
    glBindTexture(GL_TEXTURE_2D, g_DemonTextureID);

    /* Load demon decal texture with mipmaps. */
    gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB8, 128, 128, GL_RGB, GL_UNSIGNED_BYTE, myDemonTextureImage );

    // Load the effects
    effectMgr.CreateEffectFromFile( "Shaders/C3E1_anycolor.cgfx", "C3E1_anycolor" );
    effectMgr.CreateEffectFromFile( "Shaders/C3E3_texture.cgfx", "C3E3_texture");    
}

Before we can use the EffectManager, we have to create and initialize the singleton instance.

To retrieve the instance of the EffectManager class, we use the EffectManager::Get static method.

When an effect is loaded (or reloaded), the EffectManager will invoke the EffectManager::EffectLoaded event. We have to register the event before calling EffectManager::CreateEffectFromFile so that we can handle the event the first time the effects are loaded.

On line 64, we also load the texture that will be used to render the texture mapping demo. We have to load the resources that will be used in the effect before the effect is created so that we can set the effect’s properties in the OnEffectLoaded method.

To load and create an effect instance, we use the EffectManager::CreateEffectFromFile method. The first parameter to this method is the file path to the CgFX file on disc and the second parameter is the unique name that will be used to lookup the effect later in the program.

The OnEffectLoaded Method

When an effect is loaded (or reloaded) by the EffectManager class, the OnEffectLoaded method is invoked passing a reference to an EffectLoadedEventArgs parameter which describes the effect that was loaded (or reloaded).

void OnEffectLoaded( EffectLoadedEventArgs& e )
{
    // Set the properties for the effects
    Effect& effect = e.Effect;
    if ( effect.GetEffectName() == "C3E3_texture" )
    {
        EffectParameter& textureParameter = effect.GetParameterByName("decalSampler");
        textureParameter.Set( g_DemonTextureID );
        textureParameter.UpdateParameter();
    }
}

If the “C3E3_texture” effect was just loaded, we need to tell the effect which texture to use for the “decalSampler” effect parameter.

On line 83, the effect parameter is committed to the Cg runtime using the EffectParameter::UpdateParameter method.

The OnUpdate Method

The OnUpdate method will be invoked whenever the application doesn’t have any other events to handle. For this demo, there aren’t any variables to update but in order to support the auto-loading of effect files, we have to tell the EffectManager to check if any of the loaded effects needs to be reloaded. We do this by calling the EffectManager::ReloadEffects method. This will only reload effects who’s effect file has been modified since the effect was last loaded.

void OnUpdate( UpdateEventArgsamp; e )
{
    static float fReloadTimer = 0.0f;

    // Every two seconds, we'll check to see if the effects need to be reloaded.
    fReloadTimer += e.ElapsedTime;
    if ( fReloadTimer > 2.0f )
    {
        EffectManager::Get().ReloadEffects();
        fReloadTimer = 0.0f;
    }
}

The OnRender Method

Depending on which scene is currently selected, the OnRender method will render either the first or second scene.

void OnRender( RenderEventArgs& e )
{
    glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );

    switch ( gs_iCurrentScene )
    {
    case 0:
        {
            RenderScene0();
        }
        break;
    case 1:
        {
            RenderScene1();
        }
        break;
    }

    g_App.Present();
}

We first clear the scene using the glClear method and render the appropriate scene.

After the scene is loaded we call Application::Present method to flip the front-buffer to the back-buffer and present the scene.

The RenderScene0 Method

The first scene defines a single uniform variable of type float4 called “gConstantColor” that is used to determine the vertex color of the rendered geometry. We can get a reference to the uniform property using the Effect::GetParameterByName method.

void RenderScene0()
{
    static ElapsedTime elapsedTime;
    static float fTotalTime = 0;
    float fElapsedTime = elapsedTime.GetElapsedTime();

    fTotalTime += fElapsedTime;

    Effect& greenEffect = EffectManager::Get().GetEffect( "C3E1_anycolor" );
    EffectParameter& constantColor = greenEffect.GetParameterByName("gConstantColor");
    if ( constantColor.IsValid() )
    {
        float red = ( sinf( fTotalTime ) + 1.0f ) * 0.5f;
        float green = ( cosf( fTotalTime ) + 1.0f ) * 0.5f;

        glm::vec4 color( red, green, 0.0f, 1.0f );
        constantColor.Set( color );
        constantColor.UpdateParameter();
    }

    Technique& technique = greenEffect.GetFirstValidTechnique();

    technique.BeginTechnique();
    foreach( Pass* pass, technique.GetPasses() )
    {        
        if ( pass->BeginPass() )
        {
            /* Rendering code verbatim from Chapter 1, Section 2.4.1 "Rendering
            a Triangle with OpenGL" (page 57). */
            glBegin(GL_TRIANGLES);
            {
                glVertex2f(-0.8f, 0.8f);
                glVertex2f(0.8f, 0.8f);
                glVertex2f(0.0f, -0.8f);
            }
            glEnd();

            pass->EndPass();
        }
    }
}

On line 179 we retrieve a reference to the effect using the EffectManager::GetEffect method. Next, we query the effect for the global parameter named “gConstantColor” and if the parameter is valid, we set the value of the parameter using the EffectParameter::Set method. To ensure the value is updated in the Cg runtime, we also have to commit the parameter using the EffectParameter::UpdateParameter method.

To render the geometry, we need to get a reference to a valid technique defined in the effect. To get the first valid technique for our platform we use the Effect::GetFirstValidTechnique method.

Each technique will define one or more passes. To access all of the passes of a technique, we can use the Technique::GetPasses method which returns a vector of type Pass.

To iterate over the passes, we can use the foreach macro which is an alias for the BOOST_FOREACH macro. Of course you can just get the passes vector iterator yourself and loop through the list of passes, but I prefer the foreach as it is much neater and readable.

The pass is bound to the rendering API using the Pass::BeginPass method. Once the pass is bound and all the render states updated, the geometry is rendered normally using OpenGL calls. The geometry needs to be rendered once for each pass you want to use for the effect.

After the geometry has been rendered, the pass is ended with the Pass::EndPass method.

The result should be a triangle that changes color from green to red.

Cg Runtime - Example 1

Cg Runtime - Example 1

I know this is not very exciting. Maybe we can improve it by adding textures.

The RenderScene1 Method

The second simple scene renders the same triangle, except this triangle is textured using the demon texture that was previously loaded. Since the sampler parameter is not changing, we only need to set and update the parameter once (which was done in the OnEffectLoaded method shown above).

void RenderScene1()
{
    Effect& textureEffect = EffectManager::Get().GetEffect( "C3E3_texture" );
    Technique& technique = textureEffect.GetFirstValidTechnique();

    technique.BeginTechnique();
    foreach( Pass* pass, technique.GetPasses() )
    {        
        if ( pass->BeginPass() )
        {
            glBegin(GL_TRIANGLES);
            {
                glTexCoord2f(0.0f, 0.0f);
                glVertex2f(-0.8f, 0.8f);

                glTexCoord2f(1.0f, 0.0f);
                glVertex2f(0.8f, 0.8f);

                glTexCoord2f(0.5f, 1.0f);
                glVertex2f(0.0f, -0.8f);
            }
            glEnd();

            pass->EndPass();
        }
    }
}

This is pretty much the same method except we have to provide texture coordinates for each vertex. In the effect file the texture coordinate will be used to “sample” the color of the texture for every pixel that is rendered on the triangle.

The result should be a texture that looks like the demon image shown below.

Cg Runtime - Example 2

Cg Runtime - Example 2

Now that’s what I call scary!

References

The Cg Tutorial

The Cg Tutorial

The Cg Tutorial: The Definitive Guide to Programmable Real-Time Graphics (2003). Randima Fernando and Mark J. Kilgard. Addison Wesley.

Download the Source

You can download the source for this demo including the Visual Studio 2008 project and solution files from the link below.

[EffectFramework.zip]

3 thoughts on “Introduction to Cg Runtime with OpenGL

  1. If you were looking for an article about Cg shader programming. Stay tuned. I will be posting several articles about creating shaders in the the Cg shader programming language and showing the back-end runtime code to implement those shaders in C++ (using OpenGL).

  2. You’re using CgFX and not Cg? I love you! CgFX is so much more powerful, yet can be slower than regular Cg unfortunately. But in combination with the right profiles and state handlers this can be optimized severely.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>