WebGL繪製有寬度的線

  WebGL中有寬度的線一直都是初學者的一道門檻,由於在windows系統中底層的渲染接口都是D3D提供的,因此不管你的lineWidth設置爲多少,最終繪製出來的只有一像素。即便在移動端能夠設置有寬度的線,可是在拐彎處原生api沒有作任何處理,因此每每達不到項目需求,再者好比對於虛線、導航線的繪製,原生api是無能爲力。差很少從事WebGL開發已經一週年,總結一下繪製線的方法和踩過的坑,聊以慰藉後來者。html

寬度線繪製原理

  寬度線的繪製最核心的思想就是利用三角形來繪製線,將一根有寬度的線,當作是多個三角形的拼接ios

  將線剖分紅三角形的過程是一個計算密集型的過程,若是放在主線程中會阻塞渲染形成卡頓,一般來說都是放到頂點着色器中來處理,利用GPU並行計算來處理。一般來着色中,將頂點沿着法線方向平移lineWidth/2的距離。對於一個頂點只能平移一次,因此在cpu中咱們須要把一個頂點複製兩份傳給gpu同時提早肯定好剖分出來的三角形的頂點索引順序。web

  對於拐彎處,須要作一系列的計算來肯定拐角的距離,好比:算法

  但這幅圖過於複雜,我比較喜歡下面這個比較簡單的圖canvas

  假設dir1爲向量last->current的單位向量,dir2爲向量current->next的單位向量,根據這兩個向量求出avg向量,avg向量 = normalize(dir1 + dir2);將avg向量旋轉九十度便可求出在拐角處的偏移向量,固然這個向量可向下,也能夠向上,因此通常對上文中重複的頂點還有對應的一個side變量,來告訴着色器應該向下仍是向上偏移,一樣上面圖中的last和next也要傳入對應上一個和下一個頂點的座標值。對應的着色器代碼:windows

// ios11下直接使用==判斷會有精度問題致使兩個數字不相同引出bug
'    if( abs(nextP.x - currentP.x)<=0.000001 && abs(nextP.y - currentP.y)<=0.000001) dir = normalize( currentP - prevP );',
'    else if( abs(prevP.x - currentP.x)<=0.000001 && abs(prevP.y - currentP.y) <=0.000001) dir = normalize( nextP - currentP );',
// '    if( nextP.x == currentP.x && nextP.y == currentP.y) dir = normalize( currentP - prevP );',
// '    else if( prevP.x == currentP.x && prevP.y == currentP.y ) dir = normalize( nextP - currentP );',
'    else {',
'        vec2 dir1 = normalize( currentP - prevP );',
'        vec2 dir2 = normalize( nextP - currentP );',
'        dir = normalize( dir1 + dir2 );',
'',
'',
'    }',
'',
'    vec2 normal = vec2( -dir.y, dir.x );',

着色器中的實踐

  原理上面已經實現,那麼在具體的繪製中,咱們還要明白一個問題,lineWidth的單位是什麼,若是你須要繪製的是以像素爲單位,那麼咱們就須要將3d座標映射到屏幕座標來進行計算,這樣繪製出來的線不會有明顯的透視效果,即不會受相機距離遠近的影響。api

  咱們須要幾個函數來幫忙,第一個是transform函數,用來將3D座標轉換成透視座標系下的座標:iphone

'vec4 transform(vec3 coord) {',
'    return projectionMatrix * modelViewMatrix * vec4(coord, 1.0);',
'}',

  

  接下來是project函數,這個函數傳入的是透視座標,也就是通過transform函數返回的座標;ide

'vec2 project(vec4 device) {',
'    vec3 device_normal = device.xyz / device.w;',
'    vec2 clip_pos = (device_normal * 0.5 + 0.5).xy;',
'    return clip_pos * resolution;',
'}',

  其中第一步device.xyz / device.w將座標轉化成ndc座標系下的座標,這個座標下,xyz的範圍所有都是-1~1之間。函數

  第二步device_normal * 0.5後全部座標的取值範圍在-0.5~0.5之間,後面在加上0.5後坐標範圍變爲0~1之間,因爲咱們繪線在屏幕空間,因此z值無用能夠丟棄,這裏咱們只取xy座標。

  第三部resolution是一個vec2類型,表明最終展現canvas的寬高。將clip_pos * resolution徹底轉化成屏幕座標,這時候x取值範圍在0~width之間,y取值範圍在0~height之間,單位像素。

  

  接下來的unproject函數,這個函數的做用是當咱們在屏幕空間中計算好最終頂點位置後,將該屏幕座標從新轉化成透視空間下的座標。是project的逆向過程。

'vec4 unproject(vec2 screen, float z, float w) {',
'    vec2 clip_pos = screen / resolution;',
'    vec2 device_normal = clip_pos * 2.0 - 1.0;',
'    return vec4(device_normal * w, z, w);',
'}',

  因爲屏幕空間的座標沒有z值和w值,因此須要外界傳入。

   最終着色器代碼:

// 請聯繫博主

  

虛線以及箭頭的繪製原理

  上面介紹了有寬度線的繪製,可是在一些地圖場景中,每每須要繪製虛線、地鐵線以及導航路線等有必定規則的路線。這裏主要介紹導航線的繪製,明白這個後虛線以及地鐵的線繪製就很簡單了。首先介紹一下導航線的核心原理,要繪製導航線咱們有幾個問題須要解決,好比:

  • 箭頭的間隔
  • 一個箭頭應該繪製在幾米的範圍內(範圍計算不許圖片會失真)
  • 如何讓線區域範圍內的每一個像素取的紋理重對應像素
  • 以及一些各個機型上兼容性問題

  不管是虛線、地鐵線、導航線均可以用這個圖來表達。咱們能夠規定每一個markerDelta米在halfd(halfd = markerDelta/2)到uvDelta長的距離裏繪製一個標識(虛線的空白區域,地鐵線的黑色區域、導航線的箭頭)。那麼問題來了如何讓每個像素都清楚的知道本身應該成爲線的哪一部分?這個時候個人方案是求出每一個頂點距離起始座標點的 ~距離/路線總長度~,將這個距離存入紋理座標中,利用紋理座標的插值保證每一個像素都能均勻的知道本身的長度佔比;在着色器中乘以路線總長度,算出這個像素距離起始點距離uvx。uvx對markerDelta取模運算得muvx,求出在本間隔中的長度,在根據規則(if(muvx >= halfd && muvx <= halfd + uvDelta))計算這個像素是否在uvDelta中。對於導航線,咱們須要從箭頭圖片的紋理中取紋素,因此該像素對應的真正的紋理座標是float s = (muvx - halfd) / uvDelta;對應着色器代碼爲

float uvx = vUV.x * repeat.x;',
'    float muvx = mod(uvx, markerDelta);',
'    float halfd = markerDelta / 2.0;',
'    if(muvx >= halfd && muvx <= halfd + uvDelta) {',
'      float s = (muvx - halfd) / uvDelta;',
'      tc = texture2D( map, vec2(s, vUV.y));',
'      c.xyzw = tc.w >= 0.5 ? tc.xyzw : c.xyzw;',
'    }',

  最終完整着色器代碼爲:

//請聯繫博主

  關於markerDelta和uvDelta來講,則須要跟相機距離、紋理圖片性質等因素來綜合計算,好比在個人項目中的計算法方式:

let meterPerPixel = this._getPixelMeterRatio();
      let radio = meterPerPixel / 0.0746455; // 當前比例尺與21級比例尺相比
      let mDelta = Math.min(30, Math.max(radio * 10, 1)); // 最大間隔爲10米
      let uvDelta = 8 * meterPerPixel;// 8是經驗值,實際要根據線實際像素寬度、紋理圖片寬高比來計算
      uvDelta = /*isIOSPlatform() ? 8 * meterPerPixel : */parseFloat(uvDelta.toFixed(2));

      this.routes.forEach(r => {
        if (r._isVirtual) {
          return;
        }
        r._material.uniforms.uvDelta = {type: 'f', value: uvDelta};// 暫時取一米
        r._material.uniforms.markerDelta = {type: 'f', value: mDelta};
      });

  另外一個問題如何繪製有邊框的線,能夠在着色器中來控制,好比設定一個閾值,超過這個閾值的就繪製成border的顏色;或者簡單點也能夠把一條線繪製兩遍,寬的使用border的顏色,窄的使用主線的顏色,同時控制兩條線的繪製順序,讓主線壓住border線。

兼容性的坑

  首先發如今iphone6p 10.3.3中紋理失真;

  紋理失真確定是設備像素與紋理紋素沒有對應,可是爲何沒有對應呢?紋理失真就是uv方向上對應問題,爲了排查這個過程我把只要落在紋理區域的範圍都設置成紅色,發如今縱向方向上無論紋理在什麼尺度下紅色區域範圍都是同樣的,並且結合圖片發現縱向上基本覆蓋了整個紋理圖片,因此縱向沒有問題。

 

   那麼就是橫向上的取值,問題,可是橫向是經過紋理座標產生的,沒有計算的內容;最後懷疑到數字精度問題;將其中的mediump改爲highp;這個問題獲得解決;iphone6上能畫出完美的箭頭

'precision mediump float;',

 

 

   然而又碰到了另外一個很是棘手的問題,iphone7以上的設備箭頭周圍有碎點。。。

  首先要搞清楚這些碎點是什麼,發現不論換那張圖片都有碎點,一開始我覺得這些碎點是紋理座標計算時的精度問題,後來發現不論怎麼調整紋理u的取值範圍都沒法作到在任什麼時候刻徹底避免這個問題。

  最後偶然發現改變一下這個等式就能解決問題。

 

  因此確定這個些碎點確定是從紋理中取得的,有可能在這個區域內,Linear過濾模式恰好取得了幾個像素的平均值,致使這裏的alpha通道非是0.0同時取到了必定的平均顏色纔會顯示這些碎點;最後懷疑這是由於mipmap方式致使這個設備像素恰好落到先後兩章圖片的像素上,綜合差值後獲得一個碎點;至因而否是跟mipmap有關還須要後續驗證,因爲項目時間關係先往下解決。解決完這個問題已是凌晨四點多

 

  然而又出現了另外一個問題,iphone6下在某些角度下,紋理會消失,發現是由於上面的判斷引發的

  將閾值範圍改爲可以解決問題,後續這塊須要梳理一下,做爲一個外部可傳入的變量來處理

   如今的線並無對端頭作處理,也就是沒有沒有實現lineCap效果,若是想知道lineCap的實現原理能夠看個人這篇文章:WebGL繪製有端頭的線

參考文章

http://codeflow.org/entries/2012/aug/05/webgl-rendering-of-solid-trails/

https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader

Drawing Antialiased Lines with OpenGLhttps://www.mapbox.com/blog/drawing-antialiased-lines/

Smooth thick lines using geometry shader

Drawing Lines is Hard

相關文章
相關標籤/搜索