How to prevent texture edges disappearing when zooming out?

Godot Version

Godot v4.4.1 stable mono

Question

My problem is I am creating a texture atlas dynamically and applying the atlas to a shader in which I calculate the correct UV for the selected texture id which works great, but when I start zooming out the texture edges start disappearing. As far as I know the reason for it is because my textures are 64x64 px big and when I reach a point there is not enough pixels to scan so the edges disappear. But this is not ideal obviously. Is there any workaround for this?

My setup is like this:

I have a CanvasLayer for the MultiMeshInstance2Ds which have the materials with the shaders, and the Camera is also attached to this CanvasLayer. The layer is set to Follow the viewport.

My shader code is this:

shader_type canvas_item;

uniform sampler2D atlas : filter_nearest, repeat_disable;
uniform vec2 tile_size;

varying float tile_x;
varying float tile_y;
varying float is_preview;

void vertex()
{
	int b = int(INSTANCE_CUSTOM.b * 255.0);

	tile_x = INSTANCE_CUSTOM.r * 255.0;
	tile_y = INSTANCE_CUSTOM.g * 255.0;

	is_preview = float((b >> 0) & 1);
}

void fragment()
{
	vec2 tile_offset = vec2(tile_x, tile_y) * tile_size;
	vec2 tile_uv = tile_offset + vec2(UV.x, 1.0 - UV.y) * tile_size;
	vec4 tex = texture(atlas, tile_uv);

	// Some color adjustment if the object is preview or not

	COLOR = tex;
}

Some examples how does it look, inside the red rectangle are the zoomed out, looking bad textures and on the right inside the green one the correct one.

Honestly any help would be appreciated, because I’m very close to giving up on my whole project if I can’t solve this problem, because I cannot work with this bottleneck, I’m very lost.

Additional information: I create no mipmaps for the texture atlas, I import the images/textures as Image in png format. Here is my code for creating the texture atlas:

internal ImageTexture CreateTextureAtlas(List<Image> textures, Dictionary<string, int> indexTextureDictonary)
{
    int rowsNeeded = Mathf.CeilToInt((float)textures.Count / GameTextureManager.TILES_PER_ROW);
    int atlasHeight = rowsNeeded * GenerationSettings.PIXELS_PER_TILE;

    Image atlasImage = Image.CreateEmpty(GameTextureManager.TEXTURE_ATLAS_SIZE, atlasHeight, false, Image.Format.Rgba8);
    atlasImage.Fill(new Color(0, 0, 0, 0));

    int index = 0;
    foreach (KeyValuePair<string, int> keyValuePair in indexTextureDictonary)
    {
        if (index >= textures.Count)
            break;

        int tileX = index % GameTextureManager.TILES_PER_ROW;
        int tileY = index / GameTextureManager.TILES_PER_ROW;

        Rect2I destRect = new Rect2I(tileX * GenerationSettings.PIXELS_PER_TILE,
                                     tileY * GenerationSettings.PIXELS_PER_TILE,
                                     GenerationSettings.PIXELS_PER_TILE,
                                     GenerationSettings.PIXELS_PER_TILE);

        Rect2I srcRect = new Rect2I(0,
                                    0,
                                    GenerationSettings.PIXELS_PER_TILE,
                                    GenerationSettings.PIXELS_PER_TILE);


        atlasImage.BlitRect(textures[index], srcRect, destRect.Position);
        index++;
    }

    return ImageTexture.CreateFromImage(atlasImage);
}

Some constants that are used:

    internal const int TEXTURE_ATLAS_SIZE = 8192;
    internal const int TILES_PER_ROW = TEXTURE_ATLAS_SIZE / GenerationSettings.PIXELS_PER_TILE;
    internal const float TILE_SIZE = (float)GenerationSettings.PIXELS_PER_TILE / TEXTURE_ATLAS_SIZE;
    internal const int PIXELS_PER_TILE = 64;

If there is any more information needed let me know and I will provide it promptly!

Thank you for your help in advance mighty hero!

Downscaling uses nearest-neighbor scaling, try turning on mipmaps or creating lower resolution assets to swap out if the zoom is enough.

1 Like

This can happen if your game’s preview window is set to a lower resolution than your main viewport. Basically, if the preview window is smaller than your project’s resolution, you’ll see these issues. Try making the preview window bigger or lowering your project’s resolution to see if that helps.

Another thing to try: enable “Snap 2D Transforms to Pixel” in the project settings (Rendering > 2D), especially if your meshes are moving at sub-pixel values.

Non-integer scaling and rotation can also mess with small textures when using nearest-neighbor scaling. If you plan to use a lot of transformations like that in this context, it might be better to draw your objects with Line2D and Polygon2D nodes (like a square with a multi-line border) instead of using a texture.

1 Like

Thank you for your reply, and I have tried it, but sadly haven’t fixed my problem. My monitor’s display is 3840 x 1080 and I have set the Project Settings → Display → Window → Viewport Width and Height to these values but the disappearing still happens. Previously it was a lower value, the default is 1152 x 648. Any tip?

I have also tried this, set Project Settings → Rendering → 2D → Snap 2D Transforms to Pixel and Snap 2D Vertices to Pixel to true or enabled, but unfortunately didn’t help either. I don’t understand what causes this to happen.

Honestly I’m not trying to create anything fancy, just normal 2D top down grid map procedurally as chunks and every tile’s center and vertices are integer values. The “first” vertex is at (0, 0) and it changes by 64px every direction. The reason I am not using TileMaps is because I’m creating larger chunks because that’s what my game requires and they cannot be made multi-threaded, sadly, but creating MultiMeshInstance2D data can be. So any tip what else could cause these problems? I’d be happy to hop on a discord call and showcase too, or send my prototype. I’m pretty desperate for help!

Thank you in advance!

I’m not sure I understand your reply correctly. There is no way to “trick” or make textures permanent size, so that zoom levels don’t affect them? Because I’m not zooming out crazy, the texture should be still the same from the zoom levels I’m trying, because could be clearly seen by the player, but I’m guessing there isn’t as many pixels for the GPU to render? By the way I tried to enable mipmaps when creating the atlas texture, but haven’t changed a single thing. Could you provide more info, please? Thank you!

Additional information: when I attach the standalone texture to a Sprite2D and zoom out the same thing happens. So I’m guessing it’s not the shader’s fault then, but some setting for my Viewport?

Sorry, I should have been more clear with my explanation. The size of the game preview should be at least (preferably matching) your project resolution. You can check the resolution of your game preview by checking the top-right corner of the preview window.

I also notice now, very importantly, that the size of the scaled textures in the screenshot you provided have a width/height lower than the base resolution of 64x64. If you scale a texture with a specific base resolution to be displayed in an area smaller than the size of that texture, some pixels will necessarily be removed because there’s simply no room to fit all of them. The renderer has no preference between cutting out centered and outlining pixels.

If you want to, replacing your texture with a black and white checkerboard pattern with a 1px red outline should help you get an understanding for how scaling behaves when a texture’s resolution is larger than the area it’s rendered in.

The two previously mentioned methods of manually creating mipmaps and drawing from primitives should work. Unfortunately, this is a core limitation with rendering in general; you can partly mitigate it with anti-aliasing methods, but of course in a pixel art game that’s generally not acceptable.

1 Like

Thank you for your reply again!

It’s matching most of the times, but I’m using the popped out version of the preview window so I resize it time to time, but it doesn’t affect the results.

So basically there is not enough pixels to be displayed of the texture because by zooming out you cut pixels out and with mipmaps you can control what to display with lower space available. Do I understand correctly? This was the thing I was afraid of, but one thing I don’t understand is that I don’t want to showcase a crazy number of pixels/tiles by zooming out. For example I want to display 128 x 128 tiles and each tile is 64 px, so display 8192x8192 px at once, but I am unable to do so because the textures get messed up. Other games don’t have this kind of problem, for example RimWorld can showcase around 200x200 tiles and each of them is 64 px as far as I know, so what is their trick? Is there a workaround like scaling the Canvas, or something? I don’t want to create mipmaps, because the zoom level of 0.07 isn’t that far to have mipmaps for it in my opinion. Would a workaround like rendering the tiles and textures as 128x128 px, but the base texture being 64x64 px would solve it? Granted it will look more pixelish, but I just have no other idea. How do other games display larger areas with pixel graphics and not use mipmaps?

Your understanding is correct, yes. When a texture is mipmapped, it has multiple levels of detail, which are actually stored as a set of textures. The game then calculates which texture in that set to show based on how zoomed out the object is relative to the camera.

Rimworld likely uses a combination of texture filtering, anti-aliasing, and its textures are smooth, generally and dont have high contrast 1px borders seen in your example texture. Try enabling between 4x and 8x MSAA and linear texture filtering. On the project I made attempting to match yours, that helps a lot (down to a 0.3x scale, at least.) If that’s still not acceptable for any reason, try making the outline 2px, high contrast 1px borders/lines are just finicky when scaling smaller than the base texture size.

If you want a feel for how some pixels get thrown out and filtered when scaled down, you can use this texture to replace your current one to visualize it. It’s a 64x64 checkerboard pattern with a 1px red outline.

image

If all else fails you can screenshare your project to me, I’m in the Godot Discord, my tag is lunar_lepidoptera

1 Like

Yes, you are trying to draw a 128x128 texture in a space that is only 64x64, half of the pixels willl be discarded as the default option.

Kind of, if you are using the .dds file format, but generating mipmaps uses a different technique that doesn’t just discard pixels but attempts to blend them together. Your 1pixel of yellow outline may turn into a half-black half-yellow border at best, and a darker blend as ratio of dowscaled pixels increases.

Probably mipmaps, and certainly more careful textures. Notice Prison Architect and RimWorld avoid hard borders, each tile has a slightly darker edge, not a single pixel wildly different color edge.

A zoom of 0.5 is enough to warrant mipmaps, you are way over that line. Can you show how you enabled mipmaps? Make sure you enable it under your Node2D’s Texture > Filter > Linear Mipmap

1 Like

So I have talked with Jonathan_Bryant on discord and I sent him my whole code and export as well and it’s not as easy as just saying mimaps as it turns out, because I’m not having a 1px border, but a 4-5px border which completely disappears we checked these things:

Texture:

Project settings
Display → Window → Viewport Width and Height to be bigger
Rendering → 2D → Snap 2D Transforms and Vertices to Pixel
Rendering → Anti Aliasing → MSAA 2D → 4x (Slow)
Rendering → Textures → Texture Filter → Linear
… → Anisotropic Filtering Level → 4x (Fast)

MultiMeshInstance2D:
Set TextureFilter to linear
Set TextureReapeat to disabled

Test with Sprite2D instead of the shader (check if it’s because of the shader code)

Import settings (I import the png files as Images not Texture2D when we tried with Texture2D we tried to mess around with the settings)

I have specifically tested the RimWorld textures to see how theirs appear and they mostly are the same:


RimWorldFullyZoomedOut

While mine looks like this when zoomed out a little bit, not even close to what I zoom out in RimWorld:


MineFullyZoomedOutLittle

Theoretically there would be enough space for it to render the yellow edges because in pixels they are big enough even at this small zoom out level, but for some reason it’s not happening.

We suspect driver problem on my end, but honestly I have no idea still what could cause this.

We also tested with the piano tiles:





4zoomout

Something seems off. I don’t believe it’s because of the mipmaps, are you 110% sure after all this info it’s because of not using mipmaps? If so how can RimWorld achieve this kind of look without mipmaps for their objects? What is the trick?

Edit: Some more information on Linux he could achieve a RimWorld like texture filtering but on Windows his looks the same as mine, so I’m out of ideas.

You did not turn on mipmaps, only Linear filtering, not Linear Mipmap. You also must enable “Generate Mipmaps” on the texture import settings. Mipmaps will help but not solve the issue entirely, the piano texture for instance will become gray instead of a checkerboard, your GPU cannot display 64x64 quality within a 8x8 square it is physically and logically impossible.

Rimworld uses a secondary shadowing texture/shader which helps border objects when zoomed out, but notice the black edge on most of the objects is heavily faded or gone, such as the billards table.

1 Like

Trying it now:

Default Texture Filtering → Linear Mipmap

Added generating mipmaps for the textureAtlas creation

    internal ImageTexture CreateTextureAtlas(List<Image> textures, Dictionary<string, int> indexTextureDictonary)
    {
        int rowsNeeded = Mathf.CeilToInt((float)textures.Count / GameTextureManager.TILES_PER_ROW);
        int atlasHeight = rowsNeeded * GenerationSettings.PIXELS_PER_TILE;

        Image atlasImage = Image.CreateEmpty(GameTextureManager.TEXTURE_ATLAS_SIZE, atlasHeight, true, Image.Format.Rgba8); // Set to true to create mipmap
        atlasImage.Fill(new Color(0, 0, 0, 0));

        int index = 0;
        foreach (KeyValuePair<string, int> keyValuePair in indexTextureDictonary)
        {
            if (index >= textures.Count)
                break;

            int tileX = index % GameTextureManager.TILES_PER_ROW;
            int tileY = index / GameTextureManager.TILES_PER_ROW;

            Rect2I destRect = new Rect2I(tileX * GenerationSettings.PIXELS_PER_TILE,
                                         tileY * GenerationSettings.PIXELS_PER_TILE,
                                         GenerationSettings.PIXELS_PER_TILE,
                                         GenerationSettings.PIXELS_PER_TILE);

            Rect2I srcRect = new Rect2I(0,
                                        0,
                                        GenerationSettings.PIXELS_PER_TILE,
                                        GenerationSettings.PIXELS_PER_TILE);


            atlasImage.BlitRect(textures[index], srcRect, destRect.Position);
            index++;
        }

        return ImageTexture.CreateFromImage(atlasImage);
    }

At zoom: 0.131

You are saying what I’m trying to achieve is impossible? The yellow border is 4-5 px big, why does it disappear completly then? I understand that there is less pixel so it can’t display all the 5 px big edges, but at this small zoom out level it shouldn’t make the whole thing disappear, no? There is enough pixel theoretically. We can chat on Discord as well if that’s more convenient for you.

Here’s the settings

Here’s the resulting piano texture, I tested this with the following camera script to zoom out

extends Camera2D

func _ready() -> void:
	var t := create_tween().set_loops()
	t.tween_property(self, "zoom", Vector2(0.125, 0.125), 10).from(Vector2.ONE)
	t.tween_property(self, "zoom", Vector2.ONE, 10)

More screenshots, with even a smaller zoom factor, easier to read “Zoom” label
2025-05-16-140023_130x96_scrot 2025-05-16-140119_127x98_scrot

1 Like

For your code you will need to insert atlasImage.GenerateMipmaps(), and your shader will use filter_linear_mipmap, or filter_nearest_mipmap.

// ...

        }
        atlasImage.GenerateMipmaps();
        return ImageTexture.CreateFromImage(atlasImage);
    }
shader_type canvas_item;

uniform sampler2D atlas : filter_linear_mipmap, repeat_disable;
uniform vec2 tile_size;

// ...
1 Like

With filter_nearest_mipmap works way better. Hats off to you! I owe you one big time!

With fairly zooming out: 0.084 zoom level

FairlyZoomedOut

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.