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