티스토리 뷰
three.js 교량 BIM 모델: 거더·바닥판·교각 조립하고 클릭해서 속성 보기 [교량 BIM 바이브코딩 7편]
마구자바 2026. 10. 9. 16:07
6편까지 만든 교량은 모양만 있는 3D 모형이었다. 화면에서는 그럴듯해 보이지만 거더를 클릭해도 아무 일도 일어나지 않고, 이 부재가 몇 번 거더인지, 재질이 무엇인지, 물량이 얼마인지 알 수 없다.
7편에서는 바닥판·거더·가로보·받침·교각·교대를 각각 이름과 속성을 가진 부재로 조립한다. 부재를 클릭하면 ID·재질·치수·물량이 표시되고, 종류별로 숨기거나 보일 수 있으며, 물량은 부재 정보에서 자동으로 집계된다. 형상에 정보가 붙는 순간 3D 모형이 BIM 모델이 된다.
이번 편에서 만들 것
▲ 실제로 동작하는 데모. 부재를 클릭하면 속성이 표시되고, 위쪽 체크박스로 종류별 보이기를 바꿀 수 있다. (드래그는 회전)
○ 바닥판·방호벽을 끄면 거더와 가로보가 드러난다. 거더를 하나 클릭하면 G2-2처럼 몇 번째 거더의 몇 경간 부재인지 바로 알 수 있다.
○ 아래 표와 그래프는 화면에 있는 68개 부재의 속성(userData)을 더해서 만든 것이다. 물량을 따로 계산하지 않고 모델에서 바로 뽑았다.
3D 모형과 BIM 모델의 차이
○ three.js의 메시(Mesh)는 형상(Geometry)과 재질(Material)만 가진다. 여기에 부재 정보를 담는 userData를 채우면 그 메시는 "정보를 가진 부재"가 된다.

| 구분 | 3D 모형 (1~6편) | BIM 모델 (7편) |
| 담고 있는 것 | 모양과 색 | 모양 + 부재 ID·종류·재질·치수·물량 |
| 부재 클릭 | 반응 없음 | 해당 부재의 속성 표시 |
| 물량 | 따로 계산 | 부재 속성을 더해서 자동 집계 |
| 일부만 보기 | 어려움 | 종류별 보이기·숨기기 |
| 활용 | 형상 검토, 홍보 | 형상 검토 + 물량·공정·유지관리 정보 연결 |
1단계: 부재 분류와 ID 규칙
○ BIM의 시작은 부재를 어떻게 나누고 어떤 이름을 붙일지 정하는 것이다. 거더 하나를 교량 전체 길이로 만들면 "2경간 G2 거더"만 따로 선택할 수 없다. 그래서 거더와 바닥판은 경간마다 나누었다.
○ ID는 도면이나 계산서에서 쓰는 지점 이름(A1, P1, P2, A2)과 거더 번호(G1~G4)를 그대로 쓰는 것이 좋다. 그래야 모델과 문서를 서로 찾아갈 수 있다.

| 부재 종류 | ID 규칙 | 예 | 개수 (데모) |
| 바닥판 | DK-경간 | DK-1 | 3 |
| 주거더 | G거더번호-경간 | G2-3 | 12 (4개 × 3경간) |
| 가로보 | CB-경간-위치-칸 | CB-2-1-3 | 27 (3개소 × 3칸 × 3경간) |
| 방호벽 | BR-좌/우 | BR-L | 2 |
| 받침 | BRG-지점-거더번호 | BRG-P1-4 | 16 (4지점 × 4개) |
| 교각 | 지점-기둥/코핑 | P2-COL, P2-CAP | 4 |
| 교대 | 지점(-흉벽) | A1, A1-BW | 4 |
2단계: userData에 속성 넣기
○ three.js의 모든 객체에는 userData라는 빈 객체가 있다. three.js는 이 안을 건드리지 않으므로 무엇이든 자유롭게 넣을 수 있다. 부재를 만드는 함수에서 형상과 속성을 한 번에 넣는다.
const members = []; // 클릭·집계 대상 부재 목록
function member(w, h, d, type, x, y, z, props) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), MAT[type]);
mesh.position.set(x, y, z);
mesh.userData = { type, ...props }; // ★ BIM 속성 저장
scene.add(mesh);
members.push(mesh);
}
// 2경간 G2 거더
member(29.9, 1.5, 0.4, '거더', 0, 8.75, -1.67,
{ id: 'G2-2', material: 'SM460', len: 29.9 });
| 속성 (키) | 의미 | 예 |
id |
부재 고유 번호 | 'G2-2' |
type |
부재 종류 (보이기·집계 기준) | '거더' |
name |
사람이 읽는 이름 | '주거더 G2 (2경간)' |
material |
재질 | 'SM460', 'C27' |
size |
치수 설명 | 'I 400×25 / 1445×12 / 500×30' |
vol / wt |
콘크리트 체적(m³) / 강재 중량(t) | 90.0 / 9.94 |
○ 여기에 시공 단계, 점검 일자, 손상 등급 같은 정보를 더하면 유지관리용 모델로 확장할 수 있다. 형식은 자유지만 모든 부재가 같은 키 이름을 쓰는 것이 중요하다.
IFC 클래스와 비교하면
○ BIM 표준 형식인 IFC 4.3에는 교량용 분류가 들어 있다. userData의 type을 처음부터 IFC 분류와 맞춰 두면 나중에 다른 BIM 도구로 내보낼 때 편하다. (아래는 대응 예시)
| 부재 | IFC 클래스 (예) | 비고 |
| 교량 전체 | IfcBridge | IFC 4.3에서 추가된 교량 객체 |
| 바닥판 | IfcSlab | 판 부재 |
| 주거더 · 가로보 | IfcBeam | 보 부재 |
| 교각 기둥 | IfcColumn | 기둥 부재 |
| 받침 | IfcBearing | IFC 4.3에서 추가 |
3단계: 클릭해서 부재 선택하기 (Raycaster)
○ 마우스로 클릭한 화면 위치에서 3D 공간으로 광선(ray)을 쏘아, 처음 맞는 부재를 찾는다. three.js의 Raycaster가 이 계산을 해준다.

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
addEventListener('click', (e) => {
// ① 화면 좌표(px) → -1 ~ 1 정규화 좌표
mouse.x = (e.clientX / innerWidth) * 2 - 1;
mouse.y = -(e.clientY / innerHeight) * 2 + 1;
// ② 카메라에서 클릭 방향으로 광선 설정
raycaster.setFromCamera(mouse, camera);
// ③ 부재 목록과 교차 검사 (가까운 순으로 정렬되어 나옴)
const hits = raycaster.intersectObjects(members);
if (!hits.length) return;
// ④ 첫 번째로 맞은 부재의 속성 읽기
const u = hits[0].object.userData;
info.textContent = `${u.id} (${u.type}) ${u.material}`;
});
| 순서 | 코드 | 하는 일 |
| ① | mouse.x, mouse.y |
픽셀 좌표를 −1~1 범위로 변환 (y는 위가 +) |
| ② | setFromCamera(mouse, camera) |
카메라 위치에서 클릭 방향으로 광선 생성 |
| ③ | intersectObjects(members) |
광선과 부재의 교차점 계산, 가까운 순 정렬 |
| ④ | hits[0].object |
가장 앞에 있는 부재 = 선택한 부재 |
| ⑤ | material = HIGHLIGHT |
선택 부재 색 바꾸기, 이전 선택은 원래 재질로 |
드래그와 클릭 구분하기
○ OrbitControls로 화면을 돌린 뒤 마우스를 떼면 그것도 click으로 인식된다. 데모에서는 누른 위치와 뗀 위치가 5px 이상 다르면 드래그로 보고 선택하지 않는다.
let down = null;
canvas.addEventListener('pointerdown', (e) => { down = [e.clientX, e.clientY]; });
canvas.addEventListener('pointerup', (e) => {
if (Math.hypot(e.clientX - down[0], e.clientY - down[1]) > 5) return; // 드래그
// ... 여기서 Raycaster로 선택
});
4단계: 종류별 보이기·숨기기
○ userData의 type으로 같은 종류의 부재를 한꺼번에 찾을 수 있다. visible을 끄면 화면에서 사라지고, 숨긴 부재는 클릭 대상에서도 빼야 뒤에 있는 부재를 선택할 수 있다.
function setVisible(type, on) {
members.forEach((m) => {
if (m.userData.type === type) m.visible = on;
});
}
setVisible('바닥판', false); // 바닥판 숨기기 → 거더가 보인다
// 클릭할 때는 보이는 부재만 검사
const hits = raycaster.intersectObjects(members.filter((m) => m.visible));
5단계: 부재 속성으로 물량 집계
○ 부재마다 체적과 중량이 들어 있으므로, 종류별로 더하기만 하면 물량표가 된다. 모델을 고치면 물량도 같이 바뀐다.
const total = {};
members.forEach((m) => {
const u = m.userData;
const t = (total[u.type] ||= { count: 0, vol: 0, wt: 0 });
t.count += 1;
t.vol += u.vol || 0; // 콘크리트 m³
t.wt += u.wt || 0; // 강재 t
});
console.table(total);
| 부재 종류 | 개수 | 콘크리트 (m³) | 강재 (t) |
| 바닥판 | 3 | 270.0 | - |
| 방호벽 | 2 | 64.8 | - |
| 주거더 | 12 | - | 119.3 |
| 가로보 | 27 | - | 12.9 |
| 받침 | 16 | - | - |
| 교각 (기둥·코핑) | 4 | 106.5 | - |
| 교대 (본체·흉벽) | 4 | 435.6 | - |
| 합계 | 68 | 876.8 | 132.1 |
○ 데모 교량(3경간 × 30 m, 폭 12 m, 거더 4개) 기준이다. 단면을 단순화하고 기초·날개벽·철근은 뺀 개략 물량이므로 실제 설계 물량과는 다르다.
전체 코드
○ 메모장에 붙여넣고 bridge7.html로 저장한 뒤 더블클릭하면 실행된다. 부재를 클릭하면 왼쪽 위에 속성이 나오고, F12 콘솔에는 종류별 콘크리트 체적이 표로 출력된다. (데모의 I형 단면·가로보·받침·보이기 버튼은 빠진 간단한 버전)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>three.js 교량 BIM 모델 - 클릭하면 속성 표시</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
#info { position: absolute; top: 10px; left: 10px; min-width: 240px; background: rgba(255,255,255,0.92);
padding: 10px 14px; border-radius: 8px; font-size: 14px; line-height: 1.6; }
</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="info">부재를 클릭하세요</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.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(50, 30, 60);
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, 5, 0);
controls.update();
// 2. 교량 제원 (단위 m)
const P = { nSpan: 3, L: 30, B: 12, n: 4, H: 8 };
const h = P.L / 20; // 거더 높이
const s = (P.B - 2) / (P.n - 1); // 거더 간격
const Lt = P.nSpan * P.L, x0 = -Lt / 2;
// 3. 종류별 재질 (한 번만 생성)
const MAT = {
'바닥판': new THREE.MeshStandardMaterial({ color: 0xb8bcc2 }),
'거더': new THREE.MeshStandardMaterial({ color: 0x3f6e9e, metalness: 0.3 }),
'교각': new THREE.MeshStandardMaterial({ color: 0xa9adb3 }),
'교대': new THREE.MeshStandardMaterial({ color: 0x9da2a8 }),
};
const HIGHLIGHT = new THREE.MeshStandardMaterial({ color: 0xff7a1a, emissive: 0xff5a00, emissiveIntensity: 0.5 });
// 4. 부재 생성: 형상 + 속성(userData)
const members = [];
function member(w, hh, d, type, x, y, z, props) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, hh, d), MAT[type]);
mesh.position.set(x, y, z);
mesh.userData = { type, ...props }; // ★ BIM 속성 저장
scene.add(mesh);
members.push(mesh);
}
for (let k = 0; k < P.nSpan; k++) {
const xc = x0 + (k + 0.5) * P.L, sp = k + 1;
// 바닥판 (경간별)
member(P.L, 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 * s;
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 });
}
}
// 교각 (P1, P2)
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 });
}
// 교대 (A1, A2)
for (const [id, sg] of [['A1', -1], ['A2', 1]]) {
member(2, P.H, P.B + 1, '교대', sg * Lt / 2, P.H / 2, 0,
{ id, material: 'C24', vol: 2 * P.H * (P.B + 1) });
}
// 5. 클릭하면 부재 선택 (Raycaster)
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const info = document.getElementById('info');
let selected = null;
addEventListener('click', (e) => {
// 화면 좌표 → -1 ~ 1 정규화 좌표
mouse.x = (e.clientX / innerWidth) * 2 - 1;
mouse.y = -(e.clientY / innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(members);
if (selected) selected.material = MAT[selected.userData.type]; // 이전 선택 해제
selected = hits.length ? hits[0].object : null; // 첫 번째로 맞은 부재
if (!selected) { info.textContent = '부재를 클릭하세요'; return; }
selected.material = HIGHLIGHT;
const u = selected.userData;
info.innerHTML = `<b>${u.id}</b> (${u.type})<br>재질: ${u.material}<br>` +
(u.vol ? `콘크리트: ${u.vol.toFixed(1)} m³` : `길이: ${u.len.toFixed(1)} m`);
});
// 6. 물량 집계 - userData만으로 계산
const total = {};
members.forEach((m) => {
const u = m.userData;
total[u.type] = (total[u.type] || 0) + (u.vol || 0);
});
console.table(total); // F12 콘솔에서 종류별 콘크리트 체적 확인
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로 클릭하면 부재 정보가 나오는 교량 BIM 모델을 만들어줘.
- HTML 파일 하나, three.js 0.186.1 CDN importmap
- 3경간 × 30 m, 폭 12 m, 거더 4개, 교각 높이 8 m (단위 m, X축 교축, Y축 위)
- 부재: 바닥판(경간별), 주거더(경간별 × 거더별), 교각 P1·P2, 교대 A1·A2
- 부재마다 mesh.userData에 id, type, material, 체적(vol) 또는 길이(len) 저장
- ID 규칙: 바닥판 DK-경간, 거더 G번호-경간, 교각 P번호, 교대 A번호
- 클릭하면 Raycaster로 첫 번째 부재를 선택해 주황색으로 강조하고, 화면 왼쪽 위에 속성 표시
- userData를 더해서 종류별 물량을 console.table로 출력
○ 핵심은 ID 규칙과 속성 키 이름을 프롬프트에 직접 적는 것이다. 정하지 않으면 AI가 부재마다 다른 이름을 붙이거나, 속성을 메시 대신 별도 배열에 저장해서 클릭과 연결이 끊기는 경우가 있다.
자주 겪는 문제
| 증상 | 원인 | 해결 |
| 클릭해도 반응 없음 | intersectObjects에 부재 목록이 아닌 scene을 넣고 recursive를 끔 | 부재 배열을 넘기거나 intersectObjects(scene.children, true) |
| 엉뚱한 부재가 선택됨 | 마우스 좌표를 캔버스가 아닌 창 기준으로 계산 | getBoundingClientRect()로 캔버스 기준 좌표 사용 |
| 화면을 돌리기만 해도 선택이 바뀜 | 드래그 끝도 click으로 인식 | pointerdown/up 거리로 드래그 구분 |
| 하나를 강조했는데 같은 종류가 전부 바뀜 | 재질을 공유하는데 재질 색을 직접 바꿈 | 강조용 재질을 따로 두고 교체 |
| 숨긴 바닥판 때문에 거더가 안 잡힘 | 숨긴 부재도 교차 검사 대상 | visible인 부재만 검사 |
| 물량이 두 배로 나옴 | 같은 부재를 members에 두 번 넣음 | 부재 생성 함수 한 곳에서만 push |
정리
○ BIM 모델 = 형상(Mesh) + 속성(userData). 속성은 부재를 만들 때 함께 넣는다.
○ 부재 분류와 ID 규칙을 먼저 정하고, 도면의 지점·거더 번호를 그대로 쓴다.
○ Raycaster로 클릭한 부재를 찾고, 첫 번째로 맞은 부재의 userData를 읽는다.
○ 같은 속성으로 보이기·숨기기와 물량 집계까지 할 수 있다.
다음 편 예고
지금까지 교량은 일직선이었다. 8편에서는 실제 도로처럼 곡선 선형을 따라가는 교량을 만든다. 반경과 교각 위치를 선형에서 계산하고, 편경사(횡단 경사)를 주어 바닥판이 기울어지는 곡선교 모델을 다룬다.
관련 글
→ [교량 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 바이브코딩 8편] three.js 곡선교 모델링: 곡선 반경 R과 편경사로 휘는 교량
'엔지니어링 코딩 > three.js·AI 코딩' 카테고리의 다른 글
| 바이브코딩 디버깅: AI가 만든 three.js 코드가 틀렸을 때 고치는 법 [교량 BIM 바이브코딩 9편] (0) | 2026.10.11 |
|---|---|
| three.js 곡선교 모델링: 곡선 반경 R과 편경사로 휘는 교량 [교량 BIM 바이브코딩 8편] (0) | 2026.10.09 |
| three.js 파라메트릭 모델링: 슬라이더로 바뀌는 교량 [교량 BIM 바이브코딩 6편] (0) | 2026.10.09 |
| AI 코딩 프롬프트 작성법: Claude에게 3D 모델 정확히 요청하기 [교량 BIM 바이브코딩 4편] (0) | 2026.10.07 |
| three.js ExtrudeGeometry: I형 거더 단면 3D 모델링 [교량 BIM 바이브코딩 5편] (0) | 2026.10.07 |
