[基础篇]OpenGL环境下抗锯齿线的绘制原理

2018/11/24 Map

[基础篇]OpenGL环境下抗锯齿线的绘制原理

导语 翻译自:https://www.mapbox.com/blog/drawing-antialiased-lines/

一幅地图的绘制,包含许许多多的线状要素,这其中也包含面状要素的边界线。但是,绘制线一直都是OpenGL的痛点。OpenGL在绘制模式中有GL_LINES这一选项,但是缺点很明显:它不支持线头和线交界处处理,线宽不支持整形,不能设置超过10px的线宽,不能在一条线设置不同线宽。由于这些限制,它不能用在高精度地图绘制工作中。 还有,OpenGL的抗锯齿并不是在所有平台都适用,往往效果还很差。

线绘制方案

​ 对于线要素,由于线要素存在线宽,因此绘制到地图上可以理解为面状要素的渲染。通过将线要素拆分多个三角形可以实现这一过程。

img

​ 如图所示,一段线段可以拆分为六个三角形,其中线段两边的两组三角形组成一个渐变的四边形,中间的两个三角形组成一个四边形作为线段的主体部分。渐变的部分可以在线要素在边缘消隐的时候,提供抗锯齿效果。当比例尺变大的时候,这将呈现高品质的绘线效果。

​ 但是,对于每一条线段,生成六个三角形意味着需要八个顶点数据,这需要大量的内存。我尝试每条线段使用两个顶点数据,但是这种方式会导致每条线段渲染的时候需要三次绘制调用。为了保持渲染的质量,我们需要减少每一帧的绘制次数调用。

属性插值

​ OpenGL绘制分两个阶段。首先,顶点属性传递到顶点着色器。顶点着色器是一个基本的小函数片段,它能够转换每一个顶点(模型坐标系统)到一个新的顶点(屏幕坐标系统)。有了这样的基础,可以在每一帧渲染的时候进行数据的再利用。

​ 三个连续的顶点数据构成一个三角形。范围内的所有片元都会被片元着色器处理,也称之为像素着色器。当顶点着色器对顶点数据里面所有顶点进行处理后,紧接着,片元着色器会对三角形里面的所有像素进行处理,具体反映的是每个像素的颜色。最简单的情形,像素的颜色会被赋值为一个常量值,像这样。

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

​ 这里的颜色排列是RGBA,在这里例子中,所有的片元都被渲染为不透明的黑色。如果我们渲染的线要素是通过线要素切分的三角形加上在每个三角形上固定的颜色组成的,其结果也会是一堆锯齿线。因此要达到抗锯齿的效果,对于接近边缘的部分,需要降低其Alpha值。在向顶点着色器传递顶点数据的时候,OpenGL允许我们对每个顶点数据进行属性赋值。

​ 这些属性值可以被传递到片元着色器中,有趣的是,每个片元不能直接和单个顶点产生联系,却可以在三个离散值中通过片元到三个顶点的距离进行插值。而这些插值能够在顶点中提供渐变的效果,这个也是线绘制方法的基础。

img

需求

​ 在地图中绘制线要素,我们有如下几个要求:

i. 变化的线宽 用户需要来回的放大缩小操作,这就要求我们在每一帧渲染的时候动态更改线宽,但是与此同时不能一次次的拆分线段为三角形。因此,这就意味着最终顶点的位置应该是在渲染的过程中,由顶点着色器去帮忙计算的。

ii. 端头端尾(butt,round,square) 线段绘制开始和结束部分的处理。

iii. 线段交接处(miter,round,bevel) 线段交接处的处理。

线切分

​ 由于这里需要做到线宽的动态改变,因此不能在初始阶段就对线进行拆分。这里,我对同一个顶点进行一次拷贝,因此对于一条线段,数组中包含四个顶点数据。

​ 我计算了线段的垂线并且绑定到每个顶点上,第一个顶点绑定正向的单位法向量,第二个顶点绑定负向的单位法向量。单位法向量如图小箭头所示。

img

img

​ 在我们的顶点着色器中,通过将顶点的单位法向量乘以线宽,可以在每一帧的绘图调用达到动态更改宽度的效果,最终生成两个三角形,如图红色虚线所示。

顶点着色器代码示例如下:

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

​ 在main函数中,将单位向量乘以线宽可以得到实际线宽。顶点的实际位置(屏幕坐标)则通过模型/视图变换矩阵得到。后面,我增加了额外向量是的线宽独立于模型/视图放缩。最后,通过投影矩阵,得到顶点在投影空间的位置。

抗锯齿

img

​ 现在线段可以通过线宽动态生成,但是我们仍然没有对线段进行抗锯齿。为了达到这种效果,我们需要使用单位向量,但是这次是在片元着色器中体现。在顶点着色器中,我们仅仅是将单位向量传递给了片元着色器。现在,OpenGL在单位向量进行插值,这使得片元着色器接收的向量是渐变的。这也意味着不存在单位向量了,因为它们的长度不足1。当我们计算向量的长度的时候,我们计算的是片元到原始线段的距离,范围在0~1之间。我们可以使用这个距离去计算片元的Alpha值。如果与线宽产生关联,我们可以在线宽减去”feather”距离范围内对不同距离的片元赋值不同的Alpha值。在linewidth - feather and linewidth + feather 之间,我们对其从0~1进行Alpha赋值,对于比这个距离远的片元,我们Alpha值赋为0。

​ 除去线宽的影响,我们可以单独改变feather的值去得到模糊的线,也可以得到阴影。我们也可以将它设置为0,这样得到的线就是带锯齿的线。

线交接处

​ 上面所说的是对单个线段的处理,但是大多数情况下,我们都会遇到多条线段两两连接的情况。当线段连接的时候,我们需要选择一个线连接方案并且如图所示移动顶点数据。

img

​ 早先的时候,我们计算线段的单位向量并且赋值给顶点数据。但是这种方案不适用与线段连接的情况,因为实际上我们需要计算每个顶点的单位向量而不是每个线段的单位向量。每个顶点的单位向量则是两个线段单位向量的二等分向量计算得来。

​ 单位向量同样不适用于线连接处,因为顶点在线连接处的距离往往是比1大很多的。因此,与其使用单位向量的和,我更愿意增加线段单元向量,它既不是单位向量也不是法向量。这里称之为挤出向量。

​ 不幸的是,我们又有一个问题,挤出向量不再是线段的法向量,因此两个线段的插值不再是垂直距离。作为替代,我们引入了另一个顶点属性,纹理法向量。它的值为1或者-1,这个值取决于法向量坐标朝上还是朝下。这个明显不是正常的值,它在二维空间没有明显的方位,但是在线抗锯齿中,对于实现1到-1的插值很有效。

Search

    Table of Contents