Skip to main content

External WebGL Contexts with MapLibre

This guide shows how to connect luma.gl to a WebGL context that is created and owned by MapLibre GL JS. It uses the webgl2Adapter.attach API to wrap the map's context in a WebGLDevice and keep a WebGLCanvasContext synchronized with MapLibre's canvas.

Install dependencies

Add MapLibre GL JS alongside luma.gl:

npm install @luma.gl/webgl @luma.gl/engine maplibre-gl

This example uses CARTO basemaps so you do not need an API token, but you can still supply a Mapbox token if you point MapLibre at a Mapbox-hosted style elsewhere in your application.

Attach luma.gl to MapLibre

Create the map first so MapLibre owns the WebGL canvas, then attach the device to that context:

import maplibregl from 'maplibre-gl'
import {Matrix4} from '@math.gl/core'
import {webgl2Adapter} from '@luma.gl/webgl'
import {Model} from '@luma.gl/engine'

const map = new maplibregl.Map({
container: 'map',
style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
antialias: true,
pitch: 60,
zoom: 12
})

map.on('load', async () => {
const webglContext = map.getCanvas().getContext('webgl2') as WebGL2RenderingContext
const device = await webgl2Adapter.attach(webglContext, {createCanvasContext: {autoResize: false}})

// Keep the WebGLCanvasContext aligned with MapLibre's drawing buffer
device.canvasContext.resize({width: webglContext.drawingBufferWidth, height: webglContext.drawingBufferHeight})

const modelMatrix = new Matrix4()
const viewProjection = new Matrix4()

const overlay = new Model(device, {
id: 'maplibre-overlay',
vs: `...`,
fs: `...`,
shaderLayout: {
attributes: [
{name: 'positions', location: 0, format: 'float32x3'}
],
bindings: [{name: 'app', type: 'uniform', location: 0}]
},
attributes: {
positions: new Float32Array([...])
},
vertexCount: 6,
bindings: {
app: /* uniform buffer */
}
})

map.addLayer({
id: 'luma-gl-overlay',
type: 'custom',
renderingMode: '3d',
render: (_, matrix) => {
viewProjection.fromArray(matrix as number[])
// Update uniforms and draw without clearing the map's buffers
const renderPass = device.beginRenderPass({clearColor: false, clearDepth: false})
overlay.draw(renderPass)
renderPass.end()
map.triggerRepaint()
}
})
})

Handle map resizes

Because the context comes from MapLibre, luma.gl cannot resize it automatically. Listen for MapLibre resize events and keep the WebGLCanvasContext in sync:

map.on('resize', () => {
const webglContext = map.getCanvas().getContext('webgl2') as WebGL2RenderingContext
device.canvasContext.resize({
width: webglContext.drawingBufferWidth,
height: webglContext.drawingBufferHeight
})
})

See it in action

The revived External WebGL Context example renders a luma.gl overlay driven by MapLibre's WebGL render loop and uses the webgl2Adapter.attach API to keep both frameworks in sync.