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, float w, float Cr, float Cg, float Cb, float Br, float Bg, float Bb, bool alphablend);
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, 1.2, 0.5, 0.0, 1.0, 1.0, 0,0, true);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
glPopMatrix();
glDisable(GL_BLEND);
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, 1.2, 0.5, 0.0, 1.0, 1.0, 1.0, 1.0, false);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
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();
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, 280,250, 20,250 };
float triangle_color[]=
{
1,0,0, 0,1,0, 0,0,1 };
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, 1,1,1,
1,0,0, 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, 1,1,1,
1,0,0, 1,0,0,
1,1,1, 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.