Bilinear Interpolation to resize/scale a texture or 2D array

Hello, so I’m looking for a starting point to learn how to do this. Let’s say I have a 100x100 height map that looks like this:

Then I create another 100x100 made from Perlin noise. I can multiply them and get a combined map I can then use. If I want to do a 200x200 map, how do I scale the template map to 200x200 or a 50x50 mathematically? I want to do this during runtime so it can adapt to and new size without having to remake the template for any given height map size.

The only thing I can think might be similar is some sort of image resizing algorithm, but I am completely in the dark on where to start. I should probably add, I’m not looking for a package to add to Unity that will do this for me. I want to be able to write the code myself and understand what and how it works.

Thanks

If you do already have a height map that you generate the mesh out of, then you should probably just start with the most basic element of this: how to resize an image.

This is actually something where our favourite fake AI’s are useful, as this is well travelled ground in Unity, so there’s been plenty of information for them to grift.

After getting one hallucination I did get a script that works, which I modified into a simple example:

using UnityEngine;

public class ImageResizeComponent : MonoBehaviour
{
	[SerializeField]
	private Texture2D _originalTexture;

	[SerializeField]
	private int _newWidth = 256;

	[SerializeField]
	private int _newHeight = 256;

	[ContextMenu("Resize Image")]
	private void ResizeCurrentImage()
	{
		string outputFolder = System.IO.Path.GetDirectoryName(this.gameObject.scene.path);
		Texture2D resizedTexture = ResizeTexture(_originalTexture, _newWidth, _newHeight);
		UnityEditor.AssetDatabase.CreateAsset(resizedTexture, $"{outputFolder}/result.asset");
	}

	public static Texture2D ResizeTexture(Texture2D sourceTexture, int targetWidth, int targetHeight)
	{
		Texture2D newTexture = new(targetWidth, targetHeight, sourceTexture.format, false);
		float scaleX = (float)sourceTexture.width / targetWidth;
		float scaleY = (float)sourceTexture.height / targetHeight;

		Color[] resizedColours = new Color[targetWidth * targetHeight];

		for (int y = 0; y < targetHeight; y++)
		{
			for (int x = 0; x < targetWidth; x++)
			{
				int index = (y * targetWidth) + x;
				float sourceX = x * scaleX;
				float sourceY = y * scaleY;
				Color newColor = sourceTexture.GetPixelBilinear(sourceX / sourceTexture.width, sourceY / sourceTexture.height);
				resizedColours[index] = newColor;
			}
		}

		newTexture.SetPixels(resizedColours);
		newTexture.Apply();
		return newTexture;
	}
}

Notably it uses simple bilinear interpolation. Obviously you can do this without the texture at all, and simply convert one array of colours to a different sized array of colours, using an algorithm to do the conversion. I imagine you can find these online without too much hassle.

Last reply was considered spam for some reason? Thanks for the response but I’m not looking for code.

I saw your previous response. When I mean “Fake AI” I mean tools like Copilot and ChatGPT, which are known for giving nonsense/useless responses, otherwise known as hallucinations. I figured that it was pretty common vernacular these days in the age of AI.

And if you’re not looking for code, what are you looking for? I imagine image resizing algorithms are something you can easily find with a few searches.

Just a point in the direction of where to look. Only thing I could find yesterday was a bunch of threads that ended with “just use this library from git”. I did look up bilinear interpolation, so if I can figure out how the math works I can try writing that in code. If you know any good articles/videos that go through the math on the other scaling methods, that would be very helpful.

So I found a good site that breaks down bilinear interpolation. I’d like to link the webpage but I think the that might be why the spam bot blocked my previous post.

Here is the code I have come up with:

public Texture2D ResizeHeightMapTemplate(Texture2D texture, int newWidth, int newHeight)
{
    int oldWidth = texture.width;
    int oldHeight = texture.height;
    float xRatio = (float)oldWidth / (float)newWidth;
    float yRatio = (float)oldHeight / (float)newHeight;

    float[,] unwrappedNewTex = new float[newWidth, newHeight];
    float[,] unwrappedOldTex = UnwrapTextureToFloatArray(texture);

    for (int xIndex = 0; xIndex < newWidth; xIndex++)
    {
        //Get X Values. x is the coordinate to interpolate between x1 - x2.
        //x1 and x2 are used to determine where to sample from the original
        //texture.
        float x = xIndex * xRatio;
        int x1 = (int)math.floor(x);
        int x2 = (int)math.ceil(x);

        //Precalculate X fraction values. Saves having to recalculate every loop.
        float xDenom = x2 - x1;
        float xLeft = x2 - x;
        float xRight = x - x1;

        for (int yIndex = 0; yIndex < newHeight; yIndex++)
        {
            //Get Y Values. Same as X values in previous loop.
            float y = yIndex * yRatio;
            int y1 = (int)math.floor(y);
            int y2 = (int)math.ceil(y);

            //Precalculate Y faction values.
            float yDenom = y2 - y1;
            float yLeft = y2 - y;
            float yRight = y - y1;

            //Account for 0 based indexing. This will act as an addtional row on top
            //and colomn on the right of texture
            if (x2 == oldWidth)
                x2 -= 1;
            if (y2 == oldHeight)
                y2 -= 1;

            //These are the samples of the original texture.
            float q11 = unwrappedOldTex[x1, y1];
            float q12 = unwrappedOldTex[x1, y2];
            float q21 = unwrappedOldTex[x2, y1];
            float q22 = unwrappedOldTex[x2, y2];

            //This calculates the weights for each sample across the y axis
            float r1 = ((xLeft / xDenom) * q11) + ((xRight / xDenom) * q21);
            float r2 = ((xLeft / xDenom) * q12) + ((xRight / xDenom) * q22);

            //If the sample location is in the same pixel of the orginal texture,
            //this will stop NaN results cause by dividing by 0. 
            //This is sampling at floor, alternatively you could do 
            //  r1 = q21
            //  r2 = q22
            //for sampling at ceiling
            if (x1 == x2)
            {
                r1 = q11;
                r2 = q12;
            }

            //This calculates the weights (from the previous y axis) across the x axis
            float p = ((yLeft / yDenom) * r1) + ((yRight / yDenom) * r2);

            //Another check for dividing by 0. Again this is sampling at floor.
            // p = r2 would sample at ceiling
            if (y1 == y2)
                p = r1;

            unwrappedNewTex[xIndex, yIndex] = p;
        }
    }

    Texture2D results = WrapTextureFromArray(unwrappedNewTex);
    return results;
}

private float[,] UnwrapTextureToFloatArray(Texture2D texture)
{
    float[,] results = new float[texture.width, texture.height];

    for (int x = 0; x < texture.width; x++)
    {
        for (int y = 0; y < texture.height; y++)
        {
            results[x, y] = texture.GetPixel(x, y).r;
        }
    }

    return results;
}

private Texture2D WrapTextureFromArray(float[,] array)
{
    Texture2D results = new(array.GetLength(0), array.GetLength(1));
    Color[] color = new Color[array.GetLength(0) * array.GetLength(1)];

    for (int i = 0; i < color.Length; i++)
    {
        int xCoord = i % array.GetLength(0);
        int yCoord = i / array.GetLength(0);

        float colorValue = array[xCoord, yCoord];

        color[i] = new Color(colorValue, colorValue, colorValue);
    }

    results.SetPixels(color);
    results.Apply();

    return results;
}

These are the equations from the webpage:


This is the graph that shows where the numbers come from:

Here are the results

Source 200x200

Scaled Down 100x100

Scaled Up 400x400

There are most definitely better implementations but I’m stoked this worked. I’m going to mark this as the solution, hopefully this will help some else in the future.

1 Like

Over 8 years ago I posted a solution to bilinearly resample and crop a texture to a certain target size. Note that I have an optimised version of the two methods in my TextureTools.cs. The optimisation mainly takes out method calls which allows for some simplifications and gives a huge speed boost.

Note that multidimensional arrays are slower than a flattened one dimensional array. Reading or writing the texture in a loop by using GetPixel / SetPixel in a loop is horrendously slow.

2 Likes

This was more just a proof of concept for me. This will allow me to quickly make height map templates in photoshop. I’m sure I will make many changes before I’m happy with it. I plan on using Burst Compile and Jobifying it before I’m done, which will mean flattening the nested for loops, cleaning up the Wrap and Unwrap methods, using nativearrays etc. I will definitely check out your post and code, it’s always nice to see how other go about similar tasks.

Why does the spam bot keep hiding my post? I was replying to a new comment…