TokisanGames/Terrain3D

Colormap artifact in mipmaps along region boundaries

TokisanGames opened this issue · 5 comments

Terrain3D version

v0.9.0

System information

Godot v4.1.3.stable - Windows 10.0.22621 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 3070 Laptop GPU (NVIDIA; 31.0.15.4633) - 12th Gen Intel(R) Core(TM) i9-12900H (20 Threads)

Is the issue reproducable in the demo?

Yes

Issue description

Mipmaps were generated on the colormap array in 7e4f9eb to avoid distant noise, which was very obvious when using a full colormap such as in GIS applications w/ satellite photos (farther below). However, this has introduced another artifact on the region boundary. Enabling renormalize on generate_mipmaps() has no affect.

ALBEDO=colormap*vec3(.1)

image

Regular shader

image

Though mipmaps should be generated on the whole map at once, it appears this tiling is not a problem and the individual mipmaps blend together fine. It's actually surprising that they blend at all, given how different from each other the lower lods look.

image

The only artifact is the small grid line that shows up when up close on mipmap0 shown at the start, and also far away shown here.

image

Upon further testing, I've found that indeed it is not the mipmap lods, it is actually the system that determines which LOD to use that is the problem.

image

These lines are the borders of the individual textures in the arrays and show up when using texture().

I have found I can clamp the lod based on distance.

image

I could go up to 2048 or 3172 to basically eliminate the grid at all distances. However in practice with either a full GIS colormap, or ground textures up close, 1024 gives me the best results of texture mipmaping to eliminate noise at far distances, and fix this line at close distances.

image

I'll close this issue with this workaround, though I believe this to be an engine bug as textureQueryLod() should not show a grid and should not bleed into my renders using texture().

textureLod(tex, 0) does not include this grid artifact.

Basically these two lookup blocks provide very similar results. It's as if when texture() is used on texture arrays, it adds the results of textureQueryLod() to the result.

# Texture Lookup
	color_map = texture(_color_maps, region_uv);

# TextureLod + textureQueryLod artifically added
	vec2 v = textureQueryLod(_color_maps, region_uv.xy);
	color_map.rgb = textureLod(_color_maps, region_uv, v.y).rgb + vec3(v.y*.005);

ALBEDO = albedo_height.rgb * color_map.rgb;

Upstream issues
godotengine/godot#48462

godotengine/godot#27837 (comment)

godotengine/godot#33519

I reopened this because I think this is an upstream bug in texture and textureQueryLod. 7860ecf is a workaround, but I don't think it should be there at all. @clayjohn @Calinou can you guys take a look at this please, given your comments on the referenced tickets?

In short, I've encountered an artifact on mipmapped texture array lookups. The problem exists with texture() but not with any level of textureLod(). The problem also exists in textureQueryLod() and it appears that texture() adds the result of textureQueryLod() to the output. @clayjohn In godotengine/godot#48462, you suggested meshes need UVs to properly work with texture array mipmaps, but how is that supposed to work on UV-less terrains? Start at the beginning for full context.

To test it:

  1. Download the latest release of terrain3d
  2. Import the project it and restart twice
  3. Open the demo
  4. Hide the Borders node
  5. Enable the Material/Shader override enabled and paste this minimal_shader.txt in it.

At the bottom of the shader you'll see four sections:

  1. the problem with textureQueryLod showing the texture array borders
  2. the problem with texture seemingly combining the above values into the results
  3. the near visual equivalent of 2, by actually adding textureQueryLod to textureLod
  4. the current hacky workaround which is not as good as if it were working properly.

With (2) enabled, you'll see the artifact along the path at the region borders, shown in the red artifact below. The green artifact is unrelated, tracked in #185.

image

Here is where mipmaps are created on the colormap texture array:

map->generate_mipmaps();

@TokisanGames I intend to take a look a bit more closely later, but here are the first two things I will look for:

  1. Since you are calculating UV2 by flooring the varying you calculate in the vertex shader you can easily fall prey to precision issues. Oftentimes 0.0s and 1.0s can end up slightly off. So when you floor() the result you end up with -1.0 (when you wanted 0.0) or 0.0 (when you wanted 1.0). Oftentimes using round() is safer, or just adding a very small bias.
  2. Issues with the derivative texture() and textureQueryLod() rely on the derivative of the passed UV coordinate to calculate LOD level. If you UV is non-continous (i.e. it jumps at points) then the derivative may explode causing the function to suddenly use a very high mip level. That was the issue with godotengine/godot#48462 where the user manually clamped the UV coordinate instead of using the repeat mode on the texture.

My gut is leaning towards this being an issue with the derivative. When using custom UV coordinates that are computed in the shader you pretty much always have to calculate the derivative yourself and then use textureLod() or textureGrad().

Thanks for looking at this @clayjohn

Since you are calculating UV2 by flooring the varying you calculate in the vertex shader you can easily fall prey to precision issues. Oftentimes 0.0s and 1.0s can end up slightly off.

Hmm, I understand. However here, if I change floor to round in get_region_uv/2 it breaks the terrain. Every other region is messed up.

My gut is leaning towards this being an issue with the derivative.
When using custom UV coordinates that are computed in the shader you pretty much always have to calculate the derivative yourself and then use textureLod() or textureGrad().

I haven't studied derivatives in over 20 years, and never in the context of shaders. Do you have a link where I can learn what I need? I've found it hard to find anything that is not arcane phonebook information on textureQueryLod, texturelod, texturegrad, etc. I found some links in reference below I'm looking at.

We used to use textureGrad (see update 3) on the ground texture arrays. But I found that using texture was 20% faster for the same result. Here were talking about the colormap, which is a single full terrain texture multiplied on to the ground textures.

I don't want to pay 20% to switch to textureGrad for the colormap. I'd rather use the current hacky method of clamping textureQueryLod by an estimated number and using textureLod. Is there a non-slow way to use derivatives?

I'm not really sure what to do.


Reference
Estimate lod query level in shader
Texturegrad explanation

Ben Golus has a detailed medium article discussing ways to deal with this same issue https://bgolus.medium.com/distinctive-derivative-differences-cce38d36797b. I don't really have any definitive resources to learn about derivatives though.

Manual derivatives tend to be slow compared to the automatically calculated ones as the automatically calculated ones are often done in hardware and/or with a bunch of GPU-specific tricks. So I'm not surprised that manually calculating them ended up being a bit slower. 20% seems like a lot, but I guess it depends on what your results are, if you end up with much lower LODs, the shader can become way more expensive.

Basically what you need is a good way of approximating the per-pixel change in your UV channel without the discontinuities introduced by get_region_uv2(). Since get_region_uv2() just offsets the uv value, you can just use the input value (UV2) instead.

Accordingly, the easy workaround is to do the following:

vec3 region_uv = get_region_uv2(UV2);
float lod = textureQueryLod(_color_maps, UV2).y;
color_map = textureLod(_color_maps, region_uv, lod);

Testing locally I can confirm that in the "problem 1" case the artifacts go away when using UV2 instead of region_uv. However, I can't reproduce the issue when using any of the other "problems" or when using actual color.

Screenshot from 2024-01-07 17-23-41

Screenshot from 2024-01-07 17-23-50

@clayjohn I'm sorry I'm slow to respond. Thank you very much for looking at this and providing a solution. This works well.

Fixed in a096e6c