How to resize/scale down texture without losing quality?

have big textures which are photos from my mobile camera, when I use myTexture.EncodeToJPG(); TextureScale.Bilinear(myTexture, 50, 50); the photo becomes 50x50 but with a lot of grainy noise.
For example- This is the result I get

You see how dirty the pic becomes? But If I take the source texture and run myTexture.EncodeToJPG(); and take it to a simple Windows Paint and click resize, I get this quality-

My question is how can I resize the image without being grainy. Obviously TextureScale.Bilinear is not a good option.

*I tried and didn’t work: TextureScale.Point. And another code for AvarageScale.

I use this: http://wiki.unity3d.com/index.php/TextureScale#TextureScale.cs

Yes as I mentioned it’s what I use… and you see the bad result

You could assign it to a material and render that:

public static Texture2D RenderMaterial(ref Material material, Vector2Int resolution, string filename = "")
{
  RenderTexture renderTexture = RenderTexture.GetTemporary(resolution.x, resolution.y);
  Graphics.Blit(null, renderTexture, material);

  Texture2D texture = new Texture2D(resolution.x, resolution.y, TextureFormat.ARGB32, false);
  texture.filterMode = FilterMode.Bilinear;
  texture.wrapMode = TextureWrapMode.Clamp;
  RenderTexture.active = renderTexture;
  texture.ReadPixels(new Rect(Vector2.zero, resolution), 0, 0);
#if UNITY_EDITOR
  // optional, if you want to save it:
  if (filename.Length != 0)
  {
    byte[] png = texture.EncodeToPNG();
    File.WriteAllBytes(filename, png);
    AssetDatabase.Refresh();
  }
#endif
  RenderTexture.active = null;
  RenderTexture.ReleaseTemporary(renderTexture);

  return texture;
}

Usage:

Texture2D result = RenderMaterial(ref material, new Vector2Int(50, 50));

Bonus of this approach is also having GPU-based image processing (i.e. with Shader Graph you could drop a Saturation node in there, expose a Vector1 property for it etc.).

Edit: For anyone grabbing this, please also see this post for an additional line necessary after the call to ReadPixels.

2 Likes

Your function takes material not texture2d, I have texture2d which I want to resize through script

public Material material; // set to the material you want to use (probably want to pick one that's unlit).
public Texture2D ResizeTexture(Texture2D originalTexture, int newWidth, int newHeight)
{
  material.SetTexture("_MainTex", originalTexture);  // or whichever the main texture property name is
  // material.mainTexture = originalTexture; // or can do this instead with some materials
  return RenderMaterial(ref material, new Vector2Int(newWidth, newHeight));
}

Then it’s as easy as this…

Texture2D resizedTexture = ResizeTexture(originalTexture, 50, 50);

The problem is the resize function you’re using only uses four samples of the original image to produce each pixel of the resized image. This means that if you downsample to an image that is smaller than half the dimensions of the original, information is lost and you get aliasing artifacts.

The way to achieve high quality downsampling when using bilinear is to use a loop and downsample the image multiple times, to no more than half the size of the previous step, until you reach the desired dimensions.

1 Like

@adamgolden thanks but your function returns a black texture. The material loads the texture, but after running RenderMaterial I get a black texture.

@Neto_Kokku your idea is interesting, but the more loops i add the texture becomes blurry, even with one extra step it still looks more blurry than the original scaling function
Also, all those loops give a really bad performance

The function works for me, but after I added

texture.Apply();

after

texture.ReadPixels(new Rect(Vector2.zero, resolution), 0, 0);

1 Like

Thanks @Lu_Atar - hopefully with that it’ll work for @swifter14 as well.

Edit: Added note to the post with a link to your fix. I was probably calling .Apply on the texture after it was returned by the function, because I’m pretty sure it’s necessary to have that. Good catch, thanks again.

Edit: Turns out I wasn’t calling .Apply because I wasn’t using the texture this generated in the scene, I was just using the pixel data, so it didn’t need to be uploaded to the GPU.

Applying TextureScale.Bilinear (an additional time every time the texture is larger than 2x the resize target width) or Unity’s built in function GetPixelBillinear (an example provided in this blog) multiple times did help a lot in making the texture less jarring. But it still was not as good as Paint.net’s resizing algorithms or even the Mitchell, because of the edges being low-quality.

I wish Unity would at least open up an API for using the Mitchell algorithm. I couldn’t find a way to access that algorithm. Something like Texture2D.ResizeMitchell would be great. Resizing through Paint.net or Photoshop seems to be the way to go for now.

Does anyone know if there is a better alternative today?

I also agree that the TextureScale Bilinear is not resizing that well for large differences downscaling.

FYI the TextureScale wiki link seems to be down, here’s the full code

public class TextureScale
    {
        private static Color[] texColors;
        private static Color[] newColors;
        private static int w;
        private static float ratioX;
        private static float ratioY;
        private static int w2;
        private static int finishCount;
        private static Mutex mutex;

        public static void Point(Texture2D tex, int newWidth, int newHeight)
        {
            ThreadedScale(tex, newWidth, newHeight, false);
        }

        public static void Bilinear(Texture2D tex, int newWidth, int newHeight)
        {
            ThreadedScale(tex, newWidth, newHeight, true);
        }

        private static void ThreadedScale(Texture2D tex, int newWidth, int newHeight, bool useBilinear)
        {
            texColors = tex.GetPixels();
            newColors = new Color[newWidth * newHeight];
            if (useBilinear)
            {
                ratioX = 1.0f / ((float)newWidth / (tex.width - 1));
                ratioY = 1.0f / ((float)newHeight / (tex.height - 1));
            }
            else
            {
                ratioX = (float)tex.width / newWidth;
                ratioY = (float)tex.height / newHeight;
            }

            w = tex.width;
            w2 = newWidth;
            int cores = Mathf.Min(SystemInfo.processorCount, newHeight);
            int slice = newHeight / cores;

            finishCount = 0;
            if (mutex == null)
            {
                mutex = new Mutex(false);
            }

            if (cores > 1)
            {
                int i = 0;
                ThreadData threadData;
                for (i = 0; i < cores - 1; i++)
                {
                    threadData = new ThreadData(slice * i, slice * (i + 1));
                    ParameterizedThreadStart
                        ts = useBilinear ? BilinearScale : new ParameterizedThreadStart(PointScale);
                    Thread thread = new(ts);
                    thread.Start(threadData);
                }

                threadData = new ThreadData(slice * i, newHeight);
                if (useBilinear)
                {
                    BilinearScale(threadData);
                }
                else
                {
                    PointScale(threadData);
                }

                while (finishCount < cores)
                {
                    Thread.Sleep(1);
                }
            }
            else
            {
                ThreadData threadData = new(0, newHeight);
                if (useBilinear)
                {
                    BilinearScale(threadData);
                }
                else
                {
                    PointScale(threadData);
                }
            }

            tex.Reinitialize(newWidth, newHeight);
#pragma warning disable UNT0017 // SetPixels invocation is slow
            tex.SetPixels(newColors);
#pragma warning restore UNT0017 // SetPixels invocation is slow
            tex.Apply();

            texColors = null;
            newColors = null;
        }

        public static void BilinearScale(object obj)
        {
            ThreadData threadData = (ThreadData)obj;
            for (int y = threadData.start; y < threadData.end; y++)
            {
                int yFloor = (int)Mathf.Floor(y * ratioY);
                int y1 = yFloor * w;
                int y2 = (yFloor + 1) * w;
                int yw = y * w2;

                for (int x = 0; x < w2; x++)
                {
                    int xFloor = (int)Mathf.Floor(x * ratioX);
                    float xLerp = (x * ratioX) - xFloor;
                    newColors[yw + x] = ColorLerpUnclamped(
                        ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp),
                        ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp),
                        (y * ratioY) - yFloor);
                }
            }

            mutex.WaitOne();
            finishCount++;
            mutex.ReleaseMutex();
        }

        public static void PointScale(object obj)
        {
            ThreadData threadData = (ThreadData)obj;
            for (int y = threadData.start; y < threadData.end; y++)
            {
                int thisY = (int)(ratioY * y) * w;
                int yw = y * w2;
                for (int x = 0; x < w2; x++)
                {
                    newColors[yw + x] = texColors[(int)(thisY + (ratioX * x))];
                }
            }

            mutex.WaitOne();
            finishCount++;
            mutex.ReleaseMutex();
        }

        private static Color ColorLerpUnclamped(Color c1, Color c2, float value)
        {
            return new Color(c1.r + ((c2.r - c1.r) * value),
                c1.g + ((c2.g - c1.g) * value),
                c1.b + ((c2.b - c1.b) * value),
                c1.a + ((c2.a - c1.a) * value));
        }

        public class ThreadData
        {
            public int end;
            public int start;

            public ThreadData(int s, int e)
            {
                this.start = s;
                this.end = e;
            }
        }
    }
1 Like

I got a corrupted error using Point scaler, can someone check this too?
the texture has read/write enabled and point filter

I don’t know why you got that error. However, I found a great TextureScaler which has pretty much the same API and uses the GPU instead. It also has better scaling I believe, higher quality end product. Here it is.

using UnityEngine;

    /// A unility class with functions to scale Texture2D Data.
    ///
    /// Scale is performed on the GPU using RTT, so it's blazing fast.
    /// Setting up and Getting back the texture data is the bottleneck.
    /// But Scaling itself costs only 1 draw call and 1 RTT State setup!
    /// WARNING: This script override the RTT Setup! (It sets a RTT!)   
    ///
    /// Note: This scaler does NOT support aspect ratio based scaling. You will have to do it yourself!
    /// It supports Alpha, but you will have to divide by alpha in your shaders,
    /// because of premultiplied alpha effect. Or you should use blend modes.
    public class GPUTextureScaler
    {
        /// <summary>
        ///     Returns a scaled copy of given texture.
        /// </summary>
        /// <param name="tex">Source texure to scale</param>
        /// <param name="width">Destination texture width</param>
        /// <param name="height">Destination texture height</param>
        /// <param name="mode">Filtering mode</param>
        public static Texture2D Scaled(Texture2D src, int width, int height, FilterMode mode = FilterMode.Trilinear)
        {
            Rect texR = new(0, 0, width, height);
            _gpu_scale(src, width, height, mode);

            //Get rendered data back to a new texture
            Texture2D result = new(width, height, TextureFormat.ARGB32, true);
            result.Reinitialize(width, height);
            result.ReadPixels(texR, 0, 0, true);
            return result;
        }

        /// <summary>
        ///     Scales the texture data of the given texture.
        /// </summary>
        /// <param name="tex">Texure to scale</param>
        /// <param name="width">New width</param>
        /// <param name="height">New height</param>
        /// <param name="mode">Filtering mode</param>
        public static void Scale(Texture2D tex, int width, int height, FilterMode mode = FilterMode.Trilinear)
        {
            Rect texR = new(0, 0, width, height);
            _gpu_scale(tex, width, height, mode);

            // Update new texture
            tex.Reinitialize(width, height);
            tex.ReadPixels(texR, 0, 0, true);
            tex.Apply(true); //Remove this if you hate us applying textures for you :)
        }

        // Internal unility that renders the source texture into the RTT - the scaling method itself.
        private static void _gpu_scale(Texture2D src, int width, int height, FilterMode fmode)
        {
            //We need the source texture in VRAM because we render with it
            src.filterMode = fmode;
            src.Apply(true);

            //Using RTT for best quality and performance. Thanks, Unity 5
            RenderTexture rtt = new(width, height, 32);

            //Set the RTT in order to render to it
            Graphics.SetRenderTarget(rtt);

            //Setup 2D matrix in range 0..1, so nobody needs to care about sized
            GL.LoadPixelMatrix(0, 1, 1, 0);

            //Then clear & draw the texture to fill the entire RTT.
            GL.Clear(true, true, new Color(0, 0, 0, 0));
            Graphics.DrawTexture(new Rect(0, 0, 1, 1), src);
        }
    }

Credit to the author, which is not me!
I also modified it with some general code quality improvements, but changed nothing about the functionality.

4 Likes

Great Code!
I tried many times to resize texture but couldn’t get good result.
But with this code, the problem has successfully solved.
Thanks @theforgot3n1

Does this work in Unity 2019.3? I get errors when I try to. I don’t think Reinitiatlize is in 2019, sadly, either.

Try texture2D.Resize() - which is obsolete in the latest versions but exists in 2019