앱인토스 개발자센터 로고
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;
    }
}

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