webGL 如何繪制dash array(虛線(xiàn)),有寬度的道路等地圖中的樣式?
時(shí)間:2024-01-16 13:24:01 | 來(lái)源:網(wǎng)站運(yùn)營(yíng)
時(shí)間:2024-01-16 13:24:01 來(lái)源:網(wǎng)站運(yùn)營(yíng)
webGL 如何繪制dash array(虛線(xiàn)),有寬度的道路等地圖中的樣式?:可視化研發(fā)之線(xiàn)的畫(huà)法:直線(xiàn)、曲線(xiàn)和動(dòng)畫(huà)(WebGL篇)
背景介紹項(xiàng)目的背景源自我們的自研圖表庫(kù)的目標(biāo),由于市面上的眾多圖表庫(kù)在擴(kuò)展等方面不滿(mǎn)足我們?nèi)缃癖姸喈a(chǎn)品的需求,我們需要一套靈活、高效、可擴(kuò)展性強(qiáng)的圖形語(yǔ)法來(lái)適應(yīng)各種業(yè)務(wù)場(chǎng)景。所以我們就開(kāi)始了自研圖表庫(kù)的進(jìn)程,由于WebGL相較于Canvas出色的性能(在某些場(chǎng)景中),我們的渲染層提供了基于WebGL的渲染方式。
在研發(fā)過(guò)程中有總結(jié)了很多實(shí)踐(線(xiàn)段繪制、文本渲染、反走樣、高效的cache與頂點(diǎn)等屬性的merge策略等)。那么今天先來(lái)嘮嘮我們是如何使用WebGL進(jìn)行線(xiàn)段繪制的。
摘要本文將介紹基于GPU的直線(xiàn)繪制方案,只需要在CPU中將計(jì)算好的路徑點(diǎn)傳入,動(dòng)態(tài)的在GPU中計(jì)算出指定粗細(xì),line cap,line dash的線(xiàn)段,并且具有用戶(hù)體驗(yàn)良好的動(dòng)畫(huà)。該方案具有較強(qiáng)的靈活性,對(duì)于給定路徑的線(xiàn)段,不用為了線(xiàn)段的效果重復(fù)生成線(xiàn)段的頂點(diǎn)數(shù)據(jù)。
繪制普通的線(xiàn)段
繪制一條1px粗細(xì)線(xiàn)段我們需要兩步,給出線(xiàn)段的幾個(gè)頂點(diǎn),然后在這些頂點(diǎn)之間插值畫(huà)線(xiàn)就行了。WebGL中提供了線(xiàn)段這種基本圖元(GL_LINES),但是GL_LINES只能繪制1px的線(xiàn)段,那么繪制超過(guò)1px像素的線(xiàn)段我們又該如何繪制呢?
并且即使是1px就可以直接使用GL_LINES繪制嗎?好像也不行,我們的渲染庫(kù)需要支持指定分辨率渲染,如果用戶(hù)指定2倍分辨率渲染,那么顯示在界面上的畫(huà)面是1024*1024,而需要渲染的畫(huà)面大小是2048*2048,所以在指定分辨率的情況下,包括位置、大小等屬性都需要等比縮放,所以即使用戶(hù)看到的是1px線(xiàn)段,在緩存中也可能超過(guò)1px大小。所以我們需要支持指定粗細(xì)的線(xiàn)段繪制。
基于法線(xiàn)繪制動(dòng)態(tài)粗細(xì)的線(xiàn)段
針對(duì)超過(guò)1px大小的線(xiàn)段,我們就需要使用其他的方式進(jìn)行渲染,這里介紹我們采用的方式,如圖1.1所示,也就是將每段線(xiàn)段拆分為上下兩個(gè)三角形進(jìn)行渲染:
圖1.1: 基于法線(xiàn)的渲染(圖片引自
https://mattdesl.svbtle.com/drawing-lines-is-hard)
對(duì)于上下頂點(diǎn)的計(jì)算方式,如圖1.2所示,其實(shí)就是計(jì)算p1處的角平分線(xiàn)。然后沿著角平分線(xiàn)擴(kuò)充:
圖1.2: 計(jì)算交點(diǎn)的角平分線(xiàn)(圖片引自:
https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader)
以上方案就可以在GPU中渲染出普通的指定粗細(xì)的線(xiàn)段,這不是本文重點(diǎn),就不贅述了,具體可以參考
https://mattdesl.svbtle.com/drawing-lines-is-hard這篇博客。
這里只做簡(jiǎn)單介紹,如圖1.3所示的路徑p0 p1 p2,需要將路徑擴(kuò)充為指定粗細(xì)的線(xiàn)段,也就需要計(jì)算擴(kuò)充后的線(xiàn)段邊界的頂點(diǎn)p0_0、p0_1、p1_0、p1_1、p2_0、p2_1
圖1.3:指定粗細(xì)的線(xiàn)段
這樣只需要傳入線(xiàn)段路徑,就可以計(jì)算出指定粗細(xì)大小的線(xiàn)段中的各個(gè)頂點(diǎn)了。我們直接將角平分線(xiàn)作為向量傳入,并傳入線(xiàn)段粗細(xì),就可以直接在頂點(diǎn)著色器中動(dòng)態(tài)計(jì)算出指定粗細(xì)的線(xiàn)段了。
圖1.4:繪制指定粗細(xì)的線(xiàn)段
如何繪制線(xiàn)段Cap上面介紹了如何使用GPU繪制動(dòng)態(tài)粗細(xì)的線(xiàn)段,接下來(lái)我們來(lái)聊聊如何繪制帶有l(wèi)ine cap的線(xiàn)段,line cap表示線(xiàn)段首尾的樣式,它是線(xiàn)段的額外樣式,不受線(xiàn)段長(zhǎng)度影響。
如圖2.1所示,在Canvas2d中,line cap表現(xiàn)出的是額外長(zhǎng)度,所以我們只需要在線(xiàn)段的首尾動(dòng)態(tài)添加line cap即可。
圖2.1:canvas 2d中的line cap
繪制square cap我們來(lái)聊聊如何繪制square的cap,square cap的長(zhǎng)度取決于線(xiàn)段粗細(xì),而線(xiàn)段粗細(xì)用上面的算法是在GPU里動(dòng)態(tài)渲染的,所以square cap我們最好也需要在GPU里動(dòng)態(tài)渲染,那么cap的長(zhǎng)度就需要在GPU里計(jì)算出來(lái),計(jì)算方式非常簡(jiǎn)單,我們可以使用上面的方式通過(guò)向量計(jì)算:
如圖2.2所示,圖中藍(lán)線(xiàn)就是cap中需要用到的向量,令該向量為n0
圖2.2:計(jì)算square cap
繪制round cap上面介紹了如何繪制square cap,對(duì)于round cap就是將cap變?yōu)閳A形即可,這里GPU需要知道以下信息:
- 這個(gè)像素是線(xiàn)段體中的像素還是cap中的像素(cap中的像素需要計(jì)算round)
- 對(duì)于cap中的像素,需要知道圓心的位置
對(duì)于判斷是線(xiàn)段體中的像素還是cap中的像素,有很多方法,我們采取的是使用距離的方式判斷(這主要是因?yàn)楹竺娴牟呗砸惨玫骄嚯x)
圖2.3:距離
如圖2.3所示,線(xiàn)段總長(zhǎng)度為200,起始的cap區(qū)間長(zhǎng)度始終為0,終止的cap區(qū)間長(zhǎng)度始終為200,這樣只需要判斷這個(gè)像素的距離屬性就行。對(duì)于圓心的位置,就更好判斷了,因?yàn)樽筮卌ap區(qū)間傳入頂點(diǎn)著色器的頂點(diǎn)和p0相同,所以直接傳入片元著色器就行。
如何繪制虛線(xiàn)上面我們介紹了如何繪制普通線(xiàn)段以及如何繪制線(xiàn)段Cap,那么虛線(xiàn)該如何繪制呢?在Canvas中虛線(xiàn)通過(guò)傳入line dash數(shù)組(
https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/setLineDash)進(jìn)行繪制。在WebGL中我們使用與Canvas2d相同的接口,并且為了更強(qiáng)的靈活性,我們希望在GPU中計(jì)算虛線(xiàn)。
計(jì)算虛線(xiàn)我們需要以下信息:
- 當(dāng)前像素在線(xiàn)段中的位置(用來(lái)判斷是實(shí)心區(qū)域還是空心區(qū)域,如果是空心區(qū)域再判斷是否是在cap區(qū)域內(nèi))
- line dash信息
如何繪制普通虛線(xiàn)虛線(xiàn)繪制我們需要知道每個(gè)像素的位置或者說(shuō)距離起點(diǎn)的距離,所以傳入圖2.3所示的距離,這也是為什么在繪制round cap時(shí)我們要舍近求遠(yuǎn)傳入距離了,因?yàn)樘摼€(xiàn)正好可以使用這個(gè)數(shù)據(jù)。算法比較簡(jiǎn)單,我們已知當(dāng)前像素與起點(diǎn)的距離distance,又已知line dash數(shù)組:
line dash總周期長(zhǎng)度:
當(dāng)前像素在該周期中的位置:
然后判斷該位置pos在在line dash數(shù)組中是在實(shí)心區(qū)間還是空心區(qū)間即可。
如何繪制具有square cap屬性的虛線(xiàn)
上面介紹了繪制普通虛線(xiàn),對(duì)于square cap的虛線(xiàn)需要在每個(gè)實(shí)心區(qū)間兩側(cè)添加square cap,由于首尾我們已經(jīng)自動(dòng)添加了square cap,所以我們只討論中間的線(xiàn)段區(qū)域。
通過(guò)上面的計(jì)算,我們已知像素在當(dāng)前周期中的位置pos,我們?cè)谟?jì)算square cap時(shí)只需要將實(shí)心區(qū)間左右兩側(cè)擴(kuò)充線(xiàn)段寬度即可。
如何繪制具有round cap屬性的虛線(xiàn)上面介紹了如何繪制square cap,對(duì)于round cap我們需要計(jì)算出圓形,如圖2.5所示,我已知距離邊界的u坐標(biāo)(距離),但是我們需要額外的信息來(lái)計(jì)算v坐標(biāo),通過(guò)uv,計(jì)算得到round的半徑。
圖2.5:round cap虛線(xiàn)
圖2.6:vCoord參數(shù)
如圖2.6所示,我們給每個(gè)頂點(diǎn)再傳入個(gè)vCoord參數(shù),這樣在v軸方向上就可以得到當(dāng)前像素在v軸上的距離
通過(guò)以上uv數(shù)據(jù),就可以計(jì)算出圓角:
圖2.7:虛線(xiàn)繪制
漂亮的線(xiàn)段動(dòng)畫(huà)與漂亮的虛線(xiàn)上面介紹了如何繪制線(xiàn)段、line cap、line dash,但是上面的渲染方式在虛線(xiàn)以及動(dòng)畫(huà)上具有怪異的渲染效果,如圖3.1所示,在多段折線(xiàn)的時(shí)候虛線(xiàn)就會(huì)出現(xiàn)這種怪異的效果:
圖3.1:多段折線(xiàn)虛線(xiàn)
如圖3.2所示,在繪制動(dòng)畫(huà)的時(shí)候,也會(huì)出現(xiàn)怪異的效果:
圖3.2:線(xiàn)段動(dòng)畫(huà)
如圖3.3所示,中間的紅線(xiàn)是線(xiàn)段的路徑,左右邊界是我們繪制有寬度的線(xiàn)段時(shí)放大的邊界,我們將中間的長(zhǎng)度同時(shí)給了外邊界導(dǎo)致上下邊界具有相同的長(zhǎng)度屬性,但是很明顯上邊界的真實(shí)長(zhǎng)度比下邊界小,這就導(dǎo)致了如上圖所示的斜線(xiàn)dash:
圖3.3:上下邊界實(shí)際長(zhǎng)度不同,但計(jì)算時(shí)都使用的是中間紅線(xiàn)的長(zhǎng)度
用戶(hù)希望看到的是兩邊平行的效果,所以我們需要計(jì)算出兩個(gè)邊距真實(shí)的距離,如圖3.4所示:
圖3.4:邊界的長(zhǎng)度
我們目前已知的變量包括:
- 線(xiàn)段的幾個(gè)頂點(diǎn)(在生成頂點(diǎn)數(shù)據(jù)的時(shí)候就已知)
- 線(xiàn)段的線(xiàn)寬(生成頂點(diǎn)數(shù)據(jù)時(shí)未知,渲染時(shí)動(dòng)態(tài)傳入)
因?yàn)槲覀儾捎玫氖莿?dòng)態(tài)渲染各種粗細(xì)的線(xiàn)段,所以在生成線(xiàn)段頂點(diǎn)以及屬性的時(shí)候我們是不知道這個(gè)線(xiàn)段到底是要渲染多寬的,所以這里我們引入變量delta:
圖3.5:通過(guò)delta計(jì)算邊界的實(shí)際長(zhǎng)度
如圖3.5所示,根據(jù)上面的計(jì)算,我們已知端點(diǎn)的法向量n,該端點(diǎn)的方向向量d,那么delta就是法向量在方向向量上的投影:
并且由于向量n的長(zhǎng)度表示線(xiàn)段粗細(xì),所以投影長(zhǎng)度delta的也可以用線(xiàn)段粗細(xì)表示,在著色器里的通過(guò)delta*lineWidth就可以得到delta所表示的真實(shí)長(zhǎng)度。線(xiàn)段的總長(zhǎng)度直接取上下邊界中最長(zhǎng)的即可。
如圖3.6和3.7所示的就是比較好看的動(dòng)畫(huà)和虛線(xiàn)效果了。
圖3.6:繪制虛線(xiàn)
圖3.7:繪制線(xiàn)段動(dòng)畫(huà)
原文鏈接:
關(guān)鍵詞:道路,地圖,樣式,寬度,繪制,虛線(xiàn)