A Short Course in Computer Graphics. How to Write a Simple OpenGL. Article 1 of 6

Describing the Task

In this series of articles, I want to show the way OpenGL works by writing its clone (a much simplified one). Surprisingly enough, I often meet people who cannot overcome the initial hurdle of learning OpenGL / DirectX. Thus, I have prepared a short series of six lectures, after which my students show quite good renderers.

So, the task is formulated as follows: Using no third-party libraries (especially graphic ones), get something like this picture:

Warning you, that it is training material that will generally repeat the structure of the OpenGL library. It will be a software renderer. I do not want to show how to write applications for OpenGL. I want to show how OpenGL is built. I am deeply convinced that it is impossible to write efficient applications using 3D libraries without understanding this.

I will try to make the final code not longer than 500 lines. My students need 10 to 20 programming hours to begin making such renderers. At the input, we get a test file with a polygonal wire + pictures with textures. At the output, we’ll get a rendered model. No graphical interface, the program simply generates an image.

Since the goal is to minimize external dependencies, I give my students just one class that allows working with TGA files. It’s one of the simplest formats that supports images in RGB/RGBA/black and white formats. So, as a starting point, we’ll obtain a simple way to work with pictures. You should note that the only functionality available at the very beginning (in addition to loading and saving images) is the capability to set the color of one pixel.

There are no functions for drawing line segments and triangles. We’ll have to do all of this by hand.

I provide my source code that I write in parallel with students. But I would not recommend using it, as this doesn’t make sense.

The entire code is available on github, and here you will find the source code I give to my students.

It turns out that one line is good, the second one is with holes, and there’s no third line at all. Note that the first and the second lines (in the code) give the same line of different colors. We have already seen the white one, it is drawn well. I was hoping to change the color of the white line to red, but could not do it. It’s a test for symmetry: the result of drawing a line segment should not depend on the order of points: the (a,b) line segment should be exactly the same as the (b,a) line segment.

There are holes in one of the line segments due to the fact that its height is greater than the width.

My students often suggest the following fix: if (dx>dy) {for (int x)} else {for (int y)}.

This code works great. That’s exactly the kind of complexity I want to see in the final version or our renderer.
It is definitely inefficient (multiple divisions, and the like), but it is short and readable.
Note that it has no asserts and no checks on going beyond the borders, which is bad.

But I try not to overload this particular code, as it is read a lot. At the same time, I systematically remind of the necessity to perform checks.

So, the previous code works fine, but we can optimize it.

Optimization is a dangerous thing. We should be clear about the platform the code will run on.
Optimize the code for a graphics card or just for a CPU — are completely different things.
Before and during any optimization, the code should be profiled.
Try to guess, which operation is the most recourse-intensive operation here?

For tests, 1,000,000 times I draw 3 line segments we have drawn before. My CPU is Intel® Core™ i5-3450 CPU @ 3.10GHz.

For each pixel, this code calls the TGAColor copy constructor.

Which is 1000000 * 3 line segments * approximately 50 pixels per line segment. Quite a lot of calls, isn’t it?

Where to start with optimization?

The profiler will tell us.

I compiled the code with g++ -ggdb -g3 -pg -O0 keys, and then ran gprof:

But 70% are performed in calling line()! That’s where we will optimize.

We should note that each division has the same divisor. Let’s take it out of the loop.
The error variable gives is the distance to the best straight line from our current (x, y) pixel.
Each time error is greater than one pixel, we increase (or decrease) y by one, and decrease the error by one as well.

Now, it’s enough to remove unnecessary copies during the function call by passing the color by reference (or just enable the compilation flag -O3), and it’s done. Not a single multiplication or division in code. The execution time has decreased from 2.95 to 0.64 seconds.

Comments

Most of us work with strings one way or another. There’s no way to avoid them — when writing code, you’re doomed to concatinate strings every day, split them into parts and access certain characters by index. We are used to the fact that strings are fixed-length arrays of characters, which leads to certain limitations when working with them. For instance, we cannot quickly concatenate two strings. To do this, we will at first need to allocate the required amount of memory, and then copy there the data from the concatenated strings.