在WebGL 中绘制直线 - 知乎专栏
文章推薦指數: 80 %
之前我们介绍了「 在WebGL 中绘制地图(多边形篇)」,本文会介绍关于直线 ... https://github.com/mattdesl/polyline-miter-util/blob/master/index.
首发于WebGL九浅一深无障碍写文章登录/注册之前我们介绍了「在WebGL中绘制地图(多边形篇)」,本文会介绍关于直线的绘制方法,大致包含以下内容:直接使用原生gl.LINES的问题使用沿法向拉伸后三角化的方法绘制直线接头样式、反走样等常见优化手段Cesium、Mapbox、GeoJS等成熟引擎的实现项目地址:gl.LINES存在的问题在一些场景下,尤其是涉及到地理信息的展示,直接使用原生的gl.LINES进行绘制存在一些问题:线宽无法设置,Chrome下试图设置lineWidth会得到警告,相关ISSUE:MDN:AsofJanuary2017mostimplementationsofWebGLonlysupportaminimumof1andamaximumof1asthetechnologytheyarebasedonhasthesesamelimits.无法定义相邻线段间的连接形状lineJoin以及端点形状lineCap因此我们得考虑将线段转换成其他几何图形进行绘制。
沿法向拉伸常用的做法是沿线段法线方向进行拉伸后三角化。
例如下图中线段两个端点分别沿红色虚线法向向两侧拉伸,形成4个顶点,三角化成2个三角形。
沿原始线段法向拉伸进行三角化这样实现起来很容易,例如deck.gl的LineLayer:deck.gl-LineLayer但是很明显,我们需要在相邻线段的连接处进行处理。
连接接头在CanvasAPI中可以通过lineJoin指定线段接头处的连结方式,同样SVG中也有stroke-linejoin属性。
从上到下分别为rounded、bevel和miter。
lineJoin:rounded、bevel和miter我们以miterjoint斜接接头为例,看看在WebGL中如何实现。
以下思路来自「Smooththicklinesusinggeometryshader」。
首先需要计算切线方向t,然后计算斜接接头方向m为t的法线方向,最后得到线宽(红色虚线)投影到接头方向的长度://https://github.com/mattdesl/polyline-miter-util/blob/master/index.js#L9-L20
functioncomputeMiter(tangent,miter,lineA,lineB,halfThick){
//计算切线
add(tangent,lineA,lineB)
normalize(tangent,tangent)
//计算接头方向
set(miter,-tangent[1],tangent[0])
set(tmp,-lineA[1],lineA[0])
//半线宽投影到接头方向的长度
returnhalfThick/dot(miter,tmp)
}在vs中,根据miter计算点的偏移量://https://github.com/mattdesl/three-line-2d/blob/master/shaders/basic.js
uniformfloatthickness;//线宽
attributefloatlineMiter;//miter长度
attributevec2lineNormal;//normalize之后的法线方向
vec3pointPos=position.xyz+vec3(lineNormal*thickness/2.0*lineMiter,0.0);斜接接头还存在一个很明显的问题,如果连结处的夹角很小,就会导致计算出的miter无限大,视觉效果很不好。
例如下图:相邻线段夹角很小导致miter长度过长因此我们可以设置一个miter阈值,超过之后就使用bevel接头,对应SVG中的stroke-miterlimit属性。
相比miter,在三角化时需要增加一个三角形,对于接头处的每一个顶点,内侧仍然使用miter的一个,而外侧需要生成两个:bevel接头除了miter和bevel接头,还可以使用圆角,例如deck.gl的PathLayer开启rounded选项之后:圆角接头对于圆角接头,Mapbox的做法是在CPU预处理时添加大量额外顶点。
而GeoJS完全在shader中实现,代价就是传入GPU的顶点数据量增大(当前顶点、前后两个顶点)。
反走样如果仔细观察我们目前绘制的线段,可以发现边缘处明显的走样现象:早期mapbox采用过一种办法:绘制出多边形后,在边缘使用gl.LINES再进行描边(线宽为2),利用平台对于直线的MSAA。
但是由于不允许设置非1的线宽,不得不移除了这种方法。
相关ISSUE&PR。
如果不依靠基于后处理的几何反走样方式FXAA/MLAA,我们有以下几种方式可以尝试。
增加顶点最直接的办法是增加三角化的三角形数目,例如「Mapbox-DrawingAntialiasedLineswithOpenGL」中就介绍了这种方法,在两侧各增加一对三角形用于渐变:但是这对于渲染性能显然是有较大影响的(顶点数x2,三角形x3)。
对单位法向量插值如果能在fs中获取到当前fragment到原始线段的距离,就可以对边缘进行模糊。
很自然想到之前利用重心坐标实现wireframe。
但是这种方法有一个明显的缺陷,在mapbox的一个ISSUE「Antialiasfillswithoutgl.LINES」中有清晰的展示,即不同于wireframe,我们只想处理三角形的某一条边而非三边,并且即使我们找到了这条边,也还会缺少一块:还记得在vs中我们计算出的normalize之后的法线方向吗?利用varying插值,在fs中获取到单位法向量的插值结果,显然插值后就不是单位向量了(如下图),但我们可以利用向量长度作为模糊因子。
对单位法向量进行插值varyingvec2v_normal;
floatblur=1.-smoothstep(0.98,1.,length(v_normal));
gl_FragColor=v_color;
gl_FragColor.a*=blur;效果如下,可见边缘的走样现象缓解了很多:反走样效果前后对比Prefiltered除此之外,还有基于查找表的预过滤方式。
基本思路是首先计算出拉伸后的两个edgefunction,离线将卷积(box、Gaussian)结果存储在纹理中,在运行时将当前fragment位置带入edgefunction得到距离原始线段的长度,再查表得到卷积结果:「GPUGems2-FastPrefilteredLines」「FastAntialiasingUsingPrefilteredLinesonGraphicsHardware」「PrefilteredAntialiasedLinesUsingHalf-PlaneDistanceFunctions」解决了走样问题,我们再来看几个常见问题。
在GPU中进行墨卡托投影如果投影变换是在GPU而非CPU中进行的,例如deck.gl,在使用原始经纬度坐标计算出法线方向后,还需要在shader中进行转换,例如://u_pixels_per_meter表示在当前经纬度点,一度对应多少像素
vec3offset=normalize(vec3(lineNormal,0.0)*u_pixels_per_degree)
*thickness/2.0*lineMiter,0.0;由于和缩放等级相关,在每次相机发生改变时,在CPU中需要重新计算并传入u_pixels_per_degree:constworldSize=TILE_SIZE*scale;//当前缩放等级下的像素尺寸
constlatCosine=Math.cos(latitude*DEGREES_TO_RADIANS);
constpixelsPerDegreeX=worldSize/360;
constpixelsPerDegreeY=pixelsPerDegreeX/latCosine;固定线宽2D的线在正交投影下可以保证一致的宽度,但是在透视投影下就无法保证了。
在某些需要时刻保持线宽一致的3D场景下,例如Cesium在地形图中展示滑雪路线:常见的做法是投影到屏幕空间,这就需要将之前在CPU中对于法线和miter的计算挪到vs中进行。
通过除以w分量转换到NDC坐标系,再乘以宽高比就得到了屏幕空间坐标:vec2project_to_screenspace(vec4position,floataspect){
returnposition.xy/position.w*aspect;
}由于WebGL不支持GeometryShader,因此除了当前顶点位置,还需要将前后顶点的位置一并传入顶点数据:attributevec3position;
attributefloatdirection;
attributevec3next;
attributevec3previous;
//currentPos、prevPos、nextPos已经通过mvp矩阵投影到裁剪空间
//投影到屏幕空间
vec2currentP=project_to_screenspace(currentPos,aspect);
vec2prevP=project_to_screenspace(prevPos,aspect);
vec2nextP=project_to_screenspace(nextPos,aspect);
//计算切线和miter
vec2dir1=normalize(currentP-prevP);
vec2dir2=normalize(nextP-currentP);
vec2tangent=normalize(dir1+dir2);
vec2perp=vec2(-dir1.y,dir1.x);
vec2miter=vec2(-tangent.y,tangent.x);
//投影到miter方向
len=thickness/dot(miter,perp);
vec2normal=vec2(-dir.y,dir.x);
normal*=len/2.0;
normal.x/=aspect;
//得到最终偏移量
vec4offset=vec4(normal*orientation,0.0,1.0);基于Three.js的实现:https://github.com/spite/THREE.MeshLine也采用了类似的做法。
Codrop上有一篇使用它实现各种动画效果的教程,感兴趣的也可以阅读下。
虚线「Shader-BasedAntialiased,Dashed,StrokedPolylines」中使用的方法较为复杂。
这里我们采用一种较为简单的实现,同样还是利用varying插值,在CPU中对顶点数据进行预处理,对于每个顶点计算总顶点数的占比。
但是缺点也很明显,虚线并不是按长度等分的。
如果想做到按长度等分,就需要计算每段长度和总长度的占比,相应的会加重预处理顶点数据的负担:varyingfloatv_counters;//占总顶点数比例
uniformfloatu_dash_offset;//控制起始点,SVGstroke动画中常见
uniformfloatu_dash_array;//控制虚线疏密
uniformfloatu_dash_ratio;//控制每小段可见比例
gl_FragColor.a*=ceil(mod(v_counters+u_dash_offset,u_dash_array)
-(u_dash_array*u_dash_ratio));效果如下:虚线效果总结绘制直线并不是一件简单的事,尤其是考虑到绘制效果和性能。
我们以上的讨论也只是集中在lineJoin连结方式上,线的很多其他属性例如端点处的样式lineCap并没有涉及。
另外除了直线,还可以使用弧线连接两点,例如deck.gl的ArcLayer。
值得一提的是GeoJS对于直线的各种属性支持做的很好,除了lineJoin(miter|bevel|round)、lineCap(butt|square|round),包括miter的阈值、反走样(模糊半径)都可以进行配置。
而且从内部实现来看,完全在shader中进行,不依赖CPU对于顶点数据的预处理,还是很值得进一步研究的。
GeoJS中对于直线的配置项参考资料基础算法:「DrawingLinesisHard」「Smooththicklinesusinggeometryshader」「JournalofComputerGraphicsTechniques-Shader-BasedAntialiased,Dashed,StrokedPolylines」反走样相关:「GPUGems2-FastPrefilteredLines」「FastAntialiasingUsingPrefilteredLinesonGraphicsHardware」「PrefilteredAntialiasedLinesUsingHalf-PlaneDistanceFunctions」「Mapbox-DrawingAntialiasedLineswithOpenGL」一些成熟3D引擎的实现:「Three.jsMeshLine」「Cesium-RobustPolylineRenderingwithWebGL」「GeoJS-DrawingLinesinGeoJS」「GeoJS、Mapbox、Leaflet效果对比」「EfficientWebGLstroking」编辑于2019-03-1723:27WebGLGIS(地理信息系统)计算机图形学赞同7910条评论分享喜欢收藏申请转载文章被以下专栏收录WebGL九浅一深用图形技术打造极致惊艳的互联网体验
延伸文章資訊
- 1Robust Polyline Rendering with WebGL - Cesium
Our new method for drawing polylines is to draw a screen-aligned quads for each segment of the li...
- 2在WebGL 中绘制直线 - 知乎专栏
之前我们介绍了「 在WebGL 中绘制地图(多边形篇)」,本文会介绍关于直线 ... https://github.com/mattdesl/polyline-miter-util/blob/m...
- 3Drawing Lines with WebGL - Scott Logic Blog
Connecting points on a chart with a line is one of the most basic uses of a charting library. Whe...
- 4Don't blend a polygon that crosses itself
I'm drawing two polylines (which are lines in the sample) in webgl with enabled ... between those...
- 5WebGL polyline tessellation with tesspathy - Popular Blocks
WebGL polyline tessellation with tesspathy. +-. Leaflet · Open. yet another tessellation of polyl...