Click here to Skip to main content
Click here to Skip to main content
Go to top

Drawing nearly perfect 2D line segments in OpenGL

, 18 Jul 2011
Rate this:
Please Sign up or sign in to vote.
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:

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:

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)

Believe it, it is rendered in OpenGL.

Using the code

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:

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.

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

Here is a sample usage with alpha blending:

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:

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?

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.

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.

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.

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:

then obtained:

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.

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).

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.

A final comparison:

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)

Share

About the Author

Chris H.F. Tsang

Hong Kong Hong Kong
Chris H.F. Tsang
tyt2y3@gmail.com

Comments and Discussions

 
Questioncleaning PinmemberRon Michel12-Jun-14 11:59 
AnswerRe: cleaning PinmemberChris H.F. Tsang12-Jun-14 15:38 
QuestionOpengl 3.3 PinmemberMember 47401599-May-13 21:00 
QuestionReplacement for glLineStipple? [modified] PinmemberAndornot17-Nov-12 2:17 
AnswerRe: Replacement for glLineStipple? PinmemberChris H.F. Tsang10-Dec-12 4:58 
SuggestionPython version [modified] PinmemberNicolas.Rougier5-Jul-11 7:47 
GeneralRe: Python version PinmemberTYT Chris8-Jul-11 3:11 
NewsDrawing polylines in OpenGL [modified] PinmemberTYT Chris5-Jul-11 4:59 
GeneralClassic AA techniques [modified] PinmemberAdis H.21-May-11 12:38 
GeneralRe: Classic AA techniques [modified] PinmemberTYT Chris21-May-11 18:35 
first of all thank you Big Grin | :-D .
I would not say it is completely fair to compare this technique with MSAA, CSAA and other technologies. They are designed to draw smooth-edged polygons in 3D. So they give worse quality in 2D line drawings. Anyhow figures in such a comparison is "good to know".
 
Particularly, I wonder whether MLAA can smooth a line segment correctly. The slope of a line must be obtained otherwise the line segment would look awkward in different orientations. Take a closer look to the circular fan in the image in this article. A horizontal/ vertical line does not need anti- aliasing while a 45 degree line needs the most. This comes from the fact that a LCD pixel is square in shape.
And yes, I argue on pixels.
Btw I want to try your samples, but sadly I cannot open 7zip.
 
EDIT: I read the "original" paper from IBM on MLAA. They have an algorithm to estimate the slope of a line with certain maximum error. So MLAA can (not sure if it does) smooth a line quite well.

modified on Wednesday, May 25, 2011 11:12 PM

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140926.1 | Last Updated 18 Jul 2011
Article Copyright 2011 by Chris H.F. Tsang
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid