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