- Show Effects 아키텍처 (클로저 기반 update 패턴) - 헬퍼 메서드 (_mkItem, _mkFlash, _setOp, mkMix) - 20종 효과 목록 및 설명 - UI 구성 (드롭다운 + 재생 버튼) - 메모리 관리 (_cleanupShow) - 향후 계획 Phase 번호 업데이트
22 KiB
BIM 뷰어 — Three.js + web-ifc 기반 웹 3D BIM 뷰어
작성일: 2026-03-12 상태: Phase 3 완료 (SAM Show Effects 20종) 소스:
mng/resources/views/juil/bim-viewer.blade.php
1. 개요
1.1 목적
건설PMIS(Construction Project Management Information System)의 BIM(Building Information Modeling) 뷰어 기능이다. 웹 브라우저에서 별도 플러그인 없이 3D 건물 모델을 렌더링하고, 요소를 선택하여 속성 정보를 조회할 수 있다.
1.2 주요 기능
| 기능 | 설명 | Phase |
|---|---|---|
| 데모 건물 렌더링 | 물류센터 3D 모델 (BoxGeometry 조합) | Phase 1 |
| 요소 클릭 선택 | Raycaster로 3D 요소 선택 → 속성 조회 | Phase 1 |
| 시점 전환 | 투시도/정면/우측/상부/배면 프리셋 | Phase 1 |
| 요소 토글 & 와이어프레임 | 요소 유형별 표시/숨김, 와이어프레임 모드 | Phase 1 |
| IFC 파일 업로드 | .ifc 파일 업로드 → 실제 BIM 모델 렌더링 |
Phase 2 |
| IFC 파일 다운로드 | 현재 모델을 IFC 파일로 내보내기 | Phase 2 |
| 드래그 앤 드롭 | IFC 파일을 뷰포트에 드래그하여 로드 | Phase 2 |
| SAM Show Effects | SAM/샘 텍스트 3D 애니메이션 20종 선택/재생 | Phase 3 |
1.3 핵심 기술 스택
| 기술 | 역할 | 버전/CDN |
|---|---|---|
| Three.js | WebGL 기반 3D 렌더링 엔진 | r128 (CDN) |
| OrbitControls | 카메라 회전/줌/팬 조작 | r128 (CDN) |
| web-ifc | IFC 파일 파싱 (WASM 기반) | 0.0.66 (CDN, 지연 로드) |
| React 18 | UI 컴포넌트 (사이드바, 툴바, 정보 패널) | CDN + Babel 브라우저 트랜스파일 |
| Tailwind CSS | UI 스타일링 | CDN |
1.4 아키텍처
┌─────────────────────────────────────────────────────────┐
│ Blade 템플릿 (@extends layouts.app) │
│ ┌─────────────────────────────────────────────────────┐│
│ │ React (CDN + Babel) ││
│ │ ┌──────┐ ┌──────────────────┐ ┌───────────────┐ ││
│ │ │사이드 │ │ Three.js │ │ 정보 패널 │ ││
│ │ │ 바 │ │ 3D Viewport │ │ (모델/선택/ │ ││
│ │ │(React)│ │ (WebGL) │ │ 통계) │ ││
│ │ └──────┘ └──────────────────┘ └───────────────┘ ││
│ │ ┌──────────────────┐ ││
│ │ │ 툴바 (React) │ ││
│ │ │ IFC업로드/다운로드│ ││
│ │ └──────────────────┘ ││
│ └─────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────┐ ┌──────────────────────────────┐ │
│ │ IFCHelper │ │ generateDemoIFC() │ │
│ │ (web-ifc 래퍼) │ │ (Three.js → IFC2X3 변환) │ │
│ └─────────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
- React: UI 컴포넌트 관리 (사이드바, 툴바, 정보 패널)
- Three.js: 3D 렌더링 (건물, 조명, 그림자, 카메라)
- web-ifc: IFC 파일 파싱 → 지오메트리 추출 (WASM, 지연 로드)
- 연결 방식: React
useRef로 DOM 요소를 Three.js에 전달,useEffect로 생명주기 관리
2. 듀얼 모드 — 데모 / IFC
BIM 뷰어는 두 가지 모드로 동작한다:
| 항목 | 데모 모드 (demo) |
IFC 모드 (ifc) |
|---|---|---|
| 진입 방식 | 기본 (페이지 로드 시) | IFC 파일 업로드/드래그 시 |
| 모델 원본 | createDemoBuilding() (코드 내 하드코딩) |
사용자 업로드 .ifc 파일 |
| 지오메트리 | BoxGeometry 조합 (~200개) |
web-ifc StreamAllMeshes → BufferGeometry |
| 요소 선택 시 | userData (type, name, material, floor 등) |
IFCHelper.getElementInfo() (IFC 속성) |
| 요소 토글 | 유형별 Group 표시/숨김 | 미지원 (전체 모델 표시) |
| IFC 다운로드 | generateDemoIFC() → IFC2X3 STEP 생성 |
원본 버퍼 그대로 재다운로드 |
모드 전환 흐름:
[페이지 로드] → 데모 모드
│
├── IFC 업로드 ──→ IFC 모드 (데모 숨김)
│ │
│ ├── "데모 모델" 버튼 ──→ 데모 모드 복귀
│ └── 다른 IFC 업로드 ──→ 새 IFC 로드
│
└── IFC 다운로드 → generateDemoIFC() 실행 → .ifc 파일 다운로드
3. IFC 파일 처리 (Phase 2)
3.1 web-ifc 지연 로드
web-ifc WASM 번들은 약 7MB이다. 페이지 로드 시 다운로드하지 않고, IFC 파일을 처음 업로드할 때만 로드한다:
// 동적 module script 주입 방식
function loadWebIFCLib() {
const s = document.createElement('script');
s.type = 'module';
s.textContent = `
import * as WebIFC from '${WEBIFC_CDN}web-ifc-api.js';
window._WebIFC = WebIFC;
window.dispatchEvent(new CustomEvent('_wifcOk'));
`;
document.head.appendChild(s);
}
Babel
text/babel컨텍스트에서 동적import()가 불안정하므로, 별도<script type="module">을 주입하여 ES module import를 수행하고window._WebIFC로 노출한다.
3.2 IFCHelper 클래스
web-ifc API를 래핑하여 파싱과 요소 정보 조회를 담당한다:
class IFCHelper {
async init(onMsg) // web-ifc 초기화, WASM 로드, typeMap 구축
parse(buffer, onMsg) // IFC 파일 → StreamAllMeshes → 지오메트리 배열
getElementInfo(expressID) // expressID → IFC 속성 (타입, 이름, GlobalId 등)
close() // 모델 닫기, 캐시 정리
}
3.3 IFC → Three.js 변환 파이프라인
.ifc 파일 (ArrayBuffer)
↓ IFCHelper.parse()
web-ifc StreamAllMeshes
↓ 각 메시에서 vertices + indices + transform 추출
Float32Array (위치 3 + 노멀 3 = stride 6)
↓ Three.js BufferGeometry로 변환
BufferAttribute(position, 3) + BufferAttribute(normal, 3)
↓ Matrix4.fromArray(transform) 적용
MeshPhongMaterial (IFC 원본 색상 + 투명도)
↓
THREE.Mesh → ifcGroup(THREE.Group)에 추가
↓
fitToModel() → 바운딩 박스 기준 카메라 자동 조정
3.4 IFC 파일 다운로드 (IFC Export)
| 모드 | 동작 | 출력 파일명 |
|---|---|---|
| 데모 | generateDemoIFC(meshes) → IFC2X3 STEP 텍스트 생성 |
SAM_Demo_Building.ifc |
| IFC | this.ifcBuffer (업로드 시 저장한 원본) 그대로 다운로드 |
model.ifc |
데모 모델의 IFC 변환 구조:
Three.js 메시 (BoxGeometry)
↓ geometry.parameters → width, height, depth
↓ mesh.position → x, y, z
↓ mesh.userData → type, floor, name, material
IFC2X3 STEP 엔티티 생성:
IfcProject → IfcSite → IfcBuilding → IfcBuildingStorey (1F~RF)
각 메시 → IfcRectangleProfileDef + IfcExtrudedAreaSolid + IfcLocalPlacement
유형 매핑: column→IFCCOLUMN, beam→IFCBEAM, floor/roof→IFCSLAB, ...
좌표계 변환:
Three.js (x, y, z) → IFC (x, z, y) // Y-up → Z-up
요소 기준점: 바닥 중심 (Three.js center → IFC bottom)
4. Three.js 렌더링 파이프라인
4.1 기본 구조
브라우저 (Chrome/Edge)
└── WebGL API (GPU 가속 그래픽)
└── Three.js r128 (WebGL 추상화 라이브러리)
├── Scene — 3D 공간 (모든 객체의 컨테이너)
├── Camera — 시점 (PerspectiveCamera, FOV 45도)
├── Renderer — 화면 출력 (WebGLRenderer, 안티앨리어싱)
├── Lights — 조명 (Hemisphere + Directional + Ambient)
├── Mesh — 물체 = Geometry(형태) + Material(재질)
└── FogExp2 — 거리 안개 (밀도 0.004)
4.2 렌더링 루프
Three.js는 매 프레임(~60fps)마다 장면을 다시 그린다:
animate() {
requestAnimationFrame(() => this.animate());
if (this.targetPos) {
this.camera.position.lerp(this.targetPos, 0.07);
this.controls.target.lerp(this.targetLook, 0.07);
if (this.camera.position.distanceTo(this.targetPos) < 0.2)
this.targetPos = null;
}
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
4.3 조명 구성
HemisphereLight(0xb1e1ff, 0xb97a20, 0.55) // 반구광 (하늘+지면)
DirectionalLight(0xffffff, 0.85) // 태양광 (그림자)
└── Shadow Map: 2048×2048 (PCFSoftShadowMap)
AmbientLight(0xffffff, 0.25) // 환경광
5. 데모 건물 모델
5.1 건물 사양
| 항목 | 값 |
|---|---|
| 건물 용도 | 물류센터 (데모) |
| 크기 | 60m (가로) × 30m (세로) × 12m (높이) |
| 층수 | 지상 3층 (각 4m) |
| 기둥 격자 | 7 × 4 (간격 10m × 10m) |
| 좌표계 | X = 가로(길이), Y = 높이(상향), Z = 세로(깊이) |
5.2 요소 색상 체계
| 요소 | 색상 코드 | 개수 |
|---|---|---|
| 바닥 슬래브 | 0x81C784 연두 |
3 |
| 지붕 | 0x4CAF50 초록 |
1 |
| 기둥 | 0xFF9800 주황 |
84 |
| 보 | 0xFFC107 황금 |
33 |
| 벽체 | 0x42A5F5 파랑 |
~50 |
| 창호 | 0x00BCD4 시안 (반투명) |
18 |
| 출입문 | 0x8B4513 갈색 |
3 |
| 계단실 | 0xFFAB91 살몬 |
15 |
| 합계 | ~200+ |
5.3 건물 단면
지붕 슬래브 ────── y = 12.0m
│
3F 기둥/벽/창 ─── y = 8.0 ~ 12.0m
│
2F 바닥 슬래브 ── y = 8.0m
│
2F 기둥/벽/창 ─── y = 4.0 ~ 8.0m
│
1F 바닥 슬래브 ── y = 4.0m
│
1F 기둥/벽/창 ─── y = 0.0 ~ 4.0m (후면: 하역장 도어 3개)
│
기초 슬래브 ───── y = 0.0m
│
지면 (그라운드) ─ y = -0.02m
6. 사용자 인터랙션
6.1 카메라 조작 (OrbitControls)
| 조작 | 동작 |
|---|---|
| 마우스 드래그 | 카메라가 건물 중심을 공전 (Orbit) |
| 스크롤 (휠) | 줌 인/아웃 |
| 우클릭 드래그 | 시점 평행 이동 (Pan) |
6.2 요소 클릭 선택 (Raycaster)
마우스 클릭 좌표 (2D)
↓ NDC 좌표 변환 (-1 ~ +1)
카메라에서 광선(Ray) 발사
↓ raycaster.intersectObjects()
3D 메시와 충돌 검사
↓
[데모 모드] userData (type, name, material...) 읽기
[IFC 모드] expressID → IFCHelper.getElementInfo() → IFC 속성 조회
↓
React 상태 업데이트 → 우측 정보 패널 표시
6.3 시점 전환
| 프리셋 | 용도 |
|---|---|
| 투시도 | 전체 조감 |
| 정면 | 남측 입면 |
| 우측 | 동측 입면 |
| 상부 | 평면도 |
| 배면 | 북측 (하역장) |
IFC 모드에서는 모델 바운딩 박스 기준으로 시점이 자동 계산된다.
6.4 IFC 업로드 / 다운로드
| 기능 | 방법 |
|---|---|
| IFC 업로드 | 툴바 "IFC 업로드" 버튼 또는 뷰포트에 드래그 앤 드롭 |
| IFC 다운로드 | 툴바 "IFC 다운로드" 버튼 (초록색) |
| 데모 복귀 | IFC 모드에서 "데모 모델" 버튼 |
7. React-Three.js 연동 패턴
7.1 BimScene 클래스
Three.js 로직을 BimScene 클래스로 캡슐화하여 React와 분리한다:
class BimScene {
constructor(el) // DOM 요소 참조, 상태 초기화
init() // Scene, Camera, Renderer, Controls, 데모 건물 생성
createDemoBuilding() // 데모 물류센터 모델 생성
async loadIFC(buffer) // IFC 파일 파싱 → BufferGeometry → 씬 추가
clearIFCModel() // IFC 모델 정리 (geometry/material dispose)
switchToDemo() // 데모 모드 복귀
exportIFC() // IFC 파일 다운로드 (데모: 생성, IFC: 원본)
onClick(e) // Raycaster로 요소 선택
setView(preset) // 카메라 전환 (lerp 보간)
fitToModel(obj) // 바운딩 박스 기준 카메라 자동 조정
toggleGroup(name) // 요소 유형 표시/숨김 (데모 모드)
toggleWireframe(on) // 와이어프레임 토글
getCounts() // 요소 통계 반환
animate() // 렌더 루프
dispose() // 리소스 정리
}
7.2 React 컴포넌트 구성
| 컴포넌트 | 역할 |
|---|---|
BimViewerApp |
루트 컴포넌트 — 상태 관리, 이벤트 핸들러 |
BimSidebar |
좌측 PMIS 메뉴 사이드바 |
BimToolbar |
하단 툴바 (시점, 토글, 업로드/다운로드) |
BimInfoPanel |
우측 패널 (모델 정보, 선택 요소, 통계) |
LoadingOverlay |
IFC 로딩 중 오버레이 |
7.3 통신 패턴
React → Three.js: sceneRef.current.setView('front')
sceneRef.current.exportIFC()
sceneRef.current.loadIFC(buffer)
Three.js → React: bim.onSelect = setSelected (요소 선택 콜백)
bim.onProgress = setLoading (로딩 상태 콜백)
8. 파일 구조
| 파일 | 위치 | 설명 |
|---|---|---|
bim-viewer.blade.php |
mng/resources/views/juil/ |
BIM 뷰어 전체 (~1300줄, Three.js + web-ifc + React + Show Effects) |
PlanningController.php |
mng/app/Http/Controllers/Juil/ |
bimViewer() 메서드 |
web.php |
mng/routes/ |
GET /juil/construction-pmis/bim-viewer |
9. SAM Show Effects (Phase 3)
9.1 개요
BIM 뷰어에 SAM 브랜드 3D 애니메이션 효과 20종을 추가했다. 툴바 드롭다운에서 효과를 선택하고 재생 버튼을 누르면 3D 뷰포트 위에 "SAM"(3D TextGeometry)과 "샘"(Canvas Sprite) 텍스트가 다양한 패턴으로 애니메이션된다.
9.2 아키텍처
사용자 → 드롭다운(sam1~sam20) 선택 → ▶ 재생 버튼
↓
playShow(id)
↓ FontLoader로 3D 폰트 로드 (최초 1회)
_initEffect(id)
↓ switch(id): 20개 케이스
↓ 각 케이스: items[] 생성 + update(s, elapsed) 클로저 정의
↓
this._show = { type, items, extras, startTime, duration, update }
↓
animate() 루프에서 _updateShow() 호출
↓ s.update(s, elapsed) — 클로저가 애니메이션 계산
↓ elapsed >= duration → _cleanupShow()
↓
_cleanupShow()
↓ items[].geometry.dispose() + material.dispose()
↓ extras[].geometry.dispose() + material.dispose()
↓ scene.remove(item/extra)
핵심 설계 원칙: 각 효과는 클로저 기반 update 함수로 자기 완결적이다. _updateShow()는 단순 디스패처로, s.update(s, elapsed)만 호출하고 시간 초과 시 _cleanupShow()를 호출한다.
9.3 헬퍼 메서드
| 메서드 | 역할 |
|---|---|
_makeTextSprite(text, color, size) |
Canvas 2D로 한글 텍스트("샘") 스프라이트 생성 |
_mkItem(text, color, size, pos) |
"SAM" → 3D TextGeometry, "샘" → Canvas Sprite 통합 생성 |
_mkFlash(pos) |
흰색 반투명 SphereGeometry (폭발/번쩍 효과용) |
_setOp(item, value) |
아이템 투명도 설정 (0~1 클램핑) |
mkMix(n, sMin, sMax) |
SAM/샘 혼합 아이템 N개 일괄 생성 (효과 내부 헬퍼) |
9.4 아이템 생성 패턴
// 3D TextGeometry (영문 "SAM")
const geo = new THREE.TextGeometry('SAM', { font, size, height:size*0.2 });
const mat = new THREE.MeshPhongMaterial({ color, transparent:true, opacity:1 });
const mesh = new THREE.Mesh(geo, mat);
// Canvas Sprite (한글 "샘")
const canvas = document.createElement('canvas');
ctx.fillText('샘', ...); // 한글 렌더링
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map:texture }));
각 아이템에 size, color, idx, targetPos, burstSpeed 등 애니메이션용 커스텀 속성을 부착한다.
9.5 효과 목록 (20종)
| ID | 이름 | 설명 | 패턴 |
|---|---|---|---|
| 1 | Star Gathering | 사방에서 별처럼 모여 폭발 | gather → burst |
| 2 | Spiral Galaxy | 은하 소용돌이 | 나선 회전 → 수축 |
| 3 | Fireworks | 불꽃놀이 | 상승 → 방사 폭발 |
| 4 | Matrix Rain | 매트릭스 문자비 | 상단에서 하강 |
| 5 | DNA Helix | DNA 이중나선 | 쌍 나선 회전 |
| 6 | Wormhole | 웜홀 통과 | 원형 터널 돌진 |
| 7 | Constellation | 별자리 연결 | 점 배치 → 선 연결 |
| 8 | Heartbeat | 심장 박동 | 하트 형태 펄스 |
| 9 | Tornado | 토네이도 | 원뿔형 회전 상승 |
| 10 | Domino Wave | 도미노 파동 | 순차 기울어짐 |
| 11 | Gravity Well | 중력 우물 | 공전 → 흡입 |
| 12 | Northern Lights | 오로라 커튼 | 수평 웨이브 컬러 변화 |
| 13 | Clock Mechanism | 시계 톱니바퀴 | 동심원 역방향 회전 |
| 14 | Particle Storm | 입자 폭풍 | 격렬한 랜덤 이동 |
| 15 | Bloom Garden | 꽃 피어남 | 중심에서 방사 성장 |
| 16 | Ocean Waves | 해양 파도 | 사인파 물결 |
| 17 | Lightning Storm | 번개 | 번쩍 + 분산 |
| 18 | Pendulum | 진자 시계 | 좌우 스윙 |
| 19 | Flock of Birds | 새 떼 비행 | Boids 군집 이동 |
| 20 | Grand Finale | 그랜드 피날레 | 나선 → 모임 → 대폭발 |
9.6 애니메이션 시간 관리
// 정규화된 시간 t (0.0 ~ 1.0)
const t = elapsed / s.duration;
// 페이즈 분할 예시 (Star Gathering)
if (t < 0.6) {
// GATHER 페이즈: 사방에서 중심으로 모임
const p = t / 0.6; // 0~1 정규화
} else if (t < 0.7) {
// FLASH 페이즈: 흰색 번쩍
} else {
// BURST 페이즈: 폭발 분산
const bp = (t - 0.7) / 0.3;
}
performance.now()기준 절대 시간 사용- 각 효과의
duration은 3~6초 범위 - 프레임 독립적 (delta time 기반)
9.7 UI 구성
┌─────────────────────────────────────────────┐
│ BimToolbar │
│ ┌──────────────────┐ ┌──┐ │
│ │ sam1 Star Gather ▼│ │▶│ ... 기존 버튼 │
│ └──────────────────┘ └──┘ │
│ (드롭다운 select) (재생) │
└─────────────────────────────────────────────┘
| UI 요소 | React 상태 | 설명 |
|---|---|---|
| 효과 드롭다운 | effectId |
SHOW_EFFECTS 배열(20개) 중 선택 |
| 재생 버튼 | — | sceneRef.current.playShow(effectId) 호출 |
SHOW_EFFECTS 상수:
const SHOW_EFFECTS = [
{ id:1, name:'sam1 Star Gathering' },
{ id:2, name:'sam2 Spiral Galaxy' },
// ... 20개
];
9.8 메모리 관리
_cleanupShow()가 모든 효과의 리소스를 통일적으로 정리한다:
_cleanupShow() {
if (!this._show) return;
const { items, extras } = this._show;
// items: TextGeometry 또는 Sprite
[items, extras].forEach(arr => arr.forEach(it => {
if (it.geometry) it.geometry.dispose();
if (it.material) {
if (it.material.map) it.material.map.dispose();
it.material.dispose();
}
this.scene.remove(it);
}));
this._show = null;
}
dispose() (BimScene 전체 정리) 시에도 _cleanupShow()를 호출하여 누수를 방지한다.
10. 향후 계획 (Phase 4+)
| Phase | 기능 | 기술 | 상태 |
|---|---|---|---|
| 완료 | |||
| 완료 | |||
| 완료 | |||
| Phase 4 | 요소 속성 패널 고도화, 2D 평면도 모드 | Three.js Orthographic Camera | |
| Phase 5 | 모델 파일 관리, 버전 이력, 협력업체 조회 | Laravel API + DB |
관련 문서
- 건설PMIS 대시보드 — PMIS 메인 대시보드
- PmisWorker 모델 — PMIS 작업자 프로필 테이블
최종 업데이트: 2026-03-13