렌더링 기초 (2) - 텍스처와 압축 - soo:bak
작성일 :
메쉬 위에 색을 입히다
Part 1에서 메쉬가 정점(Vertex)과 삼각형으로 3D 형태를 정의하는 구조임을 살펴보았습니다.
하지만 메쉬만으로는 형태만 있을 뿐, 표면에 색이나 질감이 없습니다. 정점과 삼각형은 “어떤 모양인가”만 알려주고, “어떤 색인가”는 알려주지 않습니다. 캐릭터의 피부색, 갑옷의 금속 질감, 바닥의 나무 무늬 — 이런 시각 정보는 메쉬 자체에 포함되어 있지 않습니다. 이 표면에 색과 질감을 입히는 것이 텍스처(Texture)입니다. 텍스처는 색상 정보를 담은 2D 이미지이며, 이 이미지를 3D 메쉬의 표면에 감싸서 색상을 부여합니다.
1
2
3
4
5
6
7
메쉬만 있는 상태 텍스처를 입힌 상태
/\ /\
/ \ / \
/ \ ← 형태만 있는 삼각형 / .. \ ← 색과 무늬가 있는 삼각형
/ \ /.''. \
/________\ /________\
텍스처의 구조
텍스처는 가로(Width) x 세로(Height) 크기의 2D 픽셀 격자이며, 각 픽셀이 색상 정보를 담고 있습니다. 가장 흔한 형식은 RGBA로, 빨강(Red), 초록(Green), 파랑(Blue), 투명도(Alpha) 네 채널을 각각 8비트(0~255)로 표현합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
텍스처 (4 x 4 예시, RGBA 32bit)
열 0 열 1 열 2 열 3
┌─────────┬─────────┬─────────┬─────────┐
행 0│ R G B A │ R G B A │ R G B A │ R G B A │ ← 4 픽셀 x 4 바이트 = 16 바이트
├─────────┼─────────┼─────────┼─────────┤
행 1│ R G B A │ R G B A │ R G B A │ R G B A │
├─────────┼─────────┼─────────┼─────────┤
행 2│ R G B A │ R G B A │ R G B A │ R G B A │
├─────────┼─────────┼─────────┼─────────┤
행 3│ R G B A │ R G B A │ R G B A │ R G B A │
└─────────┴─────────┴─────────┴─────────┘
총 크기: 4 x 4 x 4 바이트 = 64 바이트
한 픽셀이 4바이트(32비트)를 차지합니다. 텍스처 내부의 각 픽셀을 텍셀(Texel, Texture Element)이라고 부릅니다. 텍셀은 텍스처 데이터에 저장된 색상 값이고, 픽셀은 화면에 실제로 표시되는 점입니다. 화면 픽셀 하나에 여러 텍셀이 대응되기도 하고, 반대로 텍셀 하나가 여러 픽셀에 걸치기도 합니다. GPU가 렌더링할 때 텍셀 값을 읽어 화면 픽셀의 색을 결정하므로, 텍셀과 픽셀은 다른 개념입니다.
UV 매핑 — 3D 표면과 2D 텍스처의 대응
메쉬는 3D 공간에 존재하고, 텍스처는 2D 이미지입니다. 3D 표면의 각 지점이 2D 텍스처의 어디에 해당하는지를 정해야 합니다. 이 대응 관계를 UV 매핑(UV Mapping)이라고 합니다.
UV 좌표계
UV 좌표계에서 U는 가로 축, V는 세로 축입니다. 두 축 모두 0에서 1 사이의 값을 사용합니다. (0, 0)은 텍스처의 왼쪽 아래, (1, 1)은 오른쪽 위입니다.
1
2
3
4
5
6
7
8
9
10
11
V
1 ┌────────────────────────────────┐
│ │
│ 텍스처 이미지 │
│ │
│ (0.5, 0.5) │
│ * │
│ │
│ │
0 └────────────────────────────────┘ U
0 1
3D 공간 좌표로 X, Y, Z를 이미 사용하고 있으므로, 텍스처 좌표에는 알파벳 순서상 X, Y, Z 바로 앞에 오는 U와 V를 사용합니다.
정점에 UV 좌표 부여
Part 1에서 메쉬의 정점이 위치(x, y, z) 외에 법선, 색상 등의 추가 데이터를 가진다고 했습니다. UV 좌표도 그 추가 데이터 중 하나입니다. 메쉬의 각 정점에는 (u, v) 값이 지정되어 있고, 이 값이 텍스처의 어느 위치를 참조할지 결정합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
3D 메쉬의 삼각형 2D 텍스처
A (0.5, 1.0) V
/ \ 1 ┌──────────────────┐
/ \ │ A │
/ \ │ / \ │
/ \ │ / \ │
B─────────C │ B/─────\C │
(0,0) (1,0) 0 └──────────────────┘
0 1 U
정점 A: 위치(x,y,z) + UV(0.5, 1.0)
정점 B: 위치(x,y,z) + UV(0.0, 0.0)
정점 C: 위치(x,y,z) + UV(1.0, 0.0)
삼각형 내부의 각 픽셀은 세 정점의 UV 좌표를 보간(Interpolation)하여 텍스처의 어느 텍셀을 읽을지 계산합니다. 보간이란 알려진 두 개 이상의 값 사이에서 중간값을 추정하는 방법입니다. 삼각형 한가운데에 위치한 픽셀이라면 세 정점의 UV 좌표를 거리 비율에 따라 가중 평균한 값을 사용하고, 한쪽 정점에 가까운 픽셀이라면 해당 정점의 UV 좌표에 더 가까운 값을 사용합니다.
옷감을 펼쳐놓는 것과 같다
UV 매핑을 직관적으로 이해하는 비유는 옷감입니다. 3D 캐릭터 모델을 가위로 잘라 납작하게 펼쳐놓는 과정과 같습니다. 펼쳐진 평면이 UV 공간이고, 그 위에 색을 칠한 것이 텍스처입니다.
1
2
3
4
5
6
7
3D 모델 UV 펼치기 텍스처 적용
┌─┐ ┌──────┐ ┌──────┐
│ │ ──────► │ │ ◄─── │ 색상 │
│ │ 가위로 │ │ 텍스처 │ 무늬 │
│ │ 잘라서 │ │ 입히기 │ 질감 │
└─┘ 펼침 └──────┘ └──────┘
모델링 소프트웨어(Blender, Maya 등)에서 아티스트가 UV 펼치기 작업을 수행하면, 각 정점에 UV 좌표가 할당됩니다. Unity는 이 UV 좌표를 그대로 사용하여 텍스처를 메쉬에 매핑합니다.
텍스처 해상도와 메모리
UV 매핑이 텍스처를 메쉬 표면에 대응시키는 방법이었다면, 이제 텍스처 자체의 크기가 메모리와 성능에 미치는 영향을 다룹니다.
텍스처의 해상도가 높을수록 표면이 선명하게 보입니다. 하지만 해상도가 높다는 것은 텍셀 수가 많다는 뜻이고, 그만큼 GPU 메모리를 많이 차지합니다.
RGBA 32bit 기준 메모리 계산
RGBA 32비트 형식은 텍셀 하나에 빨강(R), 초록(G), 파랑(B), 투명도(A) 네 채널을 각각 8비트(1바이트)씩, 총 4바이트로 저장합니다. 텍스처의 메모리 사용량은 가로 x 세로 x 텍셀당 바이트 수로 계산합니다.
1
2
3
4
5
6
메모리 = 가로 x 세로 x 텍셀당 바이트 수
512 x 512 x 4 = 1,048,576 바이트 = 1 MB
1024 x 1024 x 4 = 4,194,304 바이트 = 4 MB
2048 x 2048 x 4 = 16,777,216 바이트 = 16 MB
4096 x 4096 x 4 = 67,108,864 바이트 = 64 MB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
해상도에 따른 메모리 증가 (RGBA 32bit, 비압축)
메모리
(MB)
64 │ ■
│
│
│
32 │
│
16 │ ■
│
4 │ ■
1 │ ■
└──────────────────────────────────────────
512x512 1024x1024 2048x2048 4096x4096
해상도
가로와 세로가 각각 두 배가 되면 텍셀 수는 네 배가 됩니다. 1024x1024에서 2048x2048로 올리면 메모리가 4MB에서 16MB로 네 배 증가하고, 2048x2048에서 4096x4096으로 올리면 16MB에서 64MB로 또 네 배 증가합니다.
모바일에서 텍스처 메모리의 비중
모바일 기기의 GPU가 사용할 수 있는 메모리는 데스크톱에 비해 제한적입니다. 데스크톱 GPU는 전용 비디오 메모리(VRAM)를 수 GB 이상 갖추고 있지만, 모바일 GPU는 CPU와 메모리를 공유하는 구조이므로 텍스처에 할당할 수 있는 용량이 훨씬 적습니다.
이런 환경에서 텍스처는 전체 GPU 메모리 사용량의 50~70%를 차지하는 경우가 흔합니다.
1
2
3
4
5
6
모바일 게임의 GPU 메모리 분포 (전형적인 예시)
텍스처 60% ████████████████████████████████████
메쉬 버퍼 15% █████████
렌더 타깃 15% █████████
셰이더, 기타 10% ██████
캐릭터 하나에 Diffuse(기본 색상), Normal(표면 굴곡), Mask(재질 구분) 등 여러 장의 텍스처가 필요합니다. 캐릭터가 여러 명이고, 배경과 이펙트도 있다면 텍스처 메모리는 빠르게 증가합니다.
앞서 계산한 수치를 적용하면, 2048x2048 비압축 텍스처 한 장이 16MB입니다. 캐릭터 한 명에 이런 텍스처가 3장이면 48MB, 화면에 캐릭터가 5명이면 240MB입니다. 모바일 기기에서 이 수치는 감당하기 어렵습니다. 텍스처의 메모리 사용량을 줄이는 것이 모바일 최적화의 핵심 과제인 이유입니다.
밉맵(Mipmap) — 거리에 따른 텍스처 최적화
텍스처의 해상도가 높을수록 메모리를 많이 소비합니다. 그런데 메모리 외에도 고해상도 텍스처가 성능 문제를 일으키는 상황이 있습니다.
멀리 있는 오브젝트의 문제
카메라에서 멀리 떨어진 오브젝트는 화면에서 작게 보입니다. 2048x2048 텍스처를 가진 오브젝트가 화면에서 50x50 픽셀 크기로 그려진다고 가정합니다.
1
2
3
4
5
6
7
8
9
10
11
2048 x 2048 텍스처 화면에 표시되는 크기
┌─────────────────┐ ┌──┐
│ │ │ │ 50 x 50 픽셀
│ │ └──┘
│ 약 420만 개의 │
│ 텍셀 │ 실제로 사용되는 텍셀:
│ │ 약 2,500개
│ │
│ │ 나머지 약 419만 개의 텍셀은
└─────────────────┘ 읽혀도 최종 결과에 기여하지 않음
화면의 한 픽셀이 텍스처의 수십 개 텍셀에 대응됩니다. GPU는 이 수십 개 텍셀 중에서 어떤 값을 사용할지 결정해야 합니다. 이때 두 가지 문제가 발생합니다.
첫째, 에일리어싱(Aliasing)입니다. 화면 픽셀 하나에 대응되는 텍셀이 너무 많으면, 카메라가 조금만 움직여도 참조하는 텍셀이 크게 바뀝니다. 이로 인해 텍스처가 깜빡이거나, 규칙적인 간섭 무늬인 모아레(Moire) 패턴이 나타납니다.
둘째, 대역폭 낭비입니다. GPU가 텍스처에서 값을 읽어오는 행위를 텍스처 샘플링(Texture Sampling)이라 합니다. 샘플링할 때마다 GPU는 메모리에서 텍셀 데이터를 읽어오는데, 이 읽기 속도에는 한계가 있습니다. GPU가 메모리에서 데이터를 읽고 쓸 수 있는 초당 전송량을 메모리 대역폭(Memory Bandwidth)이라 하며, 모바일 GPU의 대역폭은 데스크톱 GPU보다 훨씬 제한적입니다. 화면에 50×50 픽셀로 보이는 오브젝트를 위해 2048×2048 텍스처의 넓은 영역을 샘플링하면, 실제 필요한 것보다 훨씬 많은 텍셀을 읽게 되어 이 제한된 대역폭이 낭비됩니다. 이 문제는 GPU 내부의 텍스처 캐시(Texture Cache)와도 관련됩니다. 텍스처 캐시는 최근에 읽은 텍셀 데이터를 임시로 보관하며, 인접한 텍셀을 한 번에 읽어오도록 설계되어 있습니다. 넓은 영역에서 듬성듬성 텍셀을 가져오면 캐시에 보관된 데이터가 재사용되지 못하고, 그만큼 메모리 읽기가 추가로 발생하여 대역폭 소비가 더 늘어납니다.
밉맵의 원리
밉맵(Mipmap)은 이 두 문제를 동시에 해결합니다. 원본 텍스처의 축소 버전을 미리 만들어 두는 방식입니다. Mip은 라틴어 “multum in parvo(작은 공간에 많은 것)”의 약자입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
밉맵 체인 (Mip Chain)
Level 0: 2048 x 2048 (원본)
Level 1: 1024 x 1024
Level 2: 512 x 512
Level 3: 256 x 256
Level 4: 128 x 128
Level 5: 64 x 64
Level 6: 32 x 32
Level 7: 16 x 16
Level 8: 8 x 8
Level 9: 4 x 4
Level 10: 2 x 2
Level 11: 1 x 1
각 레벨은 이전 레벨의 가로, 세로를 절반으로 줄인 것입니다. 축소할 때 인접한 4개의 텍셀을 평균하여 1개의 텍셀로 합칩니다. 이 과정을 1x1이 될 때까지 반복합니다.
1
2
3
4
5
6
7
8
9
10
11
12
Level 0 (4x4) Level 1 (2x2) Level 2 (1x1)
┌────┬────┬────┬────┐ ┌─────────┬─────────┐ ┌───────────────────┐
│ 10 │ 20 │ 30 │ 40 │ │ │ │ │ │
├────┼────┼────┼────┤ │ 15 │ 35 │ │ │
│ 10 │ 20 │ 30 │ 40 │ ──► │ │ │ ──► │ 25 │
├────┼────┼────┼────┤ ├─────────┼─────────┤ │ │
│ 10 │ 20 │ 30 │ 40 │ │ │ │ │ │
├────┼────┼────┼────┤ │ 15 │ 35 │ └───────────────────┘
│ 10 │ 20 │ 30 │ 40 │ │ │ │
└────┴────┴────┴────┘ └─────────┴─────────┘
(10+20+10+20)/4=15
밉맵 레벨 선택
밉맵 체인이 준비되면, GPU는 화면 픽셀 하나가 텍스처의 텍셀 몇 개에 대응하는지를 기준으로 밉맵 레벨을 선택합니다. GPU는 인접한 화면 픽셀 사이에서 UV 좌표가 얼마나 변하는지를 계산하고, 이 변화율로부터 픽셀 1개가 커버하는 텍셀 수를 구합니다. 텍셀 대 픽셀 비율이 1:1에 가까운 밉맵 레벨이 선택됩니다.
앞서 예로 든 2048×2048 텍스처가 화면에서 50×50 픽셀로 보이는 경우, 픽셀당 텍셀 수는 약 2048 / 50 ≈ 41개입니다. 밉맵 레벨은 log₂(41) ≈ 5.4이므로 Level 5(64×64)가 선택됩니다. 이 레벨에서 픽셀당 텍셀 비율은 64 / 50 ≈ 1.3으로 거의 1:1에 가깝습니다. 이 계산은 GPU 하드웨어가 픽셀마다 자동으로 수행합니다.
1
2
3
4
5
6
7
8
9
10
11
카메라와의 거리에 따른 밉맵 레벨 선택
가까움 멀리
│ │
▼ ▼
Level 0 Level 2 Level 5 Level 8
2048x2048 512x512 64x64 8x8
(원본) (중간) (먼 거리) (아주 먼 거리)
화면에서의 크기:
큼 중간 작음 아주 작음
적절한 밉맵 레벨을 사용하면 화면 픽셀 하나에 대응되는 텍셀 수가 줄어들어, 앞서 지적한 두 문제가 동시에 해소됩니다. 에일리어싱이 감소하고, 읽어야 하는 텍스처 영역이 좁아져 캐시 효율이 올라갑니다.
밉맵의 메모리 비용
밉맵은 원본 외에 축소 버전을 추가로 저장하므로 메모리가 늘어납니다. 각 레벨은 가로와 세로가 모두 절반이므로 면적은 이전 레벨의 1/4입니다. Level 1은 원본의 1/4, Level 2는 그 1/4인 1/16, Level 3은 그 1/4인 1/64 … 이런 식으로 계속 줄어듭니다. 이 값들을 모두 더하면 원본의 약 33%(1/3)가 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
밉맵 추가 메모리 계산
원본 면적을 1이라 하면 (가로 비율의 제곱 = 면적 비율):
Level 1: 가로 1/2 → 면적 (1/2)^2 = 1/4
Level 2: 가로 1/4 → 면적 (1/4)^2 = 1/16
Level 3: 가로 1/8 → 면적 (1/8)^2 = 1/64
...
합계: 1/4 + 1/16 + 1/64 + 1/256 + ...
= 1/4 x (1 + 1/4 + 1/16 + ...)
= 1/4 x 1/(1 - 1/4)
= 1/4 x 4/3
= 1/3
≒ 0.333... (약 33%)
2048x2048 RGBA 비압축 텍스처의 경우:
1
2
3
4
원본: 16.00 MB
밉맵 추가분: 5.33 MB (16 x 0.33)
────────────────────────
밉맵 포함 총합: 21.33 MB
메모리가 33% 늘어나는 대신, 앞에서 살펴본 에일리어싱과 대역폭 낭비가 해결됩니다. 밉맵이 절약하는 대역폭은 추가 메모리 33%를 상쇄하고도 남으므로, Unity에서는 텍스처 Import Settings의 “Generate Mip Maps” 옵션이 기본으로 켜져 있습니다.
다만 모든 텍스처에 밉맵이 필요한 것은 아닙니다. UI 텍스처나, 카메라 줌 없이 고정 크기로 표시되는 2D 스프라이트처럼 화면에서 축소될 일이 없는 텍스처는 밉맵을 끄면 33%의 메모리를 절약할 수 있습니다.
텍스처 압축 — 모바일의 필수 기술
밉맵이 대역폭을 절약하는 기법이었다면, 텍스처 압축은 메모리와 대역폭을 동시에 줄이는 기법입니다. 앞에서 2048×2048 RGBA 비압축 텍스처가 16MB를 차지한다고 했습니다. 텍스처가 GPU 메모리의 60%를 차지하는 모바일 환경에서 텍스처 하나에 16MB는 큰 비중이며, 텍스처 압축은 이 크기를 1/4~1/8로 줄입니다.
GPU 텍스처 압축의 특징
PNG나 JPEG 같은 일반 이미지 압축과 GPU 텍스처 압축은 설계 목적이 다릅니다. GPU는 렌더링 중 텍스처의 임의의 위치에서 텍셀을 읽어야 합니다. PNG/JPEG는 이미지 전체를 순차적으로 압축하므로, 특정 텍셀만 읽으려면 먼저 전체 이미지를 비압축 상태로 풀어야(디코딩) 합니다. 즉, GPU 메모리에는 비압축 크기 그대로 올라가서 메모리가 절약되지 않습니다. 디스크 저장 크기는 작지만 GPU 메모리에서는 의미가 없는 셈입니다.
GPU 텍스처 압축(ASTC, ETC2 등)은 이 문제를 해결하기 위해 이미지를 4×4 같은 고정 크기의 블록 단위로 압축합니다. 각 블록이 독립적으로 디코딩되므로, GPU가 임의의 텍셀을 읽을 때 해당 블록만 처리하면 됩니다. 압축된 상태 그대로 GPU 메모리에 올라가고, GPU 하드웨어가 내장된 디코더로 실시간 디코딩합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
일반 이미지 압축 (PNG/JPEG)
디스크 CPU에서 디코딩 GPU 메모리
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ PNG 압축 │ ───► │ 비압축 │ ──────► │ 비압축 │
│ 2 MB │ │ 풀기 │ │ 16 MB │
└──────────┘ └──────────┘ └──────────────┘
↑ 메모리 절약 없음
GPU 텍스처 압축 (ASTC/ETC2)
디스크 GPU 메모리
┌──────────┐ ┌──────────────┐
│ ASTC │ ──────────────────────► │ ASTC 압축 │
│ 4 MB │ 그대로 올림 │ 4 MB │
└──────────┘ └──────────────┘
↑ 메모리 절약됨
GPU 하드웨어가 직접 읽음
블록 압축의 원리
모든 GPU 텍스처 압축 포맷은 블록 압축(Block Compression) 방식을 사용합니다. 텍스처를 작은 블록(일반적으로 4x4 텍셀)으로 나누고, 각 블록을 고정된 크기의 데이터로 압축합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2048 x 2048 텍스처를 4x4 블록으로 분할
┌──┬──┬──┬──┬──┬──┬──┬── ...
│ │ │ │ │ │ │ │
├──┼──┼──┼──┼──┼──┼──┼──
│ │ │ │ │ │ │ │ 512 x 512 = 262,144 개 블록
├──┼──┼──┼──┼──┼──┼──┼──
│ │ │ │ │ │ │ │
├──┼──┼──┼──┼──┼──┼──┼──
: : : : : : : :
하나의 블록 (4 x 4)
┌────┬────┬────┬────┐
│ P0 │ P1 │ P2 │ P3 │
├────┼────┼────┼────┤
│ P4 │ P5 │ P6 │ P7 │ 16 텍셀
├────┼────┼────┼────┤
│ P8 │ P9 │P10 │P11 │
├────┼────┼────┼────┤
│P12 │P13 │P14 │P15 │
└────┴────┴────┴────┘
블록 압축은 4x4 블록의 16개 텍셀을 개별적으로 저장하지 않습니다. 대신, 블록 내의 대표 색상 두 개(또는 그 이상)를 저장하고, 각 텍셀은 이 대표 색상 사이의 어디에 위치하는지를 인덱스로 표현합니다.
1
2
3
4
5
비압축 4x4 블록 (RGBA 32bit) 블록 압축 후 (ETC2 RGBA 예시)
16 텍셀 x 4 바이트 = 64 바이트 대표 색상 + 인덱스 테이블 = 16 바이트
압축률: 64 / 16 = 4:1
각 블록이 고정 크기라는 점도 중요합니다. 블록 하나의 크기가 항상 같으므로, GPU는 블록 번호에 고정 크기를 곱하는 것만으로 해당 블록의 메모리 위치를 바로 계산할 수 있습니다. 앞에서 설명한 랜덤 접근이 가능한 이유가 바로 이 고정 크기 구조입니다.
모바일 텍스처 압축 포맷
블록 압축의 원리는 같지만, 구체적인 압축 포맷은 여러 가지가 있습니다. 모바일에서 주로 사용되는 포맷은 ETC2, ASTC, PVRTC 세 가지입니다.
ETC2 (Ericsson Texture Compression 2)
ETC2는 OpenGL ES(모바일 GPU용 그래픽 API 표준) 3.0에 포함된 텍스처 압축 포맷입니다. OpenGL ES 3.0 이상을 지원하는 Android와 iOS 기기에서 하드웨어 디코딩이 가능합니다.
1
2
3
4
5
6
7
8
9
ETC2 사양
블록 크기: 4 x 4 텍셀 (고정)
블록당 크기 텍셀당 크기 bpp 비압축 대비
RGB 모드: 8 바이트/블록 0.5 바이트/텍셀 4 bpp 24bpp → 4bpp (6:1)
RGBA 모드: 16 바이트/블록 1.0 바이트/텍셀 8 bpp 32bpp → 8bpp (4:1)
* bpp (bits per pixel): 텍셀당 비트 수. 비압축 RGB는 24bpp, RGBA는 32bpp
RGB 모드는 알파 채널 없이 색상만 저장하여 6:1 압축이 됩니다. 투명도(Alpha)가 필요하면 RGBA 모드를 사용하며, 알파 채널을 별도의 블록으로 저장하기 때문에 블록 크기가 두 배가 되어 4:1 압축입니다.
ETC2는 4×4 블록 크기가 고정이라, 품질과 크기 사이의 세밀한 조절이 어렵습니다. 이 한계를 해결한 포맷이 ASTC입니다.
ASTC (Adaptive Scalable Texture Compression)
ASTC는 ARM과 AMD가 공동 개발하고 Khronos Group이 표준화한 텍스처 압축 포맷입니다. iOS 8 이상, Android는 OpenGL ES 3.2 이상 또는 Vulkan을 지원하는 기기에서 하드웨어 디코딩이 가능합니다.
ETC2와 가장 큰 차이점은 블록 크기가 가변적이라는 점입니다. 블록 데이터의 크기는 항상 128비트(16바이트)로 동일하지만, 블록에 포함되는 텍셀 수가 달라집니다. 블록이 커질수록 더 많은 텍셀을 16바이트로 표현하므로 bpp가 낮아집니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
ASTC 주요 블록 크기별 bpp (모든 블록은 128비트 = 16바이트)
블록 크기 텍셀 수 bpp 품질
──────────────────────────────────────────
4 x 4 16 8.00 최고
5 x 5 25 5.12 ↓
6 x 6 36 3.56 ↓
8 x 8 64 2.00 ↓
10 x 10 100 1.28 ↓
12 x 12 144 0.89 최저
──────────────────────────────────────────
* 이 외에 5x4, 6x5, 8x6 등 비정사각 블록도 지원
또한 ETC2는 RGBA 압축 시 RGB와 알파를 별도 블록으로 나누어 저장하지만, ASTC는 하나의 블록에서 RGB와 알파를 함께 처리합니다. 같은 16바이트를 사용하면서도 채널 간 상관관계를 활용할 수 있어, 알파가 포함된 텍스처에서 ETC2보다 품질이 높습니다.
블록이 커질수록 메모리는 줄어들지만, 압축 아티팩트(Compression Artifact)가 늘어납니다. 압축 아티팩트란 압축 과정에서 원본과 달라지는 시각적 열화입니다. 경계가 뭉개지거나 인접한 블록 사이에 색상이 번지는 현상으로 나타납니다. 같은 16바이트로 더 많은 텍셀을 표현해야 하므로, 텍셀당 정보량이 줄어들기 때문입니다.
1
2
3
4
5
6
7
8
9
10
11
12
블록 크기에 따른 품질-크기 트레이드오프
품질
높음 │ ■ 4x4
│ ■ 5x5
│ ■ 6x6
│ ■ 8x8
│ ■ 10x10
낮음 │ ■ 12x12
└──────────────────────────────
작음 큼
메모리 절약
캐릭터 얼굴처럼 시각적으로 중요한 텍스처에는 4×4나 5×5를, 먼 배경이나 반복 패턴에는 8×8이나 10×10을 사용하는 식으로 텍스처별로 품질과 메모리의 균형을 조절할 수 있습니다.
PVRTC (PowerVR Texture Compression)
PVRTC는 Imagination Technologies의 PowerVR GPU 전용 포맷으로, 과거 iOS 기기에서 사용되었습니다. 2bpp와 4bpp 두 가지 모드를 지원하여 높은 압축률을 제공하지만, 텍스처가 정사각형이고 크기가 2의 거듭제곱(512×512, 1024×1024 등)이어야 한다는 제약이 있습니다. 블록 크기 선택도 불가능하여 텍스처별 품질 조절이 어렵고, 같은 bpp에서 ASTC보다 품질이 떨어집니다.
Apple의 A8 칩(iPhone 6) 이후 세대에서 ASTC로 대체되었으며, ASTC를 지원하지 않는 A7 이하 기기는 현재 시점에서 시장 점유율이 무시할 수 있는 수준입니다. 신규 프로젝트에서는 ASTC를 사용하면 됩니다.
포맷 비교와 선택
1
2
3
4
5
6
7
8
9
10
11
┌────────────┬──────────────┬──────────────┬──────────────────────┐
│ 포맷 │ 블록 크기 │ bpp 범위 │ 지원 기기 │
├────────────┼──────────────┼──────────────┼──────────────────────┤
│ ETC2 │ 4x4 고정 │ 4 / 8 │ Android + iOS │
│ │ │ │ (ES 3.0+) │
├────────────┼──────────────┼──────────────┼──────────────────────┤
│ ASTC │ 4x4 ~ 12x12 │ 0.89 ~ 8.00 │ Android + iOS │
│ │ 가변 │ │ (ES 3.2+ / Vulkan) │
├────────────┼──────────────┼──────────────┼──────────────────────┤
│ PVRTC │ 고정 │ 2 / 4 │ iOS (레거시) │
└────────────┴──────────────┴──────────────┴──────────────────────┘
Unity에서는 텍스처 Import Settings의 플랫폼별 탭에서 압축 포맷을 선택합니다. Android와 iOS 모두 ASTC를 지정하면 하나의 포맷으로 양 플랫폼을 지원할 수 있습니다.
텍스처 메모리 공식
해상도와 포맷(bpp)이 텍스처 크기를 결정하고, 밉맵이 추가 메모리를 더합니다. 이 세 요소를 종합하면 텍스처의 실제 메모리 사용량을 하나의 공식으로 계산할 수 있습니다.
기본 공식
1
2
3
4
5
텍스처 메모리 = 가로 x 세로 x (bpp / 8) x 밉맵 계수
밉맵 계수:
밉맵 켜짐 = 1.33 (원본 + 33%)
밉맵 꺼짐 = 1.00 (원본만)
계산 예시
2048x2048 텍스처를 여러 포맷으로 저장했을 때의 메모리를 비교합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
포맷별 메모리 비교 (2048 x 2048, 밉맵 포함)
포맷 bpp 텍스처 크기 밉맵 포함
─────────────────────────────────────────────────────
RGBA 비압축 32 16.00 MB 21.33 MB
ETC2 RGB 4 2.00 MB 2.67 MB
ETC2 RGBA 8 4.00 MB 5.33 MB
ASTC 4x4 8 4.00 MB 5.33 MB
ASTC 6x6 3.56 1.78 MB 2.37 MB
ASTC 8x8 2 1.00 MB 1.33 MB
계산 과정 (ASTC 6x6 예시):
2048 x 2048 x (3.56 / 8) x 1.33
= 4,194,304 x 0.445 x 1.33
= 4,194,304 x 0.592
≒ 2,483,027 바이트
≒ 2.37 MB
1
2
3
4
5
6
7
메모리 절약 비교 (2048 x 2048, 밉맵 포함)
비압축 RGBA ████████████████████████████████████████ 21.33 MB (기준)
ETC2 RGBA ██████████ 5.33 MB (75% 절약)
ASTC 4x4 ██████████ 5.33 MB (75% 절약)
ASTC 6x6 ████████ 2.37 MB (89% 절약)
ASTC 8x8 █████ 1.33 MB (94% 절약)
비압축 RGBA 대비 ASTC 8x8은 약 94%의 메모리를 절약합니다. ETC2 RGBA와 ASTC 4x4는 같은 8bpp이지만, ASTC는 RGB와 알파를 하나의 블록에서 처리하므로 동일한 메모리에서 더 높은 품질을 제공합니다.
앞서 비압축 2048x2048 텍스처 기준으로 캐릭터 5명에 240MB가 필요하다고 계산했습니다. 밉맵까지 포함하면:
1
2
비압축 RGBA: 21.33 MB x 3장(Albedo, Normal, Mask) x 5명 = 약 320 MB
ASTC 6x6: 2.37 MB x 3장(Albedo, Normal, Mask) x 5명 = 약 36 MB
320MB와 36MB의 차이는 모바일에서 게임이 실행 가능한지 여부를 결정합니다.
실전 가이드라인
앞서 다룬 해상도, 밉맵, 압축 포맷의 원리를 실제 프로젝트 설정에 적용합니다.
해상도 선택
텍스처 해상도는 오브젝트가 화면에서 차지하는 크기에 맞춰야 합니다. 화면에 100x100 픽셀로 보이는 오브젝트에 2048x2048 텍스처를 할당하면 메모리만 낭비됩니다. 밉맵은 렌더링 시 불필요한 텍셀 읽기를 줄여주지만, 원본 텍스처의 메모리 점유 자체를 줄여주지는 않습니다.
1
2
3
4
5
6
7
8
9
화면 점유 크기에 따른 텍스처 해상도 가이드 (모바일 기준)
화면 점유 크기 권장 해상도
───────────────────────────────────
전체 화면 배경 1024 ~ 2048
주인공 캐릭터 1024 ~ 2048
일반 NPC / 소품 512 ~ 1024
먼 배경 오브젝트 256 ~ 512
파티클 / 이펙트 128 ~ 256
압축 포맷 선택
ASTC는 블록 크기가 클수록 용량이 줄지만 품질도 낮아집니다. 텍스처 용도에 따라 블록 크기를 다르게 설정합니다.
1
2
3
4
5
6
7
8
텍스처 용도에 따른 ASTC 블록 크기 가이드
텍스처 용도 권장 ASTC 블록 bpp
──────────────────────────────────────────────────
캐릭터 얼굴 / 중요 UI 4x4 8.00
캐릭터 몸체 / 무기 5x5 5.12
배경 건물 / 지형 6x6 3.56
먼 배경 / 스카이박스 8x8 2.00
밉맵 설정
밉맵은 약 33%의 추가 메모리를 사용합니다. 3D 공간에 배치되어 카메라와의 거리가 변하는 오브젝트에는 밉맵을 켜고, UI 요소나 고정 크기 스프라이트처럼 항상 같은 크기로 그려지는 텍스처에는 끕니다.
2의 거듭제곱 크기
텍스처 크기는 2의 거듭제곱(Power of Two, POT)으로 설정합니다. 128, 256, 512, 1024, 2048이 해당됩니다. POT 크기는 밉맵 생성 시 각 레벨이 정확히 절반으로 나뉘고, 블록 기반 압축의 블록 크기(4, 6, 8 등)로도 나누어떨어집니다.
비-POT(NPOT) 텍스처는 밉맵 레벨마다 크기가 정확히 절반이 되지 않아 반올림이 필요합니다. ASTC와 ETC2는 POT를 요구하지 않지만, 블록 크기의 배수가 아니면 가장자리에 패딩이 추가됩니다. POT 크기를 사용하면 밉맵 분할과 블록 정렬이 모두 깔끔하게 맞으므로, 특별한 이유가 없다면 POT로 설정합니다.
마무리
텍스처는 UV 좌표를 통해 메쉬 표면에 대응되는 2D 이미지입니다. 해상도가 두 배가 되면 메모리는 네 배로 늘어나고, 밉맵은 33%의 추가 메모리로 에일리어싱과 대역폭 낭비를 줄입니다. ASTC는 4×4부터 12×12까지 블록 크기를 조절하여, 같은 2048×2048 텍스처를 비압축 21.33MB에서 ASTC 6×6 기준 2.37MB까지 줄일 수 있습니다. 최종 메모리 사용량은 가로 × 세로 × (bpp / 8) × 밉맵 계수로 계산되며, 해상도·밉맵·압축 포맷 세 요소가 맞물려 결정됩니다.
Part 1에서 메쉬가 3D 형태를 정의했다면, 텍스처는 그 표면에 색과 질감을 입힙니다. 하지만 같은 메쉬와 텍스처라도 금속처럼 빛을 반사하거나 천처럼 흡수하는 등 표면의 반응이 다를 수 있습니다. 이 렌더링 과정을 GPU에서 수행하는 프로그램이 셰이더(Shader)입니다. Part 1에서 등장한 버텍스 셰이더는 정점의 위치를 화면 좌표로 변환하고, 프래그먼트 셰이더는 각 픽셀의 최종 색상을 계산합니다. 이 글에서 다룬 텍스처는 프래그먼트 셰이더가 픽셀 색상을 결정할 때 참조하는 데이터입니다.
머티리얼(Material)은 어떤 셰이더를 사용할지 지정하고, 그 셰이더에 전달할 값(텍스처, 색상, 금속성, 거칠기 등)을 저장합니다. 하나의 셰이더를 여러 머티리얼이 공유할 수 있고, 각 머티리얼이 다른 값을 전달하면 금속, 나무, 천 등 서로 다른 표면을 표현할 수 있습니다. Part 3에서 머티리얼과 셰이더의 구조를 살펴봅니다.
관련 글
시리즈
- 렌더링 기초 (1) - 메쉬의 구조
- 렌더링 기초 (2) - 텍스처와 압축 (현재 글)
- 렌더링 기초 (3) - 머티리얼과 셰이더 기초