在WebGL 中繪製直線 - GetIt01

文章推薦指數: 80 %
投票人數:10人

之前我們介紹了「在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



請為這篇文章評分?