前段时间写的mapboxgl 互联网地图纠偏插件(一)存在地图旋转时瓦片错位的问题。
这次没有再跟 mapboxgl 的变换矩阵较劲,而是另辟蹊径应用 mapboxgl 的自定义图层,从新写了一套加载瓦片的办法来实现地图纠偏。
上面把我这次打怪降级的心路历程分享一下,或者对你也有启发。
文中波及一些 webgl 的常识细节,没有接触过 webgl 的同学,能够参考看上一次给大家举荐的电子书 《WebGL编程指南》,这次再附上一个蕴含书中所有示例的 github 库,会很有帮忙。
书接上回
在钻研偏移矩阵问题束手无策时,发现用天地图的栅格瓦片没有偏移的问题,因为天地图是大地2000坐标,能够间接在 wgs84 坐标地图上应用,根本没有误差。
尝试后感觉,能够倒是能够,但就是配色有点丑,能够先作为一个保底计划,高德瓦片的纠偏还要持续钻研。
话说《WebGL编程指南》这本书看完后,始终想写个读书笔记,但又感觉光写笔记太干燥,就想着联合地图看无能点啥。
mapboxgl 通过自定图层接口反对 webgl 的扩大,这个接口的益处是,对简单的变换矩阵进行了封装,对外应用大家相熟的 web 墨卡托坐标,并提供了经纬度坐标和 web墨卡托坐标转换的接口 。
查看 mapboxgl 的官网示例时,忽然来了灵感,能够用这个接口本人写个加载栅格瓦片的程序,这样就能绕开 mapboxgl 简单的框架,更容易实现对瓦片纠偏,呈现问题也更好解决,对整体更有掌控感。
技术路线剖析:
用这个思路来实现纠偏,要搞定两大问题,一个是如何用 webgl 实现显示瓦片的性能,另一个是如何计算瓦片在屏幕上的显示地位。
如何用 webgl 显示瓦片
在 webgl 中,图形的根底是三角形,要绘制正方形的瓦片,须要用两个三角形拼成一个正方形,再把图片贴到这个正方形上,就能实现地图瓦片的显示。这个过程中,图片被称为纹理,贴图被称为纹理贴图。实现成果如下(图片地位是轻易写的):
这里有两点要留神:
1、要留神图片的跨域问题,须要通过设置图片的跨域属性来解决。
2、要留神顶点坐标的程序,正确的程序为:左上、左下、右上、右下,不然图片会像穿衣服一样,各种穿反,前后反,左右反
外围代码如下:
var picLoad = false; var tileLayer = { id: 'tileLayer', type: 'custom', //增加图层时调用 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'); //设置图形顶点坐标 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}); //顶点坐标放入webgl缓冲区中 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); //设置从缓冲区获取顶点数据的规定 gl.vertexAttribPointer(a_Pos, 2, gl.FLOAT, false, FSIZE * 4, 0); gl.vertexAttribPointer(a_TextCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2); //激活顶点数据缓冲区 gl.enableVertexAttribArray(a_Pos); gl.enableVertexAttribArray(a_TextCoord); var _this = this; var img = this.img = new Image(); img.onload = () => { // 创立纹理对象 _this.texture = gl.createTexture(); //向target绑定纹理对象 gl.bindTexture(gl.TEXTURE_2D, _this.texture); //对纹理进行Y轴反转 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; //设置容许跨域 img.src = "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x=843&y=386&z=10"; }, //渲染,地图界面变动时会调用这个办法,会调用若干次(变动时的每一帧都调用) render: function (gl, matrix) { if(picLoad){ //利用着色程序 //必须写到这里,不能写到onAdd中,不然gl中的着色程序可能不是下面写的,会导致上面的变量获取不到 gl.useProgram(this.program); //向target绑定纹理对象 gl.bindTexture(gl.TEXTURE_2D, this.texture); //开启0号纹理单元 gl.activeTexture(gl.TEXTURE0); //配置纹理参数 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不相熟的同学有可能会走弯路,我本人在钻研时,就遇到了上面几个问题:
第一个问题:
自定义图层必须要有onAdd
办法和render
办法,onadd
办法在加载图层时会被调用一次,render
办法在地图平移、缩放、旋转时会被调用若干次,来实现平滑过渡的成果。
那么问题来了,哪些 webgl 代码应该放在onadd
中,哪些应该放在render
中?
上面是 webglfundamentals 网站给出的解释,在这里,onadd
办法就是初始化阶段,render
办法就是渲染阶段。
第二个问题:
顶点坐标是一个瓦片用一个缓冲区,还是所有坐标都放在一个缓冲区中,而后定义规定来取?
答案是,一个瓦片就是一个独立的图形,一个图形对应一套本人的顶点坐标,坐标前面能够跟渲染相干的属性,如色彩、纹理坐标等,但多个图形的顶点坐标不能放在一起,放一起会被绘制为一个图形。多个图形应该应用多个缓冲区对象别离存储本人的顶点坐标。
第三个问题:
如何应用多个缓冲区?
webgl 是面向过程式的,平时用惯了面向对象的开发语言,刚接触这个时有点不适应,起初就缓缓相熟了。
咱们能够把 webgl 的设想成一台老式的机械印刷机,它依据模板印刷,一次只能只应用一个模板,如果想要印刷出多个不同的图案,就须要筹备多个不同图案的模板,而后在印刷时一直的更换模板。
webgl 中的着色器、缓冲区对象、纹理对象三者的组合就像是这个模板 ,模板和它们都蕴含了绘制图形的参数。更换模板,就是在更换着色器、缓冲区对象和纹理对象,不同的是,相比印刷机,电脑中切换这些只是一瞬间的事件,工夫能够忽略不计。
webgl 在理论工作时就是像下面的印刷机一样在不停的更换模板而后印刷,再更换模板再印刷,直到全副图像绘制实现,整个过程也是一瞬间的事件。
在 webgl 中,”印刷的机器“只有一个,但“模板”你能够创立很多,它的下限取决于你的电脑性能。
咱们要做的就是为每一个瓦片创立一个“模板”,而后在绘制时动静切换这些“模板”。
下面三个问题搞明确当前,我胜利的加载了2个瓦片。效果图:
如何计算瓦片在屏幕上的显示地位
外围还是用的上篇文章中提到的经纬度和瓦片编号互转算法
原理是:先获取以后显示范畴四个角的经纬度,再依据互转算法计算出四个角对应的瓦片编号,这样就能统计出以后地图范畴所有瓦片的瓦片编号。
而后遍历以后范畴内的所有瓦片编号。
遍历时,依据互转算法,将遍历到的每个瓦片编号转为瓦片左上角的经纬度,再用它相邻的右方、下方、右下方3个瓦片的左上角经纬度,组成瓦片的4个顶点坐标。
在这一步退出对顶点坐标的纠偏算法,实现对瓦片的纠偏。
最初再去监听地图扭转的事件,当地图产生平移、缩放、旋转时都要反复下面的计算,更新瓦片。
这里遇到个问题:纠偏后也呈现了上一篇中边缘空白的状况。于是对下面的算法优化了一下,在获取到以后显示范畴的四个角经纬度坐标后,对这4个坐标也进行纠偏,这样问题就解决了。
当初瓦片的地图的框架搭起来了,也可能浏览查看瓦片地图了,这一刻还真有点小兴奋的呢
但和最终想要的成果还有些差距,还有很多细节须要优化
细节优化
1、缓存瓦片
把申请过的瓦片放到存到变量中,这样申请过的瓦片能够防止反复申请,显示速度会更快,体验更好。
2、缓存网格经纬度
对立计算瓦片网格的经纬度并缓存起来,免得每次都进行反复计算。
3、瓦片加载的程序从两头向周围
当初的程序是从左到右,有种刷屏的感觉,须要对瓦片编号排一下序,让凑近两头的先加载,凑近边缘的后加载。
4、个别瓦片不显示问题
每次地图范畴变换时,为了实现平滑的成果render
办法会被执行几十次,工夫大略在1秒左右,
如果瓦片不能在这期间加载实现,就会被落下,导致不显示。
须要把最初一次执行render
办法时的matrix
变换矩阵记录下来,在瓦片加载实现后被动调用render
办法绘制。
5、影像图注记白底的问题
在加载影像图时,影像和注记是离开的,须要叠加显示,注记层在没有文字的中央是通明的。
但叠加到一起当前注记层在本该是通明的中央却是不通明的红色
起因一,因为在读取纹理像素数据时的配置有问题,要应用gl.RGBA
,如果应用的是gl.RGB
丢掉了透明度A
,就会缺失透明度信息,导致不通明。
起因二,因为在绘制前没有对 webgl 开启阿尔法混合(阿尔法在这里能够了解为透明度),在 webgl 中如果要实现通明成果,这个选项是必须要开启的。
解决后的成果:
6、影像图注记白底的问题还是会偶然呈现
按上一条批改后,白底问题呈现的频率明显降低,但偶然还是会呈现。
钻研法则,当注记瓦片加载的工夫稍长时就会呈现,呈现后,只有稍稍拖动一下地图就会失常,曾经浏览过的区域没有这个问题。
揣测,影像和注记是分图层绘制,当个别注记瓦片加载的工夫长,去被动调用render
办法从新绘制时,注记的图层会全部重绘,但影像图层不会绘制,这可能就导致两个图层无奈动静的混合。
目前的解决办法是,对于注记图层如果加载慢了,就不被动调用render办法从新绘制了。因为缺一小块注记不影响大局,而且下一步操作时它也会主动变失常。
地图抖动问题
一些列优化实现后,当初地图也纠偏了,旋转时也不再错位了,原本认为程序曾经很完满了,但当我叠上我的项目实在数据后,发现了一个很要命的问题,自定义图层在大比例尺时会呈现抖动的问题。
这个问题最开始就留神到了,但没太在意,认为影响不大,但叠加上业务数据后,发现基本没法用,那种感觉就像是,坐到了行驶在乡间小路的拖拉机上, ~ 颠 ~ 颠 ~ 颠 ~ 颠 ~ 颠 ~ 颠 ~ 颠 ~
起初还认为是瓦片编号和经纬度互转导致的问题,起初发现 mapboxgl 官网的自定义图层示例也有这个问题,看来是 mapboxgl 的 bug 无疑了。
帮 mapboxgl 找问题,最终定位在了render
办法的matrix
变换矩阵上,这个参数是 mapboxgl 传来的,用于将 web 墨卡托坐标转为 webgl 坐标,并对瓦片进行缩放和旋转。
当只对地图进行渺小的平移时,地图会动,matrix
矩阵却没有变,matrix
矩阵不变,自定义图层也就不会变,当地图平移的范畴加大时,matrix矩阵才会跟着变。
翻看 mapboxgl 的源码,自定义图层和底图用的不是一个变换矩阵,所以只有自定义图层有问题。
尝试了 mapboxgl 的最新版本 v2.3.1 也有这个问题。
唉! 原本认为纠偏这事儿要翻篇儿了,这么看来还要再钻研一阵子了。
启发、思路、感触
在应用自定义图层的过程中有了一些启发,上篇文章中纠偏写在了变换矩阵中,这种写法在地图旋转时会呈现瓦片错位的问题。
本篇文章中纠偏是对a_pos
变量 web 墨卡托坐标进行纠偏,在旋转时就没有呈现错位的状况。
按这个思路,是不是在上篇文章中,也对a_pos
变量纠偏,地图旋转时就不会呈现错位问题了?值得一试。
所以,接下来两个思路:一、钻研如何进步自定义图层变换矩阵的精度,让它不再抖动。二、钻研如何对 mapboxgl 源码中的a_pos
变量进行纠偏。
最初说一下应用 mapboxgl 自定义图层的感触,应用 mapboxgl 自定义图层 + webgl 扩大,就感觉关上了GIS世界的另一扇窗户,本人能够去实现各种炫酷高大上的性能了,感觉有了有限可能。
代码、示例
在线示例:http://gisarmory.xyz/blog/index.html?demo=mapboxglMapCorrection2
插件代码:http://gisarmory.xyz/blog/index.html?source=mapboxglMapCorrection2
总结
- 这次尝试用 maboxgl 的自定义图层性能,本人写了一个加载互联网瓦片的程序,来实现瓦片纠偏
- 本人写加载瓦片的程序要搞定两大问题,一个是如何用 webgl 实现显示瓦片的性能,二个是如何计算瓦片在屏幕上的显示地位
- webgl 显示瓦片的原理就是绘制个正方形再给正方形贴图片纹理
- 计算瓦片在屏幕上的显示地位,外围是应用瓦片号和经纬度的互转算法,在这个过程中对瓦片进行纠偏
- 还要进行一些细节优化,比方瓦片的加载程序等
- 最终实现了对高德瓦片进行纠偏,并且旋转时也不会呈现错位的状况
- 但这种形式有个问题,mapboxgl 的
render
办法中传过来的变换矩阵的精度不够,在大比例尺时会呈现瓦片抖动的状况,这应该是mapboxgl 的 bug - 在应用自定义图层的过程中有了一些启发,接下来两个思路:一、钻研如何进步自定义图层变换矩阵的精度。二、钻研如何对mapboxgl 源码中的
a_pos
变量进行纠偏。 - 目前的保底计划是应用天地图的瓦片,高德地图的瓦片还要持续钻研。
原文地址:http://gisarmory.xyz/blog/index.html?blog=mapboxglMapCorrection2
关注《GIS兵器库》, 只给你网上搜不到的GIS常识技能。
本文章采纳 常识共享署名-非商业性应用-雷同形式共享 4.0 国内许可协定 进行许可。欢送转载、应用、从新公布,但务必保留文章署名《GIS兵器库》(蕴含链接: http://gisarmory.xyz/blog/),不得用于商业目标,基于本文批改后的作品务必以雷同的许可公布。