Any3DAny3D
·Any3D Team

모델 다이어트 첫 수업: 정점 압축의 세 가지 무기

3d-compressionvertex-compressionmeshoptdracogltf

이전 글에서 GLB 파일을 열어보고 텍스처가 크기의 80%를 차지하고 정점은 10-20%에 불과하다는 걸 봤습니다. 그럼 정점 압축은 무의미한 걸까요?

정반대입니다. 모델의 텍스처가 이미 KTX2로 압축되고 정점이 빽빽할 때, 남은 20%가 바로 정점입니다 — 그리고 이 20%는 또 반으로, 심지어 90%까지 줄일 수 있습니다. 더 중요한 건, 정점 압축은 거의 공짜이고 즉시 효과가 나는, 몇 안 되는 최적화 중 하나입니다. 명령 몇 줄 추가하고 디코더 하나 바꾸면 파일이 날씬해집니다.

이 글은 세 가지를 정리합니다: 정점 데이터가 실제로 어떤 모습인지, 세 가지 접근법(양자화, 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%

앞의 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

디코더와 플랫폼 호환성:

플랫폼순수 양자화MeshOptDraco
데스크톱 웹✅ 네이티브✅ 네이티브✅ 디코더 설정 필요
모바일 웹✅ 네이티브✅ 네이티브⚠️ 디코더가 무거움
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 네이티브 텍스처 포맷이 어떻게 해결하는지 살펴봅니다.

후원하기