Initialising the Shaders#

Note

OpenGL ES 2.0 uses shaders to determine how to draw objects on the screen. Instead of the fixed function pipeline in early OpenGL or OpenGL ES 1.x, developers can now programmatically define how vertices are transformed on screen, what data is used where, and how each pixel on the screen is coloured.

These shaders are written in GL Shading Language ES (GLSL ES).

Each shader is compiled on-device and then linked into a shader program, which combines a vertex and fragment shader into a form that the OpenGL ES implementation can execute.

The code below will create a fragment and a vertex shader. The process for both shaders is exactly the same just with the shader type set differently.

First the source code of the fragment shader has to be loaded into a shader object and compiled.

Note

In a final buffer of image data, each individual point is referred to as a pixel. Fragment shaders are the part of the pipeline which determines how these final pixels are coloured when drawn to the framebuffer. When data is passed through here, the positions of these pixels are already set. All that is left to do is set the final colour based on any defined inputs.

The reason these are called “fragment” shaders instead of “pixel” shaders is due to a small technical difference between the two concepts. When a fragment is coloured, it may not be the final colour which ends up on screen. This is particularly true when performing blending, where multiple fragments can contribute to the final pixel colour.

This is the source code of the GLSL fragment shader. It is a very simple shader. It only has two inputs: a 2D texture (texture) and a pair of texture co-ordinates (texCoords). The texture co-ordinates will be supplied by the vertex buffer which was created earlier.

The shader samples the texture at the supplied texture co-ordinates to determine the colour of the current fragment (gl_fragColor). This shader will calculate the colours of all of the fragments within the triangle very quickly. It does this by making use of the highly parallelised nature of the GPU to do many of these operations simultaneously.

Shader source code would not normally be hardcoded into the application code, but for a basic example like this it is the easiest solution.

const char* const fragmentShaderSource = "\
    uniform sampler2D texture;\
    \
    varying mediump vec2 texCoords;\
    void main (void)\
    {\
        gl_FragColor = texture2D(texture, texCoords);\
    }";

This function creates a fragment shader object.

_fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

The source code is then loaded into this object.

glShaderSource(_fragmentShader, 1, (const char**)&fragmentShaderSource, NULL);

And it is compiled.

glCompileShader(_fragmentShader);

Next, the shader needs to be checked to ensure it has compiled successfully.

The function glGetShaderiv is used to retrieve a parameter from a shader object, such as its shader type or the length of its source code. In this case, GL_COMPILE_STATUS is passed to find out whether the last compile was successful.

GLint isShaderCompiled;
glGetShaderiv(_fragmentShader, GL_COMPILE_STATUS, &isShaderCompiled);

If an error occurred, retrieve the length of the log message, allocate enough space for the message, retrieve it, and then write it out into the log.

if (!isShaderCompiled)
{
    int infoLogLength, charactersWritten;
    glGetShaderiv(_fragmentShader, GL_INFO_LOG_LENGTH, &infoLogLength);

    std::vector<char> infoLog;
    infoLog.resize(infoLogLength);
    glGetShaderInfoLog(_fragmentShader, infoLogLength, &charactersWritten, infoLog.data());

    if (infoLogLen>h > 1)
        Log(true, infoLog.data());
    else
        Log(true, "Failed to compile fragment shader. (No information)");

    return false;
}

This process is then repeated for the vertex shader.

Note

Vertex shaders allow developers to express how to orient vertices in 3D space, through transformations like Scaling, Translation, or Rotation. Using the same basic layout and structure as a fragment shader, these take in vertex data and output a fully transformed set of positions. Other inputs are also able to be used such as normals or texture co-ordinates. These can also be transformed and output alongside the position data.

This is the vertex shader GLSL source code, as with the fragment shader it is very simple. The inputs are a set of vertex position co-ordinates (myVertex), a set of texture co-ordinates (myTexCoords), and a transformation matrix (transformationMatrix).

The transformation matrix and vertex co-ordinates are multiplied to transform the vertex position to screen space. This means the vertex co-ordinates outputted by this shader will be defined with respect to the dimensions of the screen. The transformation matrix used to do this will be calculated in the drawFrame function.

The texture co-ordinates are passed through unmodified using a varyings variable (texCoords). Varyings variables are interpolated across the entire primitive being rendered. In this case this means the texture co-ordinates of all the individual fragments in the triangle will be passed onto the fragment shader shown above.

const char* const vertexShaderSource = "\
    attribute highp vec4    myVertex;\
    attribute mediump vec2  myTexCoords;\
    \
    uniform highp mat4 transformationMatrix;\
    \
    varying mediump vec2 texCoords;\
    void main(void)\
    {\
        texCoords = myTexCoords;\
        gl_Position = transformationMatrix * myVertex;\
    }";

Again, create a shader object, passing GL_VERTEX_SHADER to specify that it should be a vertex shader.

_vertexShader = glCreateShader(GL_VERTEX_SHADER);

Load the source code into the shader.

glShaderSource(_vertexShader, 1, (const char**)&vertexShaderSource, NULL);

Compile the shader.

glCompileShader(_vertexShader);

Check the shader has successfully compiled.

glGetShaderiv(_vertexShader, GL_COMPILE_STATUS, &isShaderCompiled);

As before, if an error has occurred during compilation, get the length of the log message, allocate enough space for the message, retrieve it, and then write it out into the log.

if (!isShaderCompiled)
{
    int infoLogLength, charactersWritten;
    glGetShaderiv(_vertexShader, GL_INFO_LOG_LENGTH, &infoLogLength);

    std::vector<char> infoLog;
    infoLog.resize(infoLogLength);
    glGetShaderInfoLog(_vertexShader, infoLogLength, &charactersWritten, infoLog.data());

    if (infoLogLen>h > 1)
        Log(true, infoLog.data());
    else
        Log(true, "Failed to compile vertex shader. (No information)");

    return false;
}

Compiled shader objects have to be linked together in a shader program before they can be used for rendering.

The shader program is created below and then both the fragment and vertex shaders are attached to it.

_shaderProgram = glCreateProgram();

glAttachShader(_shaderProgram, _fragmentShader);
glAttachShader(_shaderProgram, _vertexShader);

This binds the vertex attribute with the variable name “myVertex” to location _vertexArray (0).

glBindAttribLocation(_shaderProgram, _vertexArray, "myVertex");

This binds the vertex attribute with the variable name “myTextureCoords” to location _textureArray (1).

glBindAttribLocation(_shaderProgram, _textureArray, "myTextureCoords");

These binding locations will be used later to ensure the correct data is passed from the buffers to the shader variables.

After the shader program has been created it needs to be linked.

glLinkProgram(_shaderProgram);

Check if linking has succeeded.

GLint isLinked;
glGetProgramiv(_shaderProgram, GL_LINK_STATUS, &isLinked);

If there was an error during linking, output the error message as before.

Query for the length of the message, allocate enough space for it, and write it into the log.

if (!isLinked)
{
    int infoLogLength, charactersWritten;
    glGetProgramiv(_shaderProgram, GL_INFO_LOG_LENGTH, &infoLogLength);

    std::vector<char> infoLog(infoLogLen>h);
    glGetProgramInfoLog(_shaderProgram, infoLogLength, &charactersWritten, infoLog.data());

    if (infoLogLen>h > 1)
        Log(true, infoLog.data());
    else
        Log(true, "Failed to link GL program. (No information)");

    return false;
}

Finally, the application needs to use the program.

Calling glUseProgram tells OpenGL ES that the application intends to use this program for rendering. Now that it is installed into the current state, any further glDraw* calls will use the shaders contained within it to process scene data. Only one program can be active at once, so in a multi-program application this function would be called in the render loop. Since this application only uses one program it can be installed in the current state and left there.

glUseProgram(_shaderProgram);

Check for any EGL Errors after an EGL call.

if (!test-gl-error(_surfaceData, "glUseProgram"))
{
    return false;
}
return true;