投稿日:2025-03-02
#Three.js
IFC データを Web で扱うプロジェクトとして IFC.js があったかと思いますが、久しぶりに
のリポジトリを確認すると、プロジェクト全体に大幅な変更が入り、新しいアーキテクチャの
への移行が案内されていました。従来の web-ifc-three などのパッケージが非推奨となっていたので、とりあえず新しい公式ドキュメントのチュートリアルを一通りやってみました。
OpenBIM Components は Three.js 等をベースにつくられていて、IFC データを扱うためのツールを多く提供してくれています。ブラウザベースでの BIM アプリケーション開発を楽にしてくれるライブラリって感じです。 比較的公式ドキュメントのチュートリアルが充実しており、Three.js の理解がある人にとっては使い方もさほど難しくないのですが、Three.js や WebGL の理解がないともしかしたら少し難しく感じるかもしれません。 今回はこの OpenBIM Components を使用した IFC データのモデルと属性情報の表示まで行っています。
今回の実装したコードは
※この記事では、Node.js の環境がすでに整っていることを前提 に進めています。もし Node.js をまだインストールしていない場合は、公式サイト からダウンロード・セットアップをお願いします。 また、Three.js などの基本的な使い方については詳しく触れていませんので、ご了承ください。
詳細は
を参照
※2025/03/02 時点で Three.js の最新バージョン(r174)に@thatopen 関連が対応していなかったので Three.js のバージョンを落として実装しています。
Vite + TypeScript でプロジェクトを立ち上げます。適当なプロジェクトフォルダを作って、VSCode で開きます。 以下を実行
1npm create vite@latest . --template vanilla-ts
色々聞かれるので、 Ok to proceed? (y) y Select a framework: Vanilla Select a variant: TypeScript を選択します。
その後
1npm install
でモジュールをインストールして、
1npm run dev
で以下の画面が表示されれば OK です。
次に必要なライブラリをインストールしていきます。 以下を実行
1npm install @thatopen/components@^2.4.4 @thatopen/components-front@^2.4.4 @thatopen/components-front@^2.4.4 @thatopen/ui@^2.4.2 @thatopen/ui-obc@^2.4.1 stats.js@^0.17.0 three@^0.160 web-ifc@^0.0.66
1npm install @types/three@^0.160 typescript@^5.8.2
Vite でデフォルトで生成したファイルを整理します。 まずはエントリーポイントである index.html を以下のように直します。
index.html
1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>OpenBIMComponent</title> 7 </head> 8 <body> 9 <div id="container"></div> 10 <script type="module" src="/src/main.ts"></script> 11 </body> 12</html> 13
次に src 内の counter.ts・typescript.svg は不要なので削除します。 public フォルダ内の vite.svg も不要なので削除します。
main.ts の内容も以下とりあえず以下のようにしときます。
main.ts
1import "./style.css"; 2 3console.log("Hello, world!");
style.css も以下書き換えます。
style.css
1body { 2 margin: 0; 3 padding: 0; 4}
とりあえず IFC データをロードする前に、シーンを作ってそこに適当なオブジェクトを表示していきます。公式ドキュメントだと
になります。
main.ts
1import * as THREE from "three"; 2import Stats from "stats.js"; 3import * as OBC from "@thatopen/components"; 4 5import "./style.css"; 6 7//Sceneを表示するDOMの取得 8const container = document.getElementById("container") as HTMLElement; 9 10//エントリーポイントとなるcomponentのインスタンス生成。 11const components = new OBC.Components(); 12 13//sceneのを管理するworldsを取得して 14const worlds = components.get(OBC.Worlds); 15 16//worldを生成して、scene, camera, rendererを設定しinitでレンダリング処理の開始 17const world = worlds.create< 18 OBC.SimpleScene, 19 OBC.SimpleCamera, 20 OBC.SimpleRenderer 21>(); 22 23world.scene = new OBC.SimpleScene(components); 24world.renderer = new OBC.SimpleRenderer(components, container); 25world.camera = new OBC.SimpleCamera(components); 26 27components.init(); 28 29//worldのLight等の設定は自分でやるのめんどくさかったら以下でいい感じに設定してくれる 30world.scene.setup(); 31 32//背景色の設定 33world.scene.three.background = new THREE.Color(0x000000); 34 35//オブジェクトの追加 36const geometry = new THREE.BoxGeometry(1, 1, 1); 37const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 }); 38const cube = new THREE.Mesh(geometry, material); 39world.scene.three.add(cube); 40 41//cameraの位置を設定。()内は(x, y, z, lookAtX, lookAtY, lookAtZ)となっており、cameraの位置が(3, 3, 3)、OrbitControlsの基点が(0, 0, 0)になるように設定 42world.camera.controls.setLookAt(3, 3, 3, 0, 0, 0); 43 44//scene内にgridを追加 45const grids = components.get(OBC.Grids); 46const grid = grids.create(world); 47grid.config.primarySize = 1; 48grid.config.secondarySize = 10; 49 50//performance確認 51const stats = new Stats(); 52stats.showPanel(2); 53document.body.append(stats.dom); 54stats.dom.style.left = "0px"; 55stats.dom.style.zIndex = "unset"; 56world.renderer.onBeforeUpdate.add(() => stats.begin()); 57world.renderer.onAfterUpdate.add(() => stats.end());
style.css
1body { 2 margin: 0; 3 padding: 0; 4 width: 100%; 5 height: 100vh; 6} 7 8#container { 9 width: 100%; 10 height: 100%; 11}
ざっくりとした流れは
こんな感じで表示されてれば OK
ちなみに OpenBIM Component の公式ドキュメントでは、「すべての component は Singleton で設計されている」ことがいたるところで述べられているので、コンポーネントを使用する際は new でインスタンス化せずに components.get() を使うことが推奨されています。
IFC データをロードしていきますが、sample データが公式ドキュメントのリポジトリにあるのでこれ使います。↓
この small.ifc をダウンロードしてプロジェクトの public フォルダ内に入れます。
ifc データの読み込みを行っていきます。
OpenBIMComponent では ifc データを fragment というジオメトリに変換しています。これは、内部的には Three.js の InstancedMesh を使用しているようです。
先ほど Scene 内に作成した Box を削除して、最終行から以下のコードを追加します。
main.ts
1///importを追加 2import * as WEBIFC from "web-ifc"; 3 4//cameraの位置変更 5world.camera.controls.setLookAt(12, 6, 8, 0, 0, -10); 6 7//....さっきからの続き 8 9//ifcデータの読み込み 10 11//fragmentを管理するFragmentsManagerと、ifcデータを読み込むIfcLoaderを取得 12const fragments = components.get(OBC.FragmentsManager); 13const fragmentIfcLoader = components.get(OBC.IfcLoader); 14 15//setup() を実行すると、IfcLoader が WebAssemblyを使って IFC データを処理する準備を行う。 16//ローカルにWASMファイルがなくてもこれ書いとけばOK 17await fragmentIfcLoader.setup(); 18 19//不要なカテゴリがあれば除外できる 20const excludedCats = [ 21 WEBIFC.IFCTENDONANCHOR, 22 WEBIFC.IFCREINFORCINGBAR, 23 WEBIFC.IFCREINFORCINGELEMENT, 24]; 25 26for (const cat of excludedCats) { 27 fragmentIfcLoader.settings.excludedCategories.add(cat); 28} 29 30//座標を原点に合わせる 31fragmentIfcLoader.settings.webIfc.COORDINATE_TO_ORIGIN = true; 32 33//ifcデータの読み込み 34async function loadIfc() { 35 const file = await fetch("/small.ifc"); 36 const data = await file.arrayBuffer(); 37 const buffer = new Uint8Array(data); 38 const model = await fragmentIfcLoader.load(buffer); 39 model.name = "example"; 40 world.scene.three.add(model); 41 return model; 42} 43 44const model = await loadIfc(); 45 46//fragmentの読み込みが完了したら、onFragmentsLoadedに登録した関数が実行される 47fragments.onFragmentsLoaded.add((model) => { 48 console.log(model); 49}); 50
ざっくりとした流れは
以下のようにモデルが表示されれば OK
次に属性情報を表示していきますが、このあたりも便利なツールが色々用意されています。 今回は Mouse でホバーしたらホバーしたオブジェクトの情報が取得できるものを使ってみようと思います。 公式ドキュメントだと
main.ts
1//import追加 2import * as BUI from "@thatopen/ui"; 3import * as CUI from "@thatopen/ui-obc"; 4import * as OBCF from "@thatopen/components-front"; 5 6//...さっきからの続き 7 8//属性情報の取得 9//オブジェクトのリレーション情報を高速に検索できるようにインデックス化 10const indexer = components.get(OBC.IfcRelationsIndexer); 11await indexer.process(model); 12 13//属性情報を表示するテーブルの作成 14const [propertiesTable, updatePropertiesTable] = CUI.tables.elementProperties({ 15 components, 16 fragmentIdMap: {}, 17}); 18 19propertiesTable.preserveStructureOnFilter = true; 20propertiesTable.indentationInText = false; 21 22//ホバーしたオブジェクトのハイライトの設定 23const highlighter = components.get(OBCF.Highlighter); 24highlighter.setup({ world }); 25 26highlighter.events.select.onHighlight.add((fragmentIdMap) => { 27 updatePropertiesTable({ fragmentIdMap }); 28}); 29 30highlighter.events.select.onClear.add(() => 31 updatePropertiesTable({ fragmentIdMap: {} }) 32); 33 34//BUIの初期化 35BUI.Manager.init(); 36 37//ホバーしたオブジェクトのハイライトを表示するパネルの作成 38const propertiesPanel = BUI.Component.create(() => { 39 const onTextInput = (e: Event) => { 40 const input = e.target as BUI.TextInput; 41 propertiesTable.queryString = input.value !== "" ? input.value : null; 42 }; 43 44 const expandTable = (e: Event) => { 45 const button = e.target as BUI.Button; 46 propertiesTable.expanded = !propertiesTable.expanded; 47 button.label = propertiesTable.expanded ? "Collapse" : "Expand"; 48 }; 49 50 const copyAsTSV = async () => { 51 await navigator.clipboard.writeText(propertiesTable.tsv); 52 }; 53 54 return BUI.html` 55 <bim-panel label="Properties"> 56 <bim-panel-section label="Element Data"> 57 <div style="display: flex; gap: 0.5rem;"> 58 <bim-button @click=${expandTable} label=${ 59 propertiesTable.expanded ? "Collapse" : "Expand" 60 }></bim-button> 61 <bim-button @click=${copyAsTSV} label="Copy as TSV"></bim-button> 62 </div> 63 <bim-text-input @input=${onTextInput} placeholder="Search Property" debounce="250"></bim-text-input> 64 ${propertiesTable} 65 </bim-panel-section> 66 </bim-panel> 67 `; 68}); 69 70const app = document.createElement("bim-grid"); 71app.className = "attributes-panel"; 72app.layouts = { 73 main: { 74 template: ` 75 "propertiesPanel viewport" 76 /25rem 1fr 77 `, 78 elements: { propertiesPanel, container }, 79 }, 80}; 81 82app.layout = "main"; 83container.append(app);
style.css
1body { 2 margin: 0; 3 padding: 0; 4 width: 100%; 5 height: 100vh; 6} 7 8#container { 9 width: 100%; 10 height: 100%; 11 position: relative; 12} 13 14.attributes-panel { 15 width: 20%; 16 height: 100%; 17 position: absolute; 18 top: 0; 19 right: 0; 20} 21
ざっくりとした流れは
以下のように属性情報をマウスでホバーするとサイドのパネルにそのオブジェクトの属性情報が表示されれば OK です。
index.html
1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>OpenBIMComponent</title> 7 </head> 8 <body> 9 <div id="container"></div> 10 <script type="module" src="/src/main.ts"></script> 11 </body> 12</html> 13
main.ts
1import * as THREE from "three"; 2import Stats from "stats.js"; 3import * as OBC from "@thatopen/components"; 4import * as WEBIFC from "web-ifc"; 5import * as BUI from "@thatopen/ui"; 6import * as CUI from "@thatopen/ui-obc"; 7import * as OBCF from "@thatopen/components-front"; 8 9import "./style.css"; 10 11const container = document.getElementById("container") as HTMLElement; 12 13const components = new OBC.Components(); 14 15const worlds = components.get(OBC.Worlds); 16 17const world = worlds.create< 18 OBC.SimpleScene, 19 OBC.SimpleCamera, 20 OBC.SimpleRenderer 21>(); 22 23world.scene = new OBC.SimpleScene(components); 24world.renderer = new OBC.SimpleRenderer(components, container); 25world.camera = new OBC.SimpleCamera(components); 26 27components.init(); 28 29world.scene.setup(); 30 31world.scene.three.background = new THREE.Color(0x000000); 32 33world.camera.controls.setLookAt(12, 6, 8, 0, 0, -10); 34 35const grids = components.get(OBC.Grids); 36const grid = grids.create(world); 37grid.config.primarySize = 1; 38grid.config.secondarySize = 10; 39 40const stats = new Stats(); 41stats.showPanel(2); 42document.body.append(stats.dom); 43stats.dom.style.left = "0px"; 44stats.dom.style.zIndex = "unset"; 45world.renderer.onBeforeUpdate.add(() => stats.begin()); 46world.renderer.onAfterUpdate.add(() => stats.end()); 47 48const fragments = components.get(OBC.FragmentsManager); 49const fragmentIfcLoader = components.get(OBC.IfcLoader); 50 51await fragmentIfcLoader.setup(); 52 53const excludedCats = [ 54 WEBIFC.IFCTENDONANCHOR, 55 WEBIFC.IFCREINFORCINGBAR, 56 WEBIFC.IFCREINFORCINGELEMENT, 57]; 58 59for (const cat of excludedCats) { 60 fragmentIfcLoader.settings.excludedCategories.add(cat); 61} 62 63fragmentIfcLoader.settings.webIfc.COORDINATE_TO_ORIGIN = true; 64 65async function loadIfc() { 66 const file = await fetch("/small.ifc"); 67 const data = await file.arrayBuffer(); 68 const buffer = new Uint8Array(data); 69 const model = await fragmentIfcLoader.load(buffer); 70 model.name = "example"; 71 world.scene.three.add(model); 72 return model; 73} 74 75const model = await loadIfc(); 76 77fragments.onFragmentsLoaded.add((model) => { 78 console.log(model); 79}); 80 81const indexer = components.get(OBC.IfcRelationsIndexer); 82await indexer.process(model); 83 84const [propertiesTable, updatePropertiesTable] = CUI.tables.elementProperties({ 85 components, 86 fragmentIdMap: {}, 87}); 88 89propertiesTable.preserveStructureOnFilter = true; 90propertiesTable.indentationInText = false; 91 92const highlighter = components.get(OBCF.Highlighter); 93highlighter.setup({ world }); 94 95highlighter.events.select.onHighlight.add((fragmentIdMap) => { 96 updatePropertiesTable({ fragmentIdMap }); 97}); 98 99highlighter.events.select.onClear.add(() => 100 updatePropertiesTable({ fragmentIdMap: {} }) 101); 102 103BUI.Manager.init(); 104 105const propertiesPanel = BUI.Component.create(() => { 106 const onTextInput = (e: Event) => { 107 const input = e.target as BUI.TextInput; 108 propertiesTable.queryString = input.value !== "" ? input.value : null; 109 }; 110 111 const expandTable = (e: Event) => { 112 const button = e.target as BUI.Button; 113 propertiesTable.expanded = !propertiesTable.expanded; 114 button.label = propertiesTable.expanded ? "Collapse" : "Expand"; 115 }; 116 117 const copyAsTSV = async () => { 118 await navigator.clipboard.writeText(propertiesTable.tsv); 119 }; 120 121 return BUI.html` 122 <bim-panel label="Properties"> 123 <bim-panel-section label="Element Data"> 124 <div style="display: flex; gap: 0.5rem;"> 125 <bim-button @click=${expandTable} label=${ 126 propertiesTable.expanded ? "Collapse" : "Expand" 127 }></bim-button> 128 <bim-button @click=${copyAsTSV} label="Copy as TSV"></bim-button> 129 </div> 130 <bim-text-input @input=${onTextInput} placeholder="Search Property" debounce="250"></bim-text-input> 131 ${propertiesTable} 132 </bim-panel-section> 133 </bim-panel> 134 `; 135}); 136 137const app = document.createElement("bim-grid"); 138app.className = "attributes-panel"; 139app.layouts = { 140 main: { 141 template: ` 142 "propertiesPanel viewport" 143 /25rem 1fr 144 `, 145 elements: { propertiesPanel, container }, 146 }, 147}; 148 149app.layout = "main"; 150container.append(app); 151
style.css
1body { 2 margin: 0; 3 padding: 0; 4 width: 100%; 5 height: 100vh; 6} 7 8#container { 9 width: 100%; 10 height: 100%; 11 position: relative; 12} 13 14.attributes-panel { 15 width: 20%; 16 height: 100%; 17 position: absolute; 18 top: 0; 19 right: 0; 20}
OpenBIMComponent の基本的な部分を抜粋しました。他にもいろんな機能や UI が提供されているので詳しいことは公式ドキュメント参照願います。