Multiple Shaders

Well, moving the triangle around is nice and all, but it would also be good if we could do something time-based in the fragment shader. Fragment shaders cannot affect the position of the object, but they can control its color. And this is what fragChangeColor.cpp does.

The fragment shader in this tutorial is loaded from the file data\calcColor.frag:

Example 3.9. Time-based Fragment Shader

#version 330

out vec4 outputColor;

uniform float fragLoopDuration;
uniform float time;

const vec4 firstColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
const vec4 secondColor = vec4(0.0f, 1.0f, 0.0f, 1.0f);

void main()
{
    float currTime = mod(time, fragLoopDuration);
    float currLerp = currTime / fragLoopDuration;
    
    outputColor = mix(firstColor, secondColor, currLerp);
}

This function is similar to the periodic loop in the vertex shader (which did not change from the last time we saw it). Instead of using sin/cos functions to compute the coordinates of a circle, interpolates between two colors based on how far it is through the loop. When it is at the start of the loop, the triangle will be firstColor, and when it is at the end of the loop, it will be secondColor.

The standard library function mix performs linear interpolation between two values. Like many GLSL standard functions, it can take vector parameters; it will perform component-wise operations on them. So each of the four components of the two parameters will be linearly interpolated by the 3rd parameter. The third parameter, currLerp in this case, is a value between 0 and 1. When it is 0, the return value from mix will be the first parameter; when it is 1, the return value will be the second parameter.

Here is the program initialization code:

Example 3.10. More Shader Creation

void InitializeProgram()
{
    std::vector<GLuint> shaderList;
    
    shaderList.push_back(Framework::LoadShader(GL_VERTEX_SHADER, "calcOffset.vert"));
    shaderList.push_back(Framework::LoadShader(GL_FRAGMENT_SHADER, "calcColor.frag"));
    
    theProgram = Framework::CreateProgram(shaderList);

    elapsedTimeUniform = glGetUniformLocation(theProgram, "time");
    
    GLuint loopDurationUnf = glGetUniformLocation(theProgram, "loopDuration");
    GLuint fragLoopDurUnf = glGetUniformLocation(theProgram, "fragLoopDuration");
    
    
    glUseProgram(theProgram);
    glUniform1f(loopDurationUnf, 5.0f);
    glUniform1f(fragLoopDurUnf, 10.0f);
    glUseProgram(0);
}

As before, we get the uniform locations for time and loopDuration, as well as the new fragLoopDuration. We then set the two loop durations for the program.

You may be wondering how the time uniform for the vertex shader and fragment shader get set? One of the advantages of the GLSL compilation model, which links vertex and fragment shaders together into a single object, is that uniforms of the same name and type are concatenated. So there is only one uniform location for time, and it refers to the uniform in both shaders.

The downside of this is that, if you create one uniform in one shader that has the same name as a uniform in a different shader, but a different type, OpenGL will give you a linker error and fail to generate a program. Also, it is possible to accidentally link two uniforms into one. In the tutorial, the fragment shader's loop duration had to be given a different name, or else the two shaders would have shared the same loop duration.

In any case, because of this, the rendering code is unchanged. The time uniform is updated each frame with FreeGLUT's elapsed time.

Globals in shaders. Variables at global scope in GLSL can be defined with certain storage qualifiers: const, uniform, in, and out. A const value works like it does in C99 and C++: the value does not change, period. It must have an initializer. An unqualified variable works like one would expect in C/C++; it is a global value that can be changed. GLSL shaders can call functions, and globals can be shared between functions. However, unlike in, out, and uniforms, non-const and const variables are not shared between stages.