在WebGL 中繪製直線 - GetIt01
文章推薦指數: 80 %
之前我們介紹了「在WebGL 中繪製地圖(多邊形篇)」,本文會介紹關於直線的繪製 ... https://github.com/mattdesl/polyline-miter-util/blob/master/index.js#L9-L20
標籤:GIS(地理信息系統)WebGL計算機圖形學
在WebGL中繪製直線
05-09
之前我們介紹了「在WebGL中繪製地圖(多邊形篇)」,本文會介紹關於直線的繪製方法,大致包含以下內容:
直接使用原生gl.LINES的問題
使用沿法向拉伸後三角化的方法繪製直線
接頭樣式、反走樣等常見優化手段
Cesium、Mapbox、GeoJS等成熟引擎的實現
項目地址:https://github.com/xiaoiver/custom-mapbox-layer/blob/master/src/layers/Line3DLayer.ts?github.com
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」??
推薦閱讀:
TAG:WebGL|GIS(地理信息系統)|計算機圖形學|
一點新知
GetIt01
延伸文章資訊
- 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...
- 2Don't blend a polygon that crosses itself
I'm drawing two polylines (which are lines in the sample) in webgl with enabled ... between those...
- 3WebGL polyline tessellation with tesspathy - gists · GitHub
WebGL polyline tessellation with tesspathy. GitHub Gist: instantly share code, notes, and snippets.
- 4在WebGL 中绘制直线 - 知乎专栏
之前我们介绍了「 在WebGL 中绘制地图(多边形篇)」,本文会介绍关于直线 ... https://github.com/mattdesl/polyline-miter-util/blob/m...
- 5Don't blend a polyline that has crossed lines with itself in webgl
I'm drawing two polylines (which are lines in the sample) in webgl with enabled blending. gl.unif...