The Developer’s Cry

Yet another blog by a hobbyist programmer

OpenGL line drawing

It is weird how line drawing to a computer screen has always been kind of difficult to do. In them olden days I would just call a BASIC LINETO() subroutine and it would work, albeit very slowly. Much later, I learned about Bresenham’s line algorithm and that worked pretty fast. The jagged, hard-pixel lines were fine on low-res screens, but as screen resolution increased we really wanted to have anti-aliased lines. Even with OpenGL, which renders lines on specialized GPU hardware, we still get these ugly jagged lines. In good old OpenGL 2 I would just switch polygon mode to render objects in wireframe, which was great for debugging. With OpenGL 3 and later you have a texturing shader set up, your vertex buffers are laid out for use with that shader, and it’s just not getting any easier. A solution is to render lines as flat, stretched-out rectangles.

The biggest hurdle to overcome when rendering lines as rectangles is a problem that only exists in your mind. We are not used to thinking of lines as being rectangular. In fact, we are taught since junior high that lines are infinitesimally thin. The lines we are going to render have thickness. Therefore, when we say a line runs from (0,0) to (100,25), we really mean that the line runs from (0.5, 0.5) to (100.5, 25.5) with a thickness of 1.0. For lines with a thickness of n, we will be off by half n. To obtain the coordinates for a box around a slanted line you need to get the normal vector (perpendicular to the line), and offset the vertices by half n.

With this technique you have to take into account the scale of things. When line drawing, you typically think of line width in terms of pixels, while OpenGL works in units. Note that OpenGL is perfectly happy working with half pixels; in practice I find it easy to work with ortho modes that are scaled to represent common screen resolutions.

Code for finding the green box encompassing the line from a to b:

// find normal vector
float dx = b.x - a.x;
float dy = b.y - a.y;
vec2 n(-dy, dx);

// scale by half line-width
n *= (line_width * 0.5f);

// four corners:
const vec2 c1 = a - n;
const vec2 c2 = a + n;
const vec2 c3 = b - n;
const vec2 c4 = b + n;

We use a normal vector, so this works for horizontal as well as vertical or slanted lines. Feed the coordinates of the four corners into the VBO as a triangle strip and away we go. The outcome is a beautifully rendered (and textured!) line. Because OpenGL is so good with polygons and fragment shading, we basically get anti-aliasing for free.

I have only implemented this for 2D. You can extend to 3D, but beware that perspective projection will distort the lines. To keep that from happening, you need to bring the vertices back into screen space. It’s a lot more advanced and best implemented in a shader.

This line drawing trick is not a good fit for wireframe rendering. Wireframes are best implemented in a geometry shader. If you are on OpenGLES (which lacks a geometry shader) you can get away with a trick that works just as good.

Recommended reading: