Any3DAny3D
·Any3D Team

텍스처, 당신의 VRAM을 잡아먹는 대식가

3d-compressiontexture-compressionwebglwebgpu

지난번에 정점을 반으로 줄여 모델이 좀 작아졌지만, 번쩍이는 벼락처럼 날씬해지진 않았습니다 — 진짜 크기의 주인공이 여전히 그 자리에 있었기 때문입니다: 텍스처. PBR 모델에서 텍스처는 보통 크기의 80% 이상을 차지하고, VRAM에서 가장 많이 커지는 부분입니다.

이 글은 텍스처의 "VRAM 대식가" 문제를 치료합니다. 세 가지를 설명합니다: 왜 PNG/JPG가 GPU 눈에 죄가 있는지, GPU 자체의 텍스처 포맷이 어떤 모습이고 왜 그대로는 못 쓰는지, 그리고 Basis Universal + KTX2가 셋을 어떻게 이어주는지.

복습: 왜 JPG는 VRAM을 폭발시키는가

이전 글에서 공식을 하나 드렸습니다:

VRAM 사용량 = 너비 * 높이 * 4바이트(RGBA) * 1.333(mipmap 포함)

4096×4096 텍스처는, 디스크상에서 1.5MB의 JPG든 8MB의 PNG든, VRAM에서는 약 87MB가 됩니다. 이유는 하나입니다: GPU는 JPG/PNG를 이해하지 못합니다.

GPU의 텍스처 샘플러가 이해하는 건 하나뿐입니다 — UV 좌표가 주어지면, 고정 크기의 픽셀 블록에서 색을 읽어옵니다. 텍스처는 VRAM에서 "깔린 raw pixel"이어야 합니다. 그래서 브라우저는 JPG를 GPU에 업로드하기 전에 먼저 CPU에서 완전히 RGBA 픽셀로 해제한 뒤, 그 블록 전체를 VRAM에 밀어 넣어야 합니다.

이 과정에는 삼중의 문제가 있습니다:

  1. VRAM 폭발: 해제된 raw pixel은 거대합니다. 87MB는 과장이 아니라 공식으로 계산된 값입니다.
  2. 업로드 블로킹: 큰 픽셀 블록을 CPU 메모리에서 GPU VRAM으로 옮기는 건 느린 작업으로, 첫 프레임 렌더링을 막아버립니다.
  3. CPU 해독 비용: 큰 이미지 해독 자체가 시간이 걸리고, 모바일에서 특히 뚜렷합니다.

이전 글의 "압축 스펀지" 비유를 이어가면: PNG/JPG는 운반하기 쉽게 납작하게 눌린 스펀지입니다. GPU에 올라탄 순간, 스펀지는 물을 머금고 원래 크기로 부풉니다. 다운로드는 빨라졌지만 VRAM은 전혀 절약되지 않았습니다.

GPU 자체의 텍스처 포맷: 태생부터 VRAM에서 압축되어

GPU가 압축된 PNG를 받아들이지 않는다면, 텍스처를 VRAM 안에서도 압축 상태로 유지할 수 있을까요? GPU가 샘플링할 때 개별 픽셀 블록을 그 자리에서 해독하고, 비용은 거의 없게요.

그게 바로 GPU 네이티브 텍스처 포맷이 하는 일입니다. 대표 패밀리:

포맷 패밀리정식명주요 플랫폼특징
BC1-7Block Compression데스크톱(PC, Mac)원로, 세대별 4×4 픽셀 블록 압축
ETC1/2Ericsson Texture Compression모바일(구형 Android/iOS)모바일의 구 표준
ASTCAdaptive Scalable Texture Compression모바일/VR(신형 기기)유연, 품질 최고, 블록별 조정 가능
PVRTCPowerVR구형 iOSASTC로 점차 대체 중

이 포맷들의 공통점: 텍스처가 4×4 픽셀의 작은 블록(block) 단위로 압축 저장되고, GPU는 샘플링할 때 이 작은 블록을 필요에 따라 해독합니다. 나오는 건 단일 픽셀이 아니라 한 블록입니다. 장점은 내용에 관계없이 VRAM 사용량이 고정 비율로 줄어드는 것입니다.

비교:

PNG/JPG(전통)GPU 네이티브 포맷
디스크 크기작음(JPG가 특히)중간(블록 압축, 고정 비트레이트)
VRAM 사용량큼(raw pixel로 해제)작음(블록 압축, 상주)
GPU 업로드느림(CPU 해독 + 큰 전송)빠름(그대로 옮기면 됨, 해독 불필요)
샘플링 속도빠름(이미 raw pixel)빠름(하드웨어 실시간 해독)

GPU 포맷이 완벽해 보입니다. 그럼 왜 그대로 못 쓸까요?

문제는 여기: 기기마다 인식하는 포맷이 다르다

이게 GPU 텍스처 포맷의 가장 큰 함정 — 파편화입니다.

  • 데스크톱 PC는 BC1-7을 인식, ASTC는 인식 못 함
  • 안드로이드 폰은 ETC2/ASTC를 인식, 대부분 BC는 인식 못 함
  • iOS(A7+)는 ASTC를, 구형 기기는 PVRTC를 인식
  • WebGPU/WebGL은 그 기기 뒤의 같은 하드웨어 능력에 의존

한 텍스처를 "모든 기기에서 GPU 네이티브 포맷으로 존재하게" 하려면, 플랫폼별로 따로 준비해야 합니다. 데스크톱 + 안드로이드 + iOS로 내보내는 제품이라면 같은 텍스처에 BC + ETC2/ASTC 세 버전이 필요합니다. 패키지는 3배, 공수도 3배.

더 나쁜 건, 웹에서는 사용자가 어떤 기기로 페이지를 열지 모릅니다. 모든 포맷을 미리 생성하는 건 비현실적이고, 런타임 검출은 너무 늦습니다.

Basis Universal: 한 번 인코딩, 어디서나 트랜스코드

Basis Universal(약칭 Basis)은 이 파편화를 해결하기 위해 태어났습니다. 아이디어는 한 문장입니다:

먼저 텍스처를 "중간 포맷"으로 인코딩하고, 런타임에 현재 기기의 GPU 능력에 맞춰 해당 네이티브 포맷으로 트랜스코드(transcode)합니다.

트랜스코드 흐름(개념도):

소스 텍스처(PNG/JPG)
      │  일회성 오프라인 인코딩(느림, 한 번만)
      ▼
Basis 중간 포맷(ETC1S 또는 UASTC)
      │  KTX2 컨테이너로 패키징
      ▼
웹에 공개 ──┬── 데스크톱 GPU ──→ 런타임 트랜스코드 → BC1/3/7
           ├── 안드로이드 ───→ 런타임 트랜스코드 → ETC2
           └── iOS/VR ─────→ 런타임 트랜스코드 → ASTC

핵심:

  • 오프라인 인코딩은 한 번만, 컴팩트한 중간 표현을 얻습니다
  • 런타임 트랜스코드는 매우 빠름(순수 연산, 수 밀리초), 게다가 블록 포맷을 변환하므로 픽셀 단위 해제가 필요 없습니다
  • 트랜스코드 후 VRAM에 들어가는 건 진짜 GPU 네이티브 포맷이라, VRAM 사용량은 블록 압축 레이트로 계산되어 GPU 네이티브 포맷과 동일

Basis는 두 가지 중간 인코딩 모드를 제공합니다. 다음 글에서 풀지만, 이름만 먼저 기억하세요:

  • ETC1S: 압축률 극히 높고, diffuse/albedo 같은 컬러 맵에 적합
  • UASTC: 더 높은 품질, 노멀 같은 정밀도에 민감한 맵에 적합

KTX2: GPU 텍스처를 담는 표준 컨테이너

여기서 하나의 엔지니어링 문제가 더 남습니다: 인코딩된 Basis 데이터를 어디에 두고, 어떻게 마크하고, glTF와 어떻게 연결할까요? 답은 KTX2입니다.

KTX2(Khronos Texture 2)는 또 하나의 이미지 포맷이 아니라 컨테이너 포맷입니다 — .zip이 안에 문서가 들었든 이미지가 들었든 신경 안 쓰듯이, KTX2는 GPU 텍스처 데이터(Basis 인코딩 포함)를 표준 구조로 패키징하고 메타 정보(포맷, mipmap 레벨, 색공간 등)를 덧붙이기만 합니다.

glTF에서 KTX2는 확장 KHR_texture_basisu로 연결됩니다: 텍스처는 더 이상 PNG 파일이 아니라 Basis 인코딩을 담은 KTX2 파일입니다. 로드 시 엔진이 기기 능력을 감지해 해당 BC/ETC/ASTC로 트랜스코드합니다.

세 역할을 정리하죠, 헷갈리지 않게:

이름역할비유
Basis Universal인코딩 방식(텍스처를 중간 포맷으로 어떻게 압축하는가)"압축 알고리즘"의 일종
KTX2컨테이너 포맷(인코딩된 데이터를 어떻게 패키징하는가)"상자" 하나
KHR_texture_basisuglTF 확장(엔진에 Basis 텍스처임을 알림)"라벨" 하나

KTX2 파일 내부는 Basis 인코딩(크로스플랫폼)일 수도, 어떤 네이티브 포맷(예: raw BC7)일 수도 있습니다. 웹에서 99%는 Basis를 넣습니다 — 우리가 원하는 건 "한 번 인코딩, 어디서나 트랜스코드"이기 때문입니다.

VRAM 실례: 4096 텍스처 비교

앞선 공식과 GPU 포맷을 겹쳐서, 4096×4096 RGBA 텍스처의 각 방안별 실제 점유를 봅시다:

방안디스크 크기VRAM 사용량(mipmap 포함)업로드 속도크로스플랫폼
PNG~8MB~87MB느림(해독 필요)
JPG~1.5MB~87MB느림(해독 필요)
WebP~2MB~87MB느림(해독 필요)
KTX2 (ETC1S)~2-3MB~11-14MB빠름✅(트랜스코드)
KTX2 (UASTC)~6-8MB~22MB빠름✅(트랜스코드)

VRAM 수치 출처: GPU 블록 압축은 보통 4bpp(픽셀당 4비트) 또는 8bpp로 계산합니다. 4096×4096을 4bpp로 약 8MB, mipmap 포함 ×1.333 ≈ 11MB. UASTC는 대부분 8bpp로 트랜스코드되어 약 22MB.

중요한 건 어느 한 줄의 정확한 수치가 아니라, 이 두 가지입니다:

  1. 전통 포맷(PNG/JPG/WebP)은 VRAM 사용량이 거의 같습니다 — 모두 해제된 raw pixel로 87MB. 디스크가 아무리 작아도 VRAM은 절약 안 됩니다.
  2. KTX2는 VRAM을 1/4에서 1/8로 직격, 게다가 디스크 크기도 뒤지지 않습니다.

이것이 VR와 모바일 웹에서 KTX2를 거의 필수로 쓰는 이유입니다 — 2GB VRAM의 폰에 87MB짜리 텍스처가 몇 장이나 들어갈까요? 11MB면 7장이 들어갑니다.

플랫폼 지원 매트릭스: 어떤 GPU가 어떤 포맷을 인식하는가

Basis가 세부사항을 감춰주지만, 기초 매핑을 이해하면 트러블슈팅에 도움이 됩니다. 주류 기기의 네이티브 포맷 지원 현황:

플랫폼 / 기기BC1-7ETC2ASTCPVRTC
데스크톱 PC(D3D11/12, Vulkan, WebGPU)일부(신형 GPU)
macOS(Metal)✅(신형 기기)
Android(주류)
iOS(A8+)✅(구형 기기)
WebGL 2확장에 따라일부
WebGPU✅(데스크톱)✅(기기에 따라)

Basis는 런타임에 이 능력들을 탐지해 같은 중간 인코딩을 가장 적합한 것으로 트랜스코드합니다. 그래서 웹에서는 이 Basis의 층이 거의 대체 불가능합니다 — 공개 전에 사용자 기기를 예측할 수 없으니까요.

업로드 흐름 비교: 전통 vs GPU 포맷

마지막으로 흐름도 하나로 차이를 고정합니다.

전통 PNG/JPG:

PNG 파일 ──다운로드──> CPU 메모리 ──CPU 해독(느림)──> RGBA 픽셀 블록 ──업로드(크고 느림)──> VRAM(87MB)

KTX2 + Basis:

KTX2 파일 ──다운로드──> CPU 메모리 ──런타임 트랜스코드(빠름)──> GPU 블록 포맷 ──업로드(작고 빠름)──> VRAM(11MB)

후자는 큰 "CPU 픽셀 단위 해독" 단계를 빼고, 업로드하는 데이터 양도 한 자릿수 작습니다. 첫 프레임이 빠르고, VRAM도 절약 — 이게 이 방안의 핵심 가치입니다.

다음 단계

이론은 끝, 다음 글은 실전입니다. toktxgltf-transform으로 실제 텍스처를 KTX2로 압축하고, Three.js / Babylon.js에서 로드하며, ETC1S와 UASTC를 어떻게 고르고 압축 파라미터를 어떻게 조정하는지 다룹니다.

후원하기