모델 다이어트 첫 수업: 정점 압축의 세 가지 무기
이전 글에서 GLB 파일을 열어보고 텍스처가 크기의 80%를 차지하고 정점은 10-20%에 불과하다는 걸 봤습니다. 그럼 정점 압축은 무의미한 걸까요?
정반대입니다. 모델의 텍스처가 이미 KTX2로 압축되고 정점이 빽빽할 때, 남은 20%가 바로 정점입니다 — 그리고 이 20%는 또 반으로, 심지어 90%까지 줄일 수 있습니다. 더 중요한 건, 정점 압축은 거의 공짜이고 즉시 효과가 나는, 몇 안 되는 최적화 중 하나입니다. 명령 몇 줄 추가하고 디코더 하나 바꾸면 파일이 날씬해집니다.
이 글은 세 가지를 정리합니다: 정점 데이터가 실제로 어떤 모습인지, 세 가지 접근법(양자화, MeshOpt, Draco) 각각의 성격, 그리고 함정을 피하게 해줄 결론 — '가장 좋은' 해법은 없고, '가장 알맞은' 해법만 있습니다.
정점 하나는 얼마나 클까
먼저 정점 안에 뭐가 들었는지 봅시다. glTF에서 각 정점은 여러 attribute로 구성됩니다:
| 속성 | 용도 | 기본 정밀도 | 정점당 바이트 |
|---|---|---|---|
| position(위치) | 공간 속 정점 좌표 | 3 × float32 | 12 |
| normal(법선) | 조명 방향 결정 | 3 × float32 | 12 |
| tangent(접선) | 노멀맵 계산 | 4 × float32 | 16 |
| texcoord_0(UV) | 텍스처 샘플링 좌표 | 2 × float32 | 8 |
| color(정점 색) | 정점 단위 셰이딩 | 4 × float32 | 16 |
PBR 속성을 모두 갖춘 정점은 지오메트리 데이터만 48-64 바이트입니다. 10만 정점 모델은 정점만 5-6MB입니다.
여기서 거의 모든 게 float32(32비트 부동소수점)를 쓰는 걸 주목하세요. 이게 기본 설정이며, 동시에 정점 압축의 돌파구이기도 합니다 — 절대다수의 속성은 32비트 정밀도가 전혀 필요 없기 때문입니다.
첫 번째 무기: 양자화(Quantization)
양자화는 모든 정점 압축의 근본 원리이며, Draco와 MeshOpt도 내부적으로 사용합니다.
**양자화(quantization: 고정밀 부동소수점을 저정밀 정수로 매핑)**의 본질은 이렇습니다: 부동소수점 3.14159265라면 3.14만 기억해도 충분합니다. 어떤 공간의 좌표 집합에 대해, 각 소수를 32비트로 정밀하게 기록하는 대신 더 작은 범위의 정수로 표현합니다.
원본: position.x = 1.234567 (float32, 4바이트)
양자화 후: position.x = 1234 (int16, 2바이트) + 복원용 scale/offset
양자화 전후 비교:
| 속성 | float32 바이트 | 양자화 후(16비트) | 절감 |
|---|---|---|---|
| position | 12 | 6 | 50% |
| normal | 12 | 6(또는 4, int8 + octahedral 사용) | 50-67% |
| tangent | 16 | 4-8 | 50-75% |
| texcoord | 8 | 4 | 50% |
앞의 48-64 바이트 정점은 양자화로 기본적으로 16-24 바이트로 압축되어 크기가 절반 이하로 줄어듭니다.
양자화를 쓸 때
- 단지 크기를 줄이고 싶고 극단적 압축률은 필요 없을 때
- 디코더 의존성 제로를 원할 때 — 양자화된 glTF는 표준
KHR_mesh_quantization확장을 써서 주요 엔진이 네이티브로 지원하며, 추가 디코더 라이브러리가 필요 없습니다 - 대상 플랫폼이 패키지 크기에 민감할 때(예: 위챗 미니프로그램, Draco 디코더를 같이 싸면 수십 KB 증가)
쓰지 말아야 할 때
- 모델 자체가 아주 작고 디테일이 핵심(예: 밀리미터 단위 산업 부품). 양자화는 작은 모델에서 가장 잘 드러납니다 — 텍스처는 괜찮아도 정점 위치가 0.1mm 어긋나면 클로즈업에서 보입니다.
정밀도 손실의 실제 사례: 한 주얼리 전시 씬에서 반지 모델을 16비트로 양자화했더니 클로즈업에서 금속 가장자리에 계단 현상이 나타났습니다. 원인은 정점 수 부족이 아니라 월드 좌표계가 너무 작아 16비트 정수의 표현 범위가 부족했던 겁니다. 해결책은 양자화 범위를 줄이는(
position의 바운딩 박스 축소) 것이거나 작은 모델에 더 높은 비트 깊이를 쓰는 것입니다.
두 번째 무기: MeshOpt
MeshOpt는 glTF 공식 확장 EXT_meshopt_compression으로, "괜찮은 압축률, 엄청나게 빠른 해독"이 포지셔닝입니다.
하는 일은 먼저 속성을 양자화하고(위와 동일), 그 다음 엔트로피 부호화(lossless, 무손실) 기법으로 양자화된 정수를 무손실로 한 번 더 압축합니다. 즉: 손실 양자화 + 무손실 엔트로피 부호화 = 더 작고, 화질은 양자화와 동일.
- 압축률: 양자화 단독 대비 30-50% 더 작음
- 해독 속도: 극히 빠름, 순수 C/JS 구현, 싱글스레드에서 초당 수천만 정점
- 디코더 크기: 작음(gzip 후 약 20-30KB)
- 호환성: Three.js, Babylon.js가 네이티브 지원, 웹의 사실상 표준 중 하나
MeshOpt를 쓸 때
- 더 높은 압축률이 필요하지만 Draco의 느린 해독은 받아들일 수 없을 때
- 웹/모바일/WebXR이 중심 — 해독 속도가 첫 화면 경험에 직결
- 모델을 자주 해독해야 할 때(예: 동적으로 로드되는 레벨)
쓰지 않을 때
- 대상 플랫폼이
EXT_meshopt_compression조차 인식하지 못할 때(드묾, 구형 엔진) - "돌아가기만 하면" 되고 30% 차이는 신경 안 쓸 때 — 그럼 순수 양자화가 더 단순하고 의존성도 하나 줄어듭니다
세 번째 무기: Draco
Draco는 Google의 압축 방안으로, "극한의 압축률"이 포지셔닝입니다.
앞의 둘과의 근본적 차이: Draco는 정점의 연결 관계(토폴로지)를 바꿉니다. 양자화는 각 정점의 수치 표현만 바꾸고, MeshOpt는 그 위에 무손실 부호화를 얹지만, Draco는 삼각 메시를 재구성해 "어떤 정점들이 삼각형을 이루는지"를 더 컴팩트하게 표현합니다.
- 압축률: 셋 중 최고, 정점 밀집 모델에서는 흔히 90% 이상 절감
- 해독 속도: 셋 중 가장 느리지만, 절대값으로는 여전히 빠름
- 디코더 크기: 다소 큼(약 100-200KB, 보통 별도 wasm으로 로드)
- 화질: 조정 가능하지만 극한 압축률에서는 보이는 변형이 생김
Draco를 쓸 때
- 모델이 극히 크고 정점이 초밀집(100만 정점 스캔 모델, 지형)
- 한 번 로드하면 해독 후 오래 재사용(느린 해독 감내 가능)
- 패키지 크기는 병목이 아니고 다운로드 속도가 병목일 때
쓰지 않을 때
- 모바일 + 빠른 첫 화면 필요 — 디코더와 모델 모두 다운로드해야 해서 오히려 느려짐
- 미니프로그램 등 패키지 크기에 엄격한 환경
- 스킨 애니메이션, 모프 타겟이 필요한 모델 — Draco는 이들에 대한 지원이 약해 설정을 잘못하면 문제 발생
셋을 나란히: 선정 표
아래 압축비는 커뮤니티 벤치마크(DeepKolos의 테스트 + Reddit r/threejs 토론)를 참고했습니다. 모델마다 편차는 있지만 상대적 관계는 기본적으로 안정적입니다:
| 방안 | 압축비(float32 대비) | 해독 속도 | 디코더 크기 | 손실 여부 | glTF 확장 |
|---|---|---|---|---|---|
| 순수 양자화 | ~50% | 네이티브, 해독 불필요 | 0 | 있음(정밀도) | KHR_mesh_quantization |
| MeshOpt | ~25-35% | 극히 빠름 | ~25KB | 있음(정밀도) | EXT_meshopt_compression |
| Draco | ~10-20% | 빠름(셋 중 가장 느림) | ~100-200KB | 있음(정밀도+토폴로지) | KHR_draco_mesh_compression |
디코더와 플랫폼 호환성:
| 플랫폼 | 순수 양자화 | MeshOpt | Draco |
|---|---|---|---|
| 데스크톱 웹 | ✅ 네이티브 | ✅ 네이티브 | ✅ 디코더 설정 필요 |
| 모바일 웹 | ✅ 네이티브 | ✅ 네이티브 | ⚠️ 디코더가 무거움 |
| WebXR/VR | ✅ 네이티브 | ✅ 권장 | ⚠️ 신중히 사용 |
| 위챗 미니프로그램 | ✅ 권장 | ✅ 권장 | ❌ 가급적 회피 |
한 줄 요약: 편하고 의존성 제로 → 순수 양자화; 밸런스 → MeshOpt; 최고 압축률이고 기다릴 수 있으면 → Draco.
실전: gltfpack으로 양자화와 MeshOpt
gltfpack은 glTF 공식 도구로, 명령 한 줄로 양자화와 MeshOpt를 처리합니다.
먼저 설치(바이너리는 gltfpack releases에서 다운로드):
# model.glb를 16비트로 양자화하고 MeshOpt 압축 추가
gltfpack -i model.glb -o model-packed.glb -cc
# -cc = compress(기본 양자화 위에 EXT_meshopt_compression 추가)
자주 쓰는 파라미터:
# 양자화만, MeshOpt 없음(가장 가벼움, 디코더 의존 제로)
# gltfpack은 기본적으로 정점을 16비트 양자화(KHR_mesh_quantization)하므로
# 별도 플래그가 필요 없습니다
gltfpack -i model.glb -o model-quant.glb
# 양자화하고 MeshOpt 활성화
gltfpack -i model.glb -o model-meshopt.glb -cc
# 정점이 매우 많을 때, 단순화도 함께(정점 수 감소, 모델 변경됨)
gltfpack -i model.glb -o model-simplify.glb -cc -si 0.5
# -si 0.5는 약 50% 정점까지 단순화
-cc에 대해: 이건 "compress" 스위치로 추가로EXT_meshopt_compression을 적용합니다.-cc를 안 붙여도 gltfpack은 기본적으로 양자화합니다 — 즉gltfpack -i in.glb -o out.glb단독으로 이미 "순수 양자화, 디코더 의존 제로"입니다.(-v는 verbose 상세 로그 플래그이니 헷갈리지 마세요.)
전형적 효과(5MB, 12만 정점 PBR 모델, 참고용):
| 처리 | 파일 크기 | 비고 |
|---|---|---|
| 원본(float32) | 5.0MB | 베이스라인 |
| 순수 양자화(기본) | 2.6MB | 반으로 감소, 시각적 차이 거의 없음 |
MeshOpt(-cc) | 1.7MB | 추가 35% 절감, 로드 약간 빠름 |
참고:
-si단순화는모델 지오메트리를 변경하는 손실 작업으로, 압축과는 다릅니다. 압축은 시각적 일치를 유지하려 하고, 단순화는 능동적으로 디테일을 제거합니다. 둘은 겹칠 수 있지만 씬이 허용하는지에 따라 다릅니다.
자주 하는 실수
- 양자화 후 법선 방향이 바뀜: 대개 정밀도가 너무 낮은 게 원인. 법선은 최소 16비트, 또는 8비트 octahedral 부호화 사용.
- Draco 해독 후 머티리얼 소실: Draco는 메시만 압축. 머티리얼과 텍스처는 별도 처리 필요. 로드 시 Draco 디코더와 KHR 확장을 모두 설정할 것.
- 미니프로그램에서 Draco가 안 로드됨: 일부 런타임에서 디코더 wasm 로드가 제한됨. MeshOpt로 바꾸면 보통 해결.
- 양자화 후 모델이 "뜀": 모델이 원점에서 너무 멀 때 16비트 정밀도로 큰 좌표와 작은 디테일을 동시에 표현 불가. 해결책은 원점 근처로 옮기고 양자화하거나 비트 깊이를 높이는 것.
다음 단계
정점 압축은 끝났습니다 — 너무 일찍 축하하진 마세요. 앞서 말했듯 텍스처가 모델 크기의 80%입니다. 다음엔 전장을 바꿔, 전통적 PNG/JPG가 GPU 눈에 어떤 "대식가"로 보이는지, 그리고 GPU 네이티브 텍스처 포맷이 어떻게 해결하는지 살펴봅니다.