Chapter 13. Shaders

Recent graphics hardware allows you to write vertex and fragment programs to replace the corresponding fixed functionality of earlier systems. An independently compiliable unit of such programs is referred to as a shader. OpenGL Performer supports the OpenGL Shading Language (GLSL), which provides a platform-independent interface for such programs.

This chapter describes the OpenGL Performer interface to GLSL. This implementation is based on two new classes, which are described in the following sections:

The pfShaderProgram Class

The pfShaderProgram class encapsulates the functionality associated with OpenGL shader programs. A pfShaderProgram is a comprised of a collection of pfShaderObjects and an collection of uniform variables that are an opaque type and can only be accessed through an index from the pfShaderProgram interface.

This section covers the following topics:

Allocating Memory for a Shader Program

 The function pfNewShaderObject() creates and returns a handle to a pfShaderProgram. The value arena specifies a malloc() arena out of which the pfShaderProgram is allocated or NULL for allocation off the process heap. You can delete pfShaderPrograms with pfDelete().

The function new(arena) allocates a pfShaderProgram from the specified memory arena, or from the heap if arena is NULL. The function allocates a pfShaderProgram from the default memory arena (see the pfGetSharedArena man page). Like other pfObjects, pfShaderPrograms cannot be automatically created statically on the stack or in arrays. Delete pfShaderPrograms with pfDelete() rather than with the delete operator.

The function pfGetShaderObjectClassType() returns the pfType* for the class pfShaderProgram. The pfType* returned by pfGetShaderObjectClassType() is the same as the pfType* returned by invoking pfGetType(), the virtual function getType() on any instance of class pfShaderProgram. Because OpenGL Performer allows subclassing of built-in types when decisions are made based on the type of an object, use pfIsOfType() the member function isOfType() to test if an object is of a type derived from an OpenGL Performer type rather than to test for strict equality of the pfType*s.

In order to use a pfShaderProgram as a piece of state, you must specify it as an attribute for a pfGeoState and enable that mode, as shown in the following code:

pfGeoState *gState = pfNewGState(arena);
pfShaderProgram *sProg = pfNewSProg(arena);

pfGStateMode(gState, PFSTATE_ENSHADPROG, PF_ON);
pfGStateAttr(gState, PFSTATE_SHADPROG, sProg);

Creating a Shader Program

Creating a valid pfShaderProgram involves the following steps:

  • Adding a set of pfShaderObjects to the shader program

  • Specifying uniform variables (optional)

  • Clamping uniform variables (optional)

  • Normalizing uniform variables (optional)

Adding pfShaderObjects to a Shader program

For shader programs, you can add, replace, and delete pfShaderObjects with the following functions, respectively:

pfSProgAddShader()

pfSProgReplaceShader()

pfSProgRemoveShader()

In addition to the methods for adding/deleting/replacing shader objects from a shader program, you can query an existing pfShaderProgram to determine the number of associated shader objects with pfGetSProgNumShaders(). To retreive a pointer to the ith shader object, use pfGetSProgShader().

Specifying Uniform Variables (optional)

In addition to specifying a set of pfShaderObjects for a pfShaderProgram, you can specify a set of uniform variables for the program. These uniforms, which are not necessarily the same as those set internally by OpenGL, can then be referenced by index.

A uniform variable is comprised of the following:

  • A name (stored as a GLcharARB*)

  • A type

  • A value indicating the number of variables of that type being stored in the uniform

  • A flag indicating if the value is to be clamped to a minimum and/or maximum value (or not at all)

  • A flag indicating if the value should be normalized (for those types which can be normalized)

  • Pointers to the current value (as well as the minimum and maximum values for this uniform variable if, indeed, it is clamped)

Table 13-1 shows the uniform variable types.

Table 13-1. Uniform Variable Types

Type

Description

PFUNI_FLOAT1

Single UGLfloat

PFUNI_FLOAT2

Array of two GLfloats

PFUNI_FLOAT3

Array of three GLfloats

PFUNI_FLOAT4

Array of four GLfloats

PFUNI_INT1

Single GLint

PFUNI_INT2

Array of two GLints

PFUNI_INT3

Array of three GLints

PFUNI_INT4

Array of four GLints

PFUNI_BOOL1

Single GLint specifying boolean value

PFUNI_BOOL2

Array of two GLints specifying boolean values

PFUNI_BOOL3

Array of three GLints specifying boolean values

PFUNI_BOOL4

Array of four GLints specifying boolean values

PFUNI_MAT2

Four GLfloats specifying 2x2 matrix

PFUNI_MAT3

Nine GLfloats specifying 3x3 matrix

PFUNI_MAT4

Sixteen GLfloats specifying 4x4 matrix

PFUNI_SAMP1D

Single GLint specifying which texture unit to query for this sampler

PFUNI_SAMP2D

Single GLint specifying which texture unit to query for this sampler

PFUNI_SAMP3D

Single GLint specifying which texture unit to query for this sampler

PFUNI_SAMPCUBE

Single GLint specifying which texture unit to query for this sampler

PFUNI_SAMP1DSHADOW

Single GLint specifying which texture unit to query for this sampler

PFUNI_SAMP2DSHADOW

Single GLint specifying which texture unit to query for this sampler


Adding Uniform Variables to Shader Programs

In order to add a uniform variable to a pfShaderProgram, use pfSprogAddUniform(), whose parameters follow:

name 

Specifies the name for the uniform variable.

uniType 

Specifies one of the types listed in Table 13-1.

size 

Indicates how many variables of this type will be found in the fourth and final parameter data.

Internally, OpenGL Performer will make a copy of this data if it is not a pointer to a pfMemory; if it is, then the reference count for this piece of memory will get incremented and no copy will be performed.

For example, the following code adds a uniform variable called scaleFactor, which is a 4-byte float (size of a GLfloat) set to 0.5:

int uniformIndex;
GLfloat scale = 0.5f;
pfShaderProgram *sProg = pfNewSprog(arena);

uniformIndex = pfSProgAddUniform(sProg, "scaleFactor", PFUNI_FLOAT1, 1, &scale);

Clamping Uniform Variables (optional)

You can set a flag to indicate whether or not a uniform variable should be clamped and if it should be normalized. A uniform variable can be clamped by using pfSProgClampMode() with any one of the following parameters:

  • PFUNI_CLAMP_NONE

  • PFUNI_CLAMP_MIN

  • PFUNI_CLAMP_MAX

  • PFUNI_CLAMP_ALL

The default value is PFUNI_CLAMP_NONE. In order to set the clamp value, call pfSProgUniformMin() or pfSProgUniformMax(). The following code shows an example:

GLfloat minValue = 0.0f;
GLfloat maxValue = 1.0f;

pfSProgUniformMin(sProg, uniformIndex, &minValue);
pfSProgUniformMax(sProg, uniformIndex, &maxValue);

In order to query the minimum and maximum values, use pfGetSProgUniformMin() and pfGetSProgUniformMax(). In order to determine if the value is being clamped used, you can use pfGetSProgClampMode() and check the return value.

Normalizing Uniform Variables (optional)

For uniform variables of type PFUNI_FLOAT2, PFUNI_FLOAT3, or PFUNI_FLOAT4, it is also possible to normalize the values such that they correspond to vectors of size 1.0. In order to do this, you must use pfSProgNormalizeFlag() and this flag may also be queried for a given uniform variable with pfGetSProgNormalizeFlag(). By default, uniform variables are not normalized. If this flag is set on a uniform variable of a type other than one that supports this feature, the flag will be ignored and a warning will be issued at run time.

Applying Shader Programs

You can apply a pfShaderProgram using pfShaderProgramApply(), but only in the draw process. When this operation is performed, OpenGL Performer will determine if the shader program needs to be recompiled and perform that operation if required. It is also possible to force compilation to occur by calling pfSProgLink(). This method will return the following:

Return Value 

Meaning

0 

The complilation was successful.

1 

The associated OpenGL handle is NULL or some other event occurred.

n 

The link process failed. The positive integer n indicates how many pfShaderObjects did not compile.

In the case where one would like to force OpenGL Performer to relink a pfShaderProgram once, call pfSProgForceRelink().

One can verify if a given pfShaderProgram is in a state where it is ready to be applied by calling pfSProgValidate(), which will return 1 if the program can be applied given the current state or 0, otherwise.

The OpenGL handle associated with a given pfShaderProgram can be retrieved using pfGetSProgHandle().

The pfShaderObject Class

The pfShaderObject is a class that encapsulates the functionality associated with either vertex or fragment programs used by the OpenGL Shading Language.

A pfShaderObject is represented by a string containing the source code and a shader type. A collection of pfShaderObjects can be assembled to form a valid pfShaderProgram, which can then be used as a piece of state used by pfGeoState with the PFSTATE_SHADPROG attribute.

This section describes the following topics:

Creating New Shader Objects

The function pfNewShaderObject() creates a new shader object and returns a handle to a pfShaderObject. The value arena specifies a malloc() arena out of which the pfShaderObject is allocated or NULL for allocation off the process heap. You can delete pfShaderObjects with pfDelete().

The function call new(arena) allocates a pfShaderObject from the specified memory arena or from the heap if arena is NULL. The function allocates a pfShaderObject from the default memory arena (see the pfGetSharedArena man page). Like other pfObjects, pfShaderObjects cannot be automatically created statically on the stack or in arrays. Delete pfShaderObjects with pfDelete() rather than the delete operator.

The function pfGetShaderObjectClassType() returns the pfType* for the class pfShaderObject. The pfType* returned by pfGetShaderObjectClassType() is the same as the pfType* returned by invoking pfGetType(), the virtual function getType() on any instance of class pfShaderObject. Because OpenGL Performer allows subclassing of built-in types when decisions are made based on the type of an object, use  pfIsOfType(), the member function isOfType(), to test if an object is of a type derived from an OpenGL Performer type rather than to test for strict equality of the pfType*s.

Specifying Shader Objects

A pfShaderObject can be specified either by loading the source explicitly or by simply specifying the filename parameter with pfShaderObjectName(). The location for shader objects specified by filename corresponds to the semantics of pfFindFile() and, hence, the PFPATH environment variable can be used to specify the location of source files. In order to retreive the name of the current shader source, you can use pfGetShaderObjectName(). If the return srting is NULL, it means that the source is inlined, not loaded from an external file. If the shader source is loaded from a file, you must also call pfShaderObjectLoad() in order to load the source code into the shader object.

The source code for the shader object can also be specified explicitly with pfShaderObjectSource(). The corresponding get method is pfGetShaderObjectSource().

Specifying the Object Type

In addition to setting the source in the form of either a filename or an ASCII string, you must also specify the shader type for a pfShaderObject. By default, this is set to –1 (invalid) and it must be set to either PFSHAD_FRAGMENT_SHADER or PFSHD_VERTEX_SHADER with pfShaderObjectShaderType(). You can also retrieve the shader type for a given pfShaderObject with pfGetShaderObjectShaderType().

If either the source code for the shader object or the type for the shader object have changed, the compilation status for the shader object will change. One can determine the necessity for recompiling a given pfShaderObject by calling pfGetShaderObjectCompileStatus(), which will return 1 if recompilation is required and 0, otherwise. This is used internally to determine if a pfShaderProgram needs to be re-linked and, hence, should not normally be called from a user-specified program.

Compiling Shader Objects

A pfShaderObject may be compiled with pfShaderObjectCompile(). The log for the compilation process will be stored in the log parameter. If successful, the compilation will return 1 and 0, otherwise.

Once a pfShaderObject has been bound to the current graphics context, you can retrieve its GL handle with pfGetShaderObjectHandle(). The handle is created as needed during the compilation process. If the pfShaderObject has not yet been compiled, then the handle will be NULL.

Example Code

The following example illustrates the use of the pfShaderProgram and pfShaderObject classes.

#include <Performer/pr/pfGeoState.h>
#include <Performer/pr/pfGeoArray.h>
#include <Performer/pr/pfTexture.h>
#include <Performer/pf/pfGeode.h>
#include <Performer/pr/pfShaderObject.h>
#include <Performer/pr/pfShaderProgram.h>

#include <Performer/pfdu.h>

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

  pfInit();
  pfdInitConverter("pfb");
  pfConfig();

  char *vertexShaderSource =
    "varying vec2 tc;\n\n"
    "void main() {\n"
    "  tc = gl_MultiTexCoord0.xy;\n"
    "  gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n"
    "}\n";

  FILE *fp = fopen("multiTex.frag", "w");
  if(fp) {
    for(i=1; i<argc; i++) 
      fprintf(fp,"uniform sampler2D texture_%d;\n",i-1);
    fprintf(fp,"varying vec2 tc;\n");
    fprintf(fp,
            "void main()\n"
            "{\n");

    float stepSize = 1.0/(argc-1);

    fprintf(fp,"  float stepSize = %g;\n",stepSize);

    for(i=0; i<argc-1; i++) {
      fprintf(fp,
              "  if(tc.x >= stepSize*%.1f && tc.x <= stepSize*%.1f)\n"
              "    gl_FragColor = texture2D(texture_%d, vec2(tc.x*%.1f, tc.y));\n",
              float(i), float(i+1), i, float(argc-1));
      if((i+1) != (argc-1))
         fprintf(fp,"  else ");
    }
      
    fprintf(fp,"}\n");

    fclose(fp);
  } else {
    pfNotify(PFNFY_FATAL,PFNFY_PRINT,
             "Unable to open multiTex.frag for writing.");
    pfExit();
    return 1;
  }

  pfShaderObject *soV = new pfShaderObject();
  soV->setShaderType(PFSHD_VERTEX_SHADER);
  soV->setSource(vertexShaderSource);

  pfShaderObject *so = new pfShaderObject();
  so->setShaderType(PFSHD_FRAGMENT_SHADER);
  so->setName("multiTex.frag");  

  pfShaderProgram *sp = new pfShaderProgram();
  sp->addShader(so);
  sp->addShader(soV);

  pfGeoState *gs = new pfGeoState();
  gs->setMode(PFSTATE_ENSHADPROG, PF_ON);
  gs->setAttr(PFSTATE_SHADPROG, sp);

  for(i=1; i<argc; i++) {
    pfTexture *tex = new pfTexture();
    
    tex->setName(argv[i]);

    int uniValue = i-1;
    char name[64];
    sprintf(name,"texture_%d",i-1);
    sp->addUniform(name, PFUNI_SAMP2D, 1, &uniValue);

    pfNotify(PFNFY_NOTICE,PFNFY_PRINT,"Setting texture %d to %s, named %s",
             i-1,argv[i],name);

    gs->setMultiAttr(PFSTATE_TEXTURE, i-1, tex);    
  }

  pfVec3 *verts = (pfVec3 *)pfMalloc(sizeof(pfVec3) * 4);
  
  PFSET_VEC3(verts[0], 0.f, 0.f, 0.f );
  PFSET_VEC3(verts[1], (argc-1)*1.f, 0.f, 0.f );
  PFSET_VEC3(verts[2], (argc-1)*1.f, 0.f, 1.f );
  PFSET_VEC3(verts[3], 0.f, 0.f, 1.f );

  pfVec2 *tCoords = (pfVec2 *)pfMalloc(sizeof(pfVec2) * 4);
  PFSET_VEC2(tCoords[0], 0.f, 0.f);
  PFSET_VEC2(tCoords[1], 1.f, 0.f);
  PFSET_VEC2(tCoords[2], 1.f, 1.f);
  PFSET_VEC2(tCoords[3], 0.f, 1.f);

  int lengths[1] = { 4 };

  pfGeoArray *ga = new pfGeoArray();
  ga->setNumPrims(1);
  ga->setPrimType(PFGS_TRIFANS);
  ga->setPrimLengths(lengths);
  ga->setAttr(PFGA_COORD_ARRAY, 3, GL_FLOAT, 0, verts);
  // we can use the same set of tex coords for all ...
  ga->setAttr(PFGA_TEX_ARRAY, 2, GL_FLOAT, 0, tCoords);

  ga->setGState(gs);

  pfGeode *geode = new pfGeode();
  geode->addGSet(ga);

  pfdStoreFile(geode, getenv("OUTFILE")?getenv("OUTFILE"):"multiTexShader.pfb");

  pfExit();

  return 0;
}