티스토리 뷰

반응형

three.js glTF 내보내기 대표 이미지 - 교량 모델을 GLB 파일로

 

9편까지 만든 교량 모델은 모두 브라우저 안에서만 살아 있었다. 창을 닫으면 사라지고, 다른 사람에게 보여 주려면 HTML 파일과 코드를 통째로 넘겨야 했다. 7편에서 부재마다 넣은 ID·재질·물량 같은 BIM 속성도 마찬가지다.

연재의 마지막 10편에서는 교량 모델을 glTF 파일(.glb / .gltf)로 내보낸다. 버튼 하나로 파일이 저장되고, 그 파일은 Blender, 온라인 뷰어, 웹페이지, 다시 three.js에서 열린다. 무엇보다 부재 속성(userData)이 파일 안에 함께 저장되는 것을 확인한다.

 

이번 편에서 만들 것
 

▲ 실제로 동작하는 데모. 버튼을 누르면 교량 모델이 .glb / .gltf 파일로 다운로드된다. (드래그는 회전)

 

○ .glb 내보내기를 누르면 bridge.glb 파일이 다운로드된다. 같은 모델을 .gltf(JSON 텍스트)로도 받을 수 있다.

○ 다시 불러오기는 방금 만든 .glb를 GLTFLoader로 읽어서, 부재 37개의 속성이 모두 살아 있는지 확인한다. 검은 상자에는 파일 안에 실제로 저장된 노드 하나(G2-2)가 그대로 보인다.

○ 바닥판을 끄고 내보내면 숨긴 부재는 빠진다(onlyVisible). 아래 그래프는 두 형식의 파일 크기를 그 자리에서 재서 비교한 것이다.

 

glTF란?

○ glTF(GL Transmission Format)는 Khronos 그룹이 만든 3D 모델 표준 형식이다. 웹과 앱에서 빠르게 불러오도록 설계되어 "3D의 JPEG"라고도 부른다. 텍스트(JSON)인 .gltf와, 같은 내용을 파일 하나의 바이너리로 묶은 .glb 두 가지가 있다.

형식 저장하는 것 부재 속성 저장 주 용도
glTF / GLB 형상 · 재질 · 계층 · 애니메이션 extras에 자유롭게 웹 · 앱 · 실시간 3D 공유
OBJ 형상 · 재질(.mtl 별도) 사실상 불가 오래된 범용 교환
STL 삼각형 면만 불가 3D 프린팅
FBX 형상 · 재질 · 애니메이션 제한적 게임 · 애니메이션 (Autodesk 형식)
IFC 부재 · 관계 · 속성 (BIM 표준) 표준 속성 체계 BIM 도구 간 교환

 

○ IFC가 BIM 표준이지만 웹에서 바로 쓰기에는 무겁고 별도 라이브러리가 필요하다. 이 연재처럼 three.js로 만든 모델은 glTF로 내보내고, 속성은 extras에 담는 방식이 가장 간단하다.

 

glTF 파일의 구조

○ .gltf 파일을 메모장으로 열면 JSON이다. 장면(scene) 아래 노드(node)가 있고, 노드가 형상(mesh)과 재질(material)을 가리킨다. 꼭짓점 좌표 같은 큰 데이터는 버퍼(buffer)에 들어간다.

 

glTF 파일 구조 - scene, node, mesh, material, extras

 

항목 내용 three.js에서
scenes · nodes 장면과 객체 계층 (부모-자식) Scene, Group, Mesh
name 노드 이름 mesh.name (부재 ID)
translation · rotation 위치와 회전 position, quaternion
extras 자유 형식 추가 정보 mesh.userData
meshes · accessors · buffers 꼭짓점 · 면 데이터 BufferGeometry
materials PBR 재질 (색 · 금속성 · 거칠기) MeshStandardMaterial

 

1단계: GLTFExporter로 내보내기

○ three.js에는 GLTFExporter가 들어 있다. 내보낼 객체(장면이나 그룹)와 옵션을 넣으면 glTF 데이터가 나온다.

import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';

const exporter = new GLTFExporter();
const result = await exporter.parseAsync(bridge, {
  binary: true,        // true → .glb (ArrayBuffer), false → .gltf (JSON 객체)
  onlyVisible: true,   // 숨긴 부재는 제외
  trs: true,           // 위치를 matrix 대신 translation / rotation / scale로 저장
});
옵션 기본값 설명
binary false true면 .glb(바이너리 하나), false면 .gltf(JSON)
onlyVisible true visible = false인 객체 제외
trs false true면 위치·회전·크기를 따로 저장 (사람이 읽기 쉬움)
animations [] 함께 저장할 애니메이션 클립

 

○ 조명과 카메라는 빼고 교량 그룹만 내보내는 것이 좋다. 장면 전체를 넣으면 바닥·격자·헬퍼까지 들어간다.

 

2단계: 파일로 다운로드

○ 브라우저에서는 파일을 직접 저장할 수 없으므로, 데이터를 Blob으로 만들고 임시 링크를 클릭해서 다운로드시킨다.

function download(data, filename, type) {
  const url = URL.createObjectURL(new Blob([data], { type }));
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;                              // 저장될 파일 이름
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);   // 다운로드 시작 후 정리
}

// .glb : ArrayBuffer 그대로
download(result, 'bridge.glb', 'model/gltf-binary');
// .gltf : JSON 객체를 문자열로
download(JSON.stringify(result, null, 2), 'bridge.gltf', 'model/gltf+json');

 

3단계: 부재 속성은 extras로

○ GLTFExporter는 객체의 name을 노드 이름으로, userData를 extras로 그대로 저장한다. 7편에서 부재마다 넣은 BIM 속성이 별도 작업 없이 파일에 들어간다.

 

three.js userData가 glTF extras로 저장되는 모습

 

three.js glTF 주의
mesh.name nodes[i].name 부재 ID처럼 고유하게 붙이기
mesh.userData nodes[i].extras JSON으로 바꿀 수 있는 값만 (숫자 · 문자 · 배열 · 객체)
material.name materials[i].name 재질 이름도 그대로 저장
group.userData 부모 노드 extras 교량 전체 정보(사업명 등) 저장

 

○ userData에 함수나 three.js 객체(Vector3, Mesh 참조 등)를 넣으면 JSON으로 바꿀 수 없어서 저장되지 않는다. 속성은 숫자와 문자만 넣는 습관을 들이면 된다.

 

4단계: 다시 불러와서 확인

○ 내보낸 파일이 제대로 됐는지는 GLTFLoader로 다시 읽어 보면 알 수 있다. GLTFLoader는 extras를 다시 userData로 돌려준다.

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

new GLTFLoader().load('bridge.glb', (gltf) => {
  scene.add(gltf.scene);
  gltf.scene.traverse((o) => {
    if (o.isMesh) console.log(o.name, o.userData);    // G2-2 { type: '거더', wt: 9.93, ... }
  });
});

○ 주의: 내려받은 HTML을 더블클릭해서(file:// 주소로) 열면 브라우저 보안 정책 때문에 load()로 로컬 파일을 읽지 못한다. 로컬 웹서버(VS Code Live Server 등)로 열거나, 데모처럼 parse()로 메모리의 데이터를 바로 읽는다.

 

어디서 열 수 있나

 

GLB 파일을 Blender, 온라인 뷰어, model-viewer, three.js에서 여는 흐름

 

프로그램 여는 방법 속성(extras)
Blender File → Import → glTF 2.0 (.glb/.gltf) 사용자 정의 속성(Custom Properties)으로 들어옴
온라인 glTF 뷰어 웹 뷰어 페이지에 파일을 끌어다 놓기 뷰어에 따라 다름
웹페이지 <model-viewer src="bridge.glb"> (구글 웹 컴포넌트) 표시 안 함 (형상만)
three.js GLTFLoader userData로 복원

 

○ glTF는 단위 m, Y축이 위인 좌표계를 쓴다. 9편에서 정리한 대로 처음부터 m · Y-up으로 모델을 만들었다면 다른 프로그램에서 열어도 크기와 방향이 그대로 맞는다.

 

.gltf와 .glb, 무엇을 쓸까
구분 .gltf .glb
형태 JSON 텍스트 (버퍼는 base64로 포함하거나 별도 .bin) 바이너리 파일 하나
크기 더 큼 (base64는 원래 데이터보다 약 1/3 큼) 더 작음
장점 메모장으로 열어 노드 · extras 확인 가능 파일 하나로 주고받기 편함
추천 내용 확인 · 디버깅용 공유 · 배포용

 

○ 데모 그래프처럼 같은 모델이라도 .glb가 더 작다. 평소에는 .glb로 공유하고, 속성이 제대로 들어갔는지 볼 때만 .gltf로 내보내 열어 보면 된다.

 

전체 코드

○ 메모장에 붙여넣고 bridge10.html로 저장한 뒤 실행해서 왼쪽 위 버튼을 누르면 bridge.glb / bridge.gltf가 다운로드된다. 받은 .gltf를 메모장으로 열어 "extras"를 검색해 보자.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>three.js 교량 모델 glTF 내보내기</title>
  <style>
    body { margin: 0; overflow: hidden; font-family: sans-serif; }
    #ui { position: absolute; top: 10px; left: 10px; display: flex; gap: 8px; }
    #ui button { padding: 8px 14px; border: 0; border-radius: 6px; color: #fff; cursor: pointer; font-size: 14px; }
  </style>
  <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/"
      }
    }
  </script>
</head>
<body>
<div id="ui">
  <button id="glb" style="background:#d35400">.glb 내보내기</button>
  <button id="gltf" style="background:#3f6e9e">.gltf 내보내기</button>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';

// 1. 장면 기본 설정
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xdfe8f0);
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(60, 38, 75);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff, 0x6b7f63, 0.9));
const sun = new THREE.DirectionalLight(0xffffff, 2.0);
sun.position.set(40, 70, 45);
scene.add(sun);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 4, 0);
controls.update();

// 2. BIM 교량 (형상 + userData) - 단위 m, Y축 위
const P = { nSpan: 3, L: 30, B: 12, n: 4, H: 8 };
const h = P.L / 20, Lt = P.nSpan * P.L, x0 = -Lt / 2;
const MAT = {
  '바닥판': new THREE.MeshStandardMaterial({ color: 0xb8bcc2, name: '바닥판_C27' }),
  '거더': new THREE.MeshStandardMaterial({ color: 0x3f6e9e, metalness: 0.3, name: '거더_SM460' }),
  '교각': new THREE.MeshStandardMaterial({ color: 0xa9adb3, name: '교각_C30' }),
  '교대': new THREE.MeshStandardMaterial({ color: 0x9da2a8, name: '교대_C24' }),
};
const bridge = new THREE.Group();
bridge.name = 'Bridge_3span';
bridge.userData = { project: '교량 BIM 바이브코딩', span: '3 × 30 m' };   // 교량 전체 속성
scene.add(bridge);

function member(w, hh, d, type, x, y, z, props) {
  const m = new THREE.Mesh(new THREE.BoxGeometry(w, hh, d), MAT[type]);
  m.position.set(x, y, z);
  m.name = props.id;                                  // → glTF 노드 이름
  m.userData = { type, ...props };                    // → glTF extras
  bridge.add(m);
}
for (let k = 0; k < P.nSpan; k++) {
  const xc = x0 + (k + 0.5) * P.L, sp = k + 1;
  member(P.L - 0.05, 0.25, P.B, '바닥판', xc, P.H + h + 0.125, 0, { id: `DK-${sp}`, material: 'C27', vol: P.L * P.B * 0.25 });
  for (let i = 0; i < P.n; i++) {
    const z = -(P.B / 2 - 1) + i * (P.B - 2) / (P.n - 1);
    member(P.L - 0.1, h, 0.4, '거더', xc, P.H + h / 2, z, { id: `G${i + 1}-${sp}`, material: 'SM460', len: P.L - 0.1 });
  }
}
for (let k = 1; k < P.nSpan; k++) member(1.8, P.H, 1.8, '교각', x0 + k * P.L, P.H / 2, 0, { id: `P${k}`, material: 'C30', vol: 1.8 * 1.8 * P.H });
for (const [id, s] of [['A1', -1], ['A2', 1]]) member(2, P.H, P.B + 1, '교대', s * (Lt / 2 + 0.6), P.H / 2, 0, { id, material: 'C24', vol: 2 * P.H * (P.B + 1) });

// 3. glTF 내보내기
const exporter = new GLTFExporter();
function download(data, filename, type) {
  const url = URL.createObjectURL(new Blob([data], { type }));
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;                              // 이 이름으로 다운로드
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);   // 다운로드 시작 후 정리
}
async function exportModel(binary) {
  const result = await exporter.parseAsync(bridge, { binary, onlyVisible: true, trs: true });
  if (binary) download(result, 'bridge.glb', 'model/gltf-binary');                       // ArrayBuffer
  else download(JSON.stringify(result, null, 2), 'bridge.gltf', 'model/gltf+json');      // JSON 객체
  console.log('내보낸 부재 수:', bridge.children.length);
}
document.getElementById('glb').onclick = () => exportModel(true);
document.getElementById('gltf').onclick = () => exportModel(false);

renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });
addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

 

Claude에게 이렇게 요청했다

지금 three.js 교량 모델을 glTF 파일로 내보내는 기능을 추가해줘.

- three.js 0.186.1, GLTFExporter는 three/addons/exporters/GLTFExporter.js 에서 import

- 버튼 2개: ".glb 내보내기"(binary: true), ".gltf 내보내기"(binary: false)

- 장면 전체가 아니라 bridge 그룹만 내보내기. 옵션 onlyVisible: true, trs: true

- 각 부재의 mesh.name(부재 ID)과 userData가 glTF의 name과 extras로 저장되게

- Blob과 a.download로 bridge.glb / bridge.gltf 파일 다운로드

- 기존 모델 생성 코드는 바꾸지 말 것

 

○ 기능을 추가하는 요청이므로 "기존 코드는 바꾸지 말 것"을 꼭 넣는다. 9편에서 정리한 범위 지정과 같은 이유다.

 

연재를 마치며

○ 10편 동안 Claude와 함께 three.js 교량 모델을 처음부터 파일 내보내기까지 만들었다. 각 편에서 다룬 내용을 정리하면 다음과 같다.

편 주제 핵심
1 three.js 입문 첫 3D 교량 띄우기
2 장면 · 카메라 · 렌더러 3D 화면의 기본 구조
3 OrbitControls · 조명 · 그림자 마우스로 돌려 보는 모델
4 AI 코딩 프롬프트 작성법 치수 · 단위 · 좌표를 먼저 약속
5 ExtrudeGeometry I형 거더 단면
6 파라메트릭 모델링 슬라이더로 바뀌는 교량
7 교량 BIM 모델 userData · Raycaster · 물량 집계
8 곡선교 모델링 원곡선 좌표 · sweep · 편경사
9 바이브코딩 디버깅 증상 · 근거 · 범위로 요청
10 glTF 내보내기 모델과 속성을 파일로

 

○ 코드를 한 줄씩 외우는 것보다 중요한 것은 구조물을 정확히 설명하는 능력이었다. 치수 · 단위 · 좌표 · 부재 이름을 분명히 정하면, 나머지는 AI가 코드로 바꿔 준다. 이 연재가 구조 엔지니어가 자기 손으로 BIM 도구를 만들어 보는 출발점이 되었으면 한다.

 

관련 글

     → [교량 BIM 바이브코딩 1편] three.js 입문: Claude 바이브코딩으로 3D 교량 만들기

     → [교량 BIM 바이브코딩 2편] three.js 기초: 장면·카메라·렌더러

     → [교량 BIM 바이브코딩 3편] OrbitControls·조명·그림자

     → [교량 BIM 바이브코딩 4편] AI 코딩 프롬프트 작성법: Claude에게 3D 모델 정확히 요청하기

     → [교량 BIM 바이브코딩 5편] three.js ExtrudeGeometry: I형 거더 단면 3D 모델링

     → [교량 BIM 바이브코딩 6편] three.js 파라메트릭 모델링: 슬라이더로 바뀌는 교량

     → [교량 BIM 바이브코딩 7편] three.js 교량 BIM 모델: 클릭하면 속성이 나오는 부재 조립

     → [교량 BIM 바이브코딩 8편] three.js 곡선교 모델링: 곡선 반경 R과 편경사로 휘는 교량

     → [교량 BIM 바이브코딩 9편] 바이브코딩 디버깅: AI가 만든 three.js 코드가 틀렸을 때 고치는 법

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/10   »
일 월 화 수 목 금 토
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31