Add a simple custom layer on a globe
Use a custom layer to draw simple WebGL content on a globe.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Add a simple custom layer on a globe</title>
<meta property="og:description" content="Use a custom layer to draw simple WebGL content 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>
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
zoom: 3,
center: [7.5, 58],
canvasContextAttributes: {antialias: true}
});
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',
});
});
// create a custom style layer to implement the WebGL content
const highlightLayer = {
id: 'highlight',
type: 'custom',
shaderMap: 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) {
// Pick a shader based on the current projection, defined by `variantName`.
if (this.shaderMap.has(shaderDescription.variantName)) {
return this.shaderMap.get(shaderDescription.variantName);
}
// Create GLSL source for vertex shader
//
// Note that we need to use a complex function to project from the source mercator
// coordinates to the globe. Internal shaders in MapLibre need to do this too.
// This is done using the `projectTile` function.
// In MapLibre, this function accepts vertex coordinates local to the current tile,
// in range 0..EXTENT (8192), but for custom layers MapLibre supplies uniforms such that
// the function accepts mercator coordinates of the whole world in range 0..1.
// This is controlled by the `u_projection_tile_mercator_coords` uniform.
//
// The `projectTile` function can also handle mercator to globe transitions and can
// handle the mercator projection - different code is supplied based on what projection is used,
// and for this reason we use different shaders based on what shader projection variant is currently used.
// See `variantName` usage earlier in this file.
//
// The code for the projection function and uniforms is also supplied by MapLibre
// and must be injected into custom layer shaders in order to draw on a globe.
// We simply use string interpolation for that here.
//
// See MapLibre source code for more details, especially src/shaders/_projection_globe.vertex.glsl
const vertexSource = `#version 300 es
// Inject MapLibre projection code
${shaderDescription.vertexShaderPrelude}
${shaderDescription.define}
in vec2 a_pos;
void main() {
gl_Position = projectTile(a_pos);
}`;
// create GLSL source for fragment shader
const fragmentSource = `#version 300 es
out highp vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 1.0, 0.75);
}`;
// 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');
this.shaderMap.set(shaderDescription.variantName, program);
return program;
},
// method called when the layer is added to the map
// Search for StyleImageInterface in https://maplibre.org/maplibre-gl-js/docs/API/
onAdd (map, gl) {
// define vertices of the triangle to be rendered in the custom style layer
const helsinki = maplibregl.MercatorCoordinate.fromLngLat({
lng: 25.004,
lat: 60.239
});
const berlin = maplibregl.MercatorCoordinate.fromLngLat({
lng: 13.403,
lat: 52.562
});
const kyiv = maplibregl.MercatorCoordinate.fromLngLat({
lng: 30.498,
lat: 50.541
});
// create and initialize a WebGLBuffer to store vertex and color data
this.buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
helsinki.x,
helsinki.y,
kyiv.x,
kyiv.y,
berlin.x,
berlin.y,
]),
gl.STATIC_DRAW
);
// Explanation of horizon clipping in MapLibre globe projection:
//
// When zooming in, the triangle will eventually start doing what at first glance
// appears to be clipping the underlying map.
//
// Instead it is being clipped by the "horizon" plane, which the globe uses to
// clip any geometry behind horizon (regular face culling isn't enough).
// The horizon plane is not necessarily aligned with the near/far planes.
// The clipping is done by assigning a custom value to `gl_Position.z` in the `projectTile`
// MapLibre uses a constant z value per layer, so `gl_Position.z` can be anything,
// since it later gets overwritten by `glDepthRange`.
//
// At high zooms, the triangle's three vertices can end up beyond the horizon plane,
// resulting in the triangle getting clipped.
//
// This can be fixed by subdividing the triangle's geometry.
// This is in general advisable to do, since without subdivision
// geometry would not project to a curved shape under globe projection.
// MapLibre also internally subdivides all geometry when globe projection is used.
},
// method fired on each animation frame
render (gl, args) {
const program = this.getShader(gl, args.shaderData);
gl.useProgram(program);
gl.uniformMatrix4fv(
gl.getUniformLocation(program, 'u_projection_fallback_matrix'),
false,
args.defaultProjectionData.fallbackMatrix // convert mat4 from gl-matrix to a plain array
);
gl.uniformMatrix4fv(
gl.getUniformLocation(program, 'u_projection_matrix'),
false,
args.defaultProjectionData.mainMatrix // convert mat4 from gl-matrix to a plain array
);
gl.uniform4f(
gl.getUniformLocation(program, 'u_projection_tile_mercator_coords'),
...args.defaultProjectionData.tileMercatorCoords
);
gl.uniform4f(
gl.getUniformLocation(program, 'u_projection_clipping_plane'),
...args.defaultProjectionData.clippingPlane
);
gl.uniform1f(
gl.getUniformLocation(program, 'u_projection_transition'),
args.defaultProjectionData.projectionTransition
);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.enableVertexAttribArray(this.aPos);
gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3);
}
};
// add the custom style layer to the map
map.on('load', () => {
map.addLayer(highlightLayer, 'crimea-fill');
});
</script>
</body>
</html>