diff --git a/debug/test-rastertile-fade.html b/debug/test-rastertile-fade.html
new file mode 100644
index 00000000000..8a47c3e9182
--- /dev/null
+++ b/debug/test-rastertile-fade.html
@@ -0,0 +1,101 @@
+
+
+
+ Mapbox-Raster-Reprojection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts
index c4b35983a52..1c470a7750a 100644
--- a/src/render/draw_raster.ts
+++ b/src/render/draw_raster.ts
@@ -23,6 +23,7 @@ import {mercatorXfromLng, mercatorYfromLat} from '../geo/mercator_coordinate';
import {COLOR_MIX_FACTOR} from '../style/style_layer/raster_style_layer';
import RasterArrayTile from '../source/raster_array_tile';
import RasterArrayTileSource from '../source/raster_array_tile_source';
+import browser from '../util/browser';
import type Transform from '../geo/transform';
import type {OverscaledTileID} from '../source/tile_id';
@@ -40,6 +41,7 @@ import type {CrossTileID, VariableOffset} from '../symbol/placement';
export default drawRaster;
const RASTER_COLOR_TEXTURE_UNIT = 2;
+const PREVIOUS_TILE_TEXTURE_UNIT = 3;
type RasterConfig = {
defines: DynamicDefinesType[];
@@ -185,6 +187,24 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
texture.bind(textureFilter, gl.CLAMP_TO_EDGE);
}
+ // --- ICONEM: bind previous tile texture (if any) on unit 3
+ let perTileFadeMix = 0.5;
+ const perTileFadeDuration = 1000; // because was probably initialLoad, rasterFadeDuration = 0
+ if (tile.previousTexture) {
+ const now = browser.now();
+ // If we missed setting start, treat as fully shown
+ const t0 = tile.perTileFadeStartTime ? tile.perTileFadeStartTime : now;
+ // const t0 = tile.perTileFadeEndTime ? tile.perTileFadeEndTime - perTileFadeDuration : now;
+ perTileFadeMix = Math.max(0, Math.min(1, (now - t0) / perTileFadeDuration));
+ context.activeTexture.set(gl.TEXTURE0 + PREVIOUS_TILE_TEXTURE_UNIT);
+ tile.previousTexture.bind(textureFilter, gl.CLAMP_TO_EDGE);
+ // Optional micro-GC once the fade is done
+ if (perTileFadeMix >= 1 && tile.previousTexture instanceof Texture) {
+ tile.previousTexture.destroy();
+ tile.previousTexture = null;
+ }
+ }
+
// Enable trilinear filtering on tiles only beyond 20 degrees pitch,
// to prevent it from compromising image crispness on flat or low tilted maps.
if ('useMipmap' in texture && context.extTextureFilterAnisotropic && painter.transform.pitch > 20) {
@@ -248,7 +268,9 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
rasterConfig.range,
tileSize,
buffer,
- emissiveStrength
+ emissiveStrength,
+ perTileFadeMix,
+ (tile.previousTexture && perTileFadeMix < 1.0) ? PREVIOUS_TILE_TEXTURE_UNIT : 0
);
const affectedByFog = painter.isTileAffectedByFog(coord);
diff --git a/src/render/program/raster_program.ts b/src/render/program/raster_program.ts
index eed4281f0bc..042b6546631 100644
--- a/src/render/program/raster_program.ts
+++ b/src/render/program/raster_program.ts
@@ -43,6 +43,8 @@ export type RasterUniformsType = {
['u_texture_offset']: Uniform2f;
['u_texture_res']: Uniform2f;
['u_emissive_strength']: Uniform1f;
+ ['u_per_tile_fade_mix']: Uniform1f;
+ ['u_previous_tile']: Uniform1i;
};
export type RasterDefinesType = 'RASTER_COLOR' | 'RENDER_CUTOFF' | 'RASTER_ARRAY' | 'RASTER_ARRAY_LINEAR';
@@ -74,34 +76,16 @@ const rasterUniforms = (context: Context): RasterUniformsType => ({
'u_color_ramp': new Uniform1i(context),
'u_texture_offset': new Uniform2f(context),
'u_texture_res': new Uniform2f(context),
- 'u_emissive_strength': new Uniform1f(context)
+ 'u_emissive_strength': new Uniform1f(context),
+ 'u_per_tile_fade_mix': new Uniform1f(context),
+ 'u_previous_tile': new Uniform1i(context),
});
const rasterUniformValues = (
- matrix: Float32Array,
- normalizeMatrix: Float32Array,
- globeMatrix: Float32Array,
- mercMatrix: Float32Array,
- gridMatrix: Float32Array,
- parentTL: [number, number],
- zoomTransition: number,
- mercatorCenter: [number, number],
- cutoffParams: [number, number, number, number],
- parentScaleBy: number,
- fade: {
- mix: number;
- opacity: number;
- },
- layer: RasterStyleLayer,
- perspectiveTransform: [number, number],
- elevation: number,
- colorRampUnit: number,
- colorMix: [number, number, number, number],
- colorOffset: number,
- colorRange: [number, number],
- tileSize: number,
- buffer: number,
- emissiveStrength: number,
+matrix: Float32Array, normalizeMatrix: Float32Array, globeMatrix: Float32Array, mercMatrix: Float32Array, gridMatrix: Float32Array, parentTL: [number, number], zoomTransition: number, mercatorCenter: [number, number], cutoffParams: [number, number, number, number], parentScaleBy: number, fade: {
+ mix: number;
+ opacity: number;
+}, layer: RasterStyleLayer, perspectiveTransform: [number, number], elevation: number, colorRampUnit: number, colorMix: [number, number, number, number], colorOffset: number, colorRange: [number, number], tileSize: number, buffer: number, emissiveStrength: number, perTileFadeMix: number, previousTileTextureUnit: number,
): UniformValues => ({
'u_matrix': matrix,
'u_normalize_matrix': normalizeMatrix,
@@ -136,7 +120,9 @@ const rasterUniformValues = (
tileSize / (tileSize + 2 * buffer)
],
'u_texture_res': [tileSize + 2 * buffer, tileSize + 2 * buffer],
- 'u_emissive_strength': emissiveStrength
+ 'u_emissive_strength': emissiveStrength,
+ 'u_per_tile_fade_mix': perTileFadeMix,
+ 'u_previous_tile': previousTileTextureUnit,
});
const rasterPoleUniformValues = (
@@ -178,6 +164,8 @@ const rasterPoleUniformValues = (
1,
0,
emissiveStrength,
+ 1, // perTileperTileFadeMix by default
+ 3, // previousTileTextureUnit
));
function spinWeights(angle: number): [number, number, number] {
diff --git a/src/shaders/raster.fragment.glsl b/src/shaders/raster.fragment.glsl
index 16a6e1cc1b0..5cc1207aafd 100644
--- a/src/shaders/raster.fragment.glsl
+++ b/src/shaders/raster.fragment.glsl
@@ -37,6 +37,10 @@ uniform highp float u_colorization_offset;
uniform vec2 u_texture_res;
#endif
+// ICONEM
+uniform sampler2D u_previous_tile;
+uniform float u_per_tile_fade_mix; // 0: show previous, 1: show current
+
void main() {
vec4 color0, color1, color;
@@ -64,6 +68,10 @@ void main() {
if (value.y > 0.0) value.x /= value.y;
#else
color = mix(texture(u_image0, v_pos0), texture(u_image1, v_pos1), u_fade_t);
+
+ // ICONEM
+ // color = mix(texture(u_previous_tile, v_pos0), color, u_per_tile_fade_mix);
+
value = vec2(u_colorization_offset + dot(color.rgb, u_colorization_mix.rgb), color.a);
#endif
@@ -82,9 +90,36 @@ void main() {
if (color0.a > 0.0) color0.rgb /= color0.a;
if (color1.a > 0.0) color1.rgb /= color1.a;
color = mix(color0, color1, u_fade_t);
+
+ // ICONEM
+ vec4 previousColor = texture(u_previous_tile, v_pos0);
+ color = mix(previousColor, color, u_per_tile_fade_mix);
+ // float c = color0.a;
+ // float c = u_per_tile_fade_mix;
+ // color = vec4(c, 0.,0., 1.);
+ // color = previousColor;
+
+ // 1 Per-tile previous/current crossfade
+ // color = texture(u_image0, v_pos0);
+ // if (u_per_tile_fade_mix < 1.0) {
+ // // vec4 prevColor = texture(u_previous_tile, v_pos0);
+ // vec4 previousColor = vec4(1., 0., 0., 1.);
+ // color = mix(previousColor, color, clamp(u_per_tile_fade_mix, 0.0, 1.0));
+ // }
+ // 2 Existing parent/child crossfade
+ // parentColor = texture(u_image1, v_pos1);
+ // color = mix(parentColor, color, u_fade_t);
+
+ // ICONEM
+
#endif
color.a *= u_opacity;
+
+ // ICONEM
+ color.a = 1.;
+
+
#ifdef GLOBE_POLES
color.a *= 1.0 - smoothstep(0.0, 0.05, u_zoom_transition);
#endif
diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts
index 8ed50a481d4..b7d7370a428 100644
--- a/src/source/raster_tile_source.ts
+++ b/src/source/raster_tile_source.ts
@@ -139,19 +139,28 @@ class RasterTileSource extends Evented implements IS
/**
* Reloads the source data and re-renders the map.
*
+ * @param {boolean} clearSource Optional param to load new source without clearing the tile cache (source from style).
* @example
* map.getSource('source-id').reload();
*/
- reload() {
+ reload(clearSource: boolean = true) {
this.cancelTileJSONRequest();
- const fqid = makeFQID(this.id, this.scope);
- this.load(() => this.map.style.clearSource(fqid));
+ if (clearSource) {
+ // Legacy path: full clear
+ const fqid = makeFQID(this.id, this.scope);
+ this.load(() => this.map.style.clearSource(fqid));
+ } else {
+ // Soft path: keep SourceCache tiles
+ // Note the param could be passed as object like so opts?: {preserveTiles?: boolean}
+ this.load();
+ }
}
/**
* Sets the source `tiles` property and re-renders the map.
*
* @param {string[]} tiles An array of one or more tile source URLs, as in the TileJSON spec.
+ * @param {boolean} clearSource Optional param to load new source without clearing the tile cache (source from style).
* @returns {RasterTileSource} Returns itself to allow for method chaining.
* @example
* map.addSource('source-id', {
@@ -163,9 +172,10 @@ class RasterTileSource extends Evented implements IS
* // Set the endpoint associated with a raster tile source.
* map.getSource('source-id').setTiles(['https://another_end_point.net/{z}/{x}/{y}.png']);
*/
- setTiles(tiles: Array): this {
+ setTiles(tiles: Array, clearSource: boolean = true): this {
this._options.tiles = tiles;
- this.reload();
+ // Soft reload: do NOT clear the source cache.
+ this.reload(clearSource);
return this;
}
@@ -223,7 +233,28 @@ class RasterTileSource extends Evented implements IS
if (!data) return callback(null);
if (this.map._refreshExpiredTiles) tile.setExpiryData({cacheControl, expires});
+
+ // // --- ICONEM: capture previous GL texture for per-tile fade
+ // if (tile.texture) {
+ // tile.previousTexture = tile.texture;
+ // // Important: prevent setTexture from destroying it implicitly
+ // // by clearing the reference before creating the new one.
+ // // (setTexture will only delete an existing texture if it's still attached)
+ // // @ts-expect-error intentional temporary clear to avoid auto-destroy
+ // tile.texture = null;
+ // // const perTileFadeDuration = 500;
+ // // tile.perTileFadeEndTime = browser.now() + perTileFadeDuration; // 300ms fade
+ // tile.perTileFadeStartTime = browser.now(); // 300ms fade
+ // }
+ const perTileFadeDuration = 5000;
+ const oldTile = this._tiles[tile.tileID.key];
+
+ if (oldTile && perTileFadeDuration > 0) {
+ oldTile.fadeRetainUntil = Date.now() + perTileFadeDuration;
+ }
+
tile.setTexture(data, this.map.painter);
+
tile.state = 'loaded';
cacheEntryPossiblyAdded(this.dispatcher);
@@ -236,17 +267,23 @@ class RasterTileSource extends Evented implements IS
tile.request.cancel();
delete tile.request;
}
+
+ if (tile.previousTexture && tile.previousTexture instanceof Texture) {
+ tile.previousTexture.destroy();
+ }
+ tile.previousTexture = null;
+
if (callback) callback();
}
unloadTile(tile: Tile, callback?: Callback) {
// Cache the tile texture to avoid re-allocating Textures if they'll just be reloaded
if (tile.texture && tile.texture instanceof Texture) {
- // Clean everything else up owned by the tile, but preserve the texture.
- // Destroy first to prevent racing with the texture cache being popped.
+ // // Clean everything else up owned by the tile, but preserve the texture.
+ // // Destroy first to prevent racing with the texture cache being popped.
tile.destroy(true);
- // Save the texture to the cache
+ // // Save the texture to the cache
if (tile.texture && tile.texture instanceof Texture) {
this.map.painter.saveTileTexture(tile.texture);
}
@@ -254,6 +291,15 @@ class RasterTileSource extends Evented implements IS
tile.destroy();
}
+ // ICONEM Ensure previous fade state cannot leak across lifecycles
+ if (tile.previousTexture) {
+ if (tile.previousTexture instanceof Texture) {
+ tile.previousTexture.destroy();
+ }
+ tile.previousTexture = null;
+ }
+ tile.perTileFadeStartTime = undefined;
+
if (callback) callback();
}
diff --git a/src/source/source_cache.ts b/src/source/source_cache.ts
index 52c561f207b..34e6cb04165 100644
--- a/src/source/source_cache.ts
+++ b/src/source/source_cache.ts
@@ -421,6 +421,19 @@ class SourceCache extends Evented {
}
}
+ /**
+ * Find a loaded previousTile of the given tile
+ * @private
+ */
+ findPreviousTile(tileID: OverscaledTileID): Tile | null | undefined {
+ const key = String(tileID.key);
+ const tile = this._tiles[key];
+ if (tile && tile.previousTexture) {
+ return tile;
+ }
+ return undefined;
+ }
+
_getLoadedTile(tileID: OverscaledTileID): Tile | null | undefined {
const tile = this._tiles[tileID.key];
if (tile && tile.hasData()) {
@@ -619,6 +632,25 @@ class SourceCache extends Evented {
// parent or child tiles that are *already* loaded.
const retain = this._updateRetainedTiles(idealTileIDs);
+ // ICONEM: Retain old tiles for crossfade between old/new TMS
+ if (this._tiles) {
+ console.log('cache', this._tiles);
+ for (const id in this._tiles) {
+ const tile = this._tiles[id];
+ console.log('yo', tile);
+
+ // Old texture, not already in retain, and has a fade in progress
+ retain[id] = tile.tileID;
+ // if (!retain[id] && tile.texture && tile.previousTexture) {
+ // console.log('match');
+ // const fadeDuration = 2000;
+ // if (tile.perTileFadeStartTime && Date.now() < tile.perTileFadeStartTime + fadeDuration) {
+ // retain[id] = tile.tileID; // keep this tile alive
+ // }
+ // }
+ }
+ }
+
if (isRasterType(this._source.type) && idealTileIDs.length !== 0) {
const parentsForFading: Partial> = {};
const fadingTiles: Record = {};
diff --git a/src/source/tile.ts b/src/source/tile.ts
index 6ebd69b2979..39068366b5b 100644
--- a/src/source/tile.ts
+++ b/src/source/tile.ts
@@ -185,6 +185,11 @@ class Tile {
worldview: string | undefined;
+ // ICONEM
+ perTileFadeStartTime: number | undefined;
+ previousTexture: Texture | null | undefined | UserManagedTexture;
+ fadeRetainUntil?: number;
+
/**
* @param {OverscaledTileID} tileID
* @param size