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

8 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++

  4. Hi Jim

    Your article was very informative and well written. I am new to 3D programming so the following silly questions were raised in my head once I thought about how I would use this in a real world situation.

    Do I need to load a new Program(Shader)+DrawElementsCommand for each circle I want to draw?

    If I just need just a quarter of the circle to for instance use this method to draw a rounded square it would probably just take bit more math in the shader, right?

    Why did you call glFlush() in modern OpenGL (I thought SwapBuffers() now does that automatically)?

    Thanks

    Jayson

    Like

    • Hi Jayson,

      Read the 4 posts that I wrote after this one. In them, I transform the circle off-center, add a second circle, add a triangle, and finally, discuss design decisions that I made in writing the final program. For something funky that illustrates the real power of shaders, read my posts on the ChaosExplorer program. Just select ChaosExplorer in the tags to get all of the posts.

      For rounded squares, yes just more complicated math in the shader would work. Alternatives to doing it all in one shader would be to draw a square, four rectangles and four part circles, or to draw two overlapping rectangles and four part circles. But doing that requires at least two shaders, so if you can do it with just one shader, I think you would find that preferable.

      As to why I called glFlush(), I was not then, nor am I now, an OpenGL expert. Unfortunately, many of the examples on the Internet are for old versions of OpenGL, and I missed that SwapBuffers() would perform the flush.

      Like

      • Thanks Jim

        I want to mention that I was in no way trying to be a blue smartie regarding glFlush(). I really assumed I was doing it wrong. Your articles are one of the main reason I have not thrown my code in the bin yet.

        I am using plain C so I am drawing a lot of 2D shapes (as pseudo (struct) objects) that were originally done in SVG on a 1900×1060 canvas so I basically use the ‘w’ part to have my own coordinates as pixels for vertex input. It just makes more sense to draw the circles like you did here since a fan of triangles will keep changing my original vertex count for different screen sizes and I need to shift specific known vertexes only in the shader (think rounded square) when for instance the width of an object changes. Then comes animation.

        Your articles are very helpful. I just wish I could get the viewPort to work correctly. It is out of proportion due to my lack of understanding the new stages coordinates go through and how to set up the matrices.

        Kind Regards

        Jayson

        Like

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 )

Google+ photo

You are commenting using your Google+ 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 )

w

Connecting to %s