Tutorial 34

Tutorial 34 - GLFX - An OpenGL Effect Library

Get the source!

Note: as of Mar-2014 I'm no longer using GLFX in the tutorials. I'm keeping this tutorial for people interested in using the library.

Background

This tutorial is going to be a bit different than the previous ones. Instead of exploring an OpenGL feature or 3D technique we are going to take a look at GLFX, an effect library for OpenGL. An effect is a text file that can potentially contain multiple shaders and functions and makes it easy to combine them together into programs. This overcomes the limitation of the glShaderSource() function that requires you to specify the text of a single shader stage. This forces you to use a different text file for each shader (or different buffer as we did in previous tutorials). Placing all shaders in the same file makes it simpler to share structure definitions between them. In addition, GLFX provides an easy to use API to translate effect files into GLSL programs which hides some of the complexity of the underlying OpenGL functions.

The idea of effect files is not new. Indeed, Microsoft has had this for years in the DirectX world. I'm sure that gaming studios have their own tools developed inhouse but it's a shame that this has not yet been standardized in OpenGL. The effect library that we will use is an open source project that was created by Max Aizenshtein. You can find the project homepage here.

To install GLFX simply check out the sources and build them by running the following from the command line:

Note: GLFX is dependant on GLEW. This is no problem if you are using the tutorials as a framework or already using GLEW in your application. If not, you can turn to tutorial 2 for information on how to initialize GLEW.

Code Walkthru

Integrating GLFX into the project

Add the following to get access to GLFX api:

#include <glfx.h>

Generate an effect handle:

int effect = glfxGenEffect();

Parse the effect file (we will take a look at its content momentarily):

if (!glfxParseEffectFromFile(effect, "effect.glsl")) {
#ifdef __cplusplus // C++ error handling
    std::string log = glfxGetEffectLog(effect);
    std::cout << "Error parsing effect: " << log << std::endl;
#else // C error handling
    char log[10000];
    glfxGetEffectLog(effect, log, sizeof(log));
    printf("Error parsing effect: %s:\n", log);
#endif
    return;
}

Compile a program (combination of VS, FS, etc) defined in the effect file using the following:

int shaderProg = glfxCompileProgram(effect, "ProgramName");

if (shaderProg < 0) {
    // same error handling as above
}

The program can now be used by OpenGL as usual:

glUseProgram(shaderProg);

After the effect is no longer needed release its resources using:

glfxDeleteEffect(effect);

Using GLFX

Now that we have the basic infrastructure in place let's dive into the effect files. The nice thing about GLFX is that you can continue writing GLSL shaders in pretty much the same way that you are used to. There are a few minor changes and additions and we are going to focus on them.

Place a 'program' section to combine shader stages into a complete GLSL program

program Lighting
{
    vs(410)=VSmain();
    fs(410)=FSmain();
};

In the example above the effect file contains the definition of the functions VSmain() and FSmain() somewhere else. The 'program' section defines an OpenGL program called 'Lighting'. Calling glfxCompileProgram(effect, "Lighting") will cause a compilation and linkage of VSmain() and FSmain() into a single program. Both shaders will be compiled in version 4.10 of GLSL (same as declaring '#version 410' in standard GLSL).

Use 'shader' instead of 'void' to declare main shader functions

The main entry points to shader stages must be declared as 'shader' instead of 'void'. Here's an example:

void calculate_something()
{
    ...
}

shader VSmain()
{
    calculate_something();
}

Include multiple shaders and program in a single effect file

You can place multiple occurrences of the 'program' section in a single effect file. Simply call glfxCompileProgram() for each program that you want to use.

Use structures to pass vertex attributes between shader stages

Instead of defining the in/out variables in the global section of the shader we can use GLSL structures and share them across multiple shader stages. Here's an example:

struct VSoutput
{
    vec2 TexCoord;
    vec3 Normal;
};

shader VSmain(in vec3 Pos, in vec2 TexCoord, in vec3 Normal, out VSOutput VSout)
{
    // do some transformations and update 'VSout'
    VSout.TexCoord = TexCoord;
    VSout.Normal = Normal;
}

shader FSmain(in VSOutput FSin, out vec4 FragColor)
{
    // 'FSin' matches 'VSout' from the VS. Use it
    // to do lighting calculations and write the final output to 'FragColor'
}

Unfortunately, using a structure will only work between shader stages. Input variables to the VS must be handled as separate attributes as we see in the above example. Well, I have an NVIDIA card and input structures to the VS work for me but this is not explicitly allowed by the GLSL spec and many readers have informed me that it doesn't work for them. If it works - great. If not, simply go with the above code.

Use include files to share common functionality between effect files

The keyword 'include' can be used to include one effect file in another:

#include "another_effect.glsl"

The caveat with include files is that they are not parsed by GLFX. They are simply inserted as-is into the including file at the location of the 'include' keyword. This means that you can only place pure GLSL code in them and not GLFX-only keywords such as program/etc. Tip: since part of GLSL syntax is the same as C/C++ (e.g. #define) you can even share definitions between the effect file and your application code.

Use structure suffix to define attribute locations

In the previous tutorials we have used the 'layout(location = ...)' keyword to define the location of an input attribute of the VS. By placing a colon followed by a number after an input VS parameter we can achieve the same goal. Here's an example:

struct VSInput2
{
    vec3 Normal;
    vec3 Tangent;
};

shader VSmain(in vec3 Pos : 5, in vec2 TexCoord : 6, in float colorScale : 10)

The VS above gets the position in attribute 5, the texture coordinate in 6 and the color scale in 10. The idea is very simple - the number after the colon determines the location. If there is no location suffix the attributes simply start at zero.

Use 'interface' instead of 'struct' to place qualifiers on members

GLSL provides qualifiers such as 'flat' and 'noperspective' that can be placed before attributes that are sent from the VS to the FS. These qualifiers cannot be used on structure members. The solution that GLFX provides is a new keyword called 'interface' that enables what 'struct' does not. An 'interface' can only be passed between shader stages. If you need to pass it as a whole to another function you will need to copy the contents to a struct. For example:

interface foo
{
    flat int a;
    noperspective float b;
};

struct bar
{
    int a;
    float b;
}

shader VSmain(out foo f)
{
    // ...
}

void Calc(bar c)
{
    // ...
}

shader FSmain(in foo f)
{
    struct bar c;
    c.a = f.a;
    c.b = f.b;

    Calc(c);
}

Note: 'interface' is a keyword reserved for future use (according to OpenGL 4.2). Its usage in the future in GLFX will be based on changes to the official OpenGL spec.

Tip: use 'glfxc' to verify effect files

'glfxc' is a utility which is part of GLFX. It parses effect files, compiles them using the local OpenGL installation and reports any error it finds. Run it as follows:

glfxc <effect file name> <program name>

The Demo

The code of this tutorial has been modified to work with GLFX. Since the changes are very simple I won't go over them here. You should take a look at the source, in particular the classes Technique and LightingTechnique. In addition, the shaders that used to be part of 'lighting_technique.cpp' have been removed and there is an effect file called 'lighting.glsl' in the 'shaders' subdirectory. This file contains the same shaders that you are already familiar with. They have been modified slightly to fit the rules above.