leaflet.tilelayer.gl
v2.4.1
Published
Apply WebGL shaders to your LeafletJS tile layers
Downloads
912
Readme
Leaflet.TileLayer.GL
A LeafletJS plugin to appy WebGL shaders to your tiles.
With this plugin, you can apply colour transforms to your tiles, merge two or more tiles with a custom function, perform on-the-fly hillshading, or create synthetic tile layers based only on the map coordinates.
Demos
See several examples and edit them in the interactive editable demo!
The interactive editable demo includes the code for the following, which you can also see individually:
- Basic colour inversion: This demo loads the "toner" map style by Stamen and changes the colours on-the-fly.
- Conditional colouring: This demo loads the "toner" map style by Stamen and changes the colours on-the-fly, depending on the latitude of each pixel. This highlights the tropics and arctic circles.
- Flood & height: This demo uses MapBox's "Terrain-RGB" tiles to play with the elevation: areas are coloured depending to the elevation (below 0 meters, between 0 and 5 meters, between 5 and 10 meters, above 10 meters).
- Hypsometric tint: This demo uses MapBox's "Terrain-RGB" tiles and applies a basic hypsometric tint colour ramp.
- L.tileLayer.wms: This demo uses Leaflet's TileLayer.WMS to load tiles from a WMS server and changes its brightness.
- Interactive colorizer demo: This demo defines custom uniforms for the shaders and updates them based no the user's input.
- Pixellated demo: Forces nearest-neighbour interpolation when overzooming on a
TileLayer
.
Besides those, the Mandelbrot set demo uses a map with L.CRS.Simple
coordinates and no tiles at all, to draw a fractal set.
The interactive demo includes the following, which doesn't have a stand-alone demo:
- Hue rotation (converts RGB colour space to HSV, modifies the hue, converts back)
Why?
Leaflet has been lagging behind when it comes to WebGL technology. Other map libraries (such as OpenLayers 3 and most notably Tangram) can already use WebGL shaders to apply transformations to map tiles and do fancy stuff.
The inflexion point are MapBox's "Terrain-RGB" tiles. WebGL manipulation of these tiles can provide real-time terrain relief and hill shading.
This takes some inspiration from shadertoy.com, in the sense that the shaders work on two triangles with some predefined attributes and uniforms.
Compatibility
Leaflet 1.0.3 (or newer), and a web browser that supports both WebGL and ES6 Promise
s. You can also use a Promise
polyfill for IE11.
Usage
Include Leaflet and Leaflet.TileLayer.GL in your HTML:
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha512-07I2e+7D8p6he1SIM+1twR5TIrhUQn9+I6yjqD53JQjFiMf8EtC93ty0/5vJTZGF8aAocvHYNEDJajGdNx1IsQ=="
crossorigin=""/>
<script src="https://unpkg.com/[email protected]/dist/leaflet-src.js"
integrity="sha512-WXoSHqw/t26DszhdMhOXOkI7qCiv5QWXhH9R7CgvgZMHz1ImlkVQ3uNsiQKu5wwbbxtPzFXd1hK4tzno2VqhpA=="
crossorigin=""></script>
<script src='https://unpkg.com/leaflet.tilelayer.gl@latest/src/Leaflet.TileLayer.GL'></script>
Alternatively, fetch a local copy of Leaflet and Leaflet.TileLayer.GL with npm install --save leaflet; npm install --save leaflet.tilelayer.gl
or yarn add leaflet; yarn add leaflet.tilelayer.gl
You can create instances of L.TileLayer.GL
in your code. These take two new options: fragmentShader
and tileUrls
, e.g.:
var antitoner = L.tileLayer.gl({
fragmentShader: "// String with GLSL fragment shader code",
tileUrls: ['http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png']
}).addTo(map);
Using this plugin requires some knowledge of WebGL and GLSL shaders. If you've never heard the terms "vertex shader" or "fragment shader", read this WebGL tutorial to become acquinted, or The Book Of Shaders to learn to do cool shaders, or WebGL Fundamentals to see some WebGL image processing techniques.
The fragmentShader
option contain shader code, in a string. For every map tile, two triangles are created, a simple vertex shader runs to copy data and fill the values for the varyings, and the fragment shader runs once on every pixel (to be precise, on every fragment). This plugin does not allow you to create more triangles, and does not allow to create animations.
The fragment shader receives the following varyings:
vLatLngCoords
: avec2
containing the map data coordinates for the vertices (with values likeLatLng
s).vCRSCoords
: avec2
containing the map display coordinates for the vertices (with values for the map CRS).vTextureCoords
: avec2
containing the texture coordinates for the vertices. Use this for fetching texels.
It also receives the following uniforms:
uNow
: afloat
with the number of microseconds since page load (as perperformance.now()
). If this uniform is not used, tiles will be rendered only once. If it is, then they will be re-rendered at each frame.uTexture0
: asampler2D
referring to the first loaded tile image. This exists only if thetileUrls
option is not empty.uTexture1
..uTexture7
: texture samplers for the 2nd through 8th image.uTileCoords
: avec3
containing the tile coordinates, as used in the tile URLs. Use only when the tile coordinates (or their zoom level) are relevant.
Alternatively, provide one or more instances of L.TileLayer
in the tileLayers
option. This is useful for using WMS data sources, e.g.:
var layer = L.tileLayer.gl({
fragmentShader: "// String with GLSL fragment shader code",
tileLayers: [
L.tileLayer('http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png'),
L.tileLayer.wms('http://path.to/wms/service')
]
}).addTo(map);
In the previous example, the L.TileLayer
will be in uTexture0
and the L.TileLayer.WMS
in uTexture1
.
Demo shaders
This is the code used in the "antitoner" demo, commented and explained:
// Create the fragment shader as a multi-line string. Note the "`" character, valid only in ES6 JavaScript.
// Shaders can be defined elsewhere, or loaded from other files or from the network,
// but they must be strings when used in a TileLayer.GL.
// You need to *not* define the varyings and uniforms. L.TileLayer.GL does that for you.
// // precision highp float;
// // uniform sampler2D uTexture0; // This contains a reference to the tile image loaded from the network
// // varying vec2 vTextureCoords; // This is the interpolated texel coords for this fragment
var antiTonerFragmentShader = `
void main(void) {
// Classic texel look-up (fetch the texture "pixel" color for this fragment)
vec4 texelColour = texture2D(uTexture0, vec2(vTextureCoords.s, vTextureCoords.t));
// If uncommented, this would output the image "as is"
// gl_FragColor = texelColour;
// Let's mix the colours a little bit, inverting the red and green channels.
gl_FragColor = vec4(1.0 - texelColour.rg, texelColour.b, 1.0);
}
`
// Instantiate our L.TileLayer.GL...
var antitoner = L.tileLayer.gl({
// ... with the shader we just wrote above...
fragmentShader: antiTonerFragmentShader,
// ...and loading tile images from Stamen Toner as "uTexture0".
// If this array contained more than one tile template string,
// there would be "uTexture1", "uTexture2" and so on.
tileUrls: ['http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png']
}).addTo(map);
Find more examples in the interactive demo.
Custom uniforms
It is possible to specify custom uniforms, update them from the JS side and trigger re-renders every time they change. This is done with the uniforms
property, and the setUniform()
and reRender()
methods, e.g.:
var layer = L.tileLayer.gl({
tileUrls: ['http://tileserver/{z}/{x}/{y}.png'],
fragmentShader: "gl_FragColor = vec4(uRGB, uAlpha);",
uniforms: {
uRGB: [0.5, 1.0, 0.2],
uAlpha: 1.0
}
}).addTo(map);
layer.setUniform(uAlpha, 0.8);
layer.setUniform(uRGB, [0.6, 0.9, 0.3]);
layer.reRender();
The interactive colorizer demo offers a more complete example of this feature set.
Cool things that should be doable, but nobody has yet shown interest in asking about, much less in implementing them
- Updating the shaders
- Reusing the same WebGL context for more than one
TileLayer.GL
(as the render calls are sync) - Render stuff off the main thread
Legalese
"THE BEER-WARE LICENSE": [email protected] wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return.