Last active
July 1, 2025 00:40
-
-
Save yutannihilation/120b2e60fbe0091f4503820d63afaf9f to your computer and use it in GitHub Desktop.
Add shader animation to MapLibre GL JS (based on https://maplibre.org/maplibre-gl-js/docs/examples/globe-custom-simple/)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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/[email protected]/dist/maplibre-gl.css' /> | |
<script src='https://unpkg.com/[email protected]/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} | |
}); | |
// ADDED: force redrawing every frame | |
map.repaint = 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; | |
// ADDED: pass coordinates to fragment shader | |
out vec2 o_pos; | |
void main() { | |
gl_Position = projectTile(a_pos); | |
// ADDED | |
o_pos = a_pos; | |
}`; | |
// create GLSL source for fragment shader | |
const fragmentSource = `#version 300 es | |
precision highp float; | |
// ADDED | |
uniform float u_time; | |
// ADDED | |
in vec2 o_pos; | |
out highp vec4 fragColor; | |
void main() { | |
fragColor = vec4(0.5 + 0.5 * sin(o_pos.y * u_time / 300.0), 0.0, 0.5 + 0.5 * sin(o_pos.x * u_time / 411.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 | |
); | |
// ADDED | |
this.startTime = performance.now(); | |
// 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 | |
); | |
// ADDED | |
gl.uniform1f( | |
gl.getUniformLocation(program, 'u_time'), | |
(performance.now() - this.startTime) | |
); | |
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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment