diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ef0f80f4e..56afb213d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Fixes cooperative gestures displaying the mobile help text when screen width is smaller than 480px on non-touch devices ([#5053](https://github.com/maplibre/maplibre-gl-js/pull/5053)) - Fixes incorrect cluster radius scaling in `GeoJSONSource.setClusterOptions()` ([#5055](https://github.com/maplibre/maplibre-gl-js/pull/5055)) - Improve innerHTML handling in code ([#5057](https://github.com/maplibre/maplibre-gl-js/pull/5057))) +- Fix geometry beyond tile borders being rendered ([#4868](https://github.com/maplibre/maplibre-gl-js/pull/4868)) - _...Add new stuff here..._ ## 5.0.0-pre.6 diff --git a/src/geo/projection/globe.ts b/src/geo/projection/globe.ts index 58f3fcd889..27020c8d97 100644 --- a/src/geo/projection/globe.ts +++ b/src/geo/projection/globe.ts @@ -18,15 +18,17 @@ export const globeConstants = { }; const granularitySettingsGlobe: SubdivisionGranularitySetting = new SubdivisionGranularitySetting({ - fill: new SubdivisionGranularityExpression(128, 1), - line: new SubdivisionGranularityExpression(512, 1), + fill: new SubdivisionGranularityExpression(128, 2), + line: new SubdivisionGranularityExpression(512, 0), // Always keep at least some subdivision on raster tiles, etc, // 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. - // 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), + // Stencil granularity must never be higher than fill granularity, + // otherwise we would get seams in the oceans at zoom levels where + // stencil has higher granularity than fill. + stencil: new SubdivisionGranularityExpression(128, 1), circle: 3 }); diff --git a/src/render/draw_fill.ts b/src/render/draw_fill.ts index aa1c2f87a1..9cd030a49d 100644 --- a/src/render/draw_fill.ts +++ b/src/render/draw_fill.ts @@ -15,8 +15,8 @@ import type {FillStyleLayer} from '../style/style_layer/fill_style_layer'; import type {FillBucket} from '../data/bucket/fill_bucket'; import type {OverscaledTileID} from '../source/tile_id'; import {updatePatternPositionsInProgram} from './update_pattern_positions_in_program'; -import {StencilMode} from '../gl/stencil_mode'; import {translatePosition} from '../util/util'; +import {StencilMode} from '../gl/stencil_mode'; export function drawFill(painter: Painter, sourceCache: SourceCache, layer: FillStyleLayer, coords: Array, renderOptions: RenderOptions) { const color = layer.paint.get('fill-color'); @@ -129,36 +129,12 @@ function drawFillTiles( fillOutlineUniformValues(drawingBufferSize, translateForUniforms); } - // Stencil is not really needed for anything unless we are drawing transparent things. - // - // For translucent layers, we must draw any pixel of a given layer at most once, - // otherwise we might get artifacts from the transparent geometry being drawn twice over itself, - // which can happen due to tiles having a slight overlapping border into neighboring tiles. - // Hence we use stencil tile masks for any translucent pass, including for fill. - // - // Globe rendering relies on these tile borders to hide tile seams, since under globe projection - // tiles are not squares, but slightly curved squares. At high zoom levels, the tile stencil mask - // is approximated by a square, but if the tile contains fine geometry, it might still get projected - // into a curved shape, causing a mismatch with the stencil mask, which is very visible - // if the tile border is small. - // - // The simples workaround for this is to just disable stencil masking for opaque fill layers, - // since the fine geometry will always line up perfectly with the geometry in its neighboring tiles, - // even if the border is small. Disabling stencil ensures the neighboring geometry isn't clipped. - // - // This doesn't seem to be an issue for transparent fill layers (or they don't get used enough to be noticeable), - // which is a good thing, since there is no easy solution for this problem for transparency, other than - // greatly increasing subdivision granularity for both fill layers and stencil masks, at least at tile edges. let stencil: StencilMode; - if (painter.renderPass === 'translucent') { - if (isRenderingToTexture) { - const [stencilModes] = painter.stencilConfigForOverlap(coords); - stencil = stencilModes[coord.overscaledZ]; - } else { - stencil = painter.stencilModeForClipping(coord); - } + if (painter.renderPass === 'translucent' && isRenderingToTexture) { + const [stencilModes] = painter.stencilConfigForOverlap(coords); + stencil = stencilModes[coord.overscaledZ]; } else { - stencil = StencilMode.disabled; + stencil = painter.stencilModeForClipping(coord); } program.draw(painter.context, drawMode, depthMode, diff --git a/src/shaders/_projection_globe.vertex.glsl b/src/shaders/_projection_globe.vertex.glsl index 77f3a77dc8..010d3569e1 100644 --- a/src/shaders/_projection_globe.vertex.glsl +++ b/src/shaders/_projection_globe.vertex.glsl @@ -44,7 +44,7 @@ float projectLineThickness(float tileY) { } // get position inside the tile in range 0..8192 and project it onto the surface of a unit sphere -vec3 projectToSphere(vec2 posInTile) { +vec3 projectToSphere(vec2 posInTile, vec2 rawPos) { // Compute position in range 0..1 of the base tile of web mercator vec2 mercator_pos = u_projection_tile_mercator_coords.xy + u_projection_tile_mercator_coords.zw * posInTile; @@ -61,17 +61,21 @@ vec3 projectToSphere(vec2 posInTile) { ); // North pole - if (posInTile.y < -32767.5) { + if (rawPos.y < -32767.5) { pos = vec3(0.0, 1.0, 0.0); } // South pole - if (posInTile.y > 32766.5) { + if (rawPos.y > 32766.5) { pos = vec3(0.0, -1.0, 0.0); } return pos; } +vec3 projectToSphere(vec2 posInTile) { + return projectToSphere(posInTile, vec2(0.0, 0.0)); +} + float globeComputeClippingZ(vec3 spherePos) { return (1.0 - (dot(spherePos, u_projection_clipping_plane.xyz) + u_projection_clipping_plane.w)); } @@ -117,6 +121,11 @@ vec4 projectTile(vec2 posInTile) { return interpolateProjection(posInTile, projectToSphere(posInTile), 0.0); } +// A variant that supports special pole and planet center vertices. +vec4 projectTile(vec2 posInTile, vec2 rawPos) { + return interpolateProjection(posInTile, projectToSphere(posInTile, rawPos), 0.0); +} + // Uses elevation to compute final screenspace projection // and **replaces Z** with a custom value that clips geometry // on the backfacing side of the planet. diff --git a/src/shaders/_projection_mercator.vertex.glsl b/src/shaders/_projection_mercator.vertex.glsl index bf216d3556..adc3173efe 100644 --- a/src/shaders/_projection_mercator.vertex.glsl +++ b/src/shaders/_projection_mercator.vertex.glsl @@ -8,10 +8,16 @@ float projectCircleRadius(float tileY) { // Projects a point in tile-local coordinates (usually 0..EXTENT) to screen. vec4 projectTile(vec2 p) { + vec4 result = u_projection_matrix * vec4(p, 0.0, 1.0); + return result; +} + +// Projects a point in tile-local coordinates (usually 0..EXTENT) to screen, and handle special pole or planet center vertices. +vec4 projectTile(vec2 p, vec2 rawPos) { // Kill pole vertices and triangles by placing the pole vertex so far in Z that // the clipping hardware kills the entire triangle. vec4 result = u_projection_matrix * vec4(p, 0.0, 1.0); - if (p.y < -32767.5 || p.y > 32766.5) { + if (rawPos.y < -32767.5 || rawPos.y > 32766.5) { result.z = -10000000.0; } return result; diff --git a/src/shaders/fill.vertex.glsl b/src/shaders/fill.vertex.glsl index d4dbddcd4c..f930f84ef1 100644 --- a/src/shaders/fill.vertex.glsl +++ b/src/shaders/fill.vertex.glsl @@ -9,5 +9,5 @@ void main() { #pragma mapbox: initialize highp vec4 color #pragma mapbox: initialize lowp float opacity - gl_Position = projectTile(a_pos + u_fill_translate); + gl_Position = projectTile(a_pos + u_fill_translate, a_pos); } diff --git a/src/shaders/fill_extrusion.vertex.glsl b/src/shaders/fill_extrusion.vertex.glsl index 8726b5dfb3..67d3cd7775 100644 --- a/src/shaders/fill_extrusion.vertex.glsl +++ b/src/shaders/fill_extrusion.vertex.glsl @@ -49,7 +49,7 @@ void main() { vec2 posInTile = a_pos + u_fill_translate; #ifdef GLOBE - vec3 spherePos = projectToSphere(posInTile); + vec3 spherePos = projectToSphere(posInTile, a_pos); gl_Position = interpolateProjectionFor3D(posInTile, spherePos, elevation); #else gl_Position = u_projection_matrix * vec4(posInTile, elevation, 1.0); diff --git a/src/shaders/fill_extrusion_pattern.vertex.glsl b/src/shaders/fill_extrusion_pattern.vertex.glsl index 30e485fde5..48c1bfae09 100644 --- a/src/shaders/fill_extrusion_pattern.vertex.glsl +++ b/src/shaders/fill_extrusion_pattern.vertex.glsl @@ -77,7 +77,7 @@ void main() { vec2 posInTile = a_pos + u_fill_translate; #ifdef GLOBE - vec3 spherePos = projectToSphere(posInTile); + vec3 spherePos = projectToSphere(posInTile, a_pos); vec3 elevatedPos = spherePos * (1.0 + elevation / GLOBE_RADIUS); v_sphere_pos = elevatedPos; gl_Position = interpolateProjectionFor3D(posInTile, spherePos, elevation); diff --git a/src/shaders/fill_outline.vertex.glsl b/src/shaders/fill_outline.vertex.glsl index ea38c928ea..5c704f27e3 100644 --- a/src/shaders/fill_outline.vertex.glsl +++ b/src/shaders/fill_outline.vertex.glsl @@ -15,7 +15,7 @@ void main() { #pragma mapbox: initialize highp vec4 outline_color #pragma mapbox: initialize lowp float opacity - gl_Position = projectTile(a_pos + u_fill_translate); + gl_Position = projectTile(a_pos + u_fill_translate, a_pos); v_pos = (gl_Position.xy / gl_Position.w + 1.0) / 2.0 * u_world; #ifdef GLOBE diff --git a/src/shaders/fill_outline_pattern.vertex.glsl b/src/shaders/fill_outline_pattern.vertex.glsl index c1b49bc27c..e29d3f95c5 100644 --- a/src/shaders/fill_outline_pattern.vertex.glsl +++ b/src/shaders/fill_outline_pattern.vertex.glsl @@ -35,7 +35,7 @@ void main() { float fromScale = u_scale.y; float toScale = u_scale.z; - gl_Position = projectTile(a_pos + u_fill_translate); + gl_Position = projectTile(a_pos + u_fill_translate, a_pos); vec2 display_size_a = (pattern_br_a - pattern_tl_a) / pixel_ratio_from; vec2 display_size_b = (pattern_br_b - pattern_tl_b) / pixel_ratio_to; diff --git a/src/shaders/fill_pattern.vertex.glsl b/src/shaders/fill_pattern.vertex.glsl index 37470b9ece..f3703212a2 100644 --- a/src/shaders/fill_pattern.vertex.glsl +++ b/src/shaders/fill_pattern.vertex.glsl @@ -33,7 +33,7 @@ void main() { vec2 display_size_a = (pattern_br_a - pattern_tl_a) / pixel_ratio_from; vec2 display_size_b = (pattern_br_b - pattern_tl_b) / pixel_ratio_to; - gl_Position = projectTile(a_pos + u_fill_translate); + gl_Position = projectTile(a_pos + u_fill_translate, a_pos); v_pos_a = get_pattern_pos(u_pixel_coord_upper, u_pixel_coord_lower, fromScale * display_size_a, tileZoomRatio, a_pos); v_pos_b = get_pattern_pos(u_pixel_coord_upper, u_pixel_coord_lower, toScale * display_size_b, tileZoomRatio, a_pos); diff --git a/src/shaders/hillshade.vertex.glsl b/src/shaders/hillshade.vertex.glsl index e39f92cd83..5a0b2712a8 100644 --- a/src/shaders/hillshade.vertex.glsl +++ b/src/shaders/hillshade.vertex.glsl @@ -5,7 +5,7 @@ in vec2 a_pos; out vec2 v_pos; void main() { - gl_Position = projectTile(a_pos); + gl_Position = projectTile(a_pos, a_pos); v_pos = a_pos / 8192.0; // North pole if (a_pos.y < -32767.5) { diff --git a/src/shaders/raster.vertex.glsl b/src/shaders/raster.vertex.glsl index 6f02159723..2995c727d6 100644 --- a/src/shaders/raster.vertex.glsl +++ b/src/shaders/raster.vertex.glsl @@ -14,14 +14,14 @@ void main() { // Interpolate the actual desired coordinates to get the final position. vec2 fractionalPos = a_pos / 8192.0; vec2 position = mix(mix(u_coords_top.xy, u_coords_top.zw, fractionalPos.x), mix(u_coords_bottom.xy, u_coords_bottom.zw, fractionalPos.x), fractionalPos.y); - gl_Position = projectTile(position); + gl_Position = projectTile(position, position); // We are using Int16 for texture position coordinates to give us enough precision for // fractional coordinates. We use 8192 to scale the texture coordinates in the buffer // as an arbitrarily high number to preserve adequate precision when rendering. // This is also the same value as the EXTENT we are using for our tile buffer pos coordinates, // so math for modifying either is consistent. - v_pos0 = ((fractionalPos - 0.5) / u_buffer_scale ) + 0.5; + v_pos0 = ((fractionalPos - 0.5) / u_buffer_scale) + 0.5; // When globe rendering is enabled, pole vertices need special handling to get nice texture coordinates. #ifdef GLOBE diff --git a/test/build/min.test.ts b/test/build/min.test.ts index a4e493bd37..b9da7aa9b9 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -37,7 +37,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 = 897777; + const expectedBytes = 898319; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota); diff --git a/test/integration/assets/tiles/outside_ring.mvt b/test/integration/assets/tiles/outside_ring.mvt new file mode 100644 index 0000000000..cf5e7bf27a Binary files /dev/null and b/test/integration/assets/tiles/outside_ring.mvt differ diff --git a/test/integration/render/tests/fill-outside-tile/expected.png b/test/integration/render/tests/fill-outside-tile/expected.png new file mode 100644 index 0000000000..b6b5a85783 Binary files /dev/null and b/test/integration/render/tests/fill-outside-tile/expected.png differ diff --git a/test/integration/render/tests/fill-outside-tile/style.json b/test/integration/render/tests/fill-outside-tile/style.json new file mode 100644 index 0000000000..afe5248c90 --- /dev/null +++ b/test/integration/render/tests/fill-outside-tile/style.json @@ -0,0 +1,37 @@ +{ + "version": 8, + "metadata": { + "test": { + "description": "Tests that tiles are properly clipped by drawing a tile that has all its geometry outside its visible area. See issue #4803. The expected (correct) result is that nothing is drawn.", + "width": 64, + "height": 64 + } + }, + "zoom": 2.125, + "sources": { + "vector_tiles": { + "type": "vector", + "tiles": [ + "local://tiles/outside_ring.mvt" + ] + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "ocean", + "type": "fill", + "source": "vector_tiles", + "source-layer": "water", + "paint": { + "fill-color": "black" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/projection/globe/fill-planet-pole/expected_flaky.png b/test/integration/render/tests/projection/globe/fill-planet-pole/expected_flaky.png new file mode 100644 index 0000000000..772c2c50fe Binary files /dev/null and b/test/integration/render/tests/projection/globe/fill-planet-pole/expected_flaky.png differ diff --git a/test/integration/render/tests/projection/globe/fill-seams/checkerboard/expected-win2.png b/test/integration/render/tests/projection/globe/fill-seams/checkerboard/expected-win2.png new file mode 100644 index 0000000000..e7d76fdf2b Binary files /dev/null and b/test/integration/render/tests/projection/globe/fill-seams/checkerboard/expected-win2.png differ diff --git a/test/integration/render/tests/projection/globe/raster-pole/expected-win2.png b/test/integration/render/tests/projection/globe/raster-pole/expected-win2.png new file mode 100644 index 0000000000..2be3a5c329 Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-pole/expected-win2.png differ diff --git a/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky8.png b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky8.png new file mode 100644 index 0000000000..2b0bb55e2a Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky8.png differ diff --git a/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky9.png b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky9.png new file mode 100644 index 0000000000..da2718dc1d Binary files /dev/null and b/test/integration/render/tests/projection/globe/raster-warped/expected-win-flaky9.png differ diff --git a/test/integration/render/tests/projection/globe/terrain/fill-planet-pole/expected-windows-flaky.png b/test/integration/render/tests/projection/globe/terrain/fill-planet-pole/expected-windows-flaky.png new file mode 100644 index 0000000000..f7f9ab95b5 Binary files /dev/null and b/test/integration/render/tests/projection/globe/terrain/fill-planet-pole/expected-windows-flaky.png differ diff --git a/test/integration/render/tests/symbol-visibility/visible/expected-win.png b/test/integration/render/tests/symbol-visibility/visible/expected-win.png new file mode 100644 index 0000000000..61a08a23e4 Binary files /dev/null and b/test/integration/render/tests/symbol-visibility/visible/expected-win.png differ