본문 바로가기

[unity] Resize Image class

앤디가이 2022. 5. 16.

소셜 플랫폼이나 게임 앱에서 다양한 개인 사진을 서버로 업로드 할 때 용량의 문제가 발생한다. Unity에서 큰 사이즈의 Image 파일을 Resize 하여 작은 용량의 상태로 서버로 업로드 할 수 있는 방법에 대해 알아보자.

 

1.  Resize Image를 해야 하는 이유

  - 앱에서 개인 프로필 사진을 로컬에서 불러와서 설정하는 경우를 가정해보자. 사용자는 앱에서 갤러리 라이브러리를 열고, 내 사진을 선택하여 프로필 이미지를 교체하고 이 과정에서 프로필 사진은 바이트로 저장되어 서버로 전송이된다. 문제는 내 갤러리의 사진 사이즈가 2048 을 넘어가게 되면, 서버 부담도 커지고. 다운로드 받을 때 걸리는 시간도 길어질 것이다. 

 - 물론 서버 단에서 이미지 최적화 후 다시 내려주는 경우도 있지만, 클라이언트에서 이미지를 최적화 한 후 서버로 업로드를 해주면 업로드 시 들어가는 패킷 비용도 줄일 수 있기 때문에 클라이언트에서도 이미지를 줄여주는 것도 좋다.

 

  - 이런 고민을 해결해 줄 Unity에서 TextureScaleSetter.cs 클래스를 제작해보고, 적용 방법에 대해 알아보자.

 

2.  TextureScaleSetter 클래스 작성

  - 사용자의 불러온 이미지(Texture2D)의 최대값을 입력하면 최대값에 맞춰 Resize 해준다.

  - 예를 들어 입력된 이미지가 2048*1364이고 최대 값을 1024로 맞출 경우 이 이미지는 X 값은 1024 픽셀로 Resize 되며, Y값을 682 픽셀로 Resize 된다. 이 경우 기존 이미지 용량이 14Mbyte 에서 2.7Mbyte로 줄어들게 된다.

  - 참고로 유니티에서 이미지는 2의 배수 사이즈로 잡아주는 것이 메모리에 좋다. 1025*1025 픽셀의 이미지는 2048*2048의 메모리를 소요한다.

  - TextureScaleSetter.cs 클래스 작성은 아래 코드를 참고하자.

using System.Threading;
using UnityEngine;

/// <summary>
/// 업로드할 이미지 사이즈를 조절해 주는 스크립트.
/// ARBG32 이미지 포멧에서 지원함
/// </summary>
public class TextureScaleSetter
{
    public class ThreadData
    {
        public int start;
        public int end;
        public ThreadData(int s, int e)
        {
            start = s;
            end = e;
        }
    }

    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;

    /// <summary>
    /// 텍스쳐 리사이즈 함수
    /// </summary>
    /// <param name="_texture">대상 텍스쳐</param>
    /// <param name="maxSize">이미지 최대 사이즈</param>
    public static void ResizeTexture(Texture2D _texture, int maxSize)
    {
        int maxAllowResoulution = maxSize;
        int _width = _texture.width;
        int _height = _texture.height;
        if (_width > _height)
        {
            if (_width > maxAllowResoulution)
            {
                float _delta = (float)_width / (float)maxAllowResoulution;
                _height = Mathf.FloorToInt((float)_height / _delta);
                _width = maxAllowResoulution;
            }
        }
        else
        {
            if (_height > maxAllowResoulution)
            {
                float _delta = (float)_height / (float)maxAllowResoulution;
                _width = Mathf.FloorToInt((float)_width / _delta);
                _height = maxAllowResoulution;
            }
        }
        //defalt filtermode는 Bilinear
        Bilinear(_texture, _width, _height);
    }

    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;
        var cores = Mathf.Min(SystemInfo.processorCount, newHeight);
        var 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 ? new ParameterizedThreadStart(BilinearScale) : new ParameterizedThreadStart(PointScale);
                Thread thread = new Thread(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 ThreadData(0, newHeight);
            if (useBilinear)
            {
                BilinearScale(threadData);
            }
            else
            {
                PointScale(threadData);
            }
        }

        tex.Resize(newWidth, newHeight);
        tex.SetPixels(newColors);
        tex.Apply();

        texColors = null;
        newColors = null;
    }

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

            for (var x = 0; x < w2; x++)
            {
                int xFloor = (int)Mathf.Floor(x * ratioX);
                var 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(System.Object obj)
    {
        ThreadData threadData = (ThreadData)obj;
        for (var y = threadData.start; y < threadData.end; y++)
        {
            var thisY = (int)(ratioY * y) * w;
            var yw = y * w2;
            for (var 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);
    }
}

 

3. 테스트 코드 

  - TextureScaleSetter 클래스의 static 함수인 ResizeTexture() 함수를 호출하여 이미지를 Resize 해보자.

  - 두번째 인자값은 이미지가 변경되야 할 최대 픽셀 크기 값를 넘겨준다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TextureTester : MonoBehaviour
{
    public Texture2D texture;

    public void Update()
    {
        if(Input.GetKeyUp(KeyCode.Space))
        {
            TextureScaleSetter.ResizeTexture(texture, 1024);
        }
    }
}

 - Space 키를 눌러보면 Texture2D의 이미지 사이즈가 변경된 걸 확인할 수 있다.

 

댓글