maplibre-gl-js icon indicating copy to clipboard operation
maplibre-gl-js copied to clipboard

Flickering when zooming with terrain enabled

Open florianf opened this issue 2 years ago • 11 comments

Hi all,

great work on the 3d feature, looks beautiful!

When testing your branch I noticed some flickering when zooming in (using the toursprung/terrain.json style) which is quite noticeable. It seems like semi-transparent polygons are rendered twice and for a few frames and the opacity is therefore doubled. Edit: It only seems to happen on the first zooming of an area, when all tiles are cached it doesn't happen.

Tile in the southeast with wrong opacity: 2200ms

200ms later the tile is rendered correctly: 2400ms

This doesn't happen with terrain disabled, I think it's related to some networking activity (new DEM or image tiles available).

Any idea where that may come from? Happy to help if you can point me in the right direction.

Regards, Florian

Original issue in prozessor13's repo: https://github.com/prozessor13/maplibre-gl-js/issues/60

florianf avatar Mar 07 '22 18:03 florianf

I made some further tests. My initial suspicion was, it's because of the hillshade raster layer, but that's not the case. Confirmed this on another machine as well. Is this reproducible for anybody else?

Zooming in: Screenshot_20220308_125019

First artifacts appear: Screenshot_20220308_125041

Then some more: Screenshot_20220308_125054

When everything is loaded, everything is allright: Screenshot_20220308_125118

Any pointers appreciated!

florianf avatar Mar 08 '22 11:03 florianf

Hi, i think the issue is only with raster-tiles, and in the terrain-testpage the hillshading is done via raster-tiles.

In 2d mode the raster-tiles are rendered on every frame, and between zoomleves and loading the rastertiles will fade into each other, but 3d works completele different. The vectortiles will render into a texture and this texture will draped over the terrain-mesh. During zooming maplibre resample existing rastertiles while loading the new tiles in background. But because of performance the rendert-to-texture is done only when tile-data changes, so in between this time you see the artefacts (e.g. when resampled and already loaded tiles are mixed). Maybe this logic can be optimized, but currently i have no time to focus on that.

prozessor13 avatar Mar 09 '22 10:03 prozessor13

hi, thanks for your reply. My first suspicion also were raster tiles, but take a look at the screenshots from yesterday, this also happens if the raster hillshade layer is switched off and no raster tiles are used.

so in between this time you see the artefacts (e.g. when resampled and already loaded tiles are mixed) Could you point me to the location in the source?

florianf avatar Mar 09 '22 10:03 florianf

OK, you are right, i will have a look onto it!

prozessor13 avatar Mar 09 '22 11:03 prozessor13

After some more digging, I can reproduce the overlapping rendering now in a simple example with only the natural-wood layer. It seems, that in higher zoom levels there are overlapping tiles. Like here, it seems there is one parent 13 zoom tile, covered with two child tiles in 14, but not all four:

Screenshot_20220318_214019

florianf avatar Mar 18 '22 20:03 florianf

Ok thanks, i think i know now where to start!

I think the problem is when filling _coordsDescendingInv. May after the fill process (below line 56) we have to check that there are no parent-tiles in the to-render-tiles array.

prozessor13 avatar Mar 30 '22 10:03 prozessor13

You're right, there are loads of parents in the to-render array. That's clear, because some low zoom tiles (f.ex. 5) are always visible. But the problem really only shows on zooming in, until all requested tiles are loaded. A simple hacky fix reduces the flickering to almost not noticeable. I added this in the inner loop in _init

                let parentPresent = false
                for (const parentTileID of tileIDs) {
                    if (parentTileID != tileID && tileID.isChildOf(parentTileID) && (tileID.overscaledZ - parentTileID.overscaledZ) < 3) {
                        parentPresent = true
                    }
                }
                
                if (parentPresent) {
                    continue;
                }

The tile isn't rendered, if there is a parent or grandparent tile visible. I don't get why the double rendering isn't notice all the time, because tiles are rendered on top of another all the time?

florianf avatar Apr 29 '22 20:04 florianf

Thx for your effort! I shortly digged into this but currently i do not have a correct answer for the issue. As soon as i have time, i will look again onto this issue.

prozessor13 avatar May 07 '22 08:05 prozessor13

@prozessor13 I finally found the issue, took me a couple of hours. Based on your #1651 PR:

The problem is in the renderLayer methode in render_to_texture.ts. When a zoom event occurs and not all tiles are completely loaded, the coords array in the layer loop contains overlapping tiles. Link to source: https://github.com/prozessor13/maplibre-gl-js/blob/c82c13eac651bf96f23e44985bbf62326fc93ad2/src/render/render_to_texture.ts#L247

grafik

My suggested fix would be, remove the overlapping tiles for fill layers. Lower zoom levels should be always rendered first, because of ordering in coords array.

                for (let l = 0; l < layers.length; l++) {
                    const layer = painter.style._layers[layers[l]];
                    let coords = layer.source ? this._coordsDescendingInv[layer.source][tile.tileID.key] : [tile.tileID];
                    if (coords && coords.length > 1 && layer.type == "fill") {
                        let parentPresent = false
                        let nonOverlappingCoords = []
                        for (const tileID of coords) {
                            for (const parentTileID of coords) {
                                if (parentTileID != tileID && tileID.isChildOf(parentTileID)) {
                                    parentPresent = true
                                    break;
                                }
                            }
                            if (!parentPresent) {
                                nonOverlappingCoords[nonOverlappingCoords.length] = tileID
                            }
                        }
                        coords = nonOverlappingCoords
                    }
                    painter._renderTileClippingMasks(layer, coords);
                    painter.renderLayer(painter, painter.style.sourceCaches[layer.source], layer, coords);
                    if (layer.source) tile.rttCoords[layer.source] = this._coordsDescendingInvStr[layer.source][tile.tileID.key];
                }

What do you think?

florianf avatar Sep 19 '22 13:09 florianf

Hi, thanks very much for digging into this. Your approach is one way, but i think the problem can be solved otherwise. In 2d Maplibre has a stencil logic to render on a pixel only the data of the tile with the highest zoomlevel. But currently in terrain-mode the stencil is not handled correct, and so this artefacts accours.

I played also with a solution you did, but run into problems when sources has no global coverage.

So i created another branch for this issue https://github.com/prozessor13/maplibre-gl-js/tree/rtt_stencil . Hopefully with this logic your problem dissapears as well, because currently i only tested with raster and hillshade layers. It would be great, if you can test this branch.

prozessor13 avatar Sep 19 '22 13:09 prozessor13

I tested your rtt_stencil branch an it works great! It also fixes another related issue where tile borders (overdraw) where overlapping when zooming out. Really fantastic!

florianf avatar Sep 19 '22 15:09 florianf

@florianf I've assigned a bounty for adding tests to the #1672 in #1540. Feel free to add some test so the PR can be merged.

HarelM avatar Mar 04 '23 19:03 HarelM

No need to assign a bounty here since it was assigned in #1540. Link to parent Bounty: https://github.com/maplibre/maplibre/issues/189

HarelM avatar Mar 04 '23 19:03 HarelM