Commit 80e27ec3 authored by Iván Sánchez Ortega's avatar Iván Sánchez Ortega

v2.2.0: Animated shaders!

parent a9dd0ec8
Pipeline #43442850 passed with stage
in 48 seconds
# 2.2.0 (2019-01-15)
# 2.1.0 (1027-12-24)
* Add a new `uNow` uniform, and a render loop. Animated shaders!
# 2.1.0 (2018-12-24)
* Add a new `tilelayers` option. Instead of just `tileUrls`, users can now provide instances of `L.TileLayer` (or even `L.TileLayer.WMS`). Doesn't break backwards compatibility (except if you're hot-reloading tile URLs).
......
......@@ -73,6 +73,7 @@ The fragment shader receives the following **varyings**:
It also receives the following **uniforms**:
* `uNow`: a `float` with the number of microseconds since page load (as per [`performance.now()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/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`: a `sampler2D` referring to the first loaded tile image. This exists only if the `tileUrls` option is not empty.
* `uTexture1`..`uTexture7`: texture samplers for the 2nd through 8th image.
......@@ -139,9 +140,7 @@ Find more examples in the [interactive demo](http://ivansanchez.gitlab.io/Leafle
* Reusing the same WebGL context for more than one `TileLayer.GL` (as the render
calls are sync)
* Custom uniforms
* Time uniform(s)
* Some kind of animations (keep the loaded images in memory, implement a rendering
loop)
* Render stuff off the main thread
## Legalese
......
......@@ -225,6 +225,36 @@ void main(void) {
"} \n"
},
"Animated hue rotation": {
tiles: ['http://{s}.tile.stamen.com/terrain/{z}/{x}/{y}.png'],
shader:
"vec3 rgb2hsv(vec3 c) { \n" +
" vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); \n" +
" vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); \n" +
" vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); \n" +
" \n" +
" float d = q.x - min(q.w, q.y); \n" +
" float e = 1.0e-10; \n" +
" return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); \n" +
"} \n" +
" \n" +
"vec3 hsv2rgb(vec3 c) { \n" +
" vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); \n" +
" vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); \n" +
" return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); \n" +
"} \n" +
" \n" +
"void main(void) { \n" +
" vec4 texelColour = texture2D(uTexture0, vec2(vTextureCoords.s, vTextureCoords.t)); \n" +
" vec3 hsv = rgb2hsv(texelColour.rgb); \n" +
" \n" +
" // Hue will rotate fully once every 2 seconds. \n" +
" hsv.x += fract(0.3 + (uNow / 2000.0)); \n" +
" \n" +
" gl_FragColor = vec4(hsv2rgb(hsv), texelColour.a); \n" +
"} \n"
},
"Flood & height": {
tiles: [
'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
......@@ -515,6 +545,7 @@ void main(void) {
// Copy-pasted from _loadGLProgram, plus comments
var fragmentShaderHeader =
"precision highp float; // Use 24-bit floating point numbers for everything\n" +
"uniform float uNow; // Microseconds since page load, as per performance.now()\n" +
"varying vec2 vTextureCoords; // Pixel coordinates of this fragment, to fetch texture color\n" +
"varying vec2 vCRSCoords; // CRS coordinates of this fragment\n" +
"varying vec2 vLatLngCoords; // Lat-Lng coordinates of this fragment (linearly interpolated)";
......
{
"name": "leaflet.tilelayer.gl",
"version": "2.1.0",
"version": "2.2.0",
"description": "Apply WebGL shaders to your LeafletJS tile layers",
"main": "src/Leaflet.TileLayer.GL.js",
"scripts": {
......
......@@ -101,6 +101,7 @@ L.TileLayer.GL = L.GridLayer.extend({
// will use the same predefined variants, and
var fragmentShaderHeader =
"precision highp float;\n" +
"uniform float uNow;\n" +
"varying vec2 vTextureCoords;\n" +
"varying vec2 vCRSCoords;\n" +
"varying vec2 vLatLngCoords;\n";
......@@ -141,9 +142,18 @@ L.TileLayer.GL = L.GridLayer.extend({
this._aTexPosition = gl.getAttribLocation(program, "aTextureCoords");
this._aCRSPosition = gl.getAttribLocation(program, "aCRSCoords");
this._aLatLngPosition = gl.getAttribLocation(program, "aLatLngCoords");
this._uNowPosition = gl.getUniformLocation(program, "uNow");
// If the shader is time-dependent (i.e. animated), init the texture cache
if (this._uNowPosition) {
this._fetchedTextures = {};
this._2dContexts = {};
gl.uniform1f(this._uNowPosition, performance.now());
}
// console.log('Tex position: ', this._aTexPosition);
// console.log('CRS position: ', this._aCRSPosition);
// console.log("uNow position: ", this._uNowPosition);
// Create three data buffer with 8 elements each - the (easting,northing)
// CRS coords, the (s,t) texture coords and the viewport coords for each
......@@ -261,66 +271,124 @@ L.TileLayer.GL = L.GridLayer.extend({
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
},
_bindTexture: function(index, imageData) {
// Helper function. Binds a ImageData (HTMLImageElement, HTMLCanvasElement or
// ImageBitmap) to a texture, given its index (0 to 7).
// The image data is assumed to be in RGBA format.
var gl = this._gl;
gl.activeTexture(gl.TEXTURE0 + index);
gl.bindTexture(gl.TEXTURE_2D, this._textures[index]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageData);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.generateMipmap(gl.TEXTURE_2D);
},
// Gets called by L.GridLayer before createTile(), just before coord wrapping happens.
// Needed to store the context of each <canvas> tile when the tile coords is wrapping.
_addTile(coords, container) {
// This is quite an ugly hack, but WTF.
this._unwrappedKey = this._tileCoordsToKey(coords);
L.GridLayer.prototype._addTile.call(this, coords, container);
},
createTile: function(coords, done) {
var tile = L.DomUtil.create("canvas", "leaflet-tile");
tile.width = tile.height = this.options.tileSize;
tile.onselectstart = tile.onmousemove = L.Util.falseFn;
var ctx = tile.getContext("2d");
if (this._tileLayers.length === 0) {
this._render(coords);
ctx.drawImage(this._renderer, 0, 0);
setTimeout(done, 50);
} else {
var texFetches = [];
for (var i = 0; i < this._tileLayers.length && i < 8; i++) {
// this.options.tileUrls[i]
texFetches.push(this._getNthTile(i, coords));
}
Promise.all(texFetches).then(
function(textureImages) {
if (!this._map) {
return;
}
// console.log(textureImages);
var gl = this._gl;
for (var i = 0; i < this._tileLayers.length && i < 8; i++) {
gl.activeTexture(gl.TEXTURE0 + i);
gl.bindTexture(gl.TEXTURE_2D, this._textures[i]);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
textureImages[i]
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER,
gl.LINEAR_MIPMAP_NEAREST
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.generateMipmap(gl.TEXTURE_2D);
}
this._render(coords);
ctx.drawImage(this._renderer, 0, 0);
done();
}.bind(this),
function(err) {
L.TileLayer.prototype._tileOnError.call(this, done, tile, err);
}.bind(this)
);
var unwrappedKey = this._unwrappedKey;
var texFetches = [];
for (var i = 0; i < this._tileLayers.length && i < 8; i++) {
// this.options.tileUrls[i]
texFetches.push(this._getNthTile(i, coords));
}
Promise.all(texFetches).then(
function(textureImages) {
if (!this._map) {
return;
}
// If the shader is time-dependent (i.e. animated),
// save the textures for later access
if (this._uNowPosition) {
var key = this._tileCoordsToKey(coords);
this._fetchedTextures[key] = textureImages;
this._2dContexts[unwrappedKey] = ctx;
}
var gl = this._gl;
for (var i = 0; i < this._tileLayers.length && i < 8; i++) {
this._bindTexture(i, textureImages[i]);
}
this._render(coords);
ctx.drawImage(this._renderer, 0, 0);
done();
}.bind(this),
function(err) {
L.TileLayer.prototype._tileOnError.call(this, done, tile, err);
}.bind(this)
);
return tile;
},
// // Gets the tile for the Nth `TileLayer` in `this._tileLayers`,
_removeTile: function(key) {
if (this._uNowPosition) {
delete this._fetchedTextures[key];
delete this._2dContexts[key];
}
L.TileLayer.prototype._removeTile.call(this, key);
},
onAdd: function() {
// If the shader is time-dependent (i.e. animated), start an animation loop.
if (this._uNowPosition) {
L.Util.cancelAnimFrame(this._animFrame);
this._animFrame = L.Util.requestAnimFrame(this._onFrame, this);
}
L.TileLayer.prototype.onAdd.call(this);
},
onRemove: function(map) {
// Stop the animation loop, if any.
L.Util.cancelAnimFrame(this._animFrame);
L.TileLayer.prototype.onRemove.call(this, map);
},
_onFrame: function() {
if (this._uNowPosition && this._map) {
var gl = this._gl;
gl.uniform1f(this._uNowPosition, performance.now());
for (var key in this._tiles) {
var tile = this._tiles[key];
if (!tile.current || !tile.loaded) {
continue;
}
var coords = this._keyToTileCoords(key);
wrappedKey = this._tileCoordsToKey(this._wrapCoords(coords));
for (var i = 0; i < this._tileLayers.length && i < 8; i++) {
this._bindTexture(i, this._fetchedTextures[wrappedKey][i]);
}
this._render(coords);
this._2dContexts[key].drawImage(this._renderer, 0, 0);
}
this._animFrame = L.Util.requestAnimFrame(this._onFrame, this, false);
}
},
// Gets the tile for the Nth `TileLayer` in `this._tileLayers`,
// for the given tile coords, returns a promise to the tile.
_getNthTile: function(n, coords) {
var layer = this._tileLayers[n];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment