More Power to the Shaders

It's all well and good that we are no longer having to transform vertices manually. But perhaps we can move more things to the vertex shader. Could it be possible to move all of ComputePositionOffsets to the vertex shader?

Well, no. The call to glutGet(GL_ELAPSED_TIME) cannot be moved there, since GLSL code cannot directly call C/C++ functions. But everything else can be moved. This is what vertCalcOffset.cpp does.

The vertex program is found in data\calcOffset.vert.

Example 3.6. Offset Computing Vertex Shader

#version 330

layout(location = 0) in vec4 position;
uniform float loopDuration;
uniform float time;

void main()
{
    float timeScale = 3.14159f * 2.0f / loopDuration;
    
    float currTime = mod(time, loopDuration);
    vec4 totalOffset = vec4(
        cos(currTime * timeScale) * 0.5f,
        sin(currTime * timeScale) * 0.5f,
        0.0f,
        0.0f);
    
    gl_Position = position + totalOffset;
}

This shader takes two uniforms: the duration of the loop and the elapsed time.

In this shader, we use a number of standard GLSL functions, like mod, cos, and sin. We saw mix in the last tutorial. And these are just the tip of the iceberg; there are a lot of standard GLSL functions available.

The rendering code looks quite similar to the previous rendering code:

Example 3.7. Rendering with Time

void display()
{
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    glUseProgram(theProgram);
    
    glUniform1f(elapsedTimeUniform, glutGet(GLUT_ELAPSED_TIME) / 1000.0f);
    
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
    
    glDrawArrays(GL_TRIANGLES, 0, 3);
    
    glDisableVertexAttribArray(0);
    glUseProgram(0);
    
    glutSwapBuffers();
    glutPostRedisplay();
}

This time, we do not need any code to use the elapsed time; we simply pass it unmodified to the shader.

You may be wondering exactly how it is that the loopDuration uniform gets set. This is done in our shader initialization routine, and it is done only once:

Example 3.8. Loop Duration Setting

void InitializeProgram()
{
    std::vector<GLuint> shaderList;
    
    shaderList.push_back(Framework::LoadShader(GL_VERTEX_SHADER, "calcOffset.vert"));
    shaderList.push_back(Framework::LoadShader(GL_FRAGMENT_SHADER, "standard.frag"));
    
    theProgram = Framework::CreateProgram(shaderList);
    
    elapsedTimeUniform = glGetUniformLocation(theProgram, "time");
    
    GLuint loopDurationUnf = glGetUniformLocation(theProgram, "loopDuration");
    glUseProgram(theProgram);
    glUniform1f(loopDurationUnf, 5.0f);
    glUseProgram(0);
}

We get the time uniform as normal with glGetUniformLocation. For the loop duration, we get that in a local variable. Then we immediately set the current program object, set the uniform to a value, and then unset the current program object.

Program objects, like all objects that contain internal state, will retain their state unless you explicitly change it. So the value of loopDuration will be 5.0f in perpetuity; we do not need to set it every frame.