這篇文章是使用遊戲引擎探索地圖可視化的開篇。傳統的地圖渲染一般是在iOS/Android/Web平臺進行的,爲了探究更酷炫的地圖展現,會記錄基於UE4/Unity進行地圖渲染的探索過程。程序員
線做爲地圖渲染的基本元素,在地圖中能夠表明各類形式的道路。道路數據一般以離散點串形式存儲,所以如何將點串繪製成有寬度的線是渲染最關注的問題。本文記錄了繪製有寬度的線的方法,並對優化線展現效果的各類線帽和拐角進行了闡述。工具
道路數據一般以離散點串和其對應線寬進行存儲,爲了在遊戲引擎中進行顯示,就須要將其擴展爲有寬度的線。UE4和Unity均可以使用代碼生成Mesh進行基本圖元的渲染展現(UE4使用Procedural Mesh Component,Unity使用MeshFilter和MeshRenderer),而Mesh渲染的基本單位是三角形,所以問題就轉化爲如何根據點串和線寬,構造出一組三角形使其可以拼合產生具備寬度的線。性能
對於只有兩個點的直線,經過獲取與直線垂直的向量,向兩個方向各擴展lineWidth/2長度產生頂點,劃分爲三角形便可。優化
而對於多個離散點構成的線,繪製的時候遇到2個問題:spa
有了上面的思考,任務就變成了擴充出等寬且有拐角的線:相隔點的頂點位置會變化,但由其肯定的向量方向是不變的,所以依靠頂點兩側線段的單位向量,就能肯定出惟一的擴充向量。肯定擴充方向後,還須要肯定擴充向量的大小使得最終的線等寬。3d
僞代碼以下,擴充方向可由線段單位向量組合肯定,須要注意擴充長度並非lineWidth/2,而是須要根據線段夾角進行計算調整。擴充向量計算好以後,便可根據離散點串生擴充頂點,根據頂點座標剖分爲三角形,構建Mesh進行渲染。code
// 計算擴充方向 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) // 根據角度調整擴充長度
根據上一節操做已經能夠繪製出有寬度的線,但也可以看出線在開頭和結尾處都是矩形,不夠優雅美觀。所以本節主要會解決繪製線帽的問題。orm
較爲經常使用的LineCap主要有如下三種:blog
Square形式的線帽繪製較爲簡單,只須要在開頭和結尾部分根據延伸方向額外添加矩形便可,兩個矩形能夠很簡單的劃分爲四個三角形,添加在畫線mesh中一同渲染。而Round形式的半圓線帽在繪製上就麻煩了許多,在實踐過程當中主要探索瞭如下三個方案:遊戲
最直觀的方式就是直接繪製半圓線帽,可是渲染的最小單元是三角形,所以只能經過添加多個三角形近似表示半圓。這種方式須要根據添加三角形的個數,進行幾何運算肯定各個頂點座標,經過三角形組合成半圓,雖然方法直觀可行,但爲了使線帽圓滑,額外添加的較多頂點和進行的大量數學運算都會對性能帶來影響,存在性能和效果的取捨。
第二種方案藉助圖片能夠省去添加額外頂點和進行數學計算的步驟,近似獲得半圓線帽。
圖片工具大小爲16×16像素,左右兩部分分別繪製半圓和矩形。對於半圓部分,內部點透明度設置爲1,圓弧上覆蓋的像素點,經過調低透明度值弱化鋸齒感,圓弧以外部分則將透明度設置爲0,總體使用透明度構建出近似的半圓。矩形部分則做爲工具,用於填充非線帽部分。
這種方案在構建線Mesh時,與Square線帽方案一致,但須要將紋理uv值也與頂點進行綁定。Square線帽額外添加的矩形綁定圖片左側半圓的uv,而原有線部分綁定右側矩形uv便可。渲染時,能夠在片元着色器中逐像素提取到映射的圖片顏色值,輸出顏色使用頂點原色,但透明度值採用圖片的透明度值,從而將圓弧外側像素剔除。使用該方案須要開啓透明度混合,從而不顯示圓弧外側像素。
這種方案也是半圓的近似表示,在距離較近觀察時會出現圓弧線帽發虛,緣由是受限於圖片大小,若是增長圖片大小能夠緩解問題,但也會增長開銷,也須要作性能和效果的取捨平衡。
第三種方案由方案二演進而來,不是使用圖片剔除像素,而是藉助於半圓的特性,在片元着色器中剔除全部不知足條件的像素,作到繪製像素級的半圓線帽。其主要原理是在添加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; }
使用該方案生成的圓角,在近距離觀看時由於線帽的渲染像素增多,所以也不會產生虛化或者鋸齒感,可以獲得圓滑的效果。
線帽已經圓潤優雅以後,同時也發現繪製的線在一些極端狀況下拐角會存在bad case。例以下圖所示,對於夾角較小的線會產生很是大的尖角;而對於線段呈直角狀況顯示的也一樣是直角拐角,不夠圓潤美觀。本節主要會解決繪製線拐角的問題。
較爲經常使用的LineJoin主要有如下三種:
有了擴充線和線帽的繪製經驗,從上圖能夠看出Bevel和Round樣式不須要根據線段夾角計算擴充向量。繪製時按照矩形擴展後,Bevel樣式只須要根據擴充頂點補齊一個三角形構成切面。而對於Round樣式,除了起終點外,每個頂點擴充處根據矩形方向繪製兩個半圓,疊加就能達到圓拐角效果。
半圓部分的繪製原理和繪製半圓線帽同樣,添加矩形再剔除多餘像素,所以須要將geometryInfo擴充爲四維向量,後兩位表示頂點在當前段的相對位置,一樣在片元着色器中進行像素剔除。這裏片元着色器的代碼邏輯與圓角線帽相似,再也不贅述。最終的拐角效果以下圖。
總體的繪製流程能夠簡單總結爲下圖,等寬線做爲線渲染的主體,線帽/拐角做爲線渲染的效果優化項。在具體實踐中,能夠經過設置配置項的方式方便的更改線帽/拐角的樣式。
做者:程序員阿Tu連接:https://zhuanlan.zhihu.com/p/...
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。