티스토리 뷰

반응형

three.js 교량 BIM 모델 클릭하면 부재 속성 표시 교량 BIM 바이브코딩 7편 대표 이미지

 

6편까지 만든 교량은 모양만 있는 3D 모형이었다. 화면에서는 그럴듯해 보이지만 거더를 클릭해도 아무 일도 일어나지 않고, 이 부재가 몇 번 거더인지, 재질이 무엇인지, 물량이 얼마인지 알 수 없다.

7편에서는 바닥판·거더·가로보·받침·교각·교대를 각각 이름과 속성을 가진 부재로 조립한다. 부재를 클릭하면 ID·재질·치수·물량이 표시되고, 종류별로 숨기거나 보일 수 있으며, 물량은 부재 정보에서 자동으로 집계된다. 형상에 정보가 붙는 순간 3D 모형이 BIM 모델이 된다.

 

이번 편에서 만들 것
 

▲ 실제로 동작하는 데모. 부재를 클릭하면 속성이 표시되고, 위쪽 체크박스로 종류별 보이기를 바꿀 수 있다. (드래그는 회전)

 

○ 바닥판·방호벽을 끄면 거더와 가로보가 드러난다. 거더를 하나 클릭하면 G2-2처럼 몇 번째 거더의 몇 경간 부재인지 바로 알 수 있다.

○ 아래 표와 그래프는 화면에 있는 68개 부재의 속성(userData)을 더해서 만든 것이다. 물량을 따로 계산하지 않고 모델에서 바로 뽑았다.

 

3D 모형과 BIM 모델의 차이

○ three.js의 메시(Mesh)는 형상(Geometry)과 재질(Material)만 가진다. 여기에 부재 정보를 담는 userData를 채우면 그 메시는 "정보를 가진 부재"가 된다.

 

형상과 속성 userData를 합친 BIM 객체 개념

 

구분 3D 모형 (1~6편) BIM 모델 (7편)
담고 있는 것 모양과 색 모양 + 부재 ID·종류·재질·치수·물량
부재 클릭 반응 없음 해당 부재의 속성 표시
물량 따로 계산 부재 속성을 더해서 자동 집계
일부만 보기 어려움 종류별 보이기·숨기기
활용 형상 검토, 홍보 형상 검토 + 물량·공정·유지관리 정보 연결

 

1단계: 부재 분류와 ID 규칙

○ BIM의 시작은 부재를 어떻게 나누고 어떤 이름을 붙일지 정하는 것이다. 거더 하나를 교량 전체 길이로 만들면 "2경간 G2 거더"만 따로 선택할 수 없다. 그래서 거더와 바닥판은 경간마다 나누었다.

○ ID는 도면이나 계산서에서 쓰는 지점 이름(A1, P1, P2, A2)과 거더 번호(G1~G4)를 그대로 쓰는 것이 좋다. 그래야 모델과 문서를 서로 찾아갈 수 있다.

 

교량 부재 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가 이 계산을 해준다.

 

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과 편경사로 휘는 교량

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
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