How to display pixels "correctly" when creating a custom Texture2D?

I wrote some code to create a texture atlas from separate textures for my procedurally generated terrain.
It works, however the pixels are off. I think it has something to do with the settings / format of the Texture2D, but I don’t know how to change those. Any suggestions?

Here is my code:

public static void CreateTextureAtlas()
{
    List<Texture2D> tempAtlas = new List<Texture2D>();
    int numOfTextures = 0;
    foreach (Block block in Resources.LoadAll<Block>("Items/Blocks"))
    {
        if (block.main_tex != null) { tempAtlas.Add(block.main_tex); numOfTextures++; }
        if (block.top_tex != null) { tempAtlas.Add(block.top_tex); numOfTextures++; }
        if (block.bottom_tex != null) { tempAtlas.Add(block.bottom_tex); numOfTextures++; }
    }

    int unscaledWidth = Mathf.CeilToInt(Mathf.Sqrt(numOfTextures));
    textureDivisions = unscaledWidth;

    int atlasSize = (unscaledWidth * 8);

    Texture2D[] atlasTextures = tempAtlas.ToArray();
    Texture2D atlas = new Texture2D(atlasSize, atlasSize);
    uvCoords = atlas.PackTextures(atlasTextures, 0, atlasSize);
    atlas.filterMode = FilterMode.Point;

    voxelTex = atlas;


    //Generate UV coords for each texture
    int i = 0;
    foreach (Block block in Resources.LoadAll<Block>("Items/Blocks"))
    {
        if (block.main_tex != null)
        {
            float x = uvCoords[i].x * unscaledWidth;
            float y = uvCoords[i].y * unscaledWidth;

            block.textureAtlasPos = new Vector2(x, y);
            i++;
        }

        if (block.top_tex != null)
        {
            float x = uvCoords[i].x * unscaledWidth;
            float y = uvCoords[i].y * unscaledWidth;

            block.textureAtlasPos_Top = new Vector2(x, y);
            i++;
        }
        else
        {
            block.textureAtlasPos_Top = block.textureAtlasPos;
        }

        if (block.bottom_tex != null)
        {
            float x = uvCoords[i].x * unscaledWidth;
            float y = uvCoords[i].y * unscaledWidth;

            block.textureAtlasPos_Bottom = new Vector2(x, y);
            i++;
        }
        else
        {
            block.textureAtlasPos_Bottom = block.textureAtlasPos;
        }

    }
}

Your atlas size / dimensions should be a power of 2, otherwise the texture will be scaled to the next power of 2. So we talk about those numbers: 2.4.8.16.32.64.128.256.512.1024.2048.4096.8192, …

Any other dimenstion “can” cause issues. There are “NOPT” (Non Power Of Two) textures, however they are not necessarily supported on all platforms, usually are less performant and cause additional issues with mipmaps. Mipmapping is the idea to downscale the texture by a factor of 2 several times to get different mip-levels of the same texture. Those smaller textures would also be stored in memory and would be automatically be used when the viewed size of the surface is too small to see individual pixels / texels. This helps to reduce aliasing effects and noisy textures the further they are away. If a screen pixel essentially covers say a 4x4 texel area of your object, which color should be show? Usually the renderer just pics the closest texel. With bilinear filtering the neighboring pixels are taken into account but the renderer can not calculate the average color over a large texture area. That’s where mipmaps help. They essentially contain all the averages down to a single pixel.

Since the texture is repeatedly cut in half, of course any size that is not a power of two would run into issues.

This does not only apply to atlas texture but to textures in general, especially when you want pixel filtering and bit bilinear filtering. An atlas could of course contain non power of two textures depending on how you pack them, but you would still run into issues with the smaller mip levels as at a certain scale a single mip map texel would overlap between different texture that are next to each other.

The other problem you may have when you pack your textures “tightly” is that we’re dealing with floating point numbers. UV coordinates go from 0 to 1 and represent the left edge to the right edge of the texture. This however is an issue since the actual “pixels” are like rectangles / quads. They have a “size”. The renderer has to pick the closest pixel. Though when your UV coordinate is exactly at the boundary of two pixels / texels, slight inaccuracies in the actual UV coordinate would choose either one or the other. When bilinear filtering is used this is usually called “color bleeding” as the next texture in the atlas “bleeds” into the next one due to filtering. With point filtering it’s less a bleed but an actual row or column of pixels.

The solution is usually to try to sample the center of a texel of the texture and not the edges of the pixels. There are many ways how this can be achieved but the general idea is that the larger UV coordinates gets half the texel size subtracted while the lower coordinates gets half the size added. So the UV rectangle is “inset” / shrunk by one pixel in total in both axis. This ensures that you stay within the bounds of the desired texture area.

In what regard? Could you show screens what it is supposed to be like, and what it actually looks in close-up?

Provided that Bunny’s answer didn’t already point out the cause.

Okay turns out that instead of Mathf.CeilToInt(Mathf.Sqrt(numOfTextures)); it was Mathf.RoundToInt(Mathf.Sqrt(numOfTextures));

However, I have heard of the power of two rule. How would I implement that? When I tried to make my new texture something like 2048, it still packed to be 32 x 32; This is still a power of two, but it is a coincidental one. If I added another texture it will not be a power of two.

To get the next higher power of two you could simply use Unity’s Mathf.NextPowerOfTwo function. Unity’s implementation is quite efficient as it only uses integer arithmetic.

The mathematical way

The pure mathematical way would use the log to the base 2 of your calculated atlas size. Then ceil this value which is the exponent and then calculate 2^n

This would do the same thing:

double exp = System.Math.Log2(size); // calculate exponent
exp = System.Math.Ceiling(exp); // make sure the exponent is a whole number
double potSize = System.Math.Pow(2, exp); // calculate the next larger size as a power of two.

So for example if you had a size of “3215” just as an arbitrary example, exp in the first step would be 11.6506. When we ceil it we get of course 12. Calculating 2^12 gives us 4096 which is of course the next larger POT. If size is already a power of two, say 2048 our exp would be 11 exactly. So Ceiling wouldn’t do anything and it would stay at 2048. I used the System.Math function since Unity did not implement a float wrapper for Log2

Why Unity's implementation of NextPowerOfTwo works

The repeated bitwise or with a shifted version of the current number will result in keeping the highest bit and filling all lower bits to 1. By adding one to that number we will carry to the next full power of two. For example if your value is “2049” (so one number higher than an actual power of two) we want to get the next larger power of two. So 4096. We first subtract 1 from the original number (in order to keep the original number if it is already a power of two). Then we do bitwise combine the number with a shifted version of itself to fill all the lower bits with 1s. The proces goes like this:


10000000001 == 2049
// subtract 1
10000000000
// the shift by 16 has no effect in our example since our number only has 11 bits.
// the shift by 8 results in this number
10000000100
// the shift by 4 results in this:
10001000100
// the shift by 2 results in this
10101010101
// the final shift by 1 results in this
11111111111 == 4095

// Finally we add 1 and get 
100000000000 == 4096

When the number is already a power of two, because we subtract one in the beginning, we already get all lower bits filled with 1s so the whole shifting does essentially nothing as all bits are already set. When we add the 1 back in the final step we’re back at our original number.

The shifting and or-ing essentially copies the most significant 1 to all lower bits to ensure when we add one at the end we carry to the next higher power of two.