Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / OpenGL

Drawing nearly perfect 2D line segments in OpenGL

4.80/5 (29 votes)
18 Jul 2011CPOL5 min read 250.4K   8.4K  
With premium quality anti- aliasing, color, thickness, and minimum CPU overhead.

Introduction

OpenGL is great; when it comes to line drawing, most people would draw it by:

C++
float line_vertex[]=
{
    x1,y1, x2,y2
};
glVertexPointer(2, GL_FLOAT, 0, line_vertex);
glDrawArrays(GL_LINES, 0, 2);

It does give you a straight line, but a very ugly one. To improve this, most people would enable GL line smoothing:

C++
glEnable(GL_LINE_SMOOTH);
glHint(GL_LINE_SMOOTH_HINT,  GL_NICEST);

But this technique has a couple of drawbacks:

  • Hardware dependent. It does not necessarily look the same on different machines.
  • Average quality. It does not give perfect quality on most hardware.
  • Poor thickness control. Most drivers only support thickness of integer values. And the maximum thickness is 10.0 px.

This article focuses on 2D rendering in (sub) pixel accuracy. Make sure you view all images in their original size.

Functionality

The technique introduced in this article gives you:

  • premium quality anti-aliased lines
  • smaller CPU overhead than any other CPU rasterizing algorithm
  • finer line thickness control
  • line color control
  • alpha blend (can choose to use alpha blend or not)

Image 1

Image 2

Believe it, it is rendered in OpenGL.

Using the code

C++
void line( double x1, double y1, double x2, double y2, //coordinates of the line
    float w,                            //width/thickness of the line in pixel
    float Cr, float Cg, float Cb,    //RGB color components
    float Br, float Bg, float Bb,    //color of background when alphablend=false,
                                     //  Br=alpha of color when alphablend=true
    bool alphablend);                //use alpha blend or not

void hair_line( double x1, double y1, double x2, double y2, bool alphablend=0);

The first function line() gives you all the functionality. You can choose not to use alpha blending by setting alphablend to false; in this case, you will get color fading to the background. In no- alpha- blending mode, you still get good results when the background is solid and lines are not dense. It is also useful when doing overdraw. The below image should tell you what alphablend=false means:

Image 3

The second function hair_line() draws near-perfectly a black "hair line" of thickness 1px with no color or thickness control. You can optionally use alpha blend; otherwise, it assumes the background is white. I provide this in case you do not need all the functionalities. You can just include the header vase_rend_draft_2.h and it should work. If you copy only part of the code, make sure you also copy the function.

C++
static inline double GET_ABS(double x) {return x>0?x:-x;}

Here is a sample usage with alpha blending:

C++
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glMatrixMode(GL_PROJECTION);
glPushMatrix();
    glLoadIdentity();
    glOrtho( 0,context_width,context_height,0,0.0f,100.0f);

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
        line ( 10,100,100,300,  //coordinates
            1.2,                //thickness in px
            0.5, 0.0, 1.0, 1.0, //line color RGBA
            0,0,                //not used
            true);              //enable alphablend

        //more line() or glDrawArrays() calls
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);

//other drawing code...
glPopMatrix();
glDisable(GL_BLEND); //restore blending options

and without alpha blending, just fade to background color:

C++
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrtho( 0,context_width,context_height,0,0.0f,100.0f);

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
    line ( 20,100,110,300,  //coordinates
        1.2,                //thickness in px
        0.5, 0.0, 1.0,      //line color *RGB*
        1.0, 1.0, 1.0,      //background color
        false);             //not using alphablend

    //more line() or glDrawArrays() calls
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);

//other drawing code...
glPopMatrix();

How does that work?

Observation

You just need to know a little bit of OpenGL. Look at the hello world OpenGL program below. It merely draws a triangle with different colors on each vertex. What do you observe?

Image 4
C++
glLoadIdentity();
//window size is 300x300
glOrtho( 0,300,300,0,0.0f,100.0f);
glClearColor( 1,1,1,0.5f);
glClearDepth( 1.0f);
glClear(GL_COLOR_BUFFER_BIT |
        GL_DEPTH_BUFFER_BIT);

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

float triangle_vertex[]=
{
    150,10,     //vertex 1
    280,250,    //vertex 2
    20,250      //vertex 3
};
float triangle_color[]=
{
    1,0,0,      //red
    0,1,0,      //green
    0,0,1       //blue
};
glVertexPointer(2, GL_FLOAT, 0, 
                triangle_vertex);
glColorPointer(3, GL_FLOAT, 0, 
               triangle_color);
glDrawArrays(GL_TRIANGLES, 0, 3);

glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);

Yes, the edge is jaggy. Well, the interpolation among colors looks perfect.

The 'fade polygon' technique

The above observation is sufficient to enable us to do what we want. Now let's draw a parallelogram which changes color from white to red.

Image 5
C++
float para_vertex[]=
{
    50,270,
    100,30,
    54,270,
    104,30
};
float para_color[]=
{
    1,1,1,    //white
    1,1,1,
    1,0,0,    //red
    1,0,0
};
glVertexPointer(2, GL_FLOAT, 0, para_vertex);
glColorPointer(3, GL_FLOAT, 0, para_color);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

The right side is still jaggy. The left side is smooth. Can you now think of anything? Now let's draw two parallelograms which change color from white to red then to white again.

Image 6
C++
float para_vertex[]=
{
    50,270,
    100,30,
    54,270,
    104,30,
    58,270,
    108,30
};
float para_color[]=
{
    1,1,1,    //white
    1,1,1,
    1,0,0,    //red
    1,0,0,
    1,1,1,    //white
    1,1,1
};
glVertexPointer(2, GL_FLOAT, 0, para_vertex);
glColorPointer(3, GL_FLOAT, 0, para_color);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 6);

Let's call this the 'fade polygon technique': draw a thin quadrilateral to render the core part of a line, then draw two more beside the original one that fade in color. This gives us the effect of anti-aliasing.

Quality

This article focuses on 2D line drawing so the meaning of "perfect quality" is with respect to 2D graphics. In particular, Maxim Shemanarev (responsible for Anti-Grain Geometry) is the boss in fine grained 2D rendering. Let's see a picture from his article.

Image 7

The above picture shows lines with thickness starting from 0.3 pixels and increasing by 0.3 pixel. Using triangles to approximate line segments in the correct dimension is not easy. I did it by experiment and hand calibrated the drawing code:

Image 8

then obtained:

Image 9

Image 10

It is not perfect though, the end points are not sharp enough, and so I say "nearly perfect". I found fltk-cairo convenient to build so I actually took Cairo, the popular 2D rendering API on Linux, as a benchmark.

Image 11

Image 12

Their difference is subtle, so make sure you flip them in a slideshow program to observe. I have made one for you here.

It is seen that Cairo draws thin lines a little bit thicker than it should look. The circular fan on the right is drawn as 1px black lines by cairo_set_line_width (cr, 1.0).

Image 13

But you see, the horizontal line is a 2px grey line. In my code, I tried hard to give a 1px #000000 line when you request a 1px #000000 line on the exact pixel coordinate, especially at horizontal/vertical condition. But there is no guarantee in sub- pixel coordinates, other colors, and orientations.

Ideal 1px black lines should look very close to aliased raw 1px lines, but just smoother. Now take a closer look at the fan on the right and flip to compare there.

Image 14

Image 15

A final comparison:

Image 16

Image 17

Image 18

Image 19

Performance

Today's graphics card can render millions of triangles per second. This technique takes advantage of rasterization and is already pretty fast. If you want to boost things further up, you can generate the vertices via a geometry shader but that is up to you. By a brief benchmark, it is 30 times faster than OpenGL native line drawing with smoothing turned on. And 40 times faster than Cairo when rasterization is heavy (e.g., drawing 10000 thick lines).

Portability

I have not tested the code on many machines, so I cannot guarantee. This technique depends on rasterizing. There is (always) a higher chance that a GL driver implements rasterization correctly than smooth- line drawing. As far as I know, most hardware support sub- pixel accuracy rasterization. I observed that rasterization in OpenGL ES on iPhone looks good. It would probably work. In my testing, there are often rounding errors which cause tiny artifacts. That is not perfect, but still good. Again I cannot guarantee, the best way is to test it yourself.

Final words

Using triangles to approximate line segments is not a new thing, and I believe many programmers did that back from OpenGL 1.0. The important thing is calibrating the code to give such high quality and publishing it. Drawing good looking lines should be a basic feature of a graphics API. It is strange after all these years we do not have an elegant solution and many programs just tolerate aliasing.

The code is designed for easy integration and to replace "traditional" line drawings with ease. So download the zip file and include the header to test it out. If you find this useful, I just hope you cite this page.

The fade polygon technique is extended to achieve anti- aliasing for shapes more complex than a line segment: polylines. Do not miss the second episode, Drawing polylines by tessellation, of this article.

History

  • June 06, 2011 - Updated download file.
  • June 18, 2011 - Updated download file: fixed a visual bug and updated the sample images.
  • July 16, 2011 - Updated article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)