使用OpenGL绘制抗锯齿线

地图大多是由线条以及少量多边形构成。不幸的是,画线是OpenGL的一个薄弱点。GL_LINES绘图模式是有限的:它不支持line joins、line caps、非整数线宽、宽度大于10像素或在一条线上使用不同宽度。鉴于这些局限性,它是不适合于用于高品质地图上使用。下面是GL_LINES的一个例子:

GL_LINES demo

此外,OpenGL的抗锯齿(多重采样抗锯齿)在不同的设备支持程度不同,或者是质量较差。

作为替代原生线,我们可以通过镶嵌把线转化成多边形绘制。几个月前,我调查各种线渲染方法,尝试用六个三角形绘制一条线:

tessellate

两对三角形的构成两边渐变的边缘,中间一对三角形拼成实线。渐变提供抗锯齿,以使该线有淡入淡出的边缘。当按比例缩小,这将产生高品质的线:

six-triangles-antialiasing

不幸的是,每线段生成六个三角形意味着需要生成8个顶点,这就需要大量的内存。我的试验是每一条线段只有两个顶点,但这样绘制一条线就需要调用3次绘制。为了保持良好的帧率我们需要尽量减少每帧绘制次数。

属性插值的帮助

OpenGL的绘图分为两个阶段。首先,顶点的列表被传递到顶点着色器。顶点着色器基本上是一个小函数,将每一个顶点(在模型坐标系)到一个新的位置(屏幕坐标系),以便您每一帧使用相同的顶点数组,但仍然可以进行诸如旋转、平移,或缩放等操作。

连续的三个顶点形成一个三角形。在这个区域内的所有像素都由片段着色器处理,它也叫像素着色器。顶点着色器的为顶点数组中的每个顶点运行一次,片段着色器为每个像素运行一次,来决定三角形中的像素使用什么颜色。在最简单的情况下,它可能会分配一个固定的颜色,就像这样:

void main() {
    gl_FragColor = vec4(0, 0, 0, 1);
}

颜色顺序是RGBA,所以这个例子所有的片段都使用不透明的黑色。如果我们使用这些多边形绘制线,并且多边形的所有像素渲染流水线使用一致的颜色,那我们还是得到了可怕锯齿线。我们需要一种方法来把多边形的边界像素的alpha值从1渐变到0。在顶点着色器转化顶点坐标时,OpenGL允许我们对每个顶点指定其属性,例如:

attributes

这些属性再交由像素着色器。有趣的部分是这样的:由于一个象素不能直接与单个顶点相关联,该属性根据三角形的三个顶点构成按距离来插值出来:

attributes-interpolated

这个插值生成顶点之间的渐变效果。这是我要描述渲染方法的基础。

需求

画线时,我们有几个要求: * 可变线宽:我们要在每一帧改变线宽,当用户放大/缩小,我们不必将线一遍又一遍镶嵌为三角形。这意味着顶点的位置必须在顶点着色器计算出,而不是预先在场景中设置。 * End caps(对接,圆形,方形):这说明线路两端的绘制方式。 * Line joins(尖角,圆角,斜角):这说明两线之间接缝的绘制方式。 * 多条线:出于性能原因,我们希望与不同的宽度和颜色在一个绘制调用中完成。

线镶嵌

由于我们要动态地改变线宽,我们不能在加载时进行完整的镶嵌。相反,我们重复同一个顶点两次,所以,对于一个线段,在我们的数组有四个顶点(标记为1-4):

extrusion-source

此外,我们计算线段的单位法向量,并将它分配给每一个顶点,与第一个顶点得到正单位矢量,第二个负单位矢量。单位向量是你在这张图片中看到的小箭头:

extrusion-target

在顶点着色器中,我们在渲染过程中把线宽和顶点的单位向量相乘,并最终有两个三角形,效果如这张照片红色虚线。

顶点着色器看起来像这样:

attribute vec2 a_pos;
attribute vec2 a_normal;

uniform float u_linewidth;
uniform mat4 u_mv_matrix;
uniform mat4 u_p_matrix;

void main() {
    vec4 delta = vec4(a_normal * u_linewidth, 0, 0);
    vec4 pos = u_mv_matrix * vec4(a_pos, 0, 1);
    gl_Position = u_p_matrix * (pos + delta);
}

在主函数中,我们把单位法向量与线宽相乘的到实际的线宽。正确的顶点位置(在屏幕空间)是由模型/视图矩阵相乘得到的。之后,我们添加挤压向量线宽,它是独立于任何模型/视图缩放的。最后,我们乘以投影矩阵得到投影空间中的顶点位置(在我们的例子中,我们使用平行投影,它只是将坐标缩放屏幕空间内,坐标范围为0..1)。

抗锯齿

我们现在有任意宽度的直线段,但我们仍然没有反锯齿线。为了实现抗锯齿的效果,我们将使用单位法向量,但这次是在像素着色器中。在顶点着色器,我们只是把单位法向量传递到像素着色器。现在,OpenGL在两个法线之间进行插值,使我们在像素着色器获得两个单位向量之间的渐变。这意味着它们不再单位向量,因为它们的长度小于1。当我们计算该矢量的长度,我们得到该像素到原始线段的垂直距离,在0..1的范围内。我们可以使用这个距离来计算像素的alpha值。如果我们已线宽为参数,在线宽减去羽化feather距离之内的像素,我们只需要分配的不透明颜色(见下图)。在linewidth - featherlinewidth + feather之间,我们指定的alpha值从1和0。并且是比的单位向量远离的片段,我们分配Alpha值为零(此时,还没有像素用到这个属性,但我们很快就会用到它们)。

feather

除了线宽外,我们还可以改变羽化距离变得模糊,或阴影。我们可以将其降低到零的到有锯齿线。0.5羽化值会产生和AGG相似常规抗锯齿效果,0和0.5羽化值产生模仿Mapnik伽玛值的结果的结果。

Line Joins

上述方法适用于单条线段,但在大多数情况下,我们需要绘制多条连接在一起的线条。当连接线段,我们必须选择一条线连接样式,并相应地移动顶点:

overlong-unit-vectors

之前我们使用线的法向量来计算定点的法向量。在线连接的情况下这个方法不在适用,因为需要计算的是定点的法向量,而不是线段的法向量。每个顶点的法向量为两线段的夹角平分线。

在线连接的情况下单位向量也不在适用,因为线连接处顶点的距离比单位距离要长。而不是使用角平分线单位向量,我们只是添加线段的单位向量,从而它既不是单位向量也不是法向量。我把它叫做一个挤压向量。

不幸的是,我们现在有一个问题:挤压载体不再是垂直于线段,所以他们两个人之间的插值不会产生垂直距离。相反,我们引入另一每个顶点属性,纹理法线。这是1或-1的值,取决于正常点是否向上或向下移动。当然,这是不是一个真正的法线,因为它在二维空间中没有方向,但它足以为1和-1之间实现插值来获得我们的抗锯齿直线距离值。

因为我们不希望引入另一个字节,其中我们会有效地只使用一个单一的符号位,我们编码纹理法线到实际的顶点属性。顶点属性使用16位整数(-32768..32767),它们大到足以容纳我们的瓦片0..4095坐标。我们把每个坐标双倍到(0..8190),然后用最低有效位来存储纹理法线。在顶点着色器,我们提取该位,并使用模型/视图矩阵来缩放到我们的坐标系下的实际大小。

为了节省内存,我们用编码,每个轴一个字节的挤压载体,所以我们有一个(整数)范围-128.. 127,每轴。不幸的是,挤出载体可以成长任意长的线连接,因为挤压向量长度增长到无穷大的角度变得更加尖锐。这是一个常见的问题,当画线连接,而解决方案是引入一个“斜接限制”。如果挤压矢量变长比斜接限制,线路连接切换到一个斜角连接。这使我们能够扩展浮点正常显着,使我们保持足够的角度精度挤压载体。

Mapbox GL

访问Mapbox GL博客查看和谈论更多的设计和开发工作。画线只是设备上实时高品质的地图中的一个小而必要的部分。

原文:https://www.mapbox.com/blog/drawing-antialiased-lines/

comments powered by Disqus