Blender에서 출시까지: 엔드투엔드 압축 실전
시리즈도 여기까지 와서, 도구, 원리, 선정을 모두 다뤘습니다. 마지막 이 글에서 모든 것을 굴러가는 파이프라인으로 묶습니다: 실제 Blender 모델에서 출발해, 한 단계씩 압축하고, 각 단계에서 파일 크기, VRAM, 로드 시간의 변화를 기록하고, 마지막에 50MB의 중량급에서 폰에서 즉시 열리는 5MB 모델이 될 수 있는지 봅니다.
대상 독자: 앞선 다섯 편을 읽고 실제로 손대려는 사람. 새 개념은 안 내놓습니다, 복사 가능한 흐름, 명령, 스크립트만 놓습니다.
출발점: 실제 PBR 모델
매우 전형적인 이커머스 표시 모델을 샘플로 씁니다: 고정밀 제품 모델에 완전한 PBR 맵 포함.
| 초기 지표 | 수치 |
|---|---|
| Blender 소스 파일 | ~120MB(내보내지 않은 하이폴리 포함) |
| 내보낸 GLB(float32 + PNG) | ~50MB |
| 정점 수 | 약 18만 |
| 맵 | 6장 4096×4096(albedo, normal, roughness, metallic, AO, emissive) |
| VRAM 사용량(6장 모두 해제) | ~520MB |
| 목표 | 파일 ≤ 5MB, VRAM 제어 가능, 모바일에서 즉시 열림 |
50MB 파일에 520MB VRAM — 이 모델은 모바일에서 확실히 크래시 납니다. 한 단계씩 갑시다.
0단계: Blender에서 올바르게 내보내기
압축의 첫 관문은 사실 내보내기이며, 많은 사람이 여기서 피를 봅니다.
Blender에서 glTF를 내보낼 때 핵심 설정:
- 포맷:
glTF Binary (.glb)(단일 파일, 전송 편리) - 지오메트리:
Normals,Tangents체크(PBR 노멀맵에는 접선 필요) - UV: 확실히 내보내기(기본 켜짐)
- 텍스처:
Automatic또는JPEG(여기서 텍스처 포맷은 상관없음, 나중에 다시 압축하지만, 내보내졌는지는 필수) - 압축: Blender 자체 메시 압축은 체크하지 마세요, 더 전문적인 도구를 씁니다
- 트랜스폼:
+Y Up(glTF 표준) - 데이터: 필요한 것만 체크(애니메이션, 카메라, 라이트는 필요 없으면 내보내지 않아 크기 절감)
내보낸 후 model.glb: 50MB, PNG 맵 6장, float32 정점. 이게 베이스라인입니다.
첫 번째 흔한 함정이 바로 여기: Blender는 기본적으로 미사용 메시와 숨긴 보조 오브젝트도 함께 내보냅니다. 내보내기 전에
File > Clean Up > Purge Orphans을 실행하고, 아웃라이너에서 내보낼 오브젝트만 선택하세요.
엔드투엔드 파이프라인 전경
전체 라인을 그려 머릿속에 지도를 만듭니다:
Blender 소스 파일
│ .glb 내보내기(float32 + PNG) 50MB
▼
[1] 중복 제거 + 중복 정점 웰드(gltf-transform) ~45MB
│
[2] 정점 압축: MeshOpt(gltfpack / gltf-transform) ~30MB
│
[3] 텍스처 압축: PNG → KTX2(ETC1S/UASTC) ~6MB
│
[4](선택) 지오메트리 단순화 LOD(simplify) ~4-5MB
▼
최종 model-final.glb ~5MB
│
엔진 로드(Three.js / Babylon.js) → 런타임 트랜스코드 → 출시
각 단계의 수치는 아래 표에서 실시간 추적합니다.
툴체인: 어느 것을 고를까
압축 도구는 여럿 있습니다, 잘못 고르지 않도록 비교를:
| 도구 | 강점 | 약점 | 적합 |
|---|---|---|---|
| gltf-transform | 올라운더, 텍스처+정점을 한 방에, API화·스크립트화 가능 | 극한 압축률은 전용 도구에 못 미침 | 메인 추천, 압도적 다수의 씬 |
| gltfpack | 정점 압축 전문, MeshOpt 네이티브 지원 | 텍스처 압축 약함 | 정점 밀집, MeshOpt 세 제어 |
| toktx | 텍스처 압축에 가장 전문, 파라미터 최다 | 텍스처만 처리, 모델 전체 불가 | 단일 텍스처 정밀 튜닝 |
| gltf-pipeline | 원로, Draco 지원 | 유지보수 비활성, 기능 적음 | 기존 Draco 레거시 프로젝트 |
| 온라인 도구(gltf.report) | 제로 설치 | 자동화·대량 처리 부적합 | 시험, 일회성 작업 |
메인 추천: gltf-transform으로 전체 흐름을 굴리고, 필요 시 gltfpack으로 정점을, toktx로 단일 텍스처를 보충합니다. 아래 모든 단계는 gltf-transform 기반입니다.
1단계: 중복 제거 + 웰드
모델에는 흔히 중복 정점, 미사용 노드와 머티리얼이 있습니다. 먼저 한 번 청소합니다.
gltf-transform optimize model.glb step1.glb --weld --prune
| 단계 | 파일 크기 | VRAM | 변화 |
|---|---|---|---|
| 베이스라인 | 50MB | ~520MB | — |
| Step 1 중복 제거 | 45MB | ~520MB | -5MB(VRM 불변, 텍스처가 여전히 있어서) |
VRAM은 거의 안 움직입니다, 예상대로입니다. 중복 제거가 주로 절약하는 건 정점과 구조이고, 텍스처가 VRAM의 대부분입니다.
2단계: 정점 압축 MeshOpt
gltf-transform optimize step1.glb step2.glb --meshopt --weld --prune
--meshopt는 정점을 16비트로 양자화하고 MeshOpt 무손실 부호화를 적용, 자동으로 EXT_meshopt_compression 확장을 추가합니다.
| 단계 | 파일 크기 | VRAM | 변화 |
|---|---|---|---|
| Step 1 | 45MB | ~520MB | — |
| Step 2 + MeshOpt | 30MB | ~520MB | -15MB(정점 부분) |
VRAM이 여전히 ~520MB? 맞습니다 — 정점은 VRAM에서 작은 비중(10-20%)이라, 정점을 잘라도 VRAM 영향이 제한적입니다. 진짜 VRAM 괴물은 텍스처, 다음에 해결합니다.
3단계: 텍스처 압축 PNG → KTX2
이 단계는 비용 대비 효과의 왕입니다.
gltf-transform optimize step2.glb step3.glb \
--texture-compress basisu \
--meshopt --weld --prune
--texture-compress basisu는 각 맵을 자동 판단합니다: 컬러 맵(albedo, emissive)은 ETC1S, 데이터 맵(normal, roughness, metallic, AO)은 UASTC.
| 단계 | 파일 크기 | VRAM | 변화 |
|---|---|---|---|
| Step 2 | 30MB | ~520MB | — |
| Step 3 + KTX2 | 6MB | ~70MB | -24MB 파일 / -450MB VRAM |
이 단계가 파이프라인 전체의 전환점입니다:
- 파일이 30MB에서 6MB로 하락
- VRAM이 520MB에서 약 70MB로 하락 — 6장의 4096 맵이 "해제 후 raw pixel"에서 "블록 압축"으로 바뀌어, 각 장이 ~87MB에서 ~11-14MB로
VRAM이 한 자릿수 하락했고, 이게 모바일에서 도는지의 핵심입니다.
4단계:(선택) 지오메트리 단순화
더 작길 원하고 씬이 정점 정밀도 저하를 허용하면, 지오메트리 단순화를 추가합니다.
gltf-transform optimize step3.glb final.glb \
--texture-compress basisu \
--meshopt \
--simplify --simplify-ratio 0.5 \
--weld --prune
--simplify-ratio 0.5는 약 50% 정점을 남긴다는 뜻입니다.
| 단계 | 파일 크기 | VRAM | 변화 |
|---|---|---|---|
| Step 3 | 6MB | ~70MB | — |
| Step 4 + 단순화 0.5 | 4.5MB | ~70MB | -1.5MB(VRM 거의 불변) |
단순화는 주로 파일 크기를 절약하고 VRAM 영향은 작습니다. 대가는 모델 디테일 저하 — 근거리에서 눈에 띕니다. 이커머스 상품 페이지는 보통 과도한 단순화 비추천, 건축/대형 씬은 아주 적합합니다.
효과 추적 총표
네 단계를 겹쳐서 전체상을 봅니다(위 샘플 기반, 수치는 규모의 참고용):
| 단계 | 파일 크기 | VRAM | 누적 감소율 |
|---|---|---|---|
| 베이스라인(float32 + PNG) | 50MB | ~520MB | — |
| + 중복 제거 & 웰드 | 45MB | ~520MB | -10% |
| + MeshOpt 정점 | 30MB | ~520MB | -40% |
| + KTX2 텍스처 | 6MB | ~70MB | -88% 파일 / -87% VRAM |
| + 지오메트리 단순화(0.5) | 4.5MB | ~70MB | -91% 파일 |
결론: 텍스처 압축이 용량과 VRAM 수익의 압도적 다수를 담당합니다. 정점 압축은 보너스이고, 텍스처 압축이 진짜 구세주입니다. 이는 1편의 논단과 완전히 일치합니다 — 텍스처가 용량의 80%를 차지해, 최적화의 수익이 가장 높습니다.
한 줄 명령판: 귀찮을 때 한 방 압축
단계별로 가기 싫다면, 모든 최적화를 한 번에:
gltf-transform optimize model.glb model-final.glb \
--texture-compress basisu \
--meshopt \
--simplify --simplify-ratio 0.5 \
--weld --prune
이 한 줄 명령 = 중복 제거 + 웰드 + 정점 MeshOpt + 텍스처 KTX2 + 지오메트리 단순화. 90% 씬에서 충분하고, 단계별은 주로 이해와 파라미터 조정을 위해서입니다.
자동화 스크립트: 재사용 가능
위 흐름을 빌드에 넣을 수 있는 스크립트로 묶습니다. 이 스크립트는 대상 플랫폼별로 다른 버전을 출력하고 각 단계의 효과를 출력합니다.
// scripts/compress-model.mjs
import { optimize } from "@gltf-transform/functions";
import { NodeIO } from "@gltf-transform/core";
import { KHRONOS_EXTENSIONS } from "@gltf-transform/extensions";
import { filesize } from "filesize";
const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS);
// 플랫폼별 압축 전략
const PROFILES = {
mobile: {
textureCompression: "basisu",
meshCompression: "meshopt",
simplify: { ratio: 0.5 },
weld: true,
prune: true,
},
vr: {
textureCompression: "basisu",
meshCompression: "meshopt",
// VR 근거리 시청, 단순화 없음
simplify: null,
weld: true,
prune: true,
},
desktop: {
textureCompression: "webp",
meshCompression: "meshopt",
simplify: null,
weld: true,
prune: true,
},
};
async function compress(inputPath, profileName) {
const cfg = PROFILES[profileName];
const doc = await io.read(inputPath);
const before = Buffer.byteLength(await io.writeBinary(doc), "utf8");
await optimize(doc, {
textureCompression: cfg.textureCompression,
meshCompression: cfg.meshCompression,
simplify: cfg.simplify ?? undefined,
weld: cfg.weld,
prune: cfg.prune,
});
const bytes = await io.writeBinary(doc);
const outPath = inputPath.replace(/\.glb$/, `-${profileName}.glb`);
await io.write(outPath, doc);
const after = bytes.byteLength;
console.log(
`${profileName.padEnd(8)} ${filesize(before)} → ${filesize(after)} ` +
`(${Math.round((1 - after / before) * 100)}% smaller) → ${outPath}`
);
}
// 사용법: node scripts/compress-model.mjs path/to/model.glb
const input = process.argv[2];
for (const profile of Object.keys(PROFILES)) {
await compress(input, profile);
}
프로젝트에 넣기:
node scripts/compress-model.mjs public/models/model.glb
# mobile 50MB → 4.5MB (91% smaller) → model-mobile.glb
# vr 50MB → 6.2MB (87% smaller) → model-vr.glb
# desktop 50MB → 11MB (78% smaller) → model-desktop.glb
프론트엔드는 런타임에 기기별로 대응 버전을 로드하면 됩니다.
이 파이프라인을 직접 구축하고 싶지 않으신가요? Any3D의 온라인 도구가 위의 모든 것을 한 방에 완료합니다 — GLB를 업로드하면 자동으로 텍스처 KTX2 + 정점 MeshOpt를 돌리고, 플랫폼별로 압축 버전을 출력해 로컬 툴체인 구축의 번거로움을 덜어줍니다.
자주 묻는 함정 FAQ
압축 후 모델이 검게 변함 / 텍스처 안 보임
- 99%는 색공간: 컬러 맵의 sRGB 설정 누락. Three.js에서는
texture.colorSpace = THREE.SRGBColorSpace. - toktx에서 컬러 맵에
--srgb누락.
노멀맵 압축 후 조명이 이상함
- 노멀맵에 ETC1S 사용, UASTC로 교체.
- 노멀맵이 DirectX 스타일(그린 채널 다운), 엔진은 OpenGL 스타일 요구, G 채널을 뒤집어야 함.
모바일 로드가 첫 화면에서 멈춤
- Draco 디코더 wasm을 로드 중인지 확인(추가 요청). 모바일에서는 MeshOpt 우선.
- KTX2 트랜스코더 경로 설정 오류, 트랜스코드 실패로 CPU 해제로 폴백.
압축 후 파일이 오히려 커짐
- 맵이 너무 작음(< 128px), KTX2는 손해, 블록 압축에는 고정 오버헤드 있음.
- 모델이 이미 한 번 압축됨, 재압축은 수익 없음(오히려 마이너스).
단순화 후 모델이 깨짐
--simplify-ratio가 너무 낮음, 0.7-0.8로 올리기.- 단순화는 하드 서피스(기계, 건축)에 우호적, 유기 곡면(캐릭터)은 깨지기 쉬움.
KTX2가 일부 브라우저에서 로드 실패
- 구형 Safari / 구형 WebView 미지원. PNG/WebP 폴백을 준비하거나,
KHR_texture_basisu의fallback필드로 세이프티넷 제공.
시리즈 치트시트
전 6편의 에센스를 하나의 표로 응축, 북마크 추천.
용량 구성
| 구성 | 비율 | 최적화 도구 |
|---|---|---|
| 텍스처 맵 | 70-85% | KTX2(최대 수익) |
| 정점 데이터 | 10-20% | MeshOpt / 양자화 / Draco |
| 애니메이션 데이터 | 0-15% | 키프레임 감소 / 압축 |
| 기타 | < 2% | 중복 제거 |
VRAM 공식
전통 포맷 VRAM = 너비 * 높이 * 4바이트 * 1.333(mipmap 포함)
KTX2 블록 압축 VRAM ≈ 위 식 / 4 (ETC1S) 또는 / 2 (UASTC)
정점 압축 선정
| 시나리오 | 추천 |
|---|---|
| 의존 제로, 가장 단순 | 순수 양자화(KHR_mesh_quantization) |
| 웹 밸런스의 첫 선택 | MeshOpt |
| 극한 압축률, 해독을 기다릴 수 있음 | Draco |
| 미니프로그램 / 패키지 민감 | 순수 양자화 / MeshOpt, Draco 회피 |
텍스처 압축 선정
| 맵 타입 | 추천 인코딩 |
|---|---|
| albedo / emissive(컬러) | KTX2 ETC1S |
| normal / roughness / metallic / AO(데이터) | KTX2 UASTC |
| 데스크톱 웹, 다운로드 속도 추구 | WebP / AVIF |
| 작은 맵(< 128px) | PNG 유지, KTX2 안 함 |
한 방 명령
# 전체 최적화(텍스처 + 정점 + 단순화)
gltf-transform optimize model.glb model-final.glb \
--texture-compress basisu --meshopt \
--simplify --simplify-ratio 0.5 --weld --prune
플랫폼 치트
| 플랫폼 | 텍스처 | 정점 |
|---|---|---|
| 데스크톱 웹 | WebP / KTX2 | MeshOpt |
| 모바일 웹 | KTX2 필수 | MeshOpt |
| VR | KTX2 필수 | MeshOpt + LOD |
| 미니프로그램 | KTX2 / WebP | MeshOpt / 양자화 |
| 대형 씬 | KTX2 필수 | MeshOpt + Draco + LOD |
시리즈 회고
여섯 편을 걸어, 묶으면 하나의 완전한 체인이 됩니다:
- 왜 이렇게 무거운가: 용량 구성과 VRAM의 진실 파악
- 정점 압축의 세 가지 무기: 양자화, MeshOpt, Draco의 원리와 선택
- 텍스처의 VRAM 문제: PNG/JPG가 GPU 눈에 죄가 있는 이유
- KTX2 실전: ETC1S/UASTC 선정, 툴체인, 엔진 로드
- 선정 가이드: 플랫폼/씬별 의사결정 프레임워크
- 이 글: 엔드투엔드 파이프라인, Blender에서 출시까지
핵심은 한 문장입니다: 먼저 병목(다운로드/VRAM/프레임레이트)을 생각하고, 그 다음 도구를 고르세요. 텍스처 압축의 수익이 가장 크고, 정점 압축은 보너스입니다. 플랫폼이 다르면 운명도 다릅니다, 일도양단하지 마세요.
이 글의 스크립트와 치트시트를 따라, 모델을 50MB에서 5MB로, VRAM을 520MB에서 70MB로 가져가는 건 재현 가능한 길일 것입니다. 남은 건 그저 손대는 것뿐입니다.