Captain's Log
← Observatory
Observatory·

Blender 모델을 웹에서 돌려보기 — glTF, Draco, three.js

Blender로 만든 3D 모델을 이미지나 영상이 아니라 '진짜 3D'로 게시하고 싶었다. .blend를 애니메이션째 glTF로 내보내고, Draco로 12MB를 629KB까지 줄이고, three.js 뷰어로 이 블로그 안에서 직접 돌려보게 만든 기록.

TrainingBlender3DThree.jsCreative Coding

스크린샷이 아니라 3D 그대로

Blender에서 만든 모델은 지금까지 렌더 이미지나 영상으로만 공유해 왔다. 하지만 3D 작업의 재미는 결국 돌려보는 것에 있다. 카메라를 내가 고른 각도가 아니라, 보는 사람이 직접 잡게 하고 싶었다. 그래서 이 블로그에 three.js 기반 3D 뷰어를 만들어 넣었다.

대상은 토러스를 입체 텍스트가 휘감으며 도는 Geo DiPin 모델 — 원형 커브를 따라 변형시킨 타이포그래피 링이다. 아래에서 직접 드래그해 볼 수 있다.

3D 뷰어 로딩 중…

파이프라인 — .blend에서 브라우저까지

three.js는 .blend 파일을 직접 읽지 못한다. 웹 3D의 표준 포맷은 glTF(.glb) — "3D계의 JPEG"라 불리는 전송용 포맷이다. 변환은 Blender를 열지 않고 헤드리스 CLI로 처리했다.

# blender -b model.blend --python export_glb.py -- out.glb
bpy.ops.export_scene.gltf(
    filepath=out,
    export_format='GLB',
    export_apply=True,        # REMESH·CURVE 모디파이어를 메시에 굽기
    export_animations=True,   # 회전 애니메이션 포함
    export_lights=True,       # 씬의 컬러 스팟 조명 포함
    export_draco_mesh_compression_enable=True,
)

여기서 두 가지가 결정적이었다.

  • 모디파이어 굽기: 이 모델의 텍스트는 FONT 오브젝트에 REMESH + CURVE 모디파이어를 얹어 원형 커브를 따라 흐르게 만든 것이다. glTF는 모디파이어 개념이 없으므로 export_apply로 최종 형태를 메시에 구워야 한다
  • Draco 압축: 리메시된 텍스트 메시가 무거워 처음 내보낸 .glb가 12MB였다. Draco 지오메트리 압축 옵션 하나로 629KB, 약 1/19로 줄었다. 대신 브라우저 쪽에 디코더가 필요한데, CDN에 기대지 않도록 public/draco/에 직접 올렸다

애니메이션은 그대로 살아남는다. Blender에서 Empty 계층(Rotate 1 → Rotate 2)에 걸어둔 회전 키프레임이 glTF 애니메이션 트랙으로 변환되어, 웹에서도 같은 움직임을 반복한다.

뷰어 — react-three-fiber로 30줄

뷰어는 three.js를 React식으로 쓰는 @react-three/fiber와 헬퍼 모음 @react-three/drei로 만들었다. 핵심 로직은 놀랄 만큼 짧다.

function Model({ src }: { src: string }) {
  const gltf = useGLTF(src, "/draco/");           // Draco 디코더 경로
  const { actions } = useAnimations(gltf.animations, gltf.scene);
 
  useEffect(() => {
    Object.values(actions).forEach((a) => a?.reset().play());
  }, [actions]);
 
  return <primitive object={gltf.scene} />;
}
  • 자동 프레이밍: 모델 크기를 미리 알 필요 없이 drei의 <Bounds fit> + <Center>가 어떤 모델이든 화면에 맞게 잡아준다
  • 원본 조명 그대로: 씬의 컬러 스팟 3개가 KHR_lights_punctual로 함께 실려 온다. 다만 Blender의 와트가 칸델라로 변환되면 수십만 단위라 그대로는 화면이 하얗게 타버린다 — 배율 하나로 보정했다
  • 글로우 재현: Blender 렌더의 빛 번짐은 후처리 효과라 glTF에 담기지 않는다. three.js 쪽에서 Bloom 패스를 걸어 같은 무드를 냈다
  • 유리의 함정: 토러스는 transmission 1.0의 유리 재질인데, 투명 캔버스 위에서는 하얗게 떠 보인다. 씬 배경을 실제 검정으로 깔고, glTF에 실리지 않는 볼륨 정보 대신 thickness를 직접 줘서 굴절을 살렸다. 마지막으로 유리의 생명인 표면 반사 — 원격 HDR 없이 Lightformer 몇 장으로 가상 환경을 만들어 프레넬 반사를 얹으니 비로소 유리가 유리처럼 보였다
  • 인터랙션: OrbitControls로 드래그 회전, 가만히 두면 천천히 자동 회전. 페이지 스크롤을 뺏지 않도록 휠 줌은 기본 비활성
  • 번들 분리: three.js는 가볍지 않다. next/dynamic으로 뷰어를 지연 로드해서, 3D가 없는 포스트에는 단 1바이트도 실리지 않게 했다

이렇게 만든 <ModelViewer>는 MDX 컴포넌트로 등록해서, 앞으로는 글 어디서든 태그 한 줄로 3D 모델을 게시할 수 있다.

<ModelViewer src="/models/geo-dipin.glb" title="Geo DiPin" />

배운 것

이번에도 결론은 포맷이 절반이라는 것. .blend는 작업 파일이고 glTF는 전송 포맷이다 — PSD와 JPEG의 관계처럼, 웹에 내보내는 순간 모디파이어와 노드는 굽고, 지오메트리는 압축하고, 애니메이션은 트랙으로 평탄화해야 한다. 그 변환만 제대로 해두면 브라우저 쪽은 이미 잘 닦인 길이었다. 뷰어를 컴포넌트로 만들어 둔 것도 남는 장사다. 다음 모델부터는 .glb 하나 얹고 태그 한 줄이면 끝이니까.