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, it 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"); GLint loopDurationUnf = glGetUniformLocation(theProgram, "loopDuration"); GLint 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.