Custom Layer
Use a custom layer to draw simple WebGL content on a globe.
Based on: https://maplibre.org/maplibre-gl-js/docs/examples/globe-custom-simple/
<script lang="ts">
import { MapLibre, CustomLayer, GlobeControl, Projection } from 'svelte-maplibre-gl';
import maplibregl from 'maplibre-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 });
class CustomLayerImpl implements Omit<maplibregl.CustomLayerInterface, 'id' | 'type'> {
private shaderMap: Map<string, WebGLProgram> = new Map();
private aPos: number = 0;
private buffer: WebGLBuffer | null = null;
private getShader(
gl: WebGL2RenderingContext | WebGLRenderingContext,
shaderDescription: maplibregl.CustomRenderMethodInput['shaderData']
) {
// Pick a shader based on the current projection, defined by `variantName`.
if (this.shaderMap.has(shaderDescription.variantName)) {
return this.shaderMap.get(shaderDescription.variantName)!;
}
// Create 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 vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
const vertexSource = `#version 300 es
// Inject MapLibre projection code
${shaderDescription.vertexShaderPrelude}
${shaderDescription.define}
in vec2 a_pos;
void main() {
gl_Position = projectTile(a_pos);
}`;
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
// Create fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
const fragmentSource = `#version 300 es
out highp vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 1.0, 0.75);
}`;
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
onAdd(_map: maplibregl.Map, gl: WebGL2RenderingContext) {
// 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: WebGL2RenderingContext | WebGLRenderingContext, args: maplibregl.CustomRenderMethodInput) {
const program = this.getShader(gl, args.shaderData);
gl.useProgram(program);
gl.uniformMatrix4fv(
gl.getUniformLocation(program, 'u_projection_matrix'),
false,
args.defaultProjectionData.mainMatrix
);
gl.uniformMatrix4fv(
gl.getUniformLocation(program, 'u_projection_fallback_matrix'),
false,
args.defaultProjectionData.fallbackMatrix
);
gl.uniform1f(
gl.getUniformLocation(program, 'u_projection_transition'),
args.defaultProjectionData.projectionTransition
);
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.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);
}
}
</script>
<MapLibre
class="h-[55vh] min-h-[300px]"
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
zoom={3}
center={[20, 58]}
>
<CustomLayer implementation={new CustomLayerImpl()} />
<Projection type="globe" />
<GlobeControl />
</MapLibre>
Our examples use Tailwind CSS and shadcn-svelte.