KTX2 실전: 텍스처 압축의 올바른 사용법
이전 글에서 GPU 텍스처 포맷, Basis Universal, KTX2의 관계를 정리했습니다. 이론은 됐고, 이번엔 전부 실전입니다: ETC1S와 UASTC를 어떻게 고르는지, 어떤 도구를 쓰는지, 명령은 어떻게 치는지, 엔진에서 어떻게 로드하는지.
읽으면서 그대로 따라 해보세요.
먼저 가장 중요한 선택부터: ETC1S인가 UASTC인가
Basis는 두 가지 중간 인코딩을 제공합니다. 잘못 고르면 "충분히 좋지 않다"가 아니라 노멀맵이 그냥 망가집니다. 이 표를 먼저 외우세요:
| ETC1S | UASTC | |
|---|---|---|
| 압축률 | 극히 높음(JPG 같음) | 중간(고품질 PNG 같음) |
| 화질 | 컬러 맵에는 충분 | 원본에 가까운 품질 |
| VRAM(트랜스코드 후) | 보통 4bpp(원본의 약 1/8) | 보통 8bpp(원본의 약 1/4) |
| 인코딩 속도 | 느림(레벨 조정 가능) | 약간 빠름 |
| 적합 | albedo/diffuse, emissive | normal, metalness-roughness, 데이터 맵 |
| 부적합 | normal, 정확한 수치가 필요한 이미지 | 컬러 맵(과잉 품질, 크기가 커짐) |
왜 노멀맵에 ETC1S를 쓰면 안 될까요? 노멀맵은 방향 벡터를 저장하고, 각 픽셀의 RGB 세 채널이 서로를 구속합니다(벡터 길이 ≈ 1). ETC1S는 "색이 올바르게 보이도록" 설계된 블록 압축이라 단일 채널 정밀도에 둔감합니다. 압축 후 벡터 방향이 어긋나서 조명이 즉시 이상해집니다 — 특히 하이라이트 위치와 고주파 디테일에서. UASTC는 수치를 더 잘 보존해 이 정밀도 요구를 견딥니다.
실용 규칙:
- 컬러 맵(albedo, emissive) → ETC1S
- 데이터 맵(normal, roughness, metallic, AO, thickness) → UASTC
- 확신 없고 크기를 아끼고 싶다 → 먼저 ETC1S 시도, 클로즈업에서 망가지면 UASTC로 교체
같은 이미지, 네 가지 포맷 비교
2048×2048 albedo 맵을 기준으로(값은 커뮤니티 전형값, 참고용):
| 포맷 | 디스크 크기 | VRAM 사용량(mipmap 포함) | GPU 업로드 | 크로스플랫폼 |
|---|---|---|---|---|
| PNG | ~5MB | ~22MB | 느림 | ✅ |
| WebP | ~1MB | ~22MB | 느림 | ✅ |
| KTX2 (ETC1S) | ~0.5-0.8MB | ~2.8MB | 빠름 | ✅ |
| KTX2 (UASTC) | ~3-4MB | ~5.6MB | 빠름 | ✅ |
WebP의 VRAM 사용량이 PNG와 같음에 주의하세요 — 디스크만 작을 뿐, VRAM에서는 여전히 raw pixel로 해제됩니다. KTX2의 ETC1S는 디스크와 VRAM을 동시에 매우 낮게 만듭니다. 그게 가치 있는 이유입니다.
툴체인: 세 길 모두 통함
KTX2 압축 도구는 하나가 아닙니다, "잡기 쉬운 정도" 순으로:
1. toktx(공식, 가장 강력)
Khronos 공식 도구, 파라미터가 가장 풍부하고, 개별 텍스처 처리에 적합합니다.
# PNG를 ETC1S 인코딩 KTX2로 변환
toktx --bcmp --uastc 0 albedo.ktx2 albedo.png
# PNG를 UASTC 인코딩 KTX2로 변환
toktx --uastc 1 normal.ktx2 normal.png
자주 쓰는 파라미터:
# ETC1S + 품질 레벨(1-255, 기본 128, 높을수록 고품질/크기 증가)
toktx --bcmp --uastc 0 --qlevel 200 albedo.ktx2 albedo.png
# UASTC + 초압축(Zstandard, 디스크 크기 추가 축소)
toktx --uastc 1 --zcmp 19 normal.ktx2 normal.png
# mipmap 자동 생성(강력 권장)
toktx --bcmp --genmipmap albedo.ktx2 albedo.png
# sRGB 색공간 지정(컬러 맵은 필수)
toktx --bcmp --srgb albedo.ktx2 albedo.png
--bcmp는 ETC1S 모드 스위치(Basis Universal의 베이스 모드),--uastc 1은 UASTC 모드입니다. 둘은 상호 배타적입니다.
2. gltf-transform(가장 손편함, 강력 권장)
glTF/GLB 모델 전체가 손에 있다면, gltf-transform 명령 한 줄로 안의 모든 텍스처를 KTX2로 바꾸고, 용도별로 ETC1S/UASTC를 자동 선택합니다.
# 설치
npm install -g @gltf-transform/cli
# 모델 전체를 한 번에 압축
gltf-transform optimize model.glb model-optimized.glb \
--texture-compress basisu
내부에서 텍스처 용도를 판단합니다: 컬러류는 ETC1S, 데이터류는 UASTC, 자동으로 KHR_texture_basisu 확장도 씁니다. 이게 90% 씬에서의 최적의 선택입니다, toktx를 일일이 수동으로 칠 필요 없습니다.
3. 온라인 도구(가장 빠른 시작)
환경 구성이 귀찮을 때, 브라우저 도구를 직접 씁니다: gltf.report(온라인판 gltf-transform), KTX2 Converter 등. 업로드, 다운로드, 끝. 초기 시험이나 일회성 작업에 좋습니다.
전체 파이프라인: 소스에서 공개까지
표준 흐름을 정리합니다(흐름도):
소스 파일(PNG/JPG/PSD/TGA)
│
├── [모델 전체] gltf-transform optimize model.glb → 텍스처 타입 자동 식별
│ └─ 컬러 맵 → ETC1S
│ └─ 데이터 맵 → UASTC
│ └─ KHR_texture_basisu 확장 작성
│
└── [개별 텍스처] toktx → ETC1S/UASTC + 색공간 + mipmap 수동 지정
│
▼
압축된 KTX2 / GLB
│
▼
엔진 로드(Three.js / Babylon.js) ── 런타임 트랜스코드 → GPU 네이티브 포맷
Three.js에서 KTX2 로드
Three.js는 r129부터 KTX2를 네이티브 지원하지만, KTX2Loader를 제공하고 트랜스코더(basis transcoder wasm)를 설정해야 합니다.
import * as THREE from "three";
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
// 1. KTX2 트랜스코더 초기화, 현재 GPU가 어떤 네이티브 포맷을 지원하는지 탐지
const ktx2Loader = new KTX2Loader()
.setTranscoderPath("/basis/") // basis transcoder wasm 디렉터리
.detectSupport(renderer); // 능력 탐지 위해 renderer 전달 필수
// 2. GLTFLoader 설정, KTX2/Draco/MeshOpt 모두 부착
const gltfLoader = new GLTFLoader();
gltfLoader.setKTX2Loader(ktx2Loader);
gltfLoader.setDRACOLoader(
new DRACOLoader().setDecoderPath("/draco/")
);
gltfLoader.setMeshoptDecoder(MeshoptDecoder);
// 3. 모델 로드, 텍스처는 자동 트랜스코드
gltfLoader.load("/models/model-optimized.glb", (gltf) => {
scene.add(gltf.scene);
});
몇 가지 핵심:
detectSupport(renderer)는 필수 — 런타임에 어떤 네이티브 포맷으로 트랜스코드할지 결정합니다setTranscoderPath는 basis transcoder wasm 파일을 가리킵니다(basis_transcoder release에서 public 디렉터리로 복사)- 모델이 Draco도 쓰면 DRACOLoader도 부착, MeshOpt를 쓰면 MeshoptDecoder 부착
단일 KTX2 텍스처 로드:
const texture = await ktx2Loader.loadAsync("/textures/albedo.ktx2");
texture.colorSpace = THREE.SRGBColorSpace; // 컬러 맵은 sRGB로 설정
material.map = texture;
가장 흔한 함정이 바로 이 단계: 컬러 맵에
colorSpace = SRGBColorSpace설정을 잊으면 이미지 전체가 회색빛으로 어두워집니다. 데이터 맵(normal/roughness)은 반대로NoColorSpace(선형)를 유지하세요, 거꾸로 하지 마세요.
Babylon.js에서 KTX2 로드
Babylon의 방식은 비슷하지만 더 자동적 — KHR_texture_basisu는 GLTFFileLoader에서 기본 활성화되고, 트랜스코더 파일만 찾을 수 있으면 됩니다.
import { Scene } from "@babylonjs/core/scene";
import { Engine } from "@babylonjs/core/Engines/engine";
import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader";
import { GLTFFileLoader } from "@babylonjs/loaders/glTF/glTFFileLoader";
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_texture_basisu";
const engine = new Engine(canvas);
const scene = new Scene(engine);
SceneLoader.ImportMesh(
"",
"/models/",
"model-optimized.glb",
scene,
(meshes) => {
// 모델 로드 완료, KTX2는 자동 트랜스코드됨
}
);
Babylon은 basis transcoder를 CDN에서 자동으로 가져옵니다. 오프라인/인트라넷 환경에서는 BASISFileLoader.TranscoderModule을 수동 설정해야 합니다.
압축 파라미터 튜닝: 품질과 크기의 균형
ETC1S의 핵심 파라미터는 --qlevel(1-255)입니다. 결과에 미치는 영향:
| qlevel | 크기 | 화질 | 인코딩 시간 | 적합 |
|---|---|---|---|---|
| 128(기본) | 작음 | 충분 | 중간 | 대부분의 경우 |
| 200-255 | 약간 큼 | 거의 무손실 | 김(수배) | 고품질 요구 |
| 60-100 | 매우 작음 | 블록 노이즈 가시 | 빠름 | 원경/작은 텍스처 |
UASTC의 크기는 비교적 고정이고, 주로 --zcmp(Zstandard 초압축)로 디스크 크기를 조정합니다. VRAM에는 영향 없음(해제 후 여전히 8bpp).
튜닝 순서 제안:
- 먼저 기본값으로 한 번 압축하고 크기와 화질 확인
- 불만 →
--qlevel(ETC1S) 조정 또는--zcmp(UASTC) 추가 - 노멀맵이 망가졌다 → UASTC를 쓰는지 확인, ETC1S가 아닌지
- 색이 어둡다 → 색공간 설정 확인(sRGB 플래그, 엔진의 colorSpace)
자주 발생하는 트러블슈팅
트랜스코드 실패 / 로드 에러
- 트랜스코더 wasm 경로가 올바른지 확인(Three.js는
setTranscoderPath필요) - 엔진 버전이 현재 KTX2 버전을 지원하는지 확인(구형 basis 인코딩은 신형 트랜스코더가 미지원)
- 콘솔에 보통 구체적 에러가 나옵니다, 키워드로 검색
색이 너무 어둡거나 / 너무 밝음
- 컬러 맵(albedo)이 sRGB로 설정 안 됨, 또는 반대로 설정됨
- toktx에서
--srgb누락(컬러 맵에는 필요) - 데이터 맵(normal)에 실수로 sRGB 부여
mipmap이 없고, 멀리서 깜빡임
- 압축 시
--genmipmap을 안 붙임 - 엔진에서
texture.generateMipmaps가 켜지지 않음(Three.js에서 KTX2는 기본적으로 파일을 따르지만, 그래도 머티리얼의minFilter를 mipmap 모드로 해야 함)
클로즈업에서 노멀 방향이 이상함
- 노멀맵에 ETC1S 사용, UASTC로 교체
- 노멀맵이 OpenGL 스타일(그린 채널 업)인지 확인. DirectX 스타일은 일부 엔진에서 G 채널을 뒤집어야 함
파일이 오히려 커짐
- 작은 텍스처(< 128×128)에 KTX2는 손해, 블록 압축에는 고정 오버헤드가 있어 블록 전체를 채움
- 단색 텍스처는 KTX2로 압축하지 말고, 머티리얼 컬러 값을 쓰는 게 더 쌈
다음 단계
텍스처 압축은 여기서 거의 클리어입니다. 하지만 "도구 쓰는 법을 안다"와 "올바른 도구를 쓸 줄 안다"는 다릅니다 — 다음 글은 앞선 네 편의 모든 지식을 하나의 선정 프레임워크로 묶습니다: 데스크톱, 모바일, VR, 미니프로그램, 각 씬이 정확히 어떤 조합을 써야 하는지.