Skip to content

Commit c53e5c3

Browse files
Improve fading - dynamic bi-directional raster cross-fading and self fading (#6469)
* Add failing test where raster fading redundantly re-adds ideal tiles. * Fix false negative fov test adulterated by rendered fading tile parent * Add failing test to show execution of fading logic when raster fading is disabled. * Modify current tests and fading logic * Fix linter and comment * Update changelog * Fix comment * Address code reviews, add edge fading and tests * Linter * Build size * Comment * Improve remove tiles logic * Further improve remove tiles logic * Address code reviews, optimize edge fading logic * Fix non-loaded case... * Improve high-pitch * logic improvements * raster fade duration setting * address code reviews - tests, simplify, nesting * build, lint * ugh * re-test for fluke --------- Co-authored-by: wayofthefuture <[email protected]>
1 parent 9dba497 commit c53e5c3

File tree

15 files changed

+790
-555
lines changed

15 files changed

+790
-555
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## main
22

33
### ✨ Features and improvements
4-
- _...Add new stuff here..._
4+
- Improve fading - dynamic bi-directional raster cross-fading and self fading ([#6469](https://github.com/maplibre/maplibre-gl-js/pull/6469))
55

66
### 🐞 Bug fixes
77
- _...Add new stuff here..._
@@ -16,7 +16,7 @@
1616

1717
### 🐞 Bug fixes
1818

19-
- Fix raster flickering when using terrain 3D and optimize terrain logic.
19+
- Fix raster flickering when using terrain 3D and optimize terrain logic. ([#6446](https://github.com/maplibre/maplibre-gl-js/pull/6446))
2020
- Fix issue where parent tiles are retained when deeper descendant tiles already cover the missing ideal tile. ([#6442](https://github.com/maplibre/maplibre-gl-js/pull/6442))
2121
- Fix an issue when GeolocateControl fires outofmaxbounds event with trackUserLocation disabled ([#6464](https://github.com/maplibre/maplibre-gl-js/pull/6464))
2222
- Fix an issue with globe+terrain "zooming" in when dragging towards the poles ([#6470](https://github.com/maplibre/maplibre-gl-js/pull/6470))

src/render/draw_raster.ts

Lines changed: 95 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,27 @@ import {DepthMode} from '../gl/depth_mode';
77
import {CullFaceMode} from '../gl/cull_face_mode';
88
import {rasterUniformValues} from './program/raster_program';
99
import {EXTENT} from '../data/extent';
10-
import {coveringZoomLevel} from '../geo/projection/covering_tiles';
10+
import {FadingDirections} from '../source/tile';
1111
import Point from '@mapbox/point-geometry';
1212

1313
import type {Painter, RenderOptions} from './painter';
1414
import type {SourceCache} from '../source/source_cache';
1515
import type {RasterStyleLayer} from '../style/style_layer/raster_style_layer';
1616
import type {OverscaledTileID} from '../source/tile_id';
17-
import type {IReadonlyTransform} from '../geo/transform_interface';
1817
import type {Tile} from '../source/tile';
19-
import type {Terrain} from './terrain';
18+
19+
type FadeProperties = {
20+
parentTile: Tile;
21+
parentScaleBy: number;
22+
parentTopLeft: [number, number];
23+
fadeValues: FadeValues;
24+
};
25+
26+
type FadeValues = {
27+
tileOpacity: number;
28+
parentTileOpacity?: number;
29+
fadeMix: {opacity: number; mix: number};
30+
};
2031

2132
const cornerCoords = [
2233
new Point(0, 0),
@@ -83,37 +94,32 @@ function drawTiles(
8394

8495
const colorMode = painter.colorModeForRenderPass();
8596
const align = !painter.options.moving;
97+
const rasterOpacity = layer.paint.get('raster-opacity');
98+
const rasterResampling = layer.paint.get('raster-resampling');
99+
const fadeDuration = layer.paint.get('raster-fade-duration');
100+
const isTerrain = !!painter.style.map.terrain;
86101

87102
// Draw all tiles
88103
for (const coord of coords) {
89104
// Set the lower zoom level to sublayer 0, and higher zoom levels to higher sublayers
90105
// Use gl.LESS to prevent double drawing in areas where tiles overlap.
91106
const depthMode = painter.getDepthModeForSublayer(coord.overscaledZ - minTileZ,
92-
layer.paint.get('raster-opacity') === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS);
107+
rasterOpacity === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS);
93108

94109
const tile = sourceCache.getTile(coord);
110+
const textureFilter = rasterResampling === 'nearest' ? gl.NEAREST : gl.LINEAR;
95111

96-
tile.registerFadeDuration(layer.paint.get('raster-fade-duration'));
97-
98-
const parentTile = sourceCache.findLoadedParent(coord, 0);
99-
const siblingTile = sourceCache.findLoadedSibling(coord);
100-
// Prefer parent tile if present
101-
const fadeTileReference = parentTile || siblingTile || null;
102-
const fade = getFadeValues(tile, fadeTileReference, sourceCache, layer, painter.transform, painter.style.map.terrain);
103-
104-
let parentScaleBy, parentTL;
105-
106-
const textureFilter = layer.paint.get('raster-resampling') === 'nearest' ? gl.NEAREST : gl.LINEAR;
107-
112+
// create and bind first texture
108113
context.activeTexture.set(gl.TEXTURE0);
109114
tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST);
110115

116+
// create second texture - use either the current tile or fade tile to bind second texture below
111117
context.activeTexture.set(gl.TEXTURE1);
112-
118+
const {parentTile, parentScaleBy, parentTopLeft, fadeValues} = getFadeProperties(tile, sourceCache, fadeDuration, isTerrain);
119+
tile.fadeOpacity = fadeValues.tileOpacity;
113120
if (parentTile) {
121+
parentTile.fadeOpacity = fadeValues.parentTileOpacity;
114122
parentTile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST);
115-
parentScaleBy = Math.pow(2, parentTile.tileID.overscaledZ - tile.tileID.overscaledZ);
116-
parentTL = [tile.tileID.canonical.x * parentScaleBy % 1, tile.tileID.canonical.y * parentScaleBy % 1];
117123
} else {
118124
tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST);
119125
}
@@ -127,10 +133,9 @@ function drawTiles(
127133

128134
const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord);
129135
const projectionData = transform.getProjectionData({overscaledTileID: coord, aligned: align, applyGlobeMatrix: !isRenderingToTexture, applyTerrainMatrix: true});
130-
const uniformValues = rasterUniformValues(parentTL || [0, 0], parentScaleBy || 1, fade, layer, corners);
136+
const uniformValues = rasterUniformValues(parentTopLeft, parentScaleBy, fadeValues.fadeMix, layer, corners);
131137

132138
const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles, 'raster');
133-
134139
const stencilMode = stencilModes ? stencilModes[coord.overscaledZ] : StencilMode.disabled;
135140

136141
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, flipCullfaceMode ? CullFaceMode.frontCCW : CullFaceMode.backCCW,
@@ -139,46 +144,79 @@ function drawTiles(
139144
}
140145
}
141146

142-
function getFadeValues(tile: Tile, parentTile: Tile, sourceCache: SourceCache, layer: RasterStyleLayer, transform: IReadonlyTransform, terrain: Terrain) {
143-
const fadeDuration = layer.paint.get('raster-fade-duration');
147+
/**
148+
* Get fade properties for current tile - either cross-fading or self-fading properties.
149+
*/
150+
function getFadeProperties(tile: Tile, sourceCache: SourceCache, fadeDuration: number, isTerrain: boolean): FadeProperties {
151+
const defaults: FadeProperties = {
152+
parentTile: null,
153+
parentScaleBy: 1,
154+
parentTopLeft: [0, 0],
155+
fadeValues: {tileOpacity: 1, parentTileOpacity: 1, fadeMix: {opacity: 1, mix: 0}}
156+
};
157+
158+
if (fadeDuration === 0 || isTerrain) return defaults;
159+
160+
// cross-fade with parent first if available
161+
if (tile.fadingParentID) {
162+
const parentTile = sourceCache._getLoadedTile(tile.fadingParentID);
163+
if (!parentTile) return defaults;
164+
165+
const parentScaleBy = Math.pow(2, parentTile.tileID.overscaledZ - tile.tileID.overscaledZ);
166+
const parentTopLeft: [number, number] = [
167+
(tile.tileID.canonical.x * parentScaleBy) % 1,
168+
(tile.tileID.canonical.y * parentScaleBy) % 1
169+
];
170+
171+
const fadeValues = getCrossFadeValues(tile, parentTile, fadeDuration);
172+
return {parentTile, parentScaleBy, parentTopLeft, fadeValues};
173+
}
144174

145-
if (!terrain && fadeDuration > 0) {
146-
const now = browser.now();
147-
const sinceTile = (now - tile.timeAdded) / fadeDuration;
148-
const sinceParent = parentTile ? (now - parentTile.timeAdded) / fadeDuration : -1;
175+
// self-fade for edge tiles
176+
if (tile.selfFading) {
177+
const fadeValues = getSelfFadeValues(tile, fadeDuration);
178+
return {parentTile: null, parentScaleBy: 1, parentTopLeft: [0, 0], fadeValues};
179+
}
149180

150-
const source = sourceCache.getSource();
151-
const idealZ = coveringZoomLevel(transform, {
152-
tileSize: source.tileSize,
153-
roundZoom: source.roundZoom
154-
});
181+
return defaults;
182+
}
155183

156-
// if no parent or parent is older, fade in; if parent is younger, fade out
157-
const fadeIn = !parentTile || Math.abs(parentTile.tileID.overscaledZ - idealZ) > Math.abs(tile.tileID.overscaledZ - idealZ);
184+
/**
185+
* Cross-fade values for a base tile with a parent tile (for zooming in/out)
186+
*/
187+
function getCrossFadeValues(tile: Tile, parentTile: Tile, fadeDuration: number): FadeValues {
188+
const now = browser.now();
158189

159-
const childOpacity = (fadeIn && tile.refreshedUponExpiration) ? 1 : clamp(fadeIn ? sinceTile : 1 - sinceParent, 0, 1);
190+
const timeSinceTile = (now - tile.timeAdded) / fadeDuration;
191+
const timeSinceParent = (now - parentTile.timeAdded) / fadeDuration;
160192

161-
// we don't crossfade tiles that were just refreshed upon expiring:
162-
// once they're old enough to pass the crossfading threshold
163-
// (fadeDuration), unset the `refreshedUponExpiration` flag so we don't
164-
// incorrectly fail to crossfade them when zooming
165-
if (tile.refreshedUponExpiration && sinceTile >= 1) tile.refreshedUponExpiration = false;
193+
// get fading opacity based on current fade direction
194+
const doFadeIn = (tile.fadingDirection === FadingDirections.Incoming);
195+
const opacity1 = clamp(timeSinceTile, 0, 1);
196+
const opacity2 = clamp(1 - timeSinceParent, 0, 1);
166197

167-
if (parentTile) {
168-
return {
169-
opacity: 1,
170-
mix: 1 - childOpacity
171-
};
172-
} else {
173-
return {
174-
opacity: childOpacity,
175-
mix: 0
176-
};
177-
}
178-
} else {
179-
return {
180-
opacity: 1,
181-
mix: 0
182-
};
183-
}
198+
const tileOpacity = doFadeIn ? opacity1 : opacity2;
199+
const parentTileOpacity = doFadeIn ? opacity2 : opacity1;
200+
const fadeMix = {
201+
opacity: 1,
202+
mix: 1 - tileOpacity
203+
};
204+
205+
return {tileOpacity, parentTileOpacity, fadeMix};
206+
}
207+
208+
/**
209+
* Simple fade-in values for tile without a parent (i.e. edge tiles)
210+
*/
211+
function getSelfFadeValues(tile: Tile, fadeDuration: number): FadeValues {
212+
const now = browser.now();
213+
214+
const timeSinceTile = (now - tile.timeAdded) / fadeDuration;
215+
const tileOpacity = clamp(timeSinceTile, 0, 1);
216+
const fadeMix = {
217+
opacity: tileOpacity,
218+
mix: 0
219+
};
220+
221+
return {tileOpacity, fadeMix};
184222
}

0 commit comments

Comments
 (0)