앱인토스 개발자센터 로고
Skip to content

압축 텍스처 최적화

앱인토스 Unity 게임에서 텍스처 압축을 통해 메모리 사용량을 줄이고 로딩 성능을 향상시키는 방법을 다뤄요.

1. 텍스처 압축 전략

앱인토스 텍스처 압축 매니저

c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class TextureCompressionManager : MonoBehaviour
{
    public static TextureCompressionManager Instance { get; private set; }
    
    [System.Serializable]
    public class CompressionProfile
    {
        [Header("압축 설정")]
        public string profileName;
        public TextureImporterCompression compressionType;
        public int compressionQuality = 50;
        public bool generateMipMaps = true;
        
        [Header("해상도 설정")]
        public int maxTextureSize = 1024;
        public bool enableResolutionFallback = true;
        
        [Header("플랫폼별 설정")]
        public PlatformSettings[] platformSettings;
    }
    
    [System.Serializable]
    public class PlatformSettings
    {
        public RuntimePlatform platform;
        public TextureFormat preferredFormat;
        public int maxSize;
        public int compressionQuality;
    }
    
    [Header("압축 프로필")]
    public CompressionProfile[] compressionProfiles;
    
    [Header("실시간 압축 설정")]
    public bool enableRuntimeCompression = true;
    public bool enableAdaptiveQuality = true;
    public float memoryThresholdMB = 100f;
    
    // 내부 상태
    private Dictionary<string, CompressionProfile> profileMap;
    private Dictionary<Texture2D, TextureFormat> originalFormats;
    private Queue<Texture2D> compressionQueue;
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitializeCompressionSystem();
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    void InitializeCompressionSystem()
    {
        profileMap = new Dictionary<string, CompressionProfile>();
        originalFormats = new Dictionary<Texture2D, TextureFormat>();
        compressionQueue = new Queue<Texture2D>();
        
        foreach (var profile in compressionProfiles)
        {
            profileMap[profile.profileName] = profile;
        }
        
        // 메모리 모니터링 시작
        if (enableRuntimeCompression)
        {
            StartCoroutine(MonitorMemoryAndCompress());
        }
        
        Debug.Log("텍스처 압축 시스템 초기화 완료");
    }
    
    IEnumerator MonitorMemoryAndCompress()
    {
        while (enabled)
        {
            float currentMemoryMB = System.GC.GetTotalMemory(false) / (1024f * 1024f);
            
            if (currentMemoryMB > memoryThresholdMB)
            {
                yield return StartCoroutine(CompressLargeTextures());
            }
            
            yield return new WaitForSeconds(5f);
        }
    }
    
    IEnumerator CompressLargeTextures()
    {
        var textures = Resources.FindObjectsOfTypeAll<Texture2D>();
        var largeTextures = new List<Texture2D>();
        
        foreach (var texture in textures)
        {
            if (ShouldCompressTexture(texture))
            {
                largeTextures.Add(texture);
            }
        }
        
        // 크기별로 정렬 (큰 것부터)
        largeTextures.Sort((a, b) => (b.width * b.height).CompareTo(a.width * a.height));
        
        foreach (var texture in largeTextures)
        {
            CompressTexture(texture, "runtime_optimization");
            yield return null; // 프레임 분산
        }
        
        Debug.Log($"실시간 텍스처 압축 완료: {largeTextures.Count}개 텍스처");
    }
    
    bool ShouldCompressTexture(Texture2D texture)
    {
        // 이미 압축된 텍스처인지 확인
        if (IsCompressedFormat(texture.format))
        {
            return false;
        }
        
        // 큰 텍스처인지 확인
        int pixels = texture.width * texture.height;
        if (pixels < 512 * 512) // 512x512 미만은 압축하지 않음
        {
            return false;
        }
        
        // 중요한 UI 텍스처인지 확인
        if (texture.name.Contains("UI_") || texture.name.Contains("Icon_"))
        {
            return false;
        }
        
        return true;
    }
    
    public void CompressTexture(Texture2D texture, string profileName)
    {
        if (!profileMap.ContainsKey(profileName))
        {
            Debug.LogWarning($"압축 프로필을 찾을 수 없습니다: {profileName}");
            return;
        }
        
        var profile = profileMap[profileName];
        
        // 원본 포맷 저장
        if (!originalFormats.ContainsKey(texture))
        {
            originalFormats[texture] = texture.format;
        }
        
        // 플랫폼별 최적 포맷 선택
        var targetFormat = GetOptimalFormatForPlatform(texture, profile);
        
        // 텍스처 압축 수행
        StartCoroutine(CompressTextureAsync(texture, targetFormat, profile));
    }
    
    TextureFormat GetOptimalFormatForPlatform(Texture2D texture, CompressionProfile profile)
    {
        var currentPlatform = Application.platform;
        
        foreach (var platformSetting in profile.platformSettings)
        {
            if (platformSetting.platform == currentPlatform)
            {
                return platformSetting.preferredFormat;
            }
        }
        
        // 기본 포맷 선택
        if (HasAlpha(texture))
        {
            return TextureFormat.DXT5; // 알파 채널이 있는 경우
        }
        else
        {
            return TextureFormat.DXT1; // RGB만 있는 경우
        }
    }
    
    bool HasAlpha(Texture2D texture)
    {
        return texture.format == TextureFormat.RGBA32 ||
               texture.format == TextureFormat.ARGB32 ||
               texture.format == TextureFormat.RGBA4444 ||
               texture.format == TextureFormat.ARGB4444;
    }
    
    IEnumerator CompressTextureAsync(Texture2D texture, TextureFormat targetFormat, CompressionProfile profile)
    {
        Debug.Log($"텍스처 압축 시작: {texture.name} ({texture.format} → {targetFormat})");
        float startTime = Time.realtimeSinceStartup;
        
        // 원본 데이터 백업
        var originalPixels = texture.GetPixels32();
        var originalWidth = texture.width;
        var originalHeight = texture.height;
        
        try
        {
            // 해상도 조정이 필요한 경우
            if (originalWidth > profile.maxTextureSize || originalHeight > profile.maxTextureSize)
            {
                var newSize = CalculateTargetSize(originalWidth, originalHeight, profile.maxTextureSize);
                yield return StartCoroutine(ResizeTexture(texture, newSize.x, newSize.y));
            }
            
            // 압축 실행
            EditorApplication.delayCall += () => {
                try 
                {
                    texture.Compress(true);
                    Debug.Log($"텍스처 압축 완료: {texture.name}");
                }
                catch (System.Exception e)
                {
                    Debug.LogError($"텍스처 압축 실패: {texture.name} - {e.Message}");
                }
            };
            
            yield return new WaitForSeconds(0.1f); // 압축 완료 대기
            
            float compressionTime = Time.realtimeSinceStartup - startTime;
            
            // 압축 결과 분석
            AnalyzeCompressionResult(texture, originalWidth, originalHeight, compressionTime);
        }
        catch (System.Exception e)
        {
            Debug.LogError($"텍스처 압축 중 오류 발생: {e.Message}");
            
            // 실패시 원본 복구
            RestoreOriginalTexture(texture, originalPixels, originalWidth, originalHeight);
        }
    }
    
    Vector2Int CalculateTargetSize(int width, int height, int maxSize)
    {
        float aspectRatio = (float)width / height;
        
        if (width > height)
        {
            return new Vector2Int(maxSize, Mathf.RoundToInt(maxSize / aspectRatio));
        }
        else
        {
            return new Vector2Int(Mathf.RoundToInt(maxSize * aspectRatio), maxSize);
        }
    }
    
    IEnumerator ResizeTexture(Texture2D texture, int newWidth, int newHeight)
    {
        var rt = RenderTexture.GetTemporary(newWidth, newHeight);
        Graphics.Blit(texture, rt);
        
        var resizedTexture = new Texture2D(newWidth, newHeight, texture.format, false);
        
        RenderTexture.active = rt;
        resizedTexture.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0);
        resizedTexture.Apply();
        
        RenderTexture.active = null;
        RenderTexture.ReleaseTemporary(rt);
        
        // 원본 텍스처 데이터를 새로운 크기로 교체
        texture.Resize(newWidth, newHeight);
        texture.SetPixels32(resizedTexture.GetPixels32());
        texture.Apply();
        
        DestroyImmediate(resizedTexture);
        
        yield return null;
    }
    
    void RestoreOriginalTexture(Texture2D texture, Color32[] originalPixels, int originalWidth, int originalHeight)
    {
        texture.Resize(originalWidth, originalHeight);
        texture.SetPixels32(originalPixels);
        texture.Apply();
        
        Debug.Log($"텍스처 원본 복구: {texture.name}");
    }
    
    void AnalyzeCompressionResult(Texture2D texture, int originalWidth, int originalHeight, float compressionTime)
    {
        // 압축 전후 메모리 사용량 계산
        long originalSize = (long)originalWidth * originalHeight * 4; // RGBA32 기준
        long compressedSize = CalculateCompressedSize(texture);
        
        float compressionRatio = (float)originalSize / compressedSize;
        
        var analyticsData = new Dictionary<string, object>
        {
            {"texture_name", texture.name},
            {"original_width", originalWidth},
            {"original_height", originalHeight},
            {"compressed_width", texture.width},
            {"compressed_height", texture.height},
            {"original_format", originalFormats.ContainsKey(texture) ? originalFormats[texture].ToString() : "Unknown"},
            {"compressed_format", texture.format.ToString()},
            {"original_size_bytes", originalSize},
            {"compressed_size_bytes", compressedSize},
            {"compression_ratio", compressionRatio},
            {"compression_time", compressionTime},
            {"device_model", SystemInfo.deviceModel},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("texture_compression", analyticsData);
        
        Debug.Log($"텍스처 압축 분석: {texture.name} - 압축률 {compressionRatio:F2}x, 시간 {compressionTime:F2}초");
    }
    
    long CalculateCompressedSize(Texture2D texture)
    {
        switch (texture.format)
        {
            case TextureFormat.DXT1:
                return texture.width * texture.height / 2; // 4비트 per pixel
            case TextureFormat.DXT5:
                return texture.width * texture.height; // 8비트 per pixel
            case TextureFormat.PVRTC_RGB2:
            case TextureFormat.PVRTC_RGBA2:
                return texture.width * texture.height / 4; // 2비트 per pixel
            case TextureFormat.PVRTC_RGB4:
            case TextureFormat.PVRTC_RGBA4:
                return texture.width * texture.height / 2; // 4비트 per pixel
            case TextureFormat.ETC_RGB4:
            case TextureFormat.ETC2_RGB:
                return texture.width * texture.height / 2; // 4비트 per pixel
            case TextureFormat.ETC2_RGBA8:
                return texture.width * texture.height; // 8비트 per pixel
            default:
                return texture.width * texture.height * 4; // 기본값 (RGBA32)
        }
    }
    
    bool IsCompressedFormat(TextureFormat format)
    {
        switch (format)
        {
            case TextureFormat.DXT1:
            case TextureFormat.DXT5:
            case TextureFormat.PVRTC_RGB2:
            case TextureFormat.PVRTC_RGBA2:
            case TextureFormat.PVRTC_RGB4:
            case TextureFormat.PVRTC_RGBA4:
            case TextureFormat.ETC_RGB4:
            case TextureFormat.ETC2_RGB:
            case TextureFormat.ETC2_RGBA8:
            case TextureFormat.ASTC_4x4:
            case TextureFormat.ASTC_6x6:
            case TextureFormat.ASTC_8x8:
                return true;
            default:
                return false;
        }
    }
    
    // 공개 API
    public void CompressAllTextures(string profileName = "default")
    {
        var textures = Resources.FindObjectsOfTypeAll<Texture2D>();
        
        foreach (var texture in textures)
        {
            if (ShouldCompressTexture(texture))
            {
                compressionQueue.Enqueue(texture);
            }
        }
        
        StartCoroutine(ProcessCompressionQueue(profileName));
    }
    
    IEnumerator ProcessCompressionQueue(string profileName)
    {
        Debug.Log($"텍스처 압축 큐 처리 시작: {compressionQueue.Count}개 텍스처");
        
        while (compressionQueue.Count > 0)
        {
            var texture = compressionQueue.Dequeue();
            CompressTexture(texture, profileName);
            
            yield return new WaitForSeconds(0.1f); // 처리 간격
        }
        
        Debug.Log("텍스처 압축 큐 처리 완료");
    }
    
    public void RestoreTexture(Texture2D texture)
    {
        if (originalFormats.ContainsKey(texture))
        {
            // 원본 포맷 복구 로직 (실제 구현 시 원본 데이터 보존 필요)
            Debug.Log($"텍스처 복구: {texture.name} → {originalFormats[texture]}");
        }
    }
    
    public float GetCompressionRatio()
    {
        var textures = Resources.FindObjectsOfTypeAll<Texture2D>();
        long totalOriginal = 0;
        long totalCompressed = 0;
        
        foreach (var texture in textures)
        {
            if (originalFormats.ContainsKey(texture))
            {
                totalOriginal += texture.width * texture.height * 4; // 원본 RGBA32 추정
                totalCompressed += CalculateCompressedSize(texture);
            }
        }
        
        return totalOriginal > 0 ? (float)totalOriginal / totalCompressed : 1f;
    }
    
    public Dictionary<string, object> GetCompressionStats()
    {
        var textures = Resources.FindObjectsOfTypeAll<Texture2D>();
        var stats = new Dictionary<string, object>
        {
            {"total_textures", textures.Length},
            {"compressed_textures", 0},
            {"total_memory_mb", 0f},
            {"compression_ratio", GetCompressionRatio()}
        };
        
        int compressedCount = 0;
        long totalMemory = 0;
        
        foreach (var texture in textures)
        {
            if (IsCompressedFormat(texture.format))
            {
                compressedCount++;
            }
            totalMemory += CalculateCompressedSize(texture);
        }
        
        stats["compressed_textures"] = compressedCount;
        stats["total_memory_mb"] = totalMemory / (1024f * 1024f);
        
        return stats;
    }
}

텍스처 압축은 모바일 게임의 핵심 최적화 요소에요.
플랫폼별 최적 압축 포맷을 선택하고, 시각적 품질과 메모리 효율성 사이의 균형을 잘 맞추는 것이 중요해요.