65.9K
CodeProject 正在变化。 阅读更多。
Home

在 OpenGL 中绘制近乎完美的 2D 线段。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (28投票s)

2011 年 5 月 20 日

CPOL

5分钟阅读

viewsIcon

252848

downloadIcon

8466

具备优质的抗锯齿、颜色、粗细控制,并且 CPU 开销极小。

引言

OpenGL 很棒;说到绘制线段,大多数人会通过

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

这确实能画出直线,但非常难看。为了改进这一点,大多数人会启用 GL 线段平滑

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

但这种技术有几个缺点

  • 依赖硬件。在不同机器上的显示效果不一定相同。
  • 平均质量。在大多数硬件上无法提供完美的质量。
  • 粗细控制差。大多数驱动程序仅支持整数值的粗细。最大粗细为 10.0 像素。

本文专注于(亚)像素级精度下的 2D 渲染。请确保以原始尺寸查看所有图像。

功能

本文介绍的技术能为您提供

  • 优质的抗锯齿线段
  • 比任何其他 CPU 光栅化算法都低的 CPU 开销
  • 更精细的线段粗细控制
  • 线段颜色控制
  • Alpha 混合(可以选择是否使用 Alpha 混合)

请相信,这是用 OpenGL 渲染的。

使用代码

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

第一个函数 line() 提供了所有功能。您可以通过将 alphablend 设置为 false 来选择不使用 Alpha 混合;在这种情况下,颜色会淡出到背景。在无 Alpha 混合模式下,当背景为纯色且线段不密集时,您仍然会得到不错的结果。这在进行重叠绘制时也很有用。下图应该能说明 alphablend=false 的含义

第二个函数 hair_line() 几乎完美地绘制了一条粗细为 1 像素的黑色“发丝线”,无法控制颜色或粗细。您可以选择使用 Alpha 混合;否则,它假设背景为白色。如果您不需要所有功能,则提供此函数。您可以直接包含头文件 vase_rend_draft_2.h,它应该就能工作。如果只复制部分代码,请确保也复制该函数。

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

以下是使用 Alpha 混合的示例用法

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

以及不使用 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 ( 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();

它是如何工作的?

观察

您只需要了解一点 OpenGL。看看下面的 OpenGL Hello World 程序。它只是用不同颜色在每个顶点上绘制一个三角形。您观察到了什么?

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

是的,边缘是锯齿状的。不过,颜色之间的插值看起来很完美。

“渐变多边形”技术

上述观察足以让我们实现想要的功能。现在让我们绘制一个颜色从白色渐变为红色的平行四边形。

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

右侧仍然是锯齿状的。左侧是平滑的。您现在能想到什么吗?现在让我们绘制两个平行四边形,颜色从白色渐变为红色,然后再渐变为白色。

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

我们称之为“渐变多边形技术”:绘制一个薄四边形来渲染线段的核心部分,然后在其两侧再绘制两个颜色渐变的四边形。这就能实现抗锯齿效果。

质量

本文专注于 2D 线段绘制,因此“完美质量”是指在 2D 图形方面的质量。特别是,Maxim Shemanarev(负责 Anti-Grain Geometry 的人)是精细 2D 渲染的大师。让我们看看他文章中的一张图片。

上图显示了粗细从 0.3 像素开始并以 0.3 像素递增的线段。使用三角形在正确尺寸下近似线段并不容易。我是通过实验完成的,并手动校准了绘图代码

然后得到了

虽然还不完美,端点不够锐利,所以我称之为“近乎完美”。我发现 fltk-cairo 方便构建,所以我实际上以 Cairo(Linux 上流行的 2D 渲染 API)为基准。

它们之间的差异很微妙,请确保您在幻灯片放映程序中切换它们来观察。我已经为您准备了一个

可以看到,Cairo 绘制的细线比应有的稍微粗一点。右侧的扇形是用 cairo_set_line_width (cr, 1.0) 绘制的 1 像素黑色线条。

但是您看,水平线是一条 2 像素的灰色线。在我的代码中,我努力在精确像素坐标下提供一条 1 像素的 #000000 线,尤其是在水平/垂直方向上。但在亚像素坐标、其他颜色和方向上无法保证。

理想的 1 像素黑色线条应该非常接近未进行抗锯齿处理的原始 1 像素线条,只是更平滑。现在仔细看看右侧的扇形,并切换以进行比较这里

最后的比较

性能

如今的显卡每秒可以渲染数百万个三角形。这项技术利用了光栅化,已经相当快了。如果您想进一步提升性能,可以通过几何着色器生成顶点,但这取决于您。通过简短的基准测试,它的速度比开启平滑的 OpenGL 原生线段绘制快 30 倍。在光栅化负荷较重时(例如绘制 10000 条粗线)比 Cairo 快 40 倍。

可移植性

我没有在很多机器上测试过代码,所以无法保证。这项技术依赖于光栅化。GL 驱动程序正确实现光栅化的可能性(总是)比实现平滑线段绘制的可能性要大。据我所知,大多数硬件都支持亚像素精度光栅化。我观察到 iPhone 上的 OpenGL ES 光栅化效果很好。它很可能有效。在我的测试中,经常出现舍入误差导致微小瑕疵。这不完美,但仍然不错。再次强调,我无法保证,最好的方法是自己测试。

最后的寄语

使用三角形近似线段并不是什么新鲜事,我相信从 OpenGL 1.0 开始就有许多程序员这样做过。关键在于校准代码以提供如此高的质量并发布它。绘制好看的线条应该是图形 API 的基本功能。令人奇怪的是,多年来我们一直没有一个优雅的解决方案,而且许多程序只是容忍锯齿。

该代码设计用于轻松集成,并轻松替换“传统”的线段绘制。因此,请下载 zip 文件并包含头文件进行测试。如果您觉得有用,我希望您能引用此页面。

渐变多边形技术已扩展到实现比线段更复杂的形状的抗锯齿:折线。请不要错过本文的第二部分,通过镶嵌绘制折线

历史

  • 2011 年 6 月 6 日 - 更新了下载文件。
  • 2011 年 6 月 18 日 - 更新了下载文件:修复了一个视觉 bug 并更新了示例图像。
  • 2011 年 7 月 16 日 - 更新了文章。
© . All rights reserved.