Add a custom layer with tiles to a globe
Use custom layer to display arbitrary tiles drawn with a custom WebGL shader on a globe.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Add a custom layer with tiles to a globe</title>
<meta property="og:description" content="Use custom layer to display arbitrary tiles drawn with a custom WebGL shader on a globe." />
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel='stylesheet' href='https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.css' />
<script src='https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js'></script>
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
#project {
display: block;
position: absolute;
top: 20px;
left: 50%;
transform: translate(-50%);
width: 50%;
height: 40px;
padding: 10px;
border: none;
border-radius: 3px;
font-size: 12px;
text-align: center;
color: #fff;
background: #ee8a65;
}
</style>
</head>
<body>
<div id="map"></div>
<br />
<button id="project">Toggle projection</button>
<script>
// This example demonstrates how to draw a custom layer consisting of
// web mercator tiles with a custom shader.
// Each drawn tile must be subdivided in order to be properly curved.
// This example also handles poles.
// Note that sometimes you can encounter single-pixel seams between tiles
// of different zoom levels. To fix this, first draw all tiles using meshes
// *without* borders and mark all drawn pixels in stencil, then draw
// all tiles again, this time *with* borders and with stencil set to
// fail all previously drawn pixels.
// This approach ensures that no pixel that can be drawn inside some tile
// will be instead covered by a mesh border (which is likely to contain invalid data).
//
// MapLibre uses this approach to draw raster tiles on globe. Borders on raster tiles
// contain ugly stretched textures, we don't want that to cover pixels that could be
// filled with a valid neighboring tile.
const EXTENT = 8192;
// Generate an arbitrary list of tiles to render
function generateTileList(list, current) {
list.push(current);
const subdivide = current.z < 2 || (current.x === current.y && current.z < 3) || (current.x === 0 && current.y === 0 && current.z < 7);
if (subdivide) {
for (let x = 0; x < 2; x++) {
for (let y = 0; y < 2; y++) {
generateTileList(list, {
x: current.x * 2 + x,
y: current.y * 2 + y,
z: current.z + 1,
wrap: current.wrap,
});
}
}
}
}
const tilesToRender = [];
for (let i = -1; i <= 1; i++) {
generateTileList(tilesToRender, {x: 0, y: 0, z: 0, wrap: i});
}
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
zoom: 2,
center: [7.5, 58]
});
map.on('style.load', () => {
map.setProjection({
type: 'globe', // Set projection to globe
});
});
document.getElementById('project').addEventListener('click', () => {
// Toggle projection
const currentProjection = map.getProjection();
map.setProjection({
type: currentProjection.type === 'globe' ? 'mercator' : 'globe',
});
});
const uniforms = [
'u_matrix',
'u_projection_fallback_matrix',
'u_projection_matrix',
'u_projection_clipping_plane',
'u_projection_transition',
'u_projection_tile_mercator_coords',
];
// create a custom style layer to implement the WebGL content
const highlightLayer = {
id: 'highlight',
type: 'custom',
shaderMap: new Map(),
meshMap: new Map(),
// Helper method for creating a shader based on current map projection - globe will automatically switch to mercator when some condition is fulfilled.
getShader(gl, shaderDescription) {
if (this.shaderMap.has(shaderDescription.variantName)) {
return this.shaderMap.get(shaderDescription.variantName);
}
// Create GLSL source for vertex shader
// Note: we pass in-tile position in range 0..EXTENT to projectTile.
// By default the uniforms for custom layers are set up so that "projectTile" accepts mercator coordinates in range 0..1,
// but in this example we set the uniforms ourselves to accept range 0..EXTENT just like regular MapLibre rendering.
const vertexSource = `#version 300 es
${shaderDescription.vertexShaderPrelude}
${shaderDescription.define}
in vec2 a_pos;
out mediump vec2 v_pos;
void main() {
gl_Position = projectTile(a_pos);
// We divide by EXTENT here just so we have something reasonable to display in the pixel shader.
v_pos = a_pos / float(${EXTENT});
}`;
// create GLSL source for fragment shader
const fragmentSource = `#version 300 es
precision mediump float;
in vec2 v_pos;
out highp vec4 fragColor;
void main() {
float alpha = 0.5;
fragColor = vec4(v_pos, 0.0, 1.0) * alpha;
}`;
// create a vertex shader
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
// create a fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
// link the two shaders into a WebGL program
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
this.aPos = gl.getAttribLocation(program, 'a_pos');
const locations = {};
for (const uniform of uniforms) {
locations[uniform] = gl.getUniformLocation(program, uniform);
}
const result = {
program,
locations
};
this.shaderMap.set(shaderDescription.variantName, result);
return result;
},
getTileMesh(gl, x, y, z, border) {
// What granularity should we use? Query MapLibre's projection object's granularity settings.
const granularity = map.style.projection.subdivisionGranularity.tile.getGranularityForZoomLevel(z);
// Do we want north pole geometry?
const north = y === 0;
// Do we want south pole geometry?
const south = y === (1 << z) - 1;
const key = `${granularity}_${north}_${south}_${border}`;
if (this.meshMap.has(key)) {
return this.meshMap.get(key);
}
const meshBuffers = maplibregl.createTileMesh({
granularity,
generateBorders: border,
extendToNorthPole: north,
extendToSouthPole: south,
}, '16bit');
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(
gl.ARRAY_BUFFER,
meshBuffers.vertices,
gl.STATIC_DRAW
);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
const ibo = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
meshBuffers.indices,
gl.STATIC_DRAW
);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
const mesh = {
vbo,
ibo,
indexCount: meshBuffers.indices.byteLength / 2,
};
this.meshMap.set(key, mesh);
return mesh;
},
onAdd (map, gl) {
// Nothing to do.
},
// method fired on each animation frame
render (gl, args) {
const {program, locations} = this.getShader(gl, args.shaderData);
const isBorderDemo = true;
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.STENCIL_TEST);
gl.disable(gl.CULL_FACE);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.useProgram(program);
const isGlobeProjection = args.shaderData.variantName === 'globe';
for (const tile of tilesToRender) {
if (isGlobeProjection && tile.wrap !== 0) {
continue; // No need to draw wrapped tiles on a globe.
}
const tileID = {
wrap: tile.wrap,
canonical: {
x: tile.x,
y: tile.y,
z: tile.z,
}
};
const projectionData = map.transform.getProjectionData({overscaledTileID: tileID, applyGlobeMatrix: true});
gl.uniform4f(
locations['u_projection_clipping_plane'],
...projectionData.clippingPlane // vec4
);
gl.uniform1f(
locations['u_projection_transition'],
projectionData.projectionTransition // float
);
// Set tile mercator extents accordingly
const tileSize = 1.0 / (1 << tile.z);
gl.uniform4f(
locations['u_projection_tile_mercator_coords'],
...projectionData.tileMercatorCoords // vec4
);
// Shader variant name effectively tells us what projection code path should be used.
gl.uniformMatrix4fv(
locations['u_projection_matrix'],
false,
projectionData.mainMatrix
);
gl.uniformMatrix4fv(
locations['u_projection_fallback_matrix'],
false,
projectionData.fallbackMatrix
);
const mesh = this.getTileMesh(gl, tile.x, tile.y, tile.z, false);
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vbo);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.ibo);
gl.enableVertexAttribArray(this.aPos);
gl.vertexAttribPointer(this.aPos, 2, gl.SHORT, false, 0, 0);
gl.drawElements(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0);
}
}
};
// add the custom style layer to the map
map.on('load', () => {
map.addLayer(highlightLayer, 'countries-label');
});
</script>
</body>
</html>