diff --git a/src/geo/projection/globe.ts b/src/geo/projection/globe.ts index 26e6e2ef1c..27d986b4aa 100644 --- a/src/geo/projection/globe.ts +++ b/src/geo/projection/globe.ts @@ -10,7 +10,7 @@ import {easeCubicInOut, lerp} from '../../util/util'; import {mercatorYfromLat} from '../mercator_coordinate'; import {NORTH_POLE_Y, SOUTH_POLE_Y} from '../../render/subdivision'; import {SubdivisionGranularityExpression, SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings'; -import type {Projection, ProjectionGPUContext} from './projection'; +import type {Projection, ProjectionGPUContext, TileMeshUsage} from './projection'; import {PreparedShader, shaders} from '../../shaders/shaders'; import {MercatorProjection} from './mercator'; import {ProjectionErrorMeasurement} from './globe_projection_error_measurement'; @@ -35,7 +35,9 @@ const granularitySettingsGlobe: SubdivisionGranularitySetting = new SubdivisionG // otherwise they will be visibly warped at high zooms (before mercator transition). // This si not needed on fill, because fill geometry tends to already be // highly tessellated and granular at high zooms. - tile: new SubdivisionGranularityExpression(128, 16), + // Minimal granularity of 8 seems to be enough to avoid warped raster tiles, while also minimizing triangle count. + tile: new SubdivisionGranularityExpression(128, 32), + stencil: new SubdivisionGranularityExpression(128, 4), circle: 3 }); @@ -172,9 +174,10 @@ export class GlobeProjection implements Projection { return `${granularity.toString(36)}_${border ? 'b' : ''}${north ? 'n' : ''}${south ? 's' : ''}`; } - public getMeshFromTileID(context: Context, canonical: CanonicalTileID, hasBorder: boolean, allowPoles: boolean): Mesh { + public getMeshFromTileID(context: Context, canonical: CanonicalTileID, hasBorder: boolean, allowPoles: boolean, usage: TileMeshUsage): Mesh { // Stencil granularity must match fill granularity - const granularity = granularitySettingsGlobe.fill.getGranularityForZoomLevel(canonical.z); + const granularityConfig = usage === 'stencil' ? granularitySettingsGlobe.stencil : granularitySettingsGlobe.tile; + const granularity = granularityConfig.getGranularityForZoomLevel(canonical.z); const north = (canonical.y === 0) && allowPoles; const south = (canonical.y === (1 << canonical.z) - 1) && allowPoles; return this._getMesh(context, granularity, hasBorder, north, south); @@ -208,12 +211,16 @@ export class GlobeProjection implements Projection { const quadsPerAxisX = granularity + (border ? 2 : 0); // two extra quads for border const quadsPerAxisY = granularity + ((north || border) ? 1 : 0) + (south || border ? 1 : 0); const verticesPerAxisX = quadsPerAxisX + 1; // one more vertex than quads - //const verticesPerAxisY = quadsPerAxisY + 1; // one more vertex than quads + const verticesPerAxisY = quadsPerAxisY + 1; // one more vertex than quads const offsetX = border ? -1 : 0; const offsetY = (border || north) ? -1 : 0; const endX = granularity + (border ? 1 : 0); const endY = granularity + ((border || south) ? 1 : 0); + if (verticesPerAxisX * verticesPerAxisY > (1 << 16)) { + throw new Error('Granularity is too large and meshes would not fit inside 16 bit vertex indices.'); + } + const northY = NORTH_POLE_Y; const southY = SOUTH_POLE_Y; diff --git a/src/geo/projection/globe_transform.ts b/src/geo/projection/globe_transform.ts index 856b0b2646..a0ee9f44d7 100644 --- a/src/geo/projection/globe_transform.ts +++ b/src/geo/projection/globe_transform.ts @@ -370,6 +370,7 @@ export class GlobeTransform implements ITransform { type: 'projectiontransition', newProjection: this._globeRendering ? 'globe' : 'globe-mercator', }, + forceSourceUpdate: true, }; } } diff --git a/src/geo/projection/mercator.ts b/src/geo/projection/mercator.ts index 557d978535..d9d4d9ac07 100644 --- a/src/geo/projection/mercator.ts +++ b/src/geo/projection/mercator.ts @@ -1,4 +1,4 @@ -import type {Projection, ProjectionGPUContext} from './projection'; +import type {Projection, ProjectionGPUContext, TileMeshUsage} from './projection'; import type {CanonicalTileID} from '../../source/tile_id'; import {EXTENT} from '../../data/extent'; import {PreparedShader, shaders} from '../../shaders/shaders'; @@ -61,7 +61,7 @@ export class MercatorProjection implements Projection { // Do nothing. } - public getMeshFromTileID(context: Context, _tileID: CanonicalTileID, _hasBorder: boolean, _allowPoles: boolean): Mesh { + public getMeshFromTileID(context: Context, _tileID: CanonicalTileID, _hasBorder: boolean, _allowPoles: boolean, _usage: TileMeshUsage): Mesh { if (this._cachedMesh) { return this._cachedMesh; } diff --git a/src/geo/projection/projection.ts b/src/geo/projection/projection.ts index 49833c2c05..b4e83aeca4 100644 --- a/src/geo/projection/projection.ts +++ b/src/geo/projection/projection.ts @@ -30,6 +30,14 @@ export type ProjectionGPUContext = { useProgram: (name: string) => Program; }; +/** + * @internal + * Specifies the usage for a square tile mesh: + * - 'stencil' for drawing stencil masks + * - 'raster' for drawing raster tiles, hillshade, etc. + */ +export type TileMeshUsage = 'stencil' | 'raster'; + /** * An interface the implementations of which are used internally by MapLibre to handle different projections. */ @@ -107,6 +115,7 @@ export interface Projection { * @param tileID - The tile coordinates for which to return a mesh. Meshes for tiles that border the top/bottom mercator edge might include extra geometry for the north/south pole. * @param hasBorder - When true, the mesh will also include a small border beyond the 0..EXTENT range. * @param allowPoles - When true, the mesh will also include geometry to cover the north (south) pole, if the given tileID borders the mercator range's top (bottom) edge. + * @param usage - Specify the usage of the tile mesh, as different usages might use different levels of subdivision. */ - getMeshFromTileID(context: Context, tileID: CanonicalTileID, hasBorder: boolean, allowPoles: boolean): Mesh; + getMeshFromTileID(context: Context, tileID: CanonicalTileID, hasBorder: boolean, allowPoles: boolean, usage: TileMeshUsage): Mesh; } diff --git a/src/geo/transform_interface.ts b/src/geo/transform_interface.ts index e74e17c8a4..a558f79f00 100644 --- a/src/geo/transform_interface.ts +++ b/src/geo/transform_interface.ts @@ -50,6 +50,7 @@ export type CoveringTilesOptions = CoveringZoomOptions & { export type TransformUpdateResult = { forcePlacementUpdate?: boolean; fireProjectionEvent?: MapProjectionEvent; + forceSourceUpdate?: boolean; }; export interface ITransformGetters { diff --git a/src/render/draw_background.ts b/src/render/draw_background.ts index 08da0b7119..1b0182b1af 100644 --- a/src/render/draw_background.ts +++ b/src/render/draw_background.ts @@ -58,7 +58,7 @@ export function drawBackground(painter: Painter, sourceCache: SourceCache, layer // and also enable stencil clipping. Make sure to render a proper tile clipping mask into stencil // first though, as that doesn't seem to happen for background layers as of writing this. - const mesh = projection.getMeshFromTileID(context, tileID.canonical, false, true); + const mesh = projection.getMeshFromTileID(context, tileID.canonical, false, true, 'raster'); program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); diff --git a/src/render/draw_hillshade.ts b/src/render/draw_hillshade.ts index a27de8400b..1b6412ca03 100644 --- a/src/render/draw_hillshade.ts +++ b/src/render/draw_hillshade.ts @@ -66,7 +66,7 @@ function renderHillshade( if (!fbo) { continue; } - const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, true); + const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, true, 'raster'); const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord); diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts index 64f8249d2d..d505935b1e 100644 --- a/src/render/draw_raster.ts +++ b/src/render/draw_raster.ts @@ -123,7 +123,7 @@ function drawTiles( const projectionData = transform.getProjectionData(coord, align); const uniformValues = rasterUniformValues(parentTL || [0, 0], parentScaleBy || 1, fade, layer, corners); - const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles); + const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles, 'raster'); const stencilMode = stencilModes ? stencilModes[coord.overscaledZ] : StencilMode.disabled; diff --git a/src/render/painter.ts b/src/render/painter.ts index b363a0adcb..00705eaf8f 100644 --- a/src/render/painter.ts +++ b/src/render/painter.ts @@ -295,7 +295,7 @@ export class Painter { const stencilRef = tileStencilRefs[tileID.key]; const terrainData = this.style.map.terrain && this.style.map.terrain.getTerrainData(tileID); - const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, useBorders, true); + const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, useBorders, true, 'stencil'); const projectionData = transform.getProjectionData(tileID); diff --git a/src/render/subdivision_granularity_settings.ts b/src/render/subdivision_granularity_settings.ts index f1581333f3..e9e9627047 100644 --- a/src/render/subdivision_granularity_settings.ts +++ b/src/render/subdivision_granularity_settings.ts @@ -25,6 +25,10 @@ export class SubdivisionGranularityExpression { private readonly _minGranularity: number; constructor(baseZoomGranularity: number, minGranularity: number) { + if (minGranularity > baseZoomGranularity) { + throw new Error('Min granularity must not be greater than base granularity.'); + } + this._baseZoomGranularity = baseZoomGranularity; this._minGranularity = minGranularity; } @@ -50,10 +54,15 @@ export class SubdivisionGranularitySetting { public readonly line: SubdivisionGranularityExpression; /** - * Granularity used for geometry covering the entire tile: stencil masks, raster tiles, etc. + * Granularity used for geometry covering the entire tile: raster tiles, etc. */ public readonly tile: SubdivisionGranularityExpression; + /** + * Granularity used for stencil masks for tiles. + */ + public readonly stencil: SubdivisionGranularityExpression; + /** * Controls the granularity of `pitch-alignment: map` circles and heatmap kernels. * More granular circles will more closely follow the map's surface. @@ -73,6 +82,10 @@ export class SubdivisionGranularitySetting { * Granularity used for geometry covering the entire tile: stencil masks, raster tiles, etc. */ tile: SubdivisionGranularityExpression; + /** + * Granularity used for stencil masks for tiles. + */ + stencil: SubdivisionGranularityExpression; /** * Controls the granularity of `pitch-alignment: map` circles and heatmap kernels. * More granular circles will more closely follow the map's surface. @@ -82,6 +95,7 @@ export class SubdivisionGranularitySetting { this.fill = options.fill; this.line = options.line; this.tile = options.tile; + this.stencil = options.stencil; this.circle = options.circle; } @@ -92,6 +106,7 @@ export class SubdivisionGranularitySetting { fill: new SubdivisionGranularityExpression(0, 0), line: new SubdivisionGranularityExpression(0, 0), tile: new SubdivisionGranularityExpression(0, 0), + stencil: new SubdivisionGranularityExpression(0, 0), circle: 1 }); } diff --git a/src/ui/map.ts b/src/ui/map.ts index 5a6ebe49a9..b6a7c6f3c6 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -3092,10 +3092,12 @@ export class Map extends Camera { this.style.update(parameters); } + const transformUpdateResult = this.transform.newFrameUpdate(); + // If we are in _render for any reason other than an in-progress paint // transition, update source caches to check for and load any tiles we // need for the current transform - if (this.style && this._sourcesDirty) { + if (this.style && (this._sourcesDirty || transformUpdateResult.forceSourceUpdate)) { this._sourcesDirty = false; this.style._updateSources(this.transform); } @@ -3112,7 +3114,6 @@ export class Map extends Camera { this.transform.setElevation(0); } - const transformUpdateResult = this.transform.newFrameUpdate(); this._placementDirty = this.style && this.style._updatePlacement(this.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions, transformUpdateResult.forcePlacementUpdate); if (transformUpdateResult.fireProjectionEvent) { diff --git a/src/util/util.ts b/src/util/util.ts index a2a1781fb4..ab0a4dc019 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -62,7 +62,7 @@ export function translatePosition( /** * Returns the signed distance between a point and a plane. * @param plane - The plane equation, in the form where the first three components are the normal and the fourth component is the plane's distance from origin along normal. - * @param point - The point whose distance from plane is retunred. + * @param point - The point whose distance from plane is returned. * @returns Signed distance of the point from the plane. Positive distances are in the half space where the plane normal points to, negative otherwise. */ export function pointPlaneSignedDistance( diff --git a/test/build/min.test.ts b/test/build/min.test.ts index d49adac8a0..df896e8f94 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -36,7 +36,7 @@ describe('test min build', () => { const decreaseQuota = 4096; // feel free to update this value after you've checked that it has changed on purpose :-) - const expectedBytes = 871322; + const expectedBytes = 871711; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota); diff --git a/test/examples/globe-fill-extrusion.html b/test/examples/globe-fill-extrusion.html index 551d848746..454094b3f9 100644 --- a/test/examples/globe-fill-extrusion.html +++ b/test/examples/globe-fill-extrusion.html @@ -17,12 +17,19 @@ diff --git a/test/examples/globe-zoom-planet-size-function.html b/test/examples/globe-zoom-planet-size-function.html index 505b5373c8..1c0b04f199 100644 --- a/test/examples/globe-zoom-planet-size-function.html +++ b/test/examples/globe-zoom-planet-size-function.html @@ -35,47 +35,52 @@
diff --git a/test/integration/assets/tiles/checkerboard.png b/test/integration/assets/tiles/checkerboard.png new file mode 100644 index 0000000000..1ee3721ff7 Binary files /dev/null and b/test/integration/assets/tiles/checkerboard.png differ diff --git a/test/integration/render/tests/projection/globe/fill-seams/ocean/expected2.png b/test/integration/render/tests/projection/globe/fill-seams/ocean/expected2.png new file mode 100644 index 0000000000..894dac2a1e Binary files /dev/null and b/test/integration/render/tests/projection/globe/fill-seams/ocean/expected2.png differ diff --git a/test/integration/render/tests/projection/globe/raster-pole/expected-win-flaky.png b/test/integration/render/tests/projection/globe/raster-pole/expected-win-flaky.png new file mode 100644 index 0000000000..b7eaa69d5f Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-pole/expected-win-flaky.png differ diff --git a/test/integration/render/tests/projection/globe/raster-warped/expected-debian.png b/test/integration/render/tests/projection/globe/raster-warped/expected-debian.png new file mode 100644 index 0000000000..7215416c0a Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-warped/expected-debian.png differ diff --git a/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky.png b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky.png new file mode 100644 index 0000000000..e607782220 Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky.png differ diff --git a/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky2.png b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky2.png new file mode 100644 index 0000000000..8751c88180 Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky2.png differ diff --git a/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky3.png b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky3.png new file mode 100644 index 0000000000..2bb5d66df5 Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky3.png differ diff --git a/test/integration/render/tests/projection/globe/raster-warped/expected.png b/test/integration/render/tests/projection/globe/raster-warped/expected.png new file mode 100644 index 0000000000..d7f679d53f Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-warped/expected.png differ diff --git a/test/integration/render/tests/projection/globe/raster-warped/style.json b/test/integration/render/tests/projection/globe/raster-warped/style.json new file mode 100644 index 0000000000..b1be886e82 --- /dev/null +++ b/test/integration/render/tests/projection/globe/raster-warped/style.json @@ -0,0 +1,47 @@ +{ + "version": 8, + "metadata": { + "test": { + "description": "Tests that raster tiles are not warped under globe projection. Exact way pixels get rasterized doesn't matter, but the pattern should not be tilted." + } + }, + "sky": { + "atmosphere-blend": 0.0 + }, + "center": [ + -178.59375, + 84.92832092949962 + ], + "zoom": 8, + "projection": { + "type": "globe" + }, + "sources": { + "source": { + "type": "raster", + "tiles": [ + "local://tiles/checkerboard.png" + ], + "minzoom": 8, + "maxzoom": 8, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "red" + } + }, + { + "id": "raster", + "type": "raster", + "source": "source", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file