Any3DAny3D
·Any3D Team

KTX2 실전: 텍스처 압축의 올바른 사용법

3d-compressiontexture-compressionktx2basis-universalgltf

이전 글에서 GPU 텍스처 포맷, Basis Universal, KTX2의 관계를 정리했습니다. 이론은 됐고, 이번엔 전부 실전입니다: ETC1S와 UASTC를 어떻게 고르는지, 어떤 도구를 쓰는지, 명령은 어떻게 치는지, 엔진에서 어떻게 로드하는지.

읽으면서 그대로 따라 해보세요.

먼저 가장 중요한 선택부터: ETC1S인가 UASTC인가

Basis는 두 가지 중간 인코딩을 제공합니다. 잘못 고르면 "충분히 좋지 않다"가 아니라 노멀맵이 그냥 망가집니다. 이 표를 먼저 외우세요:

ETC1SUASTC
압축률극히 높음(JPG 같음)중간(고품질 PNG 같음)
화질컬러 맵에는 충분원본에 가까운 품질
VRAM(트랜스코드 후)보통 4bpp(원본의 약 1/8)보통 8bpp(원본의 약 1/4)
인코딩 속도느림(레벨 조정 가능)약간 빠름
적합albedo/diffuse, emissivenormal, 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_basisuGLTFFileLoader에서 기본 활성화되고, 트랜스코더 파일만 찾을 수 있으면 됩니다.

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).

튜닝 순서 제안:

  1. 먼저 기본값으로 한 번 압축하고 크기와 화질 확인
  2. 불만 → --qlevel(ETC1S) 조정 또는 --zcmp(UASTC) 추가
  3. 노멀맵이 망가졌다 → UASTC를 쓰는지 확인, ETC1S가 아닌지
  4. 색이 어둡다 → 색공간 설정 확인(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, 미니프로그램, 각 씬이 정확히 어떤 조합을 써야 하는지.

후원하기