Two Rotating Circles

At the end of the Moving The Circle Off Centre post, the CirclesAndRotators program displayed a circle located at pixel coordinates (220, -150) relative to the centre of the canvas. Before adding a second circle, we will refactor the program by adding a GlCircle class that encapsulates the the properties and functionality of circles in OpenGL, and then we will add a rotational transformation to the circle. Finally, we will add a second rotating circle.

Refactoring To Use A GlCircle Class

The GlCircleClass

In OpenGL, there are three tasks:

  • Initialization;
  • Drawing; and,
  • Cleanup.

These map nicely in the GlCircle class to the constructor, the Paint method and the destructor. The simplest way to create a circle is to define it centred at the origin and then transform it to its desired location. This is the order in which you would do things in games and other programs where objects move. So, with the centre of the circle specified for all circles, the simplest way to define a circle is in terms of its radius. Two other arguments are required in the call to the constructor; these are the transformation value between object coordinates and device coordinates, and the shader program. You will see in a moment where the shader program is used.

In the CirclesAndRotators program, pixels are used as object coordinates, with the origin at the centre of the canvas that is drawn on. This means that the transformation value between object coordinates and device coordinates is simply the fourth value in 4D space (w).

The GlCircle class constructor will create the vertex array, buffers, and buffer data required by the GPU. This code is very much a simple copy of the code that was in CirclesAndRotatorsCanvas::CreateSquareForCircle and CirclesAndRotatorsCanvas::BuildCircleShaderProgram. Here is the GlCircle constructor:

GlCircle::GlCircle(float radius, float w, GLuint shaderProgram)
: m_radius(radius), m_w(w)
{
    // create vertices for circle centred at origin
    glm::vec4 vertices[] = {
        { -m_radius, -m_radius, 0.0f, w },
        { m_radius, -m_radius, 0.0f, w },
        { m_radius, m_radius, 0.0f, w },
        { -m_radius, m_radius, 0.0f, w }
    };
    // create elements that specify the vertices.
    GLint elements[6] = { 0, 1, 2, 2, 3, 0 };

    // circle is centred at the origin. Later, in the Paint method, we transform the location to
    // where we want it.
    // VAO
    glGenVertexArrays(1, &m_vao);
    glBindVertexArray(m_vao);
    // upload vertex data
    glGenBuffers(1, &m_vbo);
    glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
    glBufferData(GL_ARRAY_BUFFER, 16 * sizeof(float), vertices, GL_STATIC_DRAW);
    // upload element data
    glGenBuffers(1, &m_ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(GLint), elements, GL_STATIC_DRAW);

    // set up position attribute used in circle vertex shader
    GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
    glEnableVertexAttribArray(posAttrib);
    glVertexAttribPointer(posAttrib, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
    // set up the uniform arguments
    m_outerRadius = glGetUniformLocation(shaderProgram, "outerRadius");
    m_viewDimensions = glGetUniformLocation(shaderProgram, "viewDimensions");
    m_transform = glGetUniformLocation(shaderProgram, "transform");
    glBindVertexArray(0);           // unbind the VAO
}

The only things of note are:

  • The vertices for the square containing the circle are defined in terms of the radius of the circle;
  • A separate set of vertex arrays and buffers are created for each circle; and,
  • The vertex array object is unbound at the end of the destructor.

Unbinding the vertex array object ensures that the buffer contents are not accidentally altered outside the constructor.

The destructor simply deletes the vertex array object, the vertex buffer, and elements buffer.

The Paint method is straight-forward:

void GlCircle::Paint(glm::mat4& transform) const noexcept
{
    glBindVertexArray(m_vao);
    glUniformMatrix4fv(m_transform, 1, GL_FALSE, glm::value_ptr(transform));
    // set outer radius for circle here. We will be modulating it in later
    // example
    glUniform1f(m_outerRadius, m_radius);
    glUniform2f(m_viewDimensions, 2.0f * m_w, 2.0f * m_w);
    // 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);
    glBindVertexArray(0);           // unbind the VAO
}

In the Paint method, the vertex array for the circle is bound, the uniform values are transferred to the shader program, the circle is drawn, and the vertex array is unbound.

Here is the GlCircle class declaration:

#pragma once
#include "GL/glew.h"
#include "wx/glcanvas.h"
#define GLM_FORCE_CXX14
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

class GlCircle
{
public:
    GlCircle(float radius, float w, GLuint shaderProgram);
    virtual ~GlCircle() noexcept;
    void Paint(glm::mat4& transform) const noexcept;

private:
    float m_radius;
    float m_w;

    GLuint m_vao;
    GLuint m_vbo;
    GLuint m_ebo;
    GLint m_transform;
    GLint m_viewDimensions;
    GLint m_outerRadius;
};

Using the GlCircle Class

The CirclesAndRotatorsCanvas::CreateSquareForCircle method is renamed to CreateCircles, and is simplified to:

void CirclesAndRotatorsCanvas::CreateCircles()
{
	// define vertices for the two triangles
    wxSize canvasSize = GetSize();
    float w = static_cast(canvasSize.x) / 2.0f; 
    m_circle1 = std::make_unique(80.0f, static_cast(canvasSize.x) / 2.0f, m_circleShaderProgram);
}

The BuildCircleShaderProgram method is also simplified to:

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);
}

Note that the code that used the shader program, but that was not actually related to building the shader program has been removed.

The OnPaint method is modified to:

void CirclesAndRotatorsCanvas::OnPaint(wxPaintEvent& event)
{
	// 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 the transform
    wxSize canvasSize = GetSize();
    float w = static_cast(canvasSize.x) / 2.0f;
    glm::mat4 transform;
    transform = glm::translate(transform, glm::vec3(220.0f / w, -150.0f / w, 0.0f / w));
    m_circle1->Paint(transform);
	glFlush();
	SwapBuffers();
}

With the changes made above, the order that the two methods are called in the SetupGraphics method must be reversed. Also, a number of changes are required to the #include‘s in CirclesAndRotatorsCanvas and CirclesAndRotatorsFrame source files.

Building and running the program should result in the circle being displayed at (220, -150) as before.

Rotational Transformation

To add a rotation to the circle, replace the call to paint the circle in CirclesAndRotatorsCanvas::OnPaint with:

    glm::mat4 rotation;
    rotation = glm::rotate(rotation, glm::radians(180.0f), glm::vec3(0.0f, 0.0f, 1.0f));
    glm::mat4 transrotate;
    transrotate = rotation * transform;
    m_circle1->Paint(transrotate);

In this code, we create two new matrices: rotation, and transrotate The rotate function is called to create a transformation matrix that rotates an object about the z-axis by 180 degrees. The transrotate matrix is the result of multiplying the translation (transform) matrix by the rotation matrix. Note that the translation is performed first and then the rotation.

Build and run the program. This is the result:

rotatecircle180

The circle was translated to (220, -150) then rotated about the centre of the canvas in a counter-clockwise direction 180° to (-220, 150).

Matrix multiplication is not transitive; that is,

    transrotate = rotation * transform;

does not produce the same result as

    transrotate = transform * rotation;

In this latter case, the rotation would be performed first and then the translation, but since the circle is initially centred at (0, 0), applying the rotation matrix simply rotates the circle about (0, 0), producing no visual change. The translation then moves the rotated circle to (220, -150).

Varying the Rotation Over Time

To vary the position of the circle over time, we need to create a timer that calls the CirclesAndRotatorsCanvas::OnPaint method, and in that method, calculate the rotation based on the time. Because I have previously described the changes needed to create and fire the timer, I will not repeat it here. See the Varying the Colour of the Triangle section of the OpenGL Shaders post for a description of the changes required to call the OnPaint method every time the timer fires.

In OnPaint, replace the line:

    rotation = glm::rotate(rotation, glm::radians(180.0f), glm::vec3(0.0f, 0.0f, 1.0f));

with:

    auto t_now = std::chrono::high_resolution_clock::now();
    auto time = (t_now - m_startTime).count();
    rotation = glm::rotate(rotation, time * 2.5e-10f, glm::vec3(0.0f, 0.0f, 1.0f));

The angle of rotation is now calculated based on the time since the CircleAndRotatorsCanvas object was created. The angle is calculated in radians, and the value 2.5e-10 was selected to provide a slow rate of rotation. Build and run the program to see the result.

Adding A Second Circle

We will be adding a second circle that rotates over top of the first circle. The fragment shader hard-codes the colour of the circle to green. In order to see the second circle as it rotates over the first, we need to set different colours for the two circles. Change the fragment shader program to accept the colour as a uniform vec4, and modify the code in OnPaint and GlCircle::Paint to set this value.

In CirclesAndRotatorsCanvas, add a second GlCircle called m_circle2, and add:

    m_circle2 = std::make_unique(30.0f, w, m_circleShaderProgram);

after the definition of m_circle1 in CirclesAndRotatorsCanvas::CreateCircles.

Now, in OnPaint, after the call to m_circle1->Paint,
add the following:

    transform = glm::mat4();
    transform = glm::translate(transform, glm::vec3(-200.0f / w, -75.0f / w, 0.0f / w));
    rotation = glm::rotate(rotation, time * 5e-10f, glm::vec3(0.0f, 0.0f, 1.0f));
    transrotate = rotation * transform;
    m_circle2->Paint(transrotate, glm::vec4(1.0f, 0.5f, 0.0f, 1.0f));

This creates and displays a smaller, orange circle that rotates twice as quickly as the blue circle. See this video for the result:

The code for this program is located in the rotate branch on GitHub.

Advertisements

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