Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

How to resize/scale down texture without losing quality?

Discussion in 'Editor & General Support' started by swifter14, Sep 26, 2020.

  1. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    166
    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.
     
  2. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,558
  3. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    166
    Yes as I mentioned it's what I use... and you see the bad result
     
  4. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,558
    You could assign it to a material and render that:

    Code (CSharp):
    1. public static Texture2D RenderMaterial(ref Material material, Vector2Int resolution, string filename = "")
    2. {
    3.   RenderTexture renderTexture = RenderTexture.GetTemporary(resolution.x, resolution.y);
    4.   Graphics.Blit(null, renderTexture, material);
    5.  
    6.   Texture2D texture = new Texture2D(resolution.x, resolution.y, TextureFormat.ARGB32, false);
    7.   texture.filterMode = FilterMode.Bilinear;
    8.   texture.wrapMode = TextureWrapMode.Clamp;
    9.   RenderTexture.active = renderTexture;
    10.   texture.ReadPixels(new Rect(Vector2.zero, resolution), 0, 0);
    11. #if UNITY_EDITOR
    12.   // optional, if you want to save it:
    13.   if (filename.Length != 0)
    14.   {
    15.     byte[] png = texture.EncodeToPNG();
    16.     File.WriteAllBytes(filename, png);
    17.     AssetDatabase.Refresh();
    18.   }
    19. #endif
    20.   RenderTexture.active = null;
    21.   RenderTexture.ReleaseTemporary(renderTexture);
    22.  
    23.   return texture;
    24. }
    Usage:
    Code (CSharp):
    1.  
    2. 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.
     
    Last edited: Jul 1, 2021
    qsw745 and Lu_Atar like this.
  5. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    166
    Your function takes material not texture2d, I have texture2d which I want to resize through script
     
  6. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,558
    Code (CSharp):
    1. public Material material; // set to the material you want to use (probably want to pick one that's unlit).
    2. public Texture2D ResizeTexture(Texture2D originalTexture, int newWidth, int newHeight)
    3. {
    4.   material.SetTexture("_MainTex", originalTexture);  // or whichever the main texture property name is
    5.   // material.mainTexture = originalTexture; // or can do this instead with some materials
    6.   return RenderMaterial(ref material, new Vector2Int(newWidth, newHeight));
    7. }
    Then it's as easy as this..
    Code (CSharp):
    1. Texture2D resizedTexture = ResizeTexture(originalTexture, 50, 50);
     
  7. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    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.
     
    SlimeProphet likes this.
  8. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    166
    @polemical thanks but your function returns a black texture. The material loads the texture, but after running RenderMaterial I get a black texture.

    @KokkuHub 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
     
    Last edited: Sep 27, 2020
  9. Lu_Atar

    Lu_Atar

    Joined:
    May 16, 2017
    Posts:
    8
    The function works for me, but after I added

    texture.Apply();

    after

    texture.ReadPixels(new Rect(Vector2.zero, resolution), 0, 0);
     
    adamgolden likes this.
  10. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,558
    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.
     
    Last edited: Jul 1, 2021
  11. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    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.
     
  12. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    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


    Code (CSharp):
    1. public class TextureScale
    2.     {
    3.         private static Color[] texColors;
    4.         private static Color[] newColors;
    5.         private static int w;
    6.         private static float ratioX;
    7.         private static float ratioY;
    8.         private static int w2;
    9.         private static int finishCount;
    10.         private static Mutex mutex;
    11.  
    12.         public static void Point(Texture2D tex, int newWidth, int newHeight)
    13.         {
    14.             ThreadedScale(tex, newWidth, newHeight, false);
    15.         }
    16.  
    17.         public static void Bilinear(Texture2D tex, int newWidth, int newHeight)
    18.         {
    19.             ThreadedScale(tex, newWidth, newHeight, true);
    20.         }
    21.  
    22.         private static void ThreadedScale(Texture2D tex, int newWidth, int newHeight, bool useBilinear)
    23.         {
    24.             texColors = tex.GetPixels();
    25.             newColors = new Color[newWidth * newHeight];
    26.             if (useBilinear)
    27.             {
    28.                 ratioX = 1.0f / ((float)newWidth / (tex.width - 1));
    29.                 ratioY = 1.0f / ((float)newHeight / (tex.height - 1));
    30.             }
    31.             else
    32.             {
    33.                 ratioX = (float)tex.width / newWidth;
    34.                 ratioY = (float)tex.height / newHeight;
    35.             }
    36.  
    37.             w = tex.width;
    38.             w2 = newWidth;
    39.             int cores = Mathf.Min(SystemInfo.processorCount, newHeight);
    40.             int slice = newHeight / cores;
    41.  
    42.             finishCount = 0;
    43.             if (mutex == null)
    44.             {
    45.                 mutex = new Mutex(false);
    46.             }
    47.  
    48.             if (cores > 1)
    49.             {
    50.                 int i = 0;
    51.                 ThreadData threadData;
    52.                 for (i = 0; i < cores - 1; i++)
    53.                 {
    54.                     threadData = new ThreadData(slice * i, slice * (i + 1));
    55.                     ParameterizedThreadStart
    56.                         ts = useBilinear ? BilinearScale : new ParameterizedThreadStart(PointScale);
    57.                     Thread thread = new(ts);
    58.                     thread.Start(threadData);
    59.                 }
    60.  
    61.                 threadData = new ThreadData(slice * i, newHeight);
    62.                 if (useBilinear)
    63.                 {
    64.                     BilinearScale(threadData);
    65.                 }
    66.                 else
    67.                 {
    68.                     PointScale(threadData);
    69.                 }
    70.  
    71.                 while (finishCount < cores)
    72.                 {
    73.                     Thread.Sleep(1);
    74.                 }
    75.             }
    76.             else
    77.             {
    78.                 ThreadData threadData = new(0, newHeight);
    79.                 if (useBilinear)
    80.                 {
    81.                     BilinearScale(threadData);
    82.                 }
    83.                 else
    84.                 {
    85.                     PointScale(threadData);
    86.                 }
    87.             }
    88.  
    89.             tex.Reinitialize(newWidth, newHeight);
    90. #pragma warning disable UNT0017 // SetPixels invocation is slow
    91.             tex.SetPixels(newColors);
    92. #pragma warning restore UNT0017 // SetPixels invocation is slow
    93.             tex.Apply();
    94.  
    95.             texColors = null;
    96.             newColors = null;
    97.         }
    98.  
    99.         public static void BilinearScale(object obj)
    100.         {
    101.             ThreadData threadData = (ThreadData)obj;
    102.             for (int y = threadData.start; y < threadData.end; y++)
    103.             {
    104.                 int yFloor = (int)Mathf.Floor(y * ratioY);
    105.                 int y1 = yFloor * w;
    106.                 int y2 = (yFloor + 1) * w;
    107.                 int yw = y * w2;
    108.  
    109.                 for (int x = 0; x < w2; x++)
    110.                 {
    111.                     int xFloor = (int)Mathf.Floor(x * ratioX);
    112.                     float xLerp = (x * ratioX) - xFloor;
    113.                     newColors[yw + x] = ColorLerpUnclamped(
    114.                         ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp),
    115.                         ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp),
    116.                         (y * ratioY) - yFloor);
    117.                 }
    118.             }
    119.  
    120.             mutex.WaitOne();
    121.             finishCount++;
    122.             mutex.ReleaseMutex();
    123.         }
    124.  
    125.         public static void PointScale(object obj)
    126.         {
    127.             ThreadData threadData = (ThreadData)obj;
    128.             for (int y = threadData.start; y < threadData.end; y++)
    129.             {
    130.                 int thisY = (int)(ratioY * y) * w;
    131.                 int yw = y * w2;
    132.                 for (int x = 0; x < w2; x++)
    133.                 {
    134.                     newColors[yw + x] = texColors[(int)(thisY + (ratioX * x))];
    135.                 }
    136.             }
    137.  
    138.             mutex.WaitOne();
    139.             finishCount++;
    140.             mutex.ReleaseMutex();
    141.         }
    142.  
    143.         private static Color ColorLerpUnclamped(Color c1, Color c2, float value)
    144.         {
    145.             return new Color(c1.r + ((c2.r - c1.r) * value),
    146.                 c1.g + ((c2.g - c1.g) * value),
    147.                 c1.b + ((c2.b - c1.b) * value),
    148.                 c1.a + ((c2.a - c1.a) * value));
    149.         }
    150.  
    151.         public class ThreadData
    152.         {
    153.             public int end;
    154.             public int start;
    155.  
    156.             public ThreadData(int s, int e)
    157.             {
    158.                 this.start = s;
    159.                 this.end = e;
    160.             }
    161.         }
    162.     }
     
    cdr9042 likes this.
  13. Saaskun

    Saaskun

    Joined:
    Nov 29, 2019
    Posts:
    51
    I got a corrupted error using Point scaler, can someone check this too?
    the texture has read/write enabled and point filter
     
  14. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    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.


    Code (CSharp):
    1. using UnityEngine;
    2.  
    3.     /// A unility class with functions to scale Texture2D Data.
    4.     ///
    5.     /// Scale is performed on the GPU using RTT, so it's blazing fast.
    6.     /// Setting up and Getting back the texture data is the bottleneck.
    7.     /// But Scaling itself costs only 1 draw call and 1 RTT State setup!
    8.     /// WARNING: This script override the RTT Setup! (It sets a RTT!)  
    9.     ///
    10.     /// Note: This scaler does NOT support aspect ratio based scaling. You will have to do it yourself!
    11.     /// It supports Alpha, but you will have to divide by alpha in your shaders,
    12.     /// because of premultiplied alpha effect. Or you should use blend modes.
    13.     public class GPUTextureScaler
    14.     {
    15.         /// <summary>
    16.         ///     Returns a scaled copy of given texture.
    17.         /// </summary>
    18.         /// <param name="tex">Source texure to scale</param>
    19.         /// <param name="width">Destination texture width</param>
    20.         /// <param name="height">Destination texture height</param>
    21.         /// <param name="mode">Filtering mode</param>
    22.         public static Texture2D Scaled(Texture2D src, int width, int height, FilterMode mode = FilterMode.Trilinear)
    23.         {
    24.             Rect texR = new(0, 0, width, height);
    25.             _gpu_scale(src, width, height, mode);
    26.  
    27.             //Get rendered data back to a new texture
    28.             Texture2D result = new(width, height, TextureFormat.ARGB32, true);
    29.             result.Reinitialize(width, height);
    30.             result.ReadPixels(texR, 0, 0, true);
    31.             return result;
    32.         }
    33.  
    34.         /// <summary>
    35.         ///     Scales the texture data of the given texture.
    36.         /// </summary>
    37.         /// <param name="tex">Texure to scale</param>
    38.         /// <param name="width">New width</param>
    39.         /// <param name="height">New height</param>
    40.         /// <param name="mode">Filtering mode</param>
    41.         public static void Scale(Texture2D tex, int width, int height, FilterMode mode = FilterMode.Trilinear)
    42.         {
    43.             Rect texR = new(0, 0, width, height);
    44.             _gpu_scale(tex, width, height, mode);
    45.  
    46.             // Update new texture
    47.             tex.Reinitialize(width, height);
    48.             tex.ReadPixels(texR, 0, 0, true);
    49.             tex.Apply(true); //Remove this if you hate us applying textures for you :)
    50.         }
    51.  
    52.         // Internal unility that renders the source texture into the RTT - the scaling method itself.
    53.         private static void _gpu_scale(Texture2D src, int width, int height, FilterMode fmode)
    54.         {
    55.             //We need the source texture in VRAM because we render with it
    56.             src.filterMode = fmode;
    57.             src.Apply(true);
    58.  
    59.             //Using RTT for best quality and performance. Thanks, Unity 5
    60.             RenderTexture rtt = new(width, height, 32);
    61.  
    62.             //Set the RTT in order to render to it
    63.             Graphics.SetRenderTarget(rtt);
    64.  
    65.             //Setup 2D matrix in range 0..1, so nobody needs to care about sized
    66.             GL.LoadPixelMatrix(0, 1, 1, 0);
    67.  
    68.             //Then clear & draw the texture to fill the entire RTT.
    69.             GL.Clear(true, true, new Color(0, 0, 0, 0));
    70.             Graphics.DrawTexture(new Rect(0, 0, 1, 1), src);
    71.         }
    72.     }
    Credit to the author, which is not me!
    I also modified it with some general code quality improvements, but changed nothing about the functionality.
     
  15. iceberg_alex

    iceberg_alex

    Joined:
    Oct 16, 2023
    Posts:
    1

    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
     
  16. Bakenshake

    Bakenshake

    Joined:
    Mar 26, 2018
    Posts:
    1
    Does this work in Unity 2019.3? I get errors when I try to. I don't think Reinitiatlize is in 2019, sadly, either.
     
  17. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,530
    Try texture2D.Resize() - which is obsolete in the latest versions but exists in 2019