1. 程式人生 > 實用技巧 >繪製地圖基礎元素-線(上篇)

繪製地圖基礎元素-線(上篇)

前言

這篇文章是使用遊戲引擎探索地圖視覺化的開篇。傳統的地圖渲染通常是在iOS/Android/Web平臺進行的,為了探究更酷炫的地圖展示,會記錄基於UE4/Unity進行地圖渲染的探索過程。

地圖基礎元素 - 線

線作為地圖渲染的基本元素,在地圖中可以代表各種形式的道路。道路資料通常以離散點串形式儲存,因此如何將點串繪製成有寬度的線是渲染最關注的問題。本文記錄了繪製有寬度的線的方法,並對優化線展示效果的各種線帽和拐角進行了闡述。

繪製有寬度的線

道路資料通常以離散點串和其對應線寬進行儲存,為了在遊戲引擎中進行顯示,就需要將其擴充套件為有寬度的線。UE4和Unity都可以使用程式碼生成Mesh進行基本圖元的渲染展示(UE4使用Procedural Mesh Component,Unity使用MeshFilter和MeshRenderer),而Mesh渲染的基本單位是三角形,因此問題就轉化為如何根據點串和線寬,構造出一組三角形使其能夠拼合產生具有寬度的線。

對於只有兩個點的直線,通過獲取與直線垂直的向量,向兩個方向各擴充套件lineWidth/2長度產生頂點,劃分為三角形即可。

而對於多個離散點構成的線,繪製的時候遇到2個問題:

  • 僅使用相鄰點計算垂直向量,導致擴充出的線拐角處會有斷裂,如下圖所示。可以看到,僅僅每個相鄰線段進行擴充是不夠的,還需要考慮如何處理線的拐角。

  • 考慮處理線的拐角,但獲取頂點擴充向量的方向和大小不對,導致繪製的線不等寬。下圖根據相隔頂點連線的垂線確定擴充向量,但因向量隨頂點位置變化而變化,因此不能作為生成等寬線的依據。

有了上面的思考,任務就變成了擴充出等寬且有拐角的線:相隔點的頂點位置會變化,但由其確定的向量方向是不變的,因此依靠頂點兩側線段的單位向量,就能確定出唯一的擴充向量。確定擴充方向後,還需要確定擴充向量的大小使得最終的線等寬。

虛擬碼如下,擴充方向可由線段單位向量組合確定,需要注意擴充長度並不是lineWidth/2,而是需要根據線段夾角進行計算調整。擴充向量計算好之後,即可根據離散點串生擴充頂點,根據頂點座標剖分為三角形,構建Mesh進行渲染。

// 計算擴充方向
Vec2f a = (P1 - P0) * normalized()
Vec2f b = (P2 - P1) * normalized()
Vec2f avg = a + b
Vec2f direction =  Vec2f(-avg.y, avg.x).normalized() //擴充方向為avg的垂直方向

// 計算擴充長度
float t =  Abs(Asin(a × b)) / 2  // 單位向量叉乘獲得夾角正弦
float length = lineWidth / 2 / Cos(t)  // 根據角度調整擴充長度

繪製線帽LineCap

根據上一節操作已經可以繪製出有寬度的線,但也能夠看出線在開頭和結尾處都是矩形,不夠優雅美觀。因此本節主要會解決繪製線帽的問題。

較為常用的LineCap主要有以下三種:

  • Butt 無線帽模式,上一節繪製的線預設即為Butt
  • Round 線上的兩端新增額外的半圓,其半徑為lineWidth/2
  • Square 線上兩端新增額外的矩形,其高度為lineWidth/2

Square形式的線帽繪製較為簡單,只需要在開頭和結尾部分根據延伸方向額外新增矩形即可,兩個矩形可以很簡單的劃分為四個三角形,新增在畫線mesh中一同渲染。而Round形式的半圓線帽在繪製上就麻煩了許多,在實踐過程中主要探索了以下三個方案:

1、使用三角形近似繪製半圓

最直觀的方式就是直接繪製半圓線帽,但是渲染的最小單元是三角形,因此只能通過新增多個三角形近似表示半圓。這種方式需要根據新增三角形的個數,進行幾何運算確定各個頂點座標,通過三角形組合成半圓,雖然方法直觀可行,但為了使線帽圓滑,額外新增的較多頂點和進行的大量數學運算都會對效能帶來影響,存在效能和效果的取捨。

2、使用圖片近似繪製半圓

第二種方案藉助圖片可以省去新增額外頂點和進行數學計算的步驟,近似得到半圓線帽。

圖片工具大小為16×16畫素,左右兩部分分別繪製半圓和矩形。對於半圓部分,內部點透明度設定為1,圓弧上覆蓋的畫素點,通過調低透明度值弱化鋸齒感,圓弧之外部分則將透明度設定為0,整體使用透明度構建出近似的半圓。矩形部分則作為工具,用於填充非線帽部分。

這種方案在構建線Mesh時,與Square線帽方案一致,但需要將紋理uv值也與頂點進行繫結。Square線帽額外新增的矩形繫結圖片左側半圓的uv,而原有線部分繫結右側矩形uv即可。渲染時,可以在片元著色器中逐畫素提取到對映的圖片顏色值,輸出顏色使用頂點原色,但透明度值採用圖片的透明度值,從而將圓弧外側畫素剔除。使用該方案需要開啟透明度混合,從而不顯示圓弧外側畫素。

這種方案也是半圓的近似表示,在距離較近觀察時會出現圓弧線帽發虛,原因是受限於圖片大小,如果增加圖片大小可以緩解問題,但也會增加開銷,也需要做效能和效果的取捨平衡。

3、逐畫素繪製半圓

第三種方案由方案二演進而來,不是使用圖片剔除畫素,而是藉助於半圓的特性,在片元著色器中剔除所有不滿足條件的畫素,做到繪製畫素級的半圓線帽。其主要原理是在新增Square線帽後,判斷渲染時畫素距離線起始頂點距離,若超過lineWidth/2(即紅色部分)則剔除畫素,從而逐畫素繪製出半圓線帽。

畫素剔除會在片元著色器中並行進行,效率高但無法儲存上下文資訊,而剔除邏輯需要獲取圓心資訊,同時片元著色器的座標已經轉化為裁剪空間的齊次座標,無法進行幾何運算,因此需要將一些輔助資訊傳遞到片元著色器中進行操作。

輔助資訊定義為二維向量geometryInfo,其含義為頂點線上中的相對位置,點串的起點作為(0,0),終點作為(1,0),中間的點根據距離轉化為[0,1]間的數值。根據擴充向量得到的頂點,則根據擴充方向,向量y值賦值為1或-1。因為已經人為定義了線寬為2的相對座標系,因此線帽上頂點的輔助資訊x值可以轉化為-1和2,這樣任何小於0和大於1的x值都可以表示該點是線帽部分,而且可以很方便的和(0,0)、(1,0)做距離計算,並與半圓半徑1進行比較。

geometryInfo繫結在每個頂點傳入shader後,會在片元著色器中按畫素進行線性插值,因此每一個畫素都會獲得一個可以標識自己區域性位置的輔助資訊,藉助於該資訊進行距離判斷就可以進行畫素剔除,這裡展示的是Unity Shader程式碼,UE4可以在Material中還原邏輯。

fixed4 frag (v2f i) : SV_Target
{
    if(i.geometryInfo.x < 0)  // 起點側線帽
    {    
        if(dot(float2(i.geometryInfo.x, i.geometryInfo.y), float2(i.geometryInfo.x, i.geometryInfo.y)) > 1)
        {   
            discard; // 距離圓心距離大於1則剔除
        }
    } 
    else if(i.geometryInfo.x > 1) // 終點側線帽
    {
        if(dot(float2(i.geometryInfo.x - 1, i.geometryInfo.y), float2(i.geometryInfo.x - 1, i.geometryInfo.y)) > 1)
        {   
            discard; 
        }
    }

    return i.color;
 }

使用該方案生成的圓角,在近距離觀看時因為線帽的渲染畫素增多,因此也不會產生虛化或者鋸齒感,能夠得到圓滑的效果。

繪製線拐角LineJoin

線帽已經圓潤優雅之後,同時也發現繪製的線在一些極端情況下拐角會存在bad case。例如下圖所示,對於夾角較小的線會產生非常大的尖角;而對於線段呈直角情況顯示的也同樣是直角拐角,不夠圓潤美觀。本節主要會解決繪製線拐角的問題。

較為常用的LineJoin主要有以下三種:

  • Miter 尖角樣式,上一節繪製的線即屬於Miter
  • Bevel 切角樣式,以橫切面替代尖角
  • Round 圓角樣式,以圓弧替代尖角

有了擴充線和線帽的繪製經驗,從上圖可以看出Bevel和Round樣式不需要根據線段夾角計算擴充向量。繪製時按照矩形擴充套件後,Bevel樣式只需要根據擴充頂點補齊一個三角形構成切面。而對於Round樣式,除了起終點外,每一個頂點擴充處根據矩形方向繪製兩個半圓,疊加就能達到圓拐角效果。

半圓部分的繪製原理和繪製半圓線帽一樣,新增矩形再剔除多餘畫素,因此需要將geometryInfo擴充為四維向量,後兩位表示頂點在當前段的相對位置,同樣在片元著色器中進行畫素剔除。這裡片元著色器的程式碼邏輯與圓角線帽類似,不再贅述。最終的拐角效果如下圖。

整體的繪製流程可以簡單總結為下圖,等寬線作為線渲染的主體,線帽/拐角作為線渲染的效果優化項。在具體實踐中,可以通過設定配置項的方式方便的更改線帽/拐角的樣式。

作者:程式設計師阿Tu

連結:https://zhuanlan.zhihu.com/p/266026334

來源:知乎

著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。