SFML2.6 graphics module–add special effects using shaders

Introduction

Shaders are small programs that execute on the graphics card. It provides more flexible and simpler control of the drawing process than using the fixed states and operations provided by OpenGL. With this additional flexibility, shaders are used to create effects that are too complex or impossible to describe using regular OpenGL functions: pixel-level lighting, shadows, and more. Modern graphics cards and newer versions of OpenGL are already entirely shader-based, and the fixed set of state and functions (called the “fixed pipeline”) that you may know about has been deprecated and may be removed in the future.

Shaders are written in GLSL (OpenGL Shading Language), which is very similar to the C programming language.

There are two types of shaders: vertex shaders and fragment (or pixel) shaders. Vertex shaders run on each vertex, while fragment shaders run on each resulting fragment (pixel). Depending on the effect you want to achieve, you can provide a vertex shader, a fragment shader, or both.

To understand what shaders do and how to use them efficiently, it’s important to understand the basics of the rendering pipeline. You must also learn how to write GLSL programs and find good tutorials and examples to get started. You can also check out the “shader” examples included with the SFML SDK.

This tutorial only focuses on a specific part of SFML: how to load and apply your shaders – not writing them.

Load shader

In SFML, shaders are represented by the sf::Shader class. It handles both vertex and fragment shaders: a sf::Shader object is a combination of both (or just one if the other is not provided).

Although shaders have become a common technology, there are still some older graphics cards that may not support them. Your first step in your program should be to check if your system supports shaders:

if (!sf::Shader::isAvailable())
{<!-- -->
    // shaders are not available...
}

If sf::Shader::isAvailable() returns false, any attempt to use the sf::Shader class will fail.

The most common way to load shaders is from a file on disk, which can be achieved using the loadFromFile function.

sf::Shader shader;

// load only the vertex shader
if (!shader.loadFromFile("vertex_shader.vert", sf::Shader::Vertex))
{<!-- -->
    // error...
}

// load only the fragment shader
if (!shader.loadFromFile("fragment_shader.frag", sf::Shader::Fragment))
{<!-- -->
    // error...
}

// load both shaders
if (!shader.loadFromFile("vertex_shader.vert", "fragment_shader.frag"))
{<!-- -->
    // error...
}

Shader source code is contained in simple text files (just like your C++ code). Their extension doesn’t matter, it can be whatever you want, you can even omit the extension. “.vert” and “.frag” are just examples of possible extensions.

The loadFromFile function sometimes fails for no apparent reason. First, check the error messages that SFML prints to standard output (check the console). If the message is that the file cannot be opened, make sure that the working directory (the directory to which any file paths will be interpreted relative to) is the directory you want: When you run the application from the desktop environment, the working directory is the executable folder. However, when you launch the program from an IDE (Visual Studio, Code::Blocks, …), the working directory may sometimes be set to the project directory. This can usually be easily changed in the project settings.

Shaders can also be loaded directly from strings using the loadFromMemory function. This is useful if you want to embed shader source code directly into your program.

const std::string vertexShader = \
    "void main()" \
    "{" \
    "..." \
    "}";

const std::string fragmentShader = \
    "void main()" \
    "{" \
    "..." \
    "}";

// load only the vertex shader
if (!shader.loadFromMemory(vertexShader, sf::Shader::Vertex))
{<!-- -->
    // error...
}

// load only the fragment shader
if (!shader.loadFromMemory(fragmentShader, sf::Shader::Fragment))
{<!-- -->
    // error...
}

// load both shaders
if (!shader.loadFromMemory(vertexShader, fragmentShader))
{<!-- -->
    // error...
}

Finally, like all other SFML resources, shaders can be loaded from a custom input stream using the loadFromStream function.

If loading fails, don’t forget to check the standard error output (console) to see a detailed report from the GLSL compiler.

Use shaders

Using the shader is simple, just pass it as an additional parameter to the draw function.

window.draw(whatever, & amp;shader);

Passing variables to the shader

Like other programs, a shader can accept parameters so that it behaves differently each time it is drawn. These parameters are declared as global variables, called uniform variables in the shader.

uniform float myvar;

void main()
{<!-- -->
    // use myvar...
}

Uniform variables can be set by C++ programs, using overloaded forms of the various setUniform functions in the sf::Shader class.

shader.setUniform("myvar", 5.f);

The overloads of setUniform support all types provided by SFML:

  • float (GLSL type float)
  • 2 floating point numbers, sf::Vector2f (GLSL type vec2)
  • 3 floating point numbers, sf::Vector3f (GLSL type vec3)
  • 4 floating point numbers (GLSL type vec4)
  • sf::Color (GLSL type vec4)
  • sf::Transform (GLSL type mat4)
  • sf::Texture (GLSL type sampler2D)

The GLSL compiler will optimize unused variables (“unused” here means “not involved in the final vertex/pixel calculation”). Therefore, it is normal to get the error message “Variable “xxx” cannot be found” when calling setUniform in a test.

The simplest shader

You won’t learn how to write GLSL shaders here, but it is crucial to understand what input data SFML provides to the shader and what it expects you to do.

Vertex Shader

SFML has a fixed vertex format, described by the sf::Vertex structure. SFML vertices contain 2D position, color and 2D texture coordinates. This is exactly the input you get in the vertex shader, stored in the built-in variables gl_Vertex, gl_Color and gl_MultiTexCoord0 (you don’t need to declare them).

void main()
{<!-- -->
    // transform the vertex position
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

    // transform the texture coordinates
    gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;

    // forward the vertex color
    gl_FrontColor = gl_Color;
}

Positions usually need to be transformed through model view and projection matrices, which contain the object transformation combined with the current view. The texture coordinates need to be transformed via a texture matrix (this matrix may not make sense to you, it’s just an implementation detail of SFML). Finally, the color just needs to be passed. Of course, if you are not using texture coordinates and/or colors, you can ignore them.

All these variables will be interpolated on the original object via the graphics card and passed to the fragment shader.

Fragment Shader

The fragment shader functions very similarly: it receives the texture coordinates and color of the generated fragment. At this point there is no position information left and the graphics card has already calculated the fragment’s final grid position. However, if you are working with textured entities, the current texture is also required.

uniform sampler2D texture;

void main()
{<!-- -->
    // lookup the pixel in the texture
    vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);

    // multiply it by the color
    gl_FragColor = gl_Color * pixel;
}

The current texture is not automatic, you need to handle it like any other input variable and set it explicitly from your C++ program. Since each entity can have a different texture, and worse, there may be no way to get it and pass it to the shader, SFML provides special overloads of the setUniform function that can do this for you.

shader.setUniform("texture", sf::Shader::CurrentTexture);

This special parameter automatically sets the texture of the entity being drawn to the shader variable with the given name. Each time a new entity is drawn, SFML updates the shader texture variables accordingly.

If you want to see a nice example of a shader, you can check out the Shader examples in the SFML SDK.

Using sf::Shader in OpenGL code

If you are using OpenGL instead of SFML’s graphics entities, you can still use sf::Shader as a wrapper around the OpenGL program object and use it in your OpenGL code.

To activate sf::Shader for drawing (equivalent to glUseProgram), you must call the bind static function:

sf::Shader shader;
...

//bind the shader
sf::Shader::bind( & amp;shader);

// draw your OpenGL entity here...

// bind no shader
sf::Shader::bind(NULL);