STUDIO TAMA


thumbnail

投稿日:2025-03-02

【Three.js-OpenBIM Component】Open BIM Componentでifcデータを扱う

  • #Three.js

IFC データを Web で扱うプロジェクトとして IFC.js があったかと思いますが、久しぶりに

GitHub

のリポジトリを確認すると、プロジェクト全体に大幅な変更が入り、新しいアーキテクチャの

への移行が案内されていました。従来の web-ifc-three などのパッケージが非推奨となっていたので、とりあえず新しい公式ドキュメントのチュートリアルを一通りやってみました。

OpenBIM Components は Three.js 等をベースにつくられていて、IFC データを扱うためのツールを多く提供してくれています。ブラウザベースでの BIM アプリケーション開発を楽にしてくれるライブラリって感じです。 比較的公式ドキュメントのチュートリアルが充実しており、Three.js の理解がある人にとっては使い方もさほど難しくないのですが、Three.js や WebGL の理解がないともしかしたら少し難しく感じるかもしれません。 今回はこの OpenBIM Components を使用した IFC データのモデルと属性情報の表示まで行っています。

今回の実装したコードは

※この記事では、Node.js の環境がすでに整っていることを前提 に進めています。もし Node.js をまだインストールしていない場合は、公式サイト からダウンロード・セットアップをお願いします。 また、Three.js などの基本的な使い方については詳しく触れていませんので、ご了承ください。

Version 一覧

詳細は

を参照

  • @thatopen/components : 2.4.4
  • @thatopen/components-front : 2.4.4
  • @thatopen/fragments : 2.4.0
  • @thatopen/ui : 2.4.2
  • @thatopen/ui-obc : 2.4.1
  • three.js : 0.160.1
  • web-ifc : 0.0.66
  • stats.js : 0.17.0
  • TypeScript : 5.8.2
  • Vite : 6.2.0
  • @types/three : 0.160.0

※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 です。

thumbnail

必要なライブラリをインストール

次に必要なライブラリをインストールしていきます。 以下を実行

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}

Open BIM Component 使っていく

とりあえず 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}

ざっくりとした流れは

  • Scene を描画するための DOM 要素を取得します。
  • エントリーポイントとなる component インスタンスを生成
  • scene を管理するための worlds を component から取得
  • scene, camera, renderer を指定して world を作成。
  • component.init()でレンダリング開始
  • world.scene.setup();でいい感じに環境設定をライブラリ側にお任せして
  • オブジェクトを scene に追加
  • world.camera.controls.setLookAt(3, 3, 3, 0, 0, 0);でカメラの位置と OrbitControl の基点を設定
  • (なくてもいい) ついでに grid 追加
  • (なくてもいい)ついでに Performance を確認するために Stats を表示

こんな感じで表示されてれば OK

thumbnail

ちなみに OpenBIM Component の公式ドキュメントでは、「すべての component は Singleton で設計されている」ことがいたるところで述べられているので、コンポーネントを使用する際は new でインスタンス化せずに components.get() を使うことが推奨されています。

ifc データのロード

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

ざっくりとした流れは

  • fragment を管理する FragmentsManager と、ifc データを読み込む IfcLoader を取得
  • fragment の解析には web-ifc の webAssembly のファイルをローカルに置く必要があるが、await fragmentIfcLoader.setup();書けばリモート・サーバーから取得してくれる。
  • 不要なカテゴリがあれば除外する
  • 必要であればモデルを原点に配置
  • loadIfc で ifc データのロードと scene への追加を行い
  • fragments.onFragmentsLoaded でデータの読み込みが終わった後の処理を実行できる

以下のようにモデルが表示されれば OK

thumbnail

属性情報の取得

次に属性情報を表示していきますが、このあたりも便利なツールが色々用意されています。 今回は 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

ざっくりとした流れは

  • ロードしたモデルのリレーション情報のインデックス化
  • 属性情報をいい感じに表示してくれるテーブルを作成
  • ホバーしたオブジェクトをハイライトさせる設定と、どのオブジェクトがホバーされたかを更新する処理
  • 開発元の That Open が提供する UI コンポーネントライブラリを使用するため BUI を初期化
  • ホバーしたオブジェクトの属性情報を表示するためのパネルを作成し配置

以下のように属性情報をマウスでホバーするとサイドのパネルにそのオブジェクトの属性情報が表示されれば OK です。

thumbnail

最終コード

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 が提供されているので詳しいことは公式ドキュメント参照願います。

参考

目 次