前段時間寫的mapboxgl 互聯(lián)網(wǎng)地圖糾偏插件(一)存在地圖旋轉(zhuǎn)時瓦片錯位的問題。
這次沒有再跟 mapboxgl 的變換矩陣較勁,而是另辟蹊徑使用 mapboxgl 的自定義圖層,重新寫了一套加載瓦片的方法來實現(xiàn)地圖糾偏。
下面把我這次打怪升級的心路歷程分享一下,或許對你也有啟發(fā)。
文中涉及一些 webgl 的知識細節(jié),沒有接觸過 webgl 的同學(xué),可以參考看上一次給大家推薦的電子書 《WebGL編程指南》,這次再附上一個包含書中所有示例的 github 庫,會很有幫助。
書接上回
在研究偏移矩陣問題一籌莫展時,發(fā)現(xiàn)用天地圖的柵格瓦片沒有偏移的問題,因為天地圖是大地2000坐標(biāo),可以直接在 wgs84 坐標(biāo)地圖上使用,基本沒有誤差。
嘗試后覺得,可以倒是可以,但就是配色有點丑,可以先作為一個保底方案,高德瓦片的糾偏還要繼續(xù)研究。
話說《WebGL編程指南》這本書看完后,一直想寫個讀書筆記,但又覺得光寫筆記太枯燥,就想著結(jié)合地圖看能干點啥。
mapboxgl 通過自定圖層接口支持 webgl 的擴展,這個接口的好處是,對復(fù)雜的變換矩陣進行了封裝,對外使用大家熟悉的 web 墨卡托坐標(biāo),并提供了經(jīng)緯度坐標(biāo)和 web墨卡托坐標(biāo)轉(zhuǎn)換的接口 。
查看 mapboxgl 的官方示例時,突然來了靈感,可以用這個接口自己寫個加載柵格瓦片的程序,這樣就能繞開 mapboxgl 復(fù)雜的框架,更容易實現(xiàn)對瓦片糾偏,出現(xiàn)問題也更好解決,對整體更有掌控感。
技術(shù)路線分析:
用這個思路來實現(xiàn)糾偏,要搞定兩大問題,一個是如何用 webgl 實現(xiàn)顯示瓦片的功能,另一個是如何計算瓦片在屏幕上的顯示位置。
如何用 webgl 顯示瓦片
在 webgl 中,圖形的基礎(chǔ)是三角形,要繪制正方形的瓦片,需要用兩個三角形拼成一個正方形,再把圖片貼到這個正方形上,就能實現(xiàn)地圖瓦片的顯示。這個過程中,圖片被稱為紋理,貼圖被稱為紋理貼圖。實現(xiàn)效果如下(圖片位置是隨便寫的):
這里有兩點要注意:
1、要注意圖片的跨域問題,需要通過設(shè)置圖片的跨域?qū)傩詠斫鉀Q。

2、要注意頂點坐標(biāo)的順序,正確的順序為:左上、左下、右上、右下,不然圖片會像穿衣服一樣,各種穿反,前后反,左右反

核心代碼如下:
var picLoad = false;
var tileLayer = {
id: 'tileLayer',
type: 'custom',
//添加圖層時調(diào)用
onAdd: function (map, gl) {
var vertexSource = "" +
"uniform mat4 u_matrix;" +
"attribute vec2 a_pos;" +
"attribute vec2 a_TextCoord;" +
"varying vec2 v_TextCoord;" +
"void main() {" +
" gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0);" +
" v_TextCoord = a_TextCoord;" +
"}";
var fragmentSource = "" +
"precision mediump float;" +
"uniform sampler2D u_Sampler; " +
"varying vec2 v_TextCoord; " +
"void main() {" +
" gl_FragColor = texture2D(u_Sampler, v_TextCoord);" +
"}";
//初始化頂點著色器
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
//初始化片元著色器
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
//初始化著色器程序
var program = this.program = gl.createProgram();
gl.attachShader(this.program, vertexShader);
gl.attachShader(this.program, fragmentShader);
gl.linkProgram(this.program);
//獲取頂點位置變量
var a_Pos = gl.getAttribLocation(this.program, "a_pos");
var a_TextCoord = gl.getAttribLocation(this.program, 'a_TextCoord');
//設(shè)置圖形頂點坐標(biāo)
var leftTop = mapboxgl.MercatorCoordinate.fromLngLat({lng: 110,lat: 40});
var rightTop = mapboxgl.MercatorCoordinate.fromLngLat({lng: 120,lat: 40});
var leftBottom = mapboxgl.MercatorCoordinate.fromLngLat({lng: 110,lat: 30});
var rightBottom = mapboxgl.MercatorCoordinate.fromLngLat({lng: 120,lat: 30});
//頂點坐標(biāo)放入webgl緩沖區(qū)中
var attrData = new Float32Array([
leftTop.x, leftTop.y, 0.0, 1.0,
leftBottom.x, leftBottom.y, 0.0, 0.0,
rightTop.x, rightTop.y, 1.0, 1.0,
rightBottom.x, rightBottom.y, 1.0, 0.0
])
var FSIZE = attrData.BYTES_PER_ELEMENT;
this.buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferData(gl.ARRAY_BUFFER, attrData, gl.STATIC_DRAW);
//設(shè)置從緩沖區(qū)獲取頂點數(shù)據(jù)的規(guī)則
gl.vertexAttribPointer(a_Pos, 2, gl.FLOAT, false, FSIZE * 4, 0);
gl.vertexAttribPointer(a_TextCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
//激活頂點數(shù)據(jù)緩沖區(qū)
gl.enableVertexAttribArray(a_Pos);
gl.enableVertexAttribArray(a_TextCoord);
var _this = this;
var img = this.img = new Image();
img.onload = () => {
// 創(chuàng)建紋理對象
_this.texture = gl.createTexture();
//向target綁定紋理對象
gl.bindTexture(gl.TEXTURE_2D, _this.texture);
//對紋理進行Y軸反轉(zhuǎn)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
//配置紋理圖像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.img);
picLoad = true;
};
img.crossOrigin = true; //設(shè)置允許跨域
img.src = "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x=843&y=386&z=10";
},
//渲染,地圖界面變化時會調(diào)用這個方法,會調(diào)用若干次(變化時的每一幀都調(diào)用)
render: function (gl, matrix) {
if(picLoad){
//應(yīng)用著色程序
//必須寫到這里,不能寫到onAdd中,不然gl中的著色程序可能不是上面寫的,會導(dǎo)致下面的變量獲取不到
gl.useProgram(this.program);
//向target綁定紋理對象
gl.bindTexture(gl.TEXTURE_2D, this.texture);
//開啟0號紋理單元
gl.activeTexture(gl.TEXTURE0);
//配置紋理參數(shù)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
// 獲取紋理的存儲位置
var u_Sampler = gl.getUniformLocation(this.program, 'u_Sampler');
//將0號紋理傳遞給著色器
gl.uniform1i(u_Sampler, 0);
//給位置變換矩陣賦值
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);
//繪制圖形
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
}
};
map.on('load', function () {
map.addLayer(tileLayer);
});
上面是加載一個瓦片,下面看一下如何加載多個瓦片,這個問題看似簡單,但對于webgl不熟悉的同學(xué)有可能會走彎路,我自己在研究時,就遇到了下面幾個問題:
第一個問題:
自定義圖層必須要有onAdd方法和render方法,onadd方法在加載圖層時會被調(diào)用一次,render方法在地圖平移、縮放、旋轉(zhuǎn)時會被調(diào)用若干次,來實現(xiàn)平滑過渡的效果。
那么問題來了,哪些 webgl 代碼應(yīng)該放在onadd中,哪些應(yīng)該放在render中?
下面是 webglfundamentals 網(wǎng)站給出的解釋,在這里,onadd方法就是初始化階段,render方法就是渲染階段。

第二個問題:
頂點坐標(biāo)是一個瓦片用一個緩沖區(qū),還是所有坐標(biāo)都放在一個緩沖區(qū)中,然后定義規(guī)則來???
答案是,一個瓦片就是一個獨立的圖形,一個圖形對應(yīng)一套自己的頂點坐標(biāo),坐標(biāo)后面可以跟渲染相關(guān)的屬性,如顏色、紋理坐標(biāo)等,多個圖形的頂點坐標(biāo)最好不要放在一起,推薦使用多個緩沖區(qū)對象分別存儲圖形的頂點坐標(biāo),這樣分開放會更清晰,實現(xiàn)也更簡單。
第三個問題:
如何使用多個緩沖區(qū)?
webgl 是面向過程式的,平時用慣了面向?qū)ο蟮拈_發(fā)語言,剛接觸這個時有點不適應(yīng),后來就慢慢熟悉了。
我們可以把 webgl 的想象成一臺老式的機械印刷機,它根據(jù)模板印刷,一次只能只使用一個模板,如果想要印刷出多個不同的圖案,就需要準(zhǔn)備多個不同圖案的模板,然后在印刷時不斷的更換模板。
webgl 中的著色器、緩沖區(qū)對象、紋理對象三者的組合就像是這個模板 ,模板和它們都包含了繪制圖形的參數(shù)。更換模板,就是在更換著色器、緩沖區(qū)對象和紋理對象,不同的是,相比印刷機,電腦中切換這些只是一瞬間的事情,時間可以忽略不計。
webgl 在實際工作時就是像上面的印刷機一樣在不停的更換模板然后印刷,再更換模板再印刷,直到全部圖像繪制完成,整個過程也是一瞬間的事情。
在 webgl 中,”印刷的機器“只有一個,但“模板”你可以創(chuàng)建很多,它的上限取決于你的電腦性能。
我們要做的就是為每一個瓦片創(chuàng)建一個“模板”,然后在繪制時動態(tài)切換這些“模板”。
上面三個問題搞明白以后,我成功的加載了2個瓦片。效果圖:
如何計算瓦片在屏幕上的顯示位置
核心還是用的上篇文章中提到的經(jīng)緯度和瓦片編號互轉(zhuǎn)算法
原理是:先獲取當(dāng)前顯示范圍四個角的經(jīng)緯度,再根據(jù)互轉(zhuǎn)算法計算出四個角對應(yīng)的瓦片編號,這樣就能統(tǒng)計出當(dāng)前地圖范圍所有瓦片的瓦片編號。
[圖片上傳失敗...(image-813e27-1625633052101)]
然后遍歷當(dāng)前范圍內(nèi)的所有瓦片編號。
遍歷時,根據(jù)互轉(zhuǎn)算法,將遍歷到的每個瓦片編號轉(zhuǎn)為瓦片左上角的經(jīng)緯度,再用它相鄰的右方、下方、右下方3個瓦片的左上角經(jīng)緯度,組成瓦片的4個頂點坐標(biāo)。
在這一步加入對頂點坐標(biāo)的糾偏算法,實現(xiàn)對瓦片的糾偏。
最后再去監(jiān)聽地圖改變的事件,當(dāng)?shù)貓D發(fā)生平移、縮放、旋轉(zhuǎn)時都要重復(fù)上面的計算,更新瓦片。
這里遇到個問題:糾偏后也出現(xiàn)了上一篇中邊緣空白的情況。于是對上面的算法優(yōu)化了一下,在獲取到當(dāng)前顯示范圍的四個角經(jīng)緯度坐標(biāo)后,對這4個坐標(biāo)也進行糾偏,這樣問題就解決了。
現(xiàn)在瓦片的地圖的框架搭起來了,也能夠瀏覽查看瓦片地圖了,這一刻還真有點小興奮的呢
但和最終想要的效果還有些差距,還有很多細節(jié)需要優(yōu)化
細節(jié)優(yōu)化
1、緩存瓦片
把請求過的瓦片放到存到變量中,這樣請求過的瓦片可以避免重復(fù)請求,顯示速度會更快,體驗更好。
2、緩存網(wǎng)格經(jīng)緯度
統(tǒng)一計算瓦片網(wǎng)格的經(jīng)緯度并緩存起來,以免每次都進行重復(fù)計算。
3、瓦片加載的順序從中間向四周
現(xiàn)在的順序是從左到右,有種刷屏的感覺,需要對瓦片編號排一下序,讓靠近中間的先加載,靠近邊緣的后加載。
4、個別瓦片不顯示問題
每次地圖范圍變換時,為了實現(xiàn)平滑的效果render方法會被執(zhí)行幾十次,時間大概在1秒左右,
如果瓦片不能在這期間加載完成,就會被落下,導(dǎo)致不顯示。
需要把最后一次執(zhí)行render方法時的matrix變換矩陣記錄下來,在瓦片加載完成后主動調(diào)用render方法繪制。
5、影像圖注記白底的問題
在加載影像圖時,影像和注記是分開的,需要疊加顯示,注記層在沒有文字的地方是透明的。
但疊加到一起以后注記層在本該是透明的地方卻是不透明的白色
原因一,因為在讀取紋理像素數(shù)據(jù)時的配置有問題,要使用gl.RGBA,如果使用的是gl.RGB丟掉了透明度A,就會缺失透明度信息,導(dǎo)致不透明。

原因二,因為在繪制前沒有對 webgl 開啟阿爾法混合(阿爾法在這里可以理解為透明度),在 webgl 中如果要實現(xiàn)透明效果,這個選項是必須要開啟的。

解決后的效果:
6、影像圖注記白底的問題還是會偶爾出現(xiàn)
按上一條修改后,白底問題出現(xiàn)的頻率明顯降低,但偶爾還是會出現(xiàn)。
研究規(guī)律,當(dāng)注記瓦片加載的時間稍長時就會出現(xiàn),出現(xiàn)后,只要稍稍拖動一下地圖就會正常,已經(jīng)瀏覽過的區(qū)域沒有這個問題。
推測,影像和注記是分圖層繪制,當(dāng)個別注記瓦片加載的時間長,去主動調(diào)用render方法重新繪制時,注記的圖層會全部重繪,但影像圖層不會繪制,這可能就導(dǎo)致兩個圖層無法動態(tài)的混合。
目前的解決方法是,對于注記圖層如果加載慢了,就不主動調(diào)用render方法重新繪制了。因為缺一小塊注記不影響大局,而且下一步操作時它也會自動變正常。
地圖抖動問題
一些列優(yōu)化完成后,現(xiàn)在地圖也糾偏了,旋轉(zhuǎn)時也不再錯位了,本來以為程序已經(jīng)很完美了,但當(dāng)我疊上項目真實數(shù)據(jù)后,發(fā)現(xiàn)了一個很要命的問題,自定義圖層在大比例尺時會出現(xiàn)抖動的問題。
這個問題最開始就注意到了,但沒太在意,以為影響不大,但疊加上業(yè)務(wù)數(shù)據(jù)后,發(fā)現(xiàn)根本沒法用,那種感覺就像是,坐到了行駛在鄉(xiāng)間小路的拖拉機上, ~ 顛 ~ 顛 ~ 顛 ~ 顛 ~ 顛 ~ 顛 ~ 顛 ~
起初還以為是瓦片編號和經(jīng)緯度互轉(zhuǎn)導(dǎo)致的問題,后來發(fā)現(xiàn) mapboxgl 官網(wǎng)的自定義圖層示例也有這個問題,看來是 mapboxgl 的 bug 無疑了。
幫 mapboxgl 找問題,最終定位在了render方法的matrix變換矩陣上,這個參數(shù)是 mapboxgl 傳來的,用于將 web 墨卡托坐標(biāo)轉(zhuǎn)為 webgl 坐標(biāo),并對瓦片進行縮放和旋轉(zhuǎn)。
當(dāng)只對地圖進行微小的平移時,地圖會動,matrix矩陣卻沒有變,matrix矩陣不變,自定義圖層也就不會變,當(dāng)?shù)貓D平移的范圍加大時,matrix矩陣才會跟著變。
翻看 mapboxgl 的源碼,自定義圖層和底圖用的不是一個變換矩陣,所以只有自定義圖層有問題。
嘗試了 mapboxgl 的最新版本 v2.3.1 也有這個問題。
唉! 本來以為糾偏這事兒要翻篇兒了,這么看來還要再研究一陣子了。
啟發(fā)、思路、感受
在使用自定義圖層的過程中有了一些啟發(fā),上篇文章中糾偏寫在了變換矩陣中,這種寫法在地圖旋轉(zhuǎn)時會出現(xiàn)瓦片錯位的問題。
本篇文章中糾偏是對a_pos變量 web 墨卡托坐標(biāo)進行糾偏,在旋轉(zhuǎn)時就沒有出現(xiàn)錯位的情況。
按這個思路,是不是在上篇文章中,也對a_pos變量糾偏,地圖旋轉(zhuǎn)時就不會出現(xiàn)錯位問題了?值得一試。
所以,接下來兩個思路:一、研究如何提高自定義圖層變換矩陣的精度,讓它不再抖動。二、研究如何對 mapboxgl 源碼中的a_pos變量進行糾偏。
最后說一下使用 mapboxgl 自定義圖層的感受,使用 mapboxgl 自定義圖層 + webgl 擴展,就感覺打開了GIS世界的另一扇窗戶,自己可以去實現(xiàn)各種炫酷高大上的功能了,感覺有了無限可能。
代碼、示例
在線示例:http://gisarmory.xyz/blog/index.html?demo=mapboxglMapCorrection2
插件代碼:http://gisarmory.xyz/blog/index.html?source=mapboxglMapCorrection2
總結(jié)
- 這次嘗試用 maboxgl 的自定義圖層功能,自己寫了一個加載互聯(lián)網(wǎng)瓦片的程序,來實現(xiàn)瓦片糾偏
- 自己寫加載瓦片的程序要搞定兩大問題,一個是如何用 webgl 實現(xiàn)顯示瓦片的功能,二個是如何計算瓦片在屏幕上的顯示位置
- webgl 顯示瓦片的原理就是繪制個正方形再給正方形貼圖片紋理
- 計算瓦片在屏幕上的顯示位置,核心是使用瓦片號和經(jīng)緯度的互轉(zhuǎn)算法,在這個過程中對瓦片進行糾偏
- 還要進行一些細節(jié)優(yōu)化,比如瓦片的加載順序等
- 最終實現(xiàn)了對高德瓦片進行糾偏,并且旋轉(zhuǎn)時也不會出現(xiàn)錯位的情況
- 但這種方式有個問題,mapboxgl 的
render方法中傳過來的變換矩陣的精度不夠,在大比例尺時會出現(xiàn)瓦片抖動的情況,這應(yīng)該是mapboxgl 的 bug - 在使用自定義圖層的過程中有了一些啟發(fā),接下來兩個思路:一、研究如何提高自定義圖層變換矩陣的精度。二、研究如何對mapboxgl 源碼中的
a_pos變量進行糾偏。 - 目前的保底方案是使用天地圖的瓦片,高德地圖的瓦片還要繼續(xù)研究。
原文地址:http://gisarmory.xyz/blog/index.html?blog=mapboxglMapCorrection2
關(guān)注《GIS兵器庫》, 只給你網(wǎng)上搜不到的GIS知識技能。
本文章采用 知識共享署名-非商業(yè)性使用-相同方式共享 4.0 國際許可協(xié)議 進行許可。歡迎轉(zhuǎn)載、使用、重新發(fā)布,但務(wù)必保留文章署名《GIS兵器庫》(包含鏈接: http://gisarmory.xyz/blog/),不得用于商業(yè)目的,基于本文修改后的作品務(wù)必以相同的許可發(fā)布。