Drawing Circles With OpenGL

The only shapes that OpenGL can draw are points, lines, and triangles,so how do you draw circles and other shapes? A reasonable approximation of a circle can be drawn using many, many triangles with one vertex at the centre of the circle and the other two vertices sharing the same location as one of the vertices of the triangles next to it on the outer edge of the circle. Triangles of course have straight sides, but the more triangles you use to define the circle, and therefore the closer the vertices on the edge of the circle are, the closer they represent a rounded edge. But that is a lot of work, and there is a better way: define a square whose sides are equal to the diameter of the circle you want to draw and have the fragment shader determine whether the point it is drawing is inside or outside the circle. That is what we will do in this post.

The program that we will create in this post will just draw a green circle at the centre of the view, but we will be modifying the program over time to display circles and other objects at various locations in the view and rotate them around the centre of the view. New techniques will be described as they are used.

Drawing A Circle

Start by creating a new empty project in Visual Studio called CirclesAndRotators, then add three new classes to it called CirclesAndRotatorsApp, CirclesAndRotatorsFrame, and CirclesAndRotatorsCanvas. Follow the steps outlined in:

  1. Creating wxWidgets Programs with Visual Studio 2015 – Part 1
  2. Visual Studio, wxWidgets, and OpenGL
  3. HelloTriangle, and
  4. OpenGL Shaders.

replacing class names as appropriate.

A number of the methods in the classes remain the same, so they will not be repeated in this post. However, the source of the full program is provided if you find you are having problems.

CirclesAndRotatorsApp.OnInit is modified to place code in a try-catch block because additional exceptions may be thrown. The catch block simply displays a message box with the text of the exception. Here is the code:

bool CirclesAndRotatorsApp::OnInit()
{
    try {
        CirclesAndRotatorsFrame* mainFrame = new CirclesAndRotatorsFrame(nullptr, L"Circles and Rotators");
        mainFrame->Show(true);
    }
    catch (std::exception& e) {
        wxMessageBox(e.what(), "CirclesAndRotators");
    }
    return true;
}

The CirclesAndRotatorsFrame constructor creates a CirclesAndRotatorsCanvas object that is 800 by 800 pixels in size. While this size can be changed, the code in this program assumes that the canvas is square (i.e. has the same number of pixels in both x and y directions). If you do not create a square canvas, you will have to modify the program to compensate.

As with the other programs I have shown so far, the majority of the code is in the canvas class.

The BuildCircleVertexShader method builds the vertex shader for the circle. Here is the code:

void CirclesAndRotatorsCanvas::BuildCircleVertexShader()
{
    const GLchar* vertexSource =
        "#version 330 core\n"
        "in vec2 position;"
        "void main()"
        "{"
        "    gl_Position = vec4(position, 0.0, 1.0);"
        "}";
    m_circleVertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(m_circleVertexShader, 1, &vertexSource, NULL);
    glCompileShader(m_circleVertexShader);
    CheckShaderCompileStatus(m_circleVertexShader, "Circle Vertex Shader did not compile.");
}

This simple vertex shader takes the two-dimensional position of the vertex and stores it in the gl_Position global variable. The rest of the code you have seen in the previous posts except for the call to CheckShaderCompileStatus. This method checks if the shader compiled, and throws an exception if it did not:

void CirclesAndRotatorsCanvas::CheckShaderCompileStatus(GLuint shader, const std::string& msg) const
{
    // check shader compile status, and throw exception if compile failed
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status != GL_TRUE) {
        throw std::exception(msg.c_str());
    }
}

Note: The status returned by glGetShaderiv contains only GL_TRUE or GL_FALSE. If GL_FALSE, then you can call glGetShaderInfoLog to retrieve information on the error. Do not expect detailed error messages. This would perhaps be a good topic for a future post.

The BuildCircleFragmentShader method determines whether the fragment (pixel) being displayed is inside or outside the circle. Here is the source code for the method:

void CirclesAndRotatorsCanvas::BuildCircleFragmentShader()
{
    const GLchar* fragmentSource =
        "#version 330 core\n"
        "uniform vec2 viewDimensions;"
        "uniform float outerRadius;"
        "out vec4 outColor;"
        "void main()"
	"{"
    // convert fragment coordinate (i.e. pixel) to view coordinate
        "   float x = (gl_FragCoord.x - viewDimensions.x / 2.0f) / (viewDimensions.x / 2.0f);"
        "   float y = (gl_FragCoord.y - viewDimensions.y / 2.0f) / (viewDimensions.y / 2.0f);"
    // discard fragment if outside the circle
        "   float len = sqrt(x * x + y * y);"
        "    if (len > outerRadius) {"
        "        discard;"
        "    }"
    // else set its colour to green
        "    outColor = vec4(0.0, 1.0, 0.0, 1.0);"
        "}";
    m_circleFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(m_circleFragmentShader, 1, &fragmentSource, NULL);
    glCompileShader(m_circleFragmentShader);
    CheckShaderCompileStatus(m_circleFragmentShader, "Circle Fragment Shader did not compile");
}

In order to determine if the fragment is inside or outside the circle, we need three values: the position of the fragment, given in gl_FragCoord, the radius of the circle, given by the uniform value outerRadius, and the dimensions of the canvas. gl_FragCoord gives the location of the pixel containing the fragment, but outerRadius is the radius of the circle in device coordinates (x and y between -1 and +1), so the dimensions of the canvas are required to convert between the pixel coordinates and the device coordinates.

The lines that define x and y convert the gl_FragCoord x and y coordinates into device coordinates. The length of the vector (len) from the origin (centre of the view) to the fragment is calculated and compared with the radius of the circle. If the fragment is inside the circle, then the pixel colour is set to green, and if the fragment is outside the circle, the fragment is discarded. If you wish, rather than discard the fragment, you could set it to a different colour than green or the background colour so that you can see the square that contains the circle.

The BuildCircleShaderProgram method first calls BuildCircleVertexShader and BuildCircleFragmentShader, then links them to create the shader program. The location of the position attribute input to the vertex shader is obtained and the enabled. The locations of the two uniform variables input to the fragment shader are obtained next, and finally, the size of the canvas is set (viewDimensions in the fragment shader). Note: the size of the canvas is not modifiable, so this uniform value needs to be set only once. Here is the source code for the BuildCircleShaderProgram method:

void CirclesAndRotatorsCanvas::BuildCircleShaderProgram()
{
    // build the circle shaders
    BuildCircleVertexShader();
    BuildCircleFragmentShader();
    // create and link circle shader program
    m_circleShaderProgram = glCreateProgram();
    glAttachShader(m_circleShaderProgram, m_circleVertexShader);
    glAttachShader(m_circleShaderProgram, m_circleFragmentShader);
    glBindFragDataLocation(m_circleShaderProgram, 0, "outColor");
    glLinkProgram(m_circleShaderProgram);

    // set up position attribute used in circle vertex shader
    GLint posAttrib = glGetAttribLocation(m_circleShaderProgram, "position");
    glEnableVertexAttribArray(posAttrib);
    glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
    // set up the uniform arguments
    m_circleOuterRadius = glGetUniformLocation(m_circleShaderProgram, "outerRadius");
    m_viewDimensions = glGetUniformLocation(m_circleShaderProgram, "viewDimensions");
    // The canvas size is fixed (and should be square), so initialize the value here
    glUseProgram(m_circleShaderProgram);
    wxSize canvasSize = GetSize();
    glUniform2f(m_viewDimensions, static_cast(canvasSize.x),
        static_cast(canvasSize.y));
}

The fragment shader determines if the a fragment is inside or outside of the circle. But to get a fragment to determine this, we have to define the square that contains the circle. This is done in the CreateSquareForCircleMethod:

void CirclesAndRotatorsCanvas::CreateSquareForCircle()
{
    // define vertices for the two triangles
    float points[] = {
        -0.2f, -0.2f,
        0.2f, -0.2f,
        0.2f, 0.2f,
        -0.2f, 0.2f
    };
    // define the indices for the triangles
    GLuint elements[] = {
        0, 1, 2,
        2, 3, 0
    };

    // setup vertex array object
    glGenVertexArrays(1, &m_circleVao);
    glBindVertexArray(m_circleVao);
    // upload vertex data
    glGenBuffers(1, &m_circleVbo);
    glBindBuffer(GL_ARRAY_BUFFER, m_circleVbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
    // upload element data
    glGenBuffers(1, &m_circleEbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_circleEbo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(elements), elements, GL_STATIC_DRAW);
}

Wait a minute. To form a square, we need two triangles which have a total of 6 vertices, but the points array only contains 4 vertices. Yes, that is true, but note that the first triangle is defined by the vertices v0, v1, and v2, and the second triangle is defined by the vertices v2, v3, and v0. Rather than requiring that these vertices be restated, OpenGL has the concept of elements. Note that the elements array contains 6 values that correspond to the six vertex numbers for the two triangles. Below these definitions, there are two calls to glBindBuffer and glBufferData, one for the points array that specifies the type as GL_ARRAY_BUFFER, and the second for the elements array that specifies the type as GL_ELEMENT_ARRAY_BUFFER. This tells the GPU that the first buffer contains vertex data and the second buffer contains an array that specifies which vertices to use when drawing.

That seems like extra work, and more buffer space in the GPU. Assuming 4 bytes required for each float and each unsigned int in the GPU, defining 6 vertices results in 48 bytes of buffer space. Using 4 vertices and 6 elements, there is a total of 56 bytes, so there is more buffer space required in this case. But what happens if the vertices are specified in 3D, as would be the case in most OpenGL programs? For just these two triangles, specifying the triangles using only the points array uses 72 bytes; specifying the triangles using both points and elements arrays, again uses 72 bytes. Now add a third triangle that shares 2 vertices with other triangles: we have 108 bytes versus 96 bytes. As the number of shared vertices increases, the use of elements increases the savings. Since a normal OpenGL program will define hundreds or even thousands of attached triangles, the saving can become quite substantial.

Finally, here is the code that draws the rectangle resulting in the circle:

void CirclesAndRotatorsCanvas::OnPaint(wxPaintEvent& event)
{
    SetCurrent(*m_context);
    // set background to black
    glClearColor(0.0, 0.0, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // use the circleShaderProgram
    glUseProgram(m_circleShaderProgram);
    // set outer radius for circle here. We will be modulating it in later
    // example
    glUniform1f(m_circleOuterRadius, 0.2f);
    // draw the square that will contain the circle.
    // The circle is created inside the square in the circle fragment shader
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    glFlush();
    SwapBuffers();
}

We have seen most of this before. The only differences are the three lines:

    glUseProgram(m_circleShaderProgram);
    glUniform1f(m_circleOuterRadius, 0.2f);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

In previous example programs, glUseProgram was called immediately after the shader program was linked. In those cases, only one shader program was created. As we develop the CirclesAndRotators program further, we will create additional shader programs. One shader program will be used for drawing some shapes, and additional shader programs will be used when drawing other shapes. Hence, we need to call glUseProgram for the appropriate shader program before drawing any objects that use that shader program.

If previous examples, glDrawArrays was called to draw the objects. If we called glDrawArrays here, only one triangle would be drawn. Try it and see. To use elements, we must call glDrawElements instead.

The only remaining task is to release the GPU resources in the CirclesAndRotatorsCanvas destructor.

Here is the resulting display:

centrecircle

The source code for the program is provided in the master branch on GitHub.

Advertisements

5 thoughts on “Drawing Circles With OpenGL

  1. Pingback: Device Coordinates and Object Coordinates | Using C++

  2. Pingback: Moving The Circle Off Centre | 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