如何在Maplibre中对栅格瓦片图层进行着色?
栅格瓦片着色性能优化:解决大尺寸瓦片导致的浏览器崩溃
我尝试用以下代码给栅格瓦片着色,但当瓦片尺寸较大时,嵌套for循环的迭代次数过多会导致浏览器崩溃。Mapbox已推出栅格瓦片图层着色功能,但Maplibre目前尚未支持。
原代码思路是从Mapbox画布读取像素,基于灰度平均值逐像素绘制叠加画布并分配颜色:
const coloriseRasterTile = () => { if (!map.getLayer(`id-${id}`) && !map.getSource(`id-${id}`)) return; var customCanvas = document.getElementById('canvasID'); var rasterTileBounds = [-76.76666666666668, 39.26666488571745, -76.73334678524573, 39.36666666666667]; var canvasWidth = Math.abs( parseFloat( map.project([rasterTileBounds[0], rasterTileBounds[1]]).x - map.project([rasterTileBounds[2], rasterTileBounds[1]]).x, 10, ), ); var canvasHeight = Math.abs( parseFloat( map.project([rasterTileBounds[0], rasterTileBounds[1]]).y - map.project([rasterTileBounds[0], rasterTileBounds[3]]).y, 10, ), ); customCanvas.width = canvasWidth; customCanvas.height = canvasHeight; var overlayCtx = customCanvas.getContext('2d'); if (overlayCtx) { customCanvas.style.display = 'none'; } const canvas = map.getCanvas(); const gl = canvas.getContext('webgl') || canvas.getContext('webgl2'); const data = new Uint8Array(4); if (!gl) return; for (let x = 0; x < canvasWidth; x++) { for (let y = 0; y < canvasHeight; y++) { const canvasX = x - canvas.offsetLeft; const canvasY = canvas.height - y - canvas.offsetTop; const bufferX = ((gl.drawingBufferWidth / canvasWidth) * x).toFixed(0); const bufferY = ((gl.drawingBufferHeight / canvasHeight) * (canvasHeight - y)).toFixed(0); gl.readPixels(bufferX, bufferY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, data); const [r, g, b, a] = data; const avg = r + g + b / 3; overlayCtx.fillStyle = avg < 125 ? 'green' : 'blue'; overlayCtx.fillRect(x, y, 1, 1); } } map.addSource('canvas-source', { type: 'canvas', canvas: 'canvasID', coordinates: [ [rasterTileBounds[0], rasterTileBounds[1]], [rasterTileBounds[2], rasterTileBounds[1]], [rasterTileBounds[2], rasterTileBounds[3]], [rasterTileBounds[0], rasterTileBounds[3]], ], }); map.addLayer({ id: 'canvas-layer', type: 'raster', source: 'canvas-source', }); };
优化方案:批量处理像素,消除性能瓶颈
逐像素调用gl.readPixels和fillRect是核心性能问题,每次调用都会产生上下文切换和DOM操作开销。优化后通过批量读取+批量绘制大幅降低迭代次数和API调用量:
const coloriseRasterTile = () => { const targetLayerId = `id-${id}`; const targetSourceId = `id-${id}`; if (!map.getLayer(targetLayerId) && !map.getSource(targetSourceId)) return; const customCanvas = document.getElementById('canvasID'); const rasterTileBounds = [-76.76666666666668, 39.26666488571745, -76.73334678524573, 39.36666666666667]; // 预计算地图投影坐标,减少重复调用 const bottomLeft = map.project([rasterTileBounds[0], rasterTileBounds[1]]); const bottomRight = map.project([rasterTileBounds[2], rasterTileBounds[1]]); const topLeft = map.project([rasterTileBounds[0], rasterTileBounds[3]]); // 计算叠加画布尺寸 const canvasWidth = Math.abs(bottomRight.x - bottomLeft.x); const canvasHeight = Math.abs(topLeft.y - bottomLeft.y); customCanvas.width = canvasWidth; customCanvas.height = canvasHeight; const overlayCtx = customCanvas.getContext('2d'); if (!overlayCtx) return; customCanvas.style.display = 'none'; const canvas = map.getCanvas(); const gl = canvas.getContext('webgl') || canvas.getContext('webgl2'); if (!gl) return; // 计算WebGL缓冲区中的目标区域范围 const bufferScaleX = gl.drawingBufferWidth / canvas.clientWidth; const bufferScaleY = gl.drawingBufferHeight / canvas.clientHeight; const bufferX = Math.floor(bottomLeft.x * bufferScaleX); const bufferY = Math.floor((canvas.clientHeight - topLeft.y) * bufferScaleY); const bufferWidth = Math.floor(canvasWidth * bufferScaleX); const bufferHeight = Math.floor(canvasHeight * bufferScaleY); // 一次性读取整个瓦片区域的像素数据 const pixelData = new Uint8Array(bufferWidth * bufferHeight * 4); gl.readPixels(bufferX, bufferY, bufferWidth, bufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixelData); // 创建ImageData用于批量绘制 const imageData = overlayCtx.createImageData(canvasWidth, canvasHeight); const outputData = imageData.data; // 批量处理像素颜色逻辑 for (let i = 0; i < pixelData.length; i += 4) { const r = pixelData[i]; const g = pixelData[i + 1]; const b = pixelData[i + 2]; // 修正原代码的灰度平均值计算错误 const grayAvg = (r + g + b) / 3; // 分配目标颜色 const [newR, newG, newB] = grayAvg < 125 ? [0, 255, 0] : [0, 0, 255]; outputData[i] = newR; outputData[i + 1] = newG; outputData[i + 2] = newB; outputData[i + 3] = 255; // 设置完全不透明 } // 一次性将处理后的像素绘制到叠加画布 overlayCtx.putImageData(imageData, 0, 0); // 避免重复添加数据源和图层 if (!map.getSource('canvas-source')) { map.addSource('canvas-source', { type: 'canvas', canvas: 'canvasID', coordinates: [ [rasterTileBounds[0], rasterTileBounds[1]], [rasterTileBounds[2], rasterTileBounds[1]], [rasterTileBounds[2], rasterTileBounds[3]], [rasterTileBounds[0], rasterTileBounds[3]], ], }); } if (!map.getLayer('canvas-layer')) { map.addLayer({ id: 'canvas-layer', type: 'raster', source: 'canvas-source', }); } };
核心优化点说明:
- 批量读取像素:用一次
gl.readPixels读取整个瓦片区域,替代数万次逐像素读取,消除WebGL API调用开销。 - 批量绘制:通过
ImageData和putImageData一次性完成绘制,避免循环中反复调用fillRect带来的DOM操作损耗。 - 修正逻辑错误:原代码中
avg = r + g + b / 3计算错误,改为正确的灰度平均值公式(r + g + b) / 3。 - 避免重复创建资源:添加数据源和图层前先检查是否已存在,防止重复初始化。
- 减少重复计算:预计算地图投影坐标和缩放比例,避免循环内重复调用
map.project和浮点数运算。
内容的提问来源于stack exchange,提问作者Phenomenal
相关产品推荐
相关产品推荐

