mapbox / mapbox-unity-sdk

Mapbox Unity SDK - https://www.mapbox.com/unity/
Other
725 stars 210 forks source link

Terrain interpolation seems to cause jittery / step-function-like artifacts #1848

Closed artoonie closed 2 years ago

artoonie commented 2 years ago

I'm not sure if this is expected or not. The terrain data at the source seems to be interpolated with a nearest neighbors at the source, creating these step artifacts if the tile sizes are too large. My tiles are 500x500, and creating a 100x100 grid for my terrain gives artifacts like the image below. Is the heightmap for each tile a lower resolution than the tile itself?

The problem seems to go away with 50x50 grids, and it in-between the problem is mitigated, so it seems directly related to grid size.

Any other explanation, and especially workarounds, to get smooth terrain?

Note: If this is a bug or support ticket, please provide the following information:

artoonie commented 2 years ago

Here's a better example, created using a high exaggeration factor: image

brnkhy commented 2 years ago

Hey @artoonie I think elevations queries don't even use interpolation at all but still I don't think you should see such a dramatic effect on sample count = 100. Elevation maps are 256x256 and no matter how big the tile is, there'll be 100x100 queries with those settings, which means using every 2.56th pixel on the sheet. Hmm am I missing something here?

It might be a bug in code or maybe... is this happening everywhere or in a certain region? it's possible the elevation map isn't high enough resolution in that region as well.

artoonie commented 2 years ago

Hi @brnkhy,

I think I've found the issue. I see here: https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-rgb-v1/ That terrain data is only available up to zoom level 15 - meaning since I am showing Zoom Level 18 here, with 1024x1024 tiles, that the data is being scaled up from 256x256 3 zoom levels away - i.e. 1/32nd the size of my imagery tiles.

In that case, it seems that with 1024x1024 tiles at zoom level 18, I should be using at most a 1024/32=32x32 grid. More than that and I'd exceed the Nyquist limit, double-sampling on the same pixel and messing up the interpolation.

Indeed, I see that:

32x32 grid: no artifacts

image

64x64 grid: artifacts

image

Anyway, I guess it might be helpful to add: log2(maxGridSize) = 8 - Mathf.Max(0, zoomLevel-15) - Mathf.Max(0, log2(tileSize) - 8)) (i.e. the max grid size is 256x256, divided by two for every zoom level above 15, divided by 2 for every time the tilesize is 2x greater than 256)

This may have unintended consequences, though: do you think this would be useful to add, or is my case sufficiently fringe that it's best left out?

brnkhy commented 2 years ago

@artoonie hmm so instead of interpolation, your solution proposes decreasing mesh resolution? I think it might be slower as SDK will need multiple different resolution meshes now. i.e. I can't remember v2.1.1 on top of my head but my latest work caches bunch of terrain meshes and reuses them all the time. If I introduce dynamic resolution, it'll have to recreate those or cache a lot more tiles now. I think it might be better to use same high resolution meshes but interpolate properly 🤔 I'll try to look into this soon; have you been able to fix the issue on your side?

artoonie commented 2 years ago

Yes, I propose decreasing mesh resolution since interpolating at the source would create a heavier mesh with no additional resolution. Why waste the resources by oversampling? I'm not sure how it would affect your current caching scheme, but it's not necessarily at odds with caching. That being said, proper interpolation when creating the mesh would also solve the problem, and would certainly have fewer hard-to-explain side-effects.

Yes, I'll call this solved from my side. Thanks for your help!

artoonie commented 2 years ago

Following up with an update that may help others encountering this issue.

You can resolve the visual distress by reducing sampling, but if you're building any computations on top of the heightmap that sample more finely than the resolution of the heighmap, you're going to get the same jittery artifacts. That's because (as @brnkhy noted) there is no bilinear interpolation of the heightmap - it just grabs the nearest neighbor.

That can lead to artifacts that look like this. Apologies for the poorly-labeled graph. Notice that X and Z are smooth, but Y is not - this happens when I sample Y too frequently. X axis is a series of points over time, and Y axis is the worldspace position.

image

With bilinear interpolation, it's pretty easy to resolve: the jitters in Y go away, and now all three lines are smooth:

image

The cost is pretty small if you're not calling this function too much. In UnityTile.cs, replace QueryHeightData with:

        public float QueryHeightData(float x, float y)
        {
            if (HeightData != null)
            {
                float xF = Mathf.Clamp01(x) * 255;
                float yF = Mathf.Clamp01(y) * 255;
                int xMin = Mathf.FloorToInt(xF);
                int yMin = Mathf.FloorToInt(yF);
                int xMax = Mathf.CeilToInt(xF);
                int yMax = Mathf.CeilToInt(yF);
                float xLerp = xF - xMin;
                float yLerp = yF - yMin;
                float topLerp = Mathf.Lerp(HeightAt(xMin, yMax), HeightAt(xMax, yMax), xLerp);
                float botLerp = Mathf.Lerp(HeightAt(xMin, yMin), HeightAt(xMax, yMin), xLerp);
                return Mathf.Lerp(botLerp, topLerp, yLerp);
            }

            return 0;
        }

        public float HeightAt(int x, int y)
        {
            return HeightData[y * 256 + x] * _tileScale;
        }
brnkhy commented 2 years ago

@artoonie I ran into this recently myself as well, I'm definitely keeping that snippet, thank you very much!