前言
這篇文章是使用游戲引擎探索地圖可視化的開篇叫乌。傳統(tǒng)的地圖渲染通常是在iOS/Android/Web平臺進行的,為了探究更酷炫的地圖展示蔗坯,會記錄基于UE4/Unity進行地圖渲染的探索過程元镀。
地圖基礎元素 - 線
線作為地圖渲染的基本元素露泊,在地圖中可以代表各種形式的道路。道路數(shù)據(jù)通常以離散點串形式存儲,因此如何將點串繪制成有寬度的線是渲染最關(guān)注的問題宣赔。本文記錄了繪制有寬度的線的方法预麸,并對優(yōu)化線展示效果的各種線帽和拐角進行了闡述。
繪制有寬度的線
道路數(shù)據(jù)通常以離散點串和其對應線寬進行存儲儒将,為了在游戲引擎中進行顯示吏祸,就需要將其擴展為有寬度的線。UE4和Unity都可以使用代碼生成Mesh進行基本圖元的渲染展示(UE4使用Procedural Mesh Component钩蚊,Unity使用MeshFilter和MeshRenderer)贡翘,而Mesh渲染的基本單位是三角形,因此問題就轉(zhuǎn)化為如何根據(jù)點串和線寬砰逻,構(gòu)造出一組三角形使其能夠拼合產(chǎn)生具有寬度的線鸣驱。
對于只有兩個點的直線,通過獲取與直線垂直的向量蝠咆,向兩個方向各擴展lineWidth/2長度產(chǎn)生頂點踊东,劃分為三角形即可。
而對于多個離散點構(gòu)成的線刚操,繪制的時候遇到2個問題:
- 僅使用相鄰點計算垂直向量闸翅,導致擴充出的線拐角處會有斷裂,如下圖所示菊霜〖峒剑可以看到,僅僅每個相鄰線段進行擴充是不夠的鉴逞,還需要考慮如何處理線的拐角记某。
- 考慮處理線的拐角,但獲取頂點擴充向量的方向和大小不對构捡,導致繪制的線不等寬辙纬。下圖根據(jù)相隔頂點連線的垂線確定擴充向量,但因向量隨頂點位置變化而變化叭喜,因此不能作為生成等寬線的依據(jù)贺拣。
有了上面的思考,任務就變成了擴充出等寬且有拐角的線:相隔點的頂點位置會變化捂蕴,但由其確定的向量方向是不變的譬涡,因此依靠頂點兩側(cè)線段的單位向量,就能確定出唯一的擴充向量啥辨。確定擴充方向后涡匀,還需要確定擴充向量的大小使得最終的線等寬。
偽代碼如下溉知,擴充方向可由線段單位向量組合確定陨瘩,需要注意擴充長度并不是lineWidth/2腕够,而是需要根據(jù)線段夾角進行計算調(diào)整。擴充向量計算好之后舌劳,即可根據(jù)離散點串生擴充頂點帚湘,根據(jù)頂點坐標剖分為三角形,構(gòu)建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) // 根據(jù)角度調(diào)整擴充長度
繪制線帽LineCap
根據(jù)上一節(jié)操作已經(jīng)可以繪制出有寬度的線大诸,但也能夠看出線在開頭和結(jié)尾處都是矩形,不夠優(yōu)雅美觀贯卦。因此本節(jié)主要會解決繪制線帽的問題资柔。
較為常用的LineCap主要有以下三種:
- Butt 無線帽模式,上一節(jié)繪制的線默認即為Butt
- Round 在線的兩端添加額外的半圓撵割,其半徑為lineWidth/2
- Square 在線兩端添加額外的矩形贿堰,其高度為lineWidth/2
Square形式的線帽繪制較為簡單,只需要在開頭和結(jié)尾部分根據(jù)延伸方向額外添加矩形即可啡彬,兩個矩形可以很簡單的劃分為四個三角形官边,添加在畫線mesh中一同渲染。而Round形式的半圓線帽在繪制上就麻煩了許多外遇,在實踐過程中主要探索了以下三個方案:
1、使用三角形近似繪制半圓
最直觀的方式就是直接繪制半圓線帽契吉,但是渲染的最小單元是三角形跳仿,因此只能通過添加多個三角形近似表示半圓。這種方式需要根據(jù)添加三角形的個數(shù)捐晶,進行幾何運算確定各個頂點坐標菲语,通過三角形組合成半圓,雖然方法直觀可行惑灵,但為了使線帽圓滑山上,額外添加的較多頂點和進行的大量數(shù)學運算都會對性能帶來影響,存在性能和效果的取舍英支。
2佩憾、使用圖片近似繪制半圓
第二種方案借助圖片可以省去添加額外頂點和進行數(shù)學計算的步驟,近似得到半圓線帽干花。
圖片工具大小為16×16像素妄帘,左右兩部分分別繪制半圓和矩形。對于半圓部分池凄,內(nèi)部點透明度設置為1抡驼,圓弧上覆蓋的像素點,通過調(diào)低透明度值弱化鋸齒感肿仑,圓弧之外部分則將透明度設置為0致盟,整體使用透明度構(gòu)建出近似的半圓碎税。矩形部分則作為工具,用于填充非線帽部分馏锡。
這種方案在構(gòu)建線Mesh時雷蹂,與Square線帽方案一致,但需要將紋理uv值也與頂點進行綁定眷篇。Square線帽額外添加的矩形綁定圖片左側(cè)半圓的uv萎河,而原有線部分綁定右側(cè)矩形uv即可。渲染時蕉饼,可以在片元著色器中逐像素提取到映射的圖片顏色值虐杯,輸出顏色使用頂點原色,但透明度值采用圖片的透明度值昧港,從而將圓弧外側(cè)像素剔除擎椰。使用該方案需要開啟透明度混合,從而不顯示圓弧外側(cè)像素创肥。
這種方案也是半圓的近似表示达舒,在距離較近觀察時會出現(xiàn)圓弧線帽發(fā)虛,原因是受限于圖片大小叹侄,如果增加圖片大小可以緩解問題巩搏,但也會增加開銷,也需要做性能和效果的取舍平衡趾代。
3贯底、逐像素繪制半圓
第三種方案由方案二演進而來,不是使用圖片剔除像素撒强,而是借助于半圓的特性禽捆,在片元著色器中剔除所有不滿足條件的像素,做到繪制像素級的半圓線帽飘哨。其主要原理是在添加Square線帽后胚想,判斷渲染時像素距離線起始頂點距離,若超過lineWidth/2(即紅色部分)則剔除像素芽隆,從而逐像素繪制出半圓線帽浊服。
像素剔除會在片元著色器中并行進行,效率高但無法存儲上下文信息胚吁,而剔除邏輯需要獲取圓心信息臼闻,同時片元著色器的坐標已經(jīng)轉(zhuǎn)化為裁剪空間的齊次坐標,無法進行幾何運算囤采,因此需要將一些輔助信息傳遞到片元著色器中進行操作述呐。
輔助信息定義為二維向量geometryInfo,其含義為頂點在線中的相對位置蕉毯,點串的起點作為(0,0)乓搬,終點作為(1,0)思犁,中間的點根據(jù)距離轉(zhuǎn)化為[0,1]間的數(shù)值。根據(jù)擴充向量得到的頂點进肯,則根據(jù)擴充方向激蹲,向量y值賦值為1或-1。因為已經(jīng)人為定義了線寬為2的相對坐標系江掩,因此線帽上頂點的輔助信息x值可以轉(zhuǎn)化為-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) // 起點側(cè)線帽
{
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) // 終點側(cè)線帽
{
if(dot(float2(i.geometryInfo.x - 1, i.geometryInfo.y), float2(i.geometryInfo.x - 1, i.geometryInfo.y)) > 1)
{
discard;
}
}
return i.color;
}
使用該方案生成的圓角钙畔,在近距離觀看時因為線帽的渲染像素增多茫陆,因此也不會產(chǎn)生虛化或者鋸齒感,能夠得到圓滑的效果擎析。
繪制線拐角LineJoin
線帽已經(jīng)圓潤優(yōu)雅之后簿盅,同時也發(fā)現(xiàn)繪制的線在一些極端情況下拐角會存在bad case。例如下圖所示叔锐,對于夾角較小的線會產(chǎn)生非常大的尖角;而對于線段呈直角情況顯示的也同樣是直角拐角见秽,不夠圓潤美觀愉烙。本節(jié)主要會解決繪制線拐角的問題。
較為常用的LineJoin主要有以下三種:
- Miter 尖角樣式解取,上一節(jié)繪制的線即屬于Miter
- Bevel 切角樣式步责,以橫切面替代尖角
- Round 圓角樣式,以圓弧替代尖角
有了擴充線和線帽的繪制經(jīng)驗禀苦,從上圖可以看出Bevel和Round樣式不需要根據(jù)線段夾角計算擴充向量蔓肯。繪制時按照矩形擴展后,Bevel樣式只需要根據(jù)擴充頂點補齊一個三角形構(gòu)成切面振乏。而對于Round樣式蔗包,除了起終點外,每一個頂點擴充處根據(jù)矩形方向繪制兩個半圓慧邮,疊加就能達到圓拐角效果调限。
半圓部分的繪制原理和繪制半圓線帽一樣舟陆,添加矩形再剔除多余像素,因此需要將geometryInfo擴充為四維向量耻矮,后兩位表示頂點在當前段的相對位置秦躯,同樣在片元著色器中進行像素剔除。這里片元著色器的代碼邏輯與圓角線帽類似裆装,不再贅述踱承。最終的拐角效果如下圖。
整體的繪制流程可以簡單總結(jié)為下圖哨免,等寬線作為線渲染的主體茎活,線帽/拐角作為線渲染的效果優(yōu)化項。在具體實踐中铁瞒,可以通過設置配置項的方式方便的更改線帽/拐角的樣式妙色。
作者:程序員阿Tu
鏈接:https://zhuanlan.zhihu.com/p/266026334
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)慧耍,非商業(yè)轉(zhuǎn)載請注明出處身辨。