Kris

모델 다이어트 첫 수업: 버텍스 압축 3가지 도구

3D 압축정점 압축MeshOptDracoglTF

지난 글에서 GLB 파일을 뜯어보며 텍스처가 부피의 80%를 차지하고, 버텍스는 10~20%에 불과하다는 것을 알게 되었습니다. 그렇다면 버텍스 압축은 중요하지 않은 걸까요?

정반대입니다. 모델의 텍스처가 이미 KTX2로 압축되었고, 버텍스가 빽빽하게 들어찼다면 남은 20%가 바로 버텍스입니다. 그리고 이 20%는 절반은 물론, 90%까지도 더 줄일 수 있습니다. 더 중요한 점은, 버텍스 압축은 거의 제로 비용으로 즉시 효과를 볼 수 있는 몇 안 되는 최적화라는 것입니다. 몇 줄의 명령어를 추가하고 디코더 하나만 바꾸면 파일 크기가 확 줄어듭니다.

이번 글에서는 세 가지를 명확히 다룹니다: 버텍스 데이터가 어떻게 생겼는지; 양자화(Quantization), MeshOpt, Draco 각각의 특성; 그리고 함정을 피할 수 있는 결론 – 가장 좋은 방법은 없고, 가장 적합한 방법만 있다는 것입니다.

버텍스 하나는 얼마나 클까?

먼저 버텍스 안에 무엇이 들어 있는지 살펴보겠습니다. glTF에서 각 버텍스는 여러 속성(attribute)으로 구성됩니다.

속성용도기본 정밀도버텍스당 바이트
position (위치)공간에서의 좌표3 × float3212
normal (법선)조명 방향 결정3 × float3212
tangent (접선)노멀 맵 계산4 × float3216
texcoord_0 (UV)텍스처 샘플링 좌표2 × float328
color (버텍스 컬러)버텍스 단위 색상4 × float3216

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비트)절감
position12650%
normal126 (또는 4, int8 + octahedral 사용)50-67%
tangent164-850-75%
texcoord8450%

앞서 4864바이트였던 버텍스는 양자화 후 기본적으로 **1624바이트**로 줄어들어, 부피가 절반 이상 감소합니다.

언제 양자화를 사용할까?

  • 부피를 줄이고 싶지만 극단적인 압축률이 필요하지 않을 때
  • 디코더 의존성을 제로로 하고 싶을 때 – 양자화된 glTF는 표준 KHR_mesh_quantization 확장을 사용하며, 주요 엔진에서 기본 지원하므로 추가 디코더 라이브러리가 필요 없음
  • 대상 플랫폼이 패키지 크기에 민감할 때 (예: 카카오톡 미니앱, Draco 디코더 하나 추가하면 수십 KB)

언제 사용하지 말아야 할까?

  • 모델 자체가 매우 작고 세부 묘사가 중요할 때 (예: 밀리미터 단위의 산업용 부품). 양자화는 작은 모델에서 특히 문제가 드러나기 쉽습니다. 텍스처는 괜찮지만, 버텍스 위치가 0.1mm만 어긋나도 클로즈업에서는 눈에 띕니다.

양자화 정밀도 손실의 실제 함정: 보석 전시 장면에서 반지 모델을 16비트로 양자화한 후, 클로즈업에서 금속 가장자리에 톱니가 나타났습니다. 원인은 버텍스 수 부족이 아니라, 월드 좌표계가 너무 작아 16비트 정수로 표현할 수 있는 범위가 충분하지 않았기 때문입니다. 해결책은 양자화 범위를 줄이거나(position의 bounding box 축소), 작은 모델의 경우 더 높은 비트 심도를 사용하는 것입니다.

두 번째 도구: MeshOpt

MeshOpt는 glTF 공식 확장 EXT_meshopt_compression으로, 압축률은 괜찮고 디코딩 속도가 매우 빠른 것이 특징입니다.

이 방식은 먼저 속성을 양자화한 후(위와 동일), LISS(무손실 엔트로피 코딩, lossless entropy coding) 라는 기법으로 양자화된 정수를 추가로 무손실 압축합니다. 즉, 양자화(손실) + 엔트로피 코딩(무손실) = 더 작은 부피, 양자화와 동일한 화질입니다.

  • 압축률: 단순 양자화보다 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를 사용할까?

  • 모델이 매우 크고 버텍스가 매우 많을 때 (수백만 버텍스 스캔 모델, 지형)
  • 한 번 로드한 후 오래 재사용할 때 (디코딩이 조금 느려도 괜찮음)
  • 패키지 크기보다 다운로드 속도가 병목일 때

언제 사용하지 말아야 할까?

  • 모바일 + 빠른 첫 화면 필요 – 디코더와 모델 자체를 모두 다운로드해야 하므로 오히려 느려짐
  • 카카오톡 미니앱 등 패키지 크기에 엄격한 환경
  • 모델에 스키닝 애니메이션, 모프 타겟(morph targets)이 있을 때 – 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

디코더 및 플랫폼 호환성:

플랫폼순수 양자화MeshOptDraco
데스크톱 웹✅ 기본✅ 기본✅ 디코더 필요
모바일 웹✅ 기본✅ 기본⚠️ 디코더 무거움
WebXR/VR✅ 기본✅ 권장⚠️ 신중히
카카오톡 미니앱✅ 권장✅ 권장❌ 가급적 피할 것

한 줄 요약: 간편하고 의존성 제로 → 순수 양자화; 균형 잡힌 선택 → MeshOpt; 극한의 압축률과 대기 가능 → Draco.

실전: gltfpack으로 양자화와 MeshOpt 사용하기

gltfpack은 glTF 공식 도구로, 한 줄 명령으로 양자화와 MeshOpt를 적용할 수 있습니다.

먼저 설치합니다 (바이너리는 gltfpack release에서 다운로드 가능).

# 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.7MB35% 추가 절감, 로딩 약간 더 빠름

주의: -si 단순화는 모델의 기하를 변경하는 손실 작업으로, 압축과는 다릅니다. 압축은 가급적 시각적 일관성을 유지하는 반면, 단순화는 적극적으로 디테일을 줄입니다. 둘을 함께 사용할 수 있지만, 장면이 허용하는지 확인해야 합니다.

자주 발생하는 함정

  • 양자화 후 법선 방향이 바뀜: 너무 낮은 정밀도를 사용한 경우가 많습니다. normal은 최소 16비트, 또는 8비트 옥타헤드럴(octahedral) 인코딩을 사용하세요.
  • 미니앱에서 Draco 로드 실패: wasm 디코더는 일부 런타임 환경에서 실행 제한이 있을 수 있습니다. MeshOpt로 전환하면 대부분 해결됩니다.
  • 양자화 후 모델 '드리프트' 현상: 모델이 원점에서 너무 멀리 떨어져 있으면 16비트 정밀도로 큰 좌표와 미세한 디테일을 동시에 표현하기 어렵습니다. 양자화 전에 모델을 원점 근처로 이동하거나 비트 심도를 높이세요.

다음 단계

버텍스 압축은 끝났지만 아직 안심하기는 이릅니다. 앞서 언급했듯이 텍스처가 모델 용량의 80%를 차지합니다. 다음 글에서는 기존 PNG/JPG 텍스처가 왜 GPU 관점에서 심각한 메모리 낭비인지, 그리고 GPU 네이티브 텍스처 포맷이 이를 어떻게 해결하는지 살펴보겠습니다.

후원하기