OpenGL Shaders

Page 10 of OpenGL Programming Guide, Eighth Edition shows the rendering pipeline for OpenGL version 4.3. You will notice that there are 5 shaders shown:

  1. Vertex shader;
  2. Tessellation control shader;
  3. Tessellation evaluation shader;
  4. Geometry shader; and,
  5. Fragment shader.

Shaders

The first four shaders are used to determine the position of the various primitives on the screen and the fragment shader determines the colour of each primitive. This is a simplified view; the vertex and geometry shaders can also affect the colours, but final colour selection is performed in the fragment shader.

Vertex Shader

For each vertex issued by the drawing program, the vertex shader is called. The purpose of the vertex shader is to output the final vertex position in device coordinates and any data that the fragment shader requires. This shader may do no more that pass the vertex on to the next filter, or it may do more complex tasks such as computing the vertex’s screen position.

Tessellation Shaders

The tessellation shaders are optional. These shaders may increase the number of geometric primitives to better describe the models.

Geometry Shader

The geometry shader is also optional. This shader allows the processing, and creation if necessary, of additional geometric primitives.

Fragment Shader

The output from the previous shaders is interpolated over all of the pixels on the screen that are covered by a primitive. These pixels, or fragments, are what the fragment shader determines the final colour of. The fragment shader also determines whether a fragment should be drawn.

HelloTriangle With Vertex and Fragment Shaders

Let’s add a vertex and a fragment shader to the HelloTriangle program. When we are done, the triangle will still be the same size and colour (white), but the program changes will make it easier to make further changes in the future.

Load TriangleCanvas.h and TriangleCanvas.cpp from the HelloTriangle project. In the TriangleCanvas class, add the following method declarations in the private section of the class:

    void BuildShaderProgram();
    void BuildVertexShader();
    void BuildFragmentShader();

and the following data members:

    GLuint m_vertexShader;
    GLuint m_fragmentShader;
    GLuint m_shaderProgram;

In TriangleCanvas.cpp, add the BuildVertexShader method:

void TriangleCanvas::BuildVertexShader()
{
    const GLchar* vertexSource =
	"#version 330 core\n"
	"in vec2 position;"
	"void main()"
	"{"
	"    gl_Position = vec4(position, 0.0, 1.0);"
	"}";
    m_vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(m_vertexShader, 1, &vertexSource, NULL);
    glCompileShader(m_vertexShader);
}

vertexSource is the source code for the vertex shader. Shaders are written in a C-like language called GLSL. The first line, “#version 330 core\n”, tells OpenGL to use the core functionality of version 3.30 of GLSL. Starting with OpenGL version 3.3, the GLSL version number matches the OpenGL version number.

The next line says that position is an input parameter of type vec2. This is an array of two floating point numbers. The remaining lines contain the actual shader that is run for each vertex passed. gl_Position is a vec4 (an array of four floats). These values are x, y, z, and w. x, y, and z are the three dimensions in 3D space. The fourth parameter, w, is the factor that each coordinate should be adjusted to specify the point in device coordinates. The three coordinates in device space are x/w, y/w and z/w. Because the vertices are already normalized to device coordinates, w is set to 1.0. Now if we go back and look at how the vertices are defined in the program (in TriangleCanvas::SetupGraphics), you will notice that only x and y coordinates are provided. The “in vec2 position” line in the shader states that we are passing in only two values. In main, this is turned into the four coordinates needed by OpenGL by adding a z-coordinate of 0, and a w value of 1.0. gl_Position is the GLSL global value that passes the vertex value onwards.

This vertex shader is a simple pass-through shader; that is, it passes the input values through to the next stage in the rendering process.

The final three lines in the BuildVertexShader method create and compile the shader.

Now add the BuildFragmentShader method:

void TriangleCanvas::BuildFragmentShader()
{
    const GLchar* fragmentSource =
	"#version 330 core\n"
	"out vec4 outColor;"
	"void main()"
	"{"
	"    outColor = vec4(1.0, 1.0, 1.0, 1.0);"
	"}";
    m_fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(m_fragmentShader, 1, &fragmentSource, NULL);
    glCompileShader(m_fragmentShader);
}

This time we provide the fragment shader. The line “out vec4 outColor;” says that the shader will output a vec4. This value will contain the colour to display the pixel in. outColor is set to a vec4 containing the red, green, blue, and alpha values of the colour for the pixel. In this case, every pixel will be set to white. Again, the final three lines of the method create and compile the fragment shader.

The BuildShaderProgram method contains:

void TriangleCanvas::BuildShaderProgram()
{
    BuildVertexShader();
    BuildFragmentShader();
    m_shaderProgram = glCreateProgram();
    glAttachShader(m_shaderProgram, m_vertexShader);
    glAttachShader(m_shaderProgram, m_fragmentShader);
    glBindFragDataLocation(m_shaderProgram, 0, "outColor");
    glLinkProgram(m_shaderProgram);
    glUseProgram(m_shaderProgram);

    GLint posAttrib = glGetAttribLocation(m_shaderProgram, "position");
    glEnableVertexAttribArray(posAttrib);
    glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
}

This method calls methods to build the two shaders, then creates a shader program consisting of the two shaders, and the outColor variable is bound to the fragment shader. Finally, the shader program is linked and OpenGL is told to use the program.

The final three lines tie the two-dimensional vertices to the input of the vertex shader.

The final changes required to use the shaders are made to the SetupGraphics method. The last three lines of the method are replaced with a call to BuildShaderProgram().

Compiling and running the program should produce the same display as before the shaders were added. If you cannot determine what the problem with your program is, compare it to TriangleCanvas with Shaders.

Fun With Shaders

Invert Triangle

Inverting the triangle using shaders is easy. Just change the vertex shader to contain

"    gl_Position = vec4(position.x, -position.y, 0.0, 1.0);"

Inverted Green Triangle

Right now, the triangle is white because that colour is hard-coded into the fragment shader. To set the colour of the rectangle in the program rather than in the shader, change the fragment shader to this:

    const GLchar* fragmentSource =
        "#version 330 core\n"
        "uniform vec3 triColor;"
	"out vec4 outColor;"
	"void main()"
	"{"
	"    outColor = vec4(triColor, 1.0);"
	"}";

The line "uniform vec3 triColor;" tells the shader that the variable triColor comes from the program and may change over time. And of course, in the main function, outColor has been changed to use triColor.

Three changes are required in the program to define and use triColor. In TriangleCanvas.h, add the following data member:

    GLint m_uniColor;

In TriangleCanvas::BuildShaderProgram add:

	m_uniColor = glGetUniformLocation(m_shaderProgram, "triColor");

as the last line. This ties m_uniColor to the location of the uniform variable triColor.

And in TriangleCanvas::OnPaint, add the line:

	glUniform3f(m_uniColor, 0.0f, 1.0f, 0.0f);

before the triangle is drawn. This sets the triColor variable in the fragment shader to green. The resulting display should be:

greentriangle

Varying the Colour of the Triangle

Now the triangle is green but that is pretty boring. Let’s modify the program so that the colour changes over time.

For the timing, we will be using a wxTimer object and a timer event handler. In TriangleCanvas.h, add a timer event handler:

	void OnTimer(wxTimerEvent& event);

and two timer related constants:

    static const int INTERVAL = 1000 / 60;
    static const int TIMERNUMBER = 3;

The interval will be used to force the timer to fire at about 60 times per second. Now add a pointer to a timer object:

    std::unique_ptr<wxTimer> m_timer;

In the TriangleCanvas constructor, tie the timer event to its handler:

Bind(wxEVT_TIMER, &TriangleCanvas::OnTimer, this);

and at the bottom of the constructor add the code needed to create the timer and start it:

    m_timer = std::make_unique<wxTimer>(this, TIMERNUMBER);
    m_timer->Start(INTERVAL);

In the destructor, stop the timer:

    m_timer->Stop();

Add the timer event handler:

void TriangleCanvas::OnTimer(wxTimerEvent& event)
{
    ProcessEvent(wxPaintEvent());
}

All this handler does is fire the paint event.

If you build and run the program now, OnPaint will execute approximately 60 times per second. You will not see any change because the colour of the triangle remains green.

Let’s add code to change the colour every time that the timer fires. In TriangleCanvas.h, add a time_point object to specify the start time:

std::chrono::time_point<std::chrono::high_resolution_clock> m_startTime;

In the TriangleCanvas constructor, set the start time before the timer is created:

    m_startTime = std::chrono::high_resolution_clock::now();

and in OnPaint, replace the colour setting code with:

    auto t_now = std::chrono::high_resolution_clock::now();
    float time = std::chrono::duration_cast<std::chrono::duration>(t_now - m_startTime).count();
    glUniform3f(m_uniColor, (sin(time * 1.0f) + 1.0f) / 2.0f, (sin(time * 0.5f) + 1.0f) / 2.0f,
        (cos(time * 0.25f) + 1.0f) / 2.0f);

Now build and run the program. The colour of the triangle should change at a rate of about 60 times per second, through all of the various combinations of intensities of red, green and blue. If you cannot get the program to work and cannot determine why, see the code listing in Varying Triangle Colours.

Blended Colour Triangle

The previous examples handled one colour at a time. This example starts with a different colour in each corner of the triangle and interpolates the colours for each pixel in the triangle.

This example starts back with the HelloTriangle program that contains the vertex and fragment shaders (the one that displays a white triangle, above). To begin, we need to define a colour for each vertex in the triangle. We will do that by adding the colour to each vertex defined in the points array:

    float points[] = {
	0.0f, 0.5f, 1.0f, 0.0f, 0.0f,        // red vertex
	0.5f, -0.5f, 0.0f, 1.0f, 0.0f,       // green vertex
	-0.5f, -0.5f, 0.0f, 0.0f, 1.0f       // blue vertex
	};

Here are the vertex and fragment programs:

    const GLchar* vertexSource =
        "#version 330 core\n"
	"in vec2 position;"
	"in vec3 color;"
	"out vec3 Color;"
	"void main()"
	"{"
	"    gl_Position = vec4(position, 0.0, 1.0);"
	"    Color = color;"
	"}";
    const GLchar* fragmentSource =
	"#version 330 core\n"
	"in vec3 Color;"
	"out vec4 outColor;"
	"void main()"
	"{"
	"    outColor = vec4(Color, 1.0f);"
	"}";

You should note that an input parameter and an output parameter have been added to the vertex shader program. The input color is simply passed through to the output Color parameter.

In the fragment shader, the output parameter from the vertex shader is added as an input. The name of the parameters must be the same. Also, outColor is calculated based on the input parameter.

One final change is required. Since color is a new input and is specified in the points array, we have to tie the color parameter to the colour specification for each vertex. Also, since each vertex now is specified with five values rather than two, the position pointer must be modified to show that. Replace the last line in the BuildShaderProgram method with the following code:

    glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), 0);
    GLint colAttrib = glGetAttribLocation(m_shaderProgram, "color");
    glEnableVertexAttribArray(colAttrib);
    glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat),
        (void*)(2 * sizeof(GLfloat)));

The colours could just as easily have been specified in a different array, but placing them all in the same array makes sure that the position and colour values do not get out of step.

Build and run the program. The output should look like this:

threecolortriangle

Review

This post described the shaders that are used in the graphics pipeline. The vertex and fragment shaders were modified to illustrate some of the control that these shaders can provide to the program output.

Advertisements

4 thoughts on “OpenGL Shaders

  1. Pingback: Drawing Circles With OpenGL | Using C++

  2. Pingback: Two Rotating Circles | Using C++

  3. Pingback: Adding A Moving Triangle | Using C++

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s