It's been quite a while since I actually worked on TreeWars. Various things have distracted me, including some other programming projects. But I actually made some progress way back in July, before I shelved the project temporarily. So, let's talk about circles.

OpenGL gives us a few different ways to draw things, which I've talked about before. When we were using the fixed-pipeline functions (glBegin(), glEnd(), etc), I could draw a circle the same way I drew it in SDL: draw a bunch of same-sized rectangles, shifting the coordinates around a central point so that they overlap. Do enough of them (using small enough increments), and it makes a very smooth-looking circle. I never did this in OpenGL, but the SDL code looked like this:

[sourcecode language="cpp" gutter="false"]
void DrawUtils::draw_circle_filled(SDL_Surface* dest, Sint16 int_x, Sint16 int_y, Uint16 int_r, Uint32 colour)
{
float x = static_cast<float> (int_x);
float y = static_cast<float> (int_y);
float r = static_cast<float> (int_r);

SDL_Rect pen;
float i;

for (i=0; i < 6.28318531; i += 0.0034906585)
{
pen.x = static_cast<int> (x + cos(i) * r);
pen.y = static_cast<int> (y + sin(i) * r);
int w = static_cast<int> (x - pen.x);
int h = static_cast<int> (y - pen.y);

if (w == 0) pen.w = 1;
else if (w < 0)
{
pen.x = x;
pen.w = abs(w);
}
else pen.w = w;

if (h == 0) pen.h = 1;
else if (h < 0)
{
pen.y = y;
pen.h = abs(h);
}
else pen.h = h;
if (pen.x >= dest->clip_rect.x &&
pen.y >= dest->clip_rect.y &&
pen.x + pen.w <= dest->clip_rect.w &&
pen.y + pen.h <= dest->clip_rect.h)
SDL_FillRect(dest, &pen,
SDL_MapRGBA(dest->format,
(colour >> 16) & 0xff,
(colour >> 8) & 0xff,
colour & 0xff, 1));
}
}
[/sourcecode]

I was pretty proud of this code when I wrote it. The magic is at the top of the for loop: from 0 to 2π, it increments a tiny bit and finds a new rectangle that has one vertex at the center of the circle, and the opposing vertex at some point along the circle. It does this 1800 times per circle, which isn't terribly efficient, but it got the job done.

With OpenGL and the shader pipeline, we *could* still do that. We could do the following in a loop, 1800 times:

[sourcecode lang="cpp" gutter="false"]
GLushort Quad::rect_elements[] = {0, 1, 2, 3};
GLfloat buffer_data[] = {x1f, y1f, x2f, y1f, x1f, y2f, x2f, y2f};

GLuint vertex_buffer;
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(buffer_data), buffer_data, GL_STATIC_DRAW);

GLuint element_buffer;
glGenBuffers(1, &element_buffer);
glBindBuffer(GL_ELEMENT_BUFFER, element_buffer);
glBufferData(GL_ELEMENT_BUFFER, sizeof(rect_elements), rect_elements, GL_STATIC_DRAW);

glUseProgram(program); // the shader program, created earlier
glUniform4f(shader->uniforms.colour,
GLUtils::convert_colour((colour >> 16) & 0xff),
GLUtils::convert_colour((colour >> 8) & 0xff),
GLUtils::convert_colour(colour & 0xff), 1.0);

// Put the vertices in an attribute
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glVertexAttribPointer(shader->attributes.position, 2, GL_FLOAT,
GL_FALSE, sizeof(GLfloat)*2, (void*)0);
glEnableVertexAttribArray(shader->attributes.position);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer);
glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, (void*)0);

// Clean up the GL state machine
glDisableVertexAttribArray(shader->attributes.position);
[/sourcecode]


Now, that's a lot of fairly hairy OpenGL code. In my actual program, that's abstracted out into several function calls within several different classes - a Quad object inherits from Drawable, and uses a GLUtils library to create the vertex and element buffers. The actual render code is in Drawable, but it calls a subclassable sub_render function that helps it know how to draw a rectangle specifically.

But we don't want to call that code 1800 times for a single circle - where it was a bit inefficient in SDL, here we're making 1800 separate calls to the OpenGL hardware system (well, actually more as we copy data into GPU buffers and such, but 1800 glDrawElements() calls). That's 1800 different writes into GPU memory. It's ugly. It'd be a horrible idea.

Luckily, we can draw a circle with a *single* call to glDrawElements(). The secret is in 'GL_TRIANGLE_STRIP'. OpenGL defines several different methods it can use to interpret the vertex data we send to it. In 'Triangle Strip', it uses the first three vertexes to draw a triangle. The next vertex you add creates a triangle from it and the previous two vertexes (the last two of the previous triangle). If you 6 vertices in your vertex buffer, and the element buffer was just 0-5, it might look something like this:

Visual explanation of GL_TRIANGLE_STRIP

We could use THAT to draw a circle too, but it would be cumbersome. Instead, we'll use GL_TRIANGLE_FAN. Like GL_TRIANGLE_STRIP, the first three vertices are used to make a triangle. Subsequent vertices, however, use the previous vertex and the first vertex to form the next triangle. In effect, this gives you a 'center point' and lets you draw triangles outward from it. Its drawing pattern looks like this:



This lets us do some really elegant, simple drawing of curved shapes.

Of course, we still need some trigonometry here:

[sourcecode lang="cpp" gutter="false"]
for (unsigned int i = 2; i < 122; i+=2)
{
unsigned int j = i/2;
float rad = j * 0.104719755; // 2*Pi / 60
buffer_data[i] = x + (cos(rad) * rx);
buffer_data[i+1] = y + (sin(rad) * ry);
element_data[j] = j;
}
[/sourcecode]


But that logic is much more concise and easy to understand than any of the previous approaches. I've chosen to use 60 points around the circle somewhat arbitrarily - using more of these will the circle look smoother, but cost more in terms of rendering power. Even 1800 points would probably be pretty trivial for the program in its current state, but better to form good optimization habits now, I suppose. Also, the circles look pretty perfectly smooth at 60 points.

And most importantly, it works!

TreeWars in its current state


Of course, this still doesn't look quite the same (or as good) as the SDL version. I have some positioning issues to work out, and a lot of stuff still isn't implemented that I had there. But I'm definitely on the right track.

Of course, as I said at the top of this post, I've had this project shelved since July. There may be some new updates in the future, but this is probably the last Project TreeWars post for a while.