Open17

IFCをglTFに変換する

kiyukakiyuka

IFCをglTFに変換するなんて簡単にできるだろ!!!!と思って挑んだら結構難しかったのでメモします。

IFC.jsとかBlenderBIMで読み込んで出力すればいいんじゃないかって?まあ、直接変換する方法もあれば便利かなって

kiyukakiyuka

最初、IfcOpenShell使えば変換できるのかと思って公式ページを見たら、BlenderBIM Add-on 使ってね、って書いてあって直接変換はできないようでした。

https://blenderbim.org/docs-python/ifcconvert.html

https://docs.ifcopenshell.org/ifcconvert.html

リンクがなくなっていたので更新。
もともとifcopenshellのドキュメントは存在しなくて、BlenderBIMのドキュメントにifcopenshellのことが書いてあったのだけれど、正式に?作られたっぽい。

kiyukakiyuka

単純にgltfにするだけでいいならこれでできる

コード
gltf出力関数
def to_gltf(geometries, material_dict, filename='output.glb', export_line=False):
    nodes = []
    meshes = []
    bufferViews = []
    accessors = []
    materials = []
    byteOffset = 0
    binary_blobs = b''

    # エッジ出力しない
    if not export_line:
        geometries = [g for g in geometries if len(g['triangles']) > 0]


    # マテリアル
    for index, (material_id, ifc_material) in enumerate(material_dict.items()):
        color = ifc_material['color']
        name = ifc_material['name']
        ifc_material['index'] = index

        alphaMode = pygltflib.OPAQUE if color[-1] == 1 else pygltflib.BLEND
        materials.append(
            pygltflib.Material(
                pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(
                    baseColorFactor=color,
                    # metallicFactor=0,
                    # roughnessFactor=1,
                ),
                alphaMode=alphaMode,
                name=name,
            )
        )

    # ジオメトリ
    for index, geometry in enumerate(geometries):
        points = geometry['vertices']
        lines = geometry.get('edges', [])
        triangles = geometry['triangles']
        name = geometry.get('name')
        material_name = geometry.get('material')
        material_index = material_dict[material_name]['index']

        # メッシュがなければエッジ
        if len(triangles) == 0:
            indices = lines
            mode = pygltflib.LINES
        else:
            indices = triangles
            mode = pygltflib.TRIANGLES

        # 型を指定する
        points = points.astype(np.float32)
        indices_max = indices.max()
        if indices_max <= np.iinfo(np.uint8).max:
            componentType = pygltflib.UNSIGNED_BYTE
            indices = indices.astype(np.uint8)
        elif indices_max <= np.iinfo(np.uint16).max:
            componentType = pygltflib.UNSIGNED_SHORT
            indices = indices.astype(np.uint16)
        else:
            componentType = pygltflib.UNSIGNED_INT
            indices = indices.astype(np.uint32)

        # バイナリに
        indices_binary_blob = indices.flatten().tobytes()
        points_binary_blob = points.tobytes()

        # ノード
        nodes.append(pygltflib.Node(mesh=index, name=name))

        # メッシュ
        meshes.append(pygltflib.Mesh(
            primitives=[
                pygltflib.Primitive(
                    attributes=pygltflib.Attributes(POSITION=1 + index * 2),
                    indices=index * 2,
                    material=material_index,
                    mode=mode
                )
            ]
        ))

        # bufferViews
        byteLength = len(indices_binary_blob) + len(points_binary_blob)
        bufferViews += [
            pygltflib.BufferView(
                buffer=0,
                byteOffset=byteOffset,
                byteLength=len(indices_binary_blob),
                target=pygltflib.ELEMENT_ARRAY_BUFFER,
            ),
            pygltflib.BufferView(
                buffer=0,
                byteOffset=byteOffset + len(indices_binary_blob),
                byteLength=len(points_binary_blob),
                target=pygltflib.ARRAY_BUFFER,
            ),
        ]
        byteOffset += byteLength

        # マテリアル
        alphaMode = pygltflib.OPAQUE if color[-1] == 1 else pygltflib.BLEND
        materials.append(
            pygltflib.Material(
                pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(
                    baseColorFactor=color,
                    # metallicFactor=0,
                    # roughnessFactor=1,
                ),
                alphaMode=alphaMode,
                doubleSided=True,
            )
        )

        # accessors
        accessors += [
            pygltflib.Accessor(
                bufferView=index * 2,
                componentType=componentType,
                count=indices.size,
                type=pygltflib.SCALAR,
                max=[int(indices.max())],
                min=[int(indices.min())],
            ),
            pygltflib.Accessor(
                bufferView=index * 2 + 1,
                componentType=pygltflib.FLOAT,
                count=len(points),
                type=pygltflib.VEC3,
                max=points.max(axis=0).tolist(),
                min=points.min(axis=0).tolist(),
            ),
        ]

        binary_blobs += indices_binary_blob + points_binary_blob

    # gltf作成
    gltf = pygltflib.GLTF2(
        scene=0,
        scenes=[pygltflib.Scene(nodes=list(range(len(geometries))))],
        nodes=nodes,
        meshes=meshes,
        accessors=accessors,
        bufferViews=bufferViews,
        buffers=[
            pygltflib.Buffer(
                byteLength=len(binary_blobs)
            )
        ],
        materials=materials,
    )
    gltf.set_binary_blob(binary_blobs)

    # 保存
    gltf.save(filename)
    return gltf
IFCをgltf出力
import ifcopenshell
import ifcopenshell.geom
import numpy as np
from tqdm.auto import tqdm

ifc_file = ifcopenshell.open(r'C:\work\ifc\model\AC20-FZK-Haus.ifc')
settings = ifcopenshell.geom.settings()
settings.set(settings.USE_WORLD_COORDS, True)
settings.set(settings.INCLUDE_CURVES, True)
settings.set(settings.STRICT_TOLERANCE, True)
settings.set(settings.USE_ELEMENT_GUIDS, True)
settings.set(settings.APPLY_DEFAULT_MATERIALS, True)

geometries = []
material_dict ={}
for element in tqdm(ifc_file.by_type("IfcProduct")):
    if element.is_a("IfcOpeningElement") or element.is_a("IfcSpace"):
        continue
    if element.Representation is None:
        continue
    try:
        shape = ifcopenshell.geom.create_shape(settings, element)
    except Exception as e:
        print(element, e)

    matrix = shape.transformation.matrix.data
    faces = shape.geometry.faces
    edges = shape.geometry.edges
    verts = shape.geometry.verts
    materials = shape.geometry.materials
    material_ids = shape.geometry.material_ids
    edges = np.array(edges).reshape(-1, 2)

    # 奥行き-Zの右手系(gltf)
    vertices = np.array(verts).reshape(-1, 3)[:, [0, 2, 1]]
    vertices[:, 0] = -vertices[:, 0]
    triangles = np.array(faces).reshape(-1, 3)

    material_ids = np.array(material_ids)
    for mat_id, material in enumerate(materials):
        color = *material.diffuse, 1 - material.transparency
        material_dict[material.name] = dict(color=color, name=material.original_name())

        mat_edges = edges[material_ids == mat_id] if len(triangles) == 0 else edges
        mat_triangles = (
            triangles[material_ids == mat_id] if len(triangles) > 0 else triangles
        )
        geometries.append(
            dict(
                name=element.is_a(),
                vertices=vertices,
                triangles=mat_triangles,
                material=material.name,
                edges=mat_edges,
            )
        )

to_gltf(geometries, material_dict, 'model.glb')

Babylon.js Sandboxで表示させるとこんな感じ。Inspector使うと色々操作できて、モデルの階層構造とかも見れる。
せっかくならモデルの階層構造もつけたいよね?

kiyukakiyuka

gltfについてはこのあたりを見つつ。pygltflibはgltfの構造をある程度理解していないと使いこなせないので、ちょっとたいへん。
わからないところは、Babylon.js のPlayground で簡単なモデル作って、gltf出力して、その中のデータ見て、ってしてた。Inspectorがなかったら結構きつかった。Inspectorは神です。

https://pypi.org/project/pygltflib

https://qiita.com/nokonoko_1203/items/827c4afc3ae88666126b

https://qiita.com/cx20/items/2b86cb5052cd7c36038a

https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#glb-file-format-specification

https://github.com/KhronosGroup/glTF-Sample-Models/tree/main/2.0/AlphaBlendModeTest

kiyukakiyuka

モデルの階層構造のイメージ
BlenderBIMで表示するとこうなる

kiyukakiyuka

階層構造を取得するだけなら難しくない

def explore_element(element, level=0):
    indent = '  ' * level
    print(f"{indent}- {element.is_a()} #{element.id()}: {element.Name if hasattr(element, 'Name') else ''}")

    # 子要素を探索する
    children = []
    if len(element.IsDecomposedBy) > 0:
        children_element = ifcopenshell.util.element.get_decomposition(element, is_recursive=False)
        for child_element in children_element:
            child = explore_element(child_element, level + 1)
            children.append(child)
        
    return dict(element=element, children=children)

project = ifc_file.by_type('IfcProject')[0]
tree = explore_element(project)
# - IfcProject #66: Projekt-FZK-Haus
#   - IfcSite #389: Gelaende
#     - IfcBuilding #434: FZK-Haus
#       - IfcBuildingStorey #479: Erdgeschoss
#         - IfcStair #14502: Wendeltreppe
#         - IfcWallStandardCase #15042: Wand-Int-ERDG-4
#         ...
#       - IfcBuildingStorey #35065: Dachgeschoss
#         - IfcMember #35169: Sparren-1
#         - IfcMember #35304: Sparren-2
#         ...

ただ、これをgltf出力用にどうこうするのが、非常にめんどくさい。

kiyukakiyuka

階層構造取得の ifcopenshell.util.element.get_decomposition だけど、ソースコード見てみたら
ContainsElements, IsDecomposedByだけじゃなくって HasOpenings, HasFillings, IsNestedBy も取得してた。

IsDecomposedByIfcProject > IfcSite > IfcBuilding > IfcBuildingStorey > IfcSpace とかで使用する。つまり空間から空間への紐づけ(だと思う)。
ContainsElementsIfcBuildingStorey > IfcWall などの、ある階層に含まれている実オブジェクトへの紐づけ。
今回必要なのはこの2つ。

HasOpeningsは壁の開口部などの紐づけで、HasFillingsは開口部にある窓など紐づけ、IsNestedByはよくわかってないけどモデルの階層構造ではなくてスケジュールとかコストの階層構造らしい。
今回、この3つは取得する必要がない。

だからこっちのコードのほうがいいかも。

def get_child_elements(element):
    for relationship in getattr(element, "IsDecomposedBy", []):
        for related_element in relationship.RelatedObjects:
            yield related_element

    for relationship in getattr(element, "ContainsElements", []):
        for related_element in relationship.RelatedElements:
            yield related_element

def explore_element(element, level=0):
    indent = '  ' * level
    print(f"{indent}- {element.is_a()} #{element.id()}: {element.Name if hasattr(element, 'Name') else ''}")

    # 子要素を探索する
    children = []
    for child_element in get_child_elements(element):
        child = explore_element(child_element, level + 1)
        children.append(child)
        
    return dict(element=element, children=children)
kiyukakiyuka

そんなわけでこうです。

コード
IFCから階層構造取得してgltf作成用のデータ作成するクラス
from types import SimpleNamespace
from functools import partial
settings = ifcopenshell.geom.settings()
settings.set(settings.USE_WORLD_COORDS, True)
# settings.set(settings.INCLUDE_CURVES, True) # データによってはバグるときがある?
settings.set(settings.STRICT_TOLERANCE, True)
settings.set(settings.USE_ELEMENT_GUIDS, True)
settings.set(settings.APPLY_DEFAULT_MATERIALS, True)
ifc_create_shape = partial(ifcopenshell.geom.create_shape, settings=settings)

class IfcTreeStructure:
    def __init__(self, element, use_edge=False, include_space=False):
        self.num_meshes = 0
        self.num_nodes = 0
        self.use_edge = use_edge
        self.include_space = include_space
        self.material_dict = {}
        self.tree = self.create_node(element)

        self.explore_element(self.tree)

    def __repr__(self):
        return repr(self.tree)

    def create_node(self, element, level=0):
        node = TreeNode(element, level, self.num_nodes)
        self.num_nodes += 1
        return node

    def get_child_elements(self, element):
        # 子要素を探索する
        for relationship in getattr(element, "IsDecomposedBy", []):
            for related_element in relationship.RelatedObjects:
                yield related_element

        # 空間要素を探索する
        for relationship in getattr(element, "ContainsElements", []):
            for related_element in relationship.RelatedElements:
                yield related_element

    def explore_element(self, node, level=0):
        # ジオメトリを取得する
        if node.has_geometry:
            geometry = self.get_geometry(node.element)
            if len(geometry) == 0:
                # ジオメトリなし
                node.has_geometry = False
            elif len(geometry) == 1:
                # ジオメトリ一つ(通常ケース)
                node.geometry = geometry[0]
                node.mesh_index = self.num_meshes
                self.num_meshes += 1
            elif len(geometry) > 1:
                # ジオメトリ複数
                node.has_geometry = False
                for g in geometry:
                    child = self.create_node(node.element, level + 1)
                    node.children.append(child)
                    material_name = self.material_dict[g["material"]]["name"]
                    child.name = f"{node.name} | {material_name}"
                    child.geometry = g
                    child.mesh_index = self.num_meshes
                    self.num_meshes += 1
                return

        # 子要素を探索する
        for child_element in self.get_child_elements(node.element):
            child = self.create_node(child_element, level + 1)
            node.children.append(child)
            self.explore_element(child, level + 1)

    def get_geometry(self, element):
        if not self.include_space and element.is_a("IfcSpace"):
            return []

        try:
            shape = ifc_create_shape(inst=element)
        except Exception as e:
            print(element, e)
            return []

        matrix = shape.transformation.matrix.data
        faces = shape.geometry.faces
        edges = shape.geometry.edges
        verts = shape.geometry.verts
        materials = shape.geometry.materials
        material_ids = shape.geometry.material_ids
        edges = np.array(edges).reshape(-1, 2)

        if not self.use_edge and len(faces) == 0:
            return []

        # 奥行き-Zの右手系(gltf)
        vertices = np.array(verts).reshape(-1, 3)[:, [0, 2, 1]]
        vertices[:, 0] = -vertices[:, 0]
        triangles = np.array(faces).reshape(-1, 3)

        geometries = []
        material_ids = np.array(material_ids)
        for mat_id, material in enumerate(materials):
            color = *material.diffuse, 1 - material.transparency
            self.material_dict[material.name] = dict(
                color=color, name=material.original_name()
            )

            mat_edges = edges[material_ids == mat_id] if len(triangles) == 0 else edges
            mat_triangles = (
                triangles[material_ids == mat_id] if len(triangles) > 0 else triangles
            )
            geometries.append(
                dict(
                    name=element.is_a(),
                    vertices=vertices,
                    triangles=mat_triangles,
                    material=material.name,
                    edges=mat_edges,
                )
            )

        return geometries

class TreeNode:
    def __init__(self, element, level=0, node_index=0):
        self.element = element
        self.children = []
        self.name = f"{element.is_a()}"
        if element.Name is not None and element.Name != "":
            self.name += f" | {element.Name}"

        self.has_geometry = getattr(element, "Representation", None) is not None
        self.geometry = None

        self.mesh_index = None
        self.node_index = node_index
        self.level = level

    def __repr__(self):
        indent = "  " * self.level
        child = (
            f"".join([f"{child}" for child in self.children])
            if len(self.children) > 0
            else ""
        )
        return (
            f"{indent}- {self.element.is_a()} #{self.element.id()}: "
            f"{self.element.Name if hasattr(self.element, 'Name') else ''}\n"
            f"{child}"
        )
階層構造からgltf作成
def create_gltf_mesh(geometry, material_dict, index, byteOffset):
    points = geometry["vertices"]
    lines = geometry.get("edges", [])
    triangles = geometry["triangles"]
    material_name = geometry.get("material")
    material_index = material_dict[material_name]["index"]

    # メッシュがなければエッジ
    if len(triangles) == 0:
        indices = lines
        mode = pygltflib.LINES
    else:
        indices = triangles
        mode = pygltflib.TRIANGLES

    # 型を指定する
    points = points.astype(np.float32)
    indices_max = indices.max()
    if indices_max <= np.iinfo(np.uint8).max:
        componentType = pygltflib.UNSIGNED_BYTE
        indices = indices.astype(np.uint8)
    elif indices_max <= np.iinfo(np.uint16).max:
        componentType = pygltflib.UNSIGNED_SHORT
        indices = indices.astype(np.uint16)
    else:
        componentType = pygltflib.UNSIGNED_INT
        indices = indices.astype(np.uint32)

    # バイナリに
    indices_binary_blob = indices.flatten().tobytes()
    points_binary_blob = points.tobytes()

    # メッシュ
    mesh = pygltflib.Mesh(
        primitives=[
            pygltflib.Primitive(
                attributes=pygltflib.Attributes(POSITION=1 + index * 2),
                indices=index * 2,
                material=material_index,
                mode=mode,
            )
        ]
    )

    # bufferViews
    byteLength = len(indices_binary_blob) + len(points_binary_blob)
    bufferViews = [
        pygltflib.BufferView(
            buffer=0,
            byteOffset=byteOffset,
            byteLength=len(indices_binary_blob),
            target=pygltflib.ELEMENT_ARRAY_BUFFER,
        ),
        pygltflib.BufferView(
            buffer=0,
            byteOffset=byteOffset + len(indices_binary_blob),
            byteLength=len(points_binary_blob),
            target=pygltflib.ARRAY_BUFFER,
        ),
    ]
    byteOffset += byteLength

    # accessors
    accessors = [
        pygltflib.Accessor(
            bufferView=index * 2,
            componentType=componentType,
            count=indices.size,
            type=pygltflib.SCALAR,
            max=[int(indices.max())],
            min=[int(indices.min())],
        ),
        pygltflib.Accessor(
            bufferView=index * 2 + 1,
            componentType=pygltflib.FLOAT,
            count=len(points),
            type=pygltflib.VEC3,
            max=points.max(axis=0).tolist(),
            min=points.min(axis=0).tolist(),
        ),
    ]

    binary_blobs = indices_binary_blob + points_binary_blob

    return mesh, bufferViews, accessors, binary_blobs


def to_gltf(mesh_tree, filename="output.glb"):
    # マテリアル
    materials = []
    for index, (_, material_data) in enumerate(mesh_tree.material_dict.items()):
        color = material_data["color"]
        name = material_data["name"]
        material_data["index"] = index

        alphaMode = pygltflib.OPAQUE if color[-1] == 1 else pygltflib.BLEND
        materials.append(
            pygltflib.Material(
                pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(
                    baseColorFactor=color,
                    # metallicFactor=0,
                    # roughnessFactor=1,
                ),
                alphaMode=alphaMode,
                doubleSided=True,
                name=name,
            )
        )

    # ノードとメッシュ作成
    gltf_data = SimpleNamespace(
        nodes=[],
        meshes=[],
        bufferViews=[],
        accessors=[],
        byteOffset=0,
        binary_blobs=b"",
    )

    def create_gltf_node_mesh(node):
        gltf_data.nodes.append(
            pygltflib.Node(
                name=node.name,
                mesh=node.mesh_index,
                children=[child.node_index for child in node.children],
            )
        )

        if node.has_geometry:
            mesh, bufferView, accessor, binary_blob = create_gltf_mesh(
                node.geometry,
                mesh_tree.material_dict,
                node.mesh_index,
                gltf_data.byteOffset,
            )
            gltf_data.meshes.append(mesh)
            gltf_data.bufferViews.extend(bufferView)
            gltf_data.accessors.extend(accessor)
            gltf_data.binary_blobs += binary_blob
            gltf_data.byteOffset += len(binary_blob)

        for child in node.children:
            create_gltf_node_mesh(child)

    root_node = mesh_tree.tree
    create_gltf_node_mesh(root_node)

    # gltf作成
    gltf = pygltflib.GLTF2(
        scene=0,
        scenes=[pygltflib.Scene(nodes=[0])],
        nodes=gltf_data.nodes,
        meshes=gltf_data.meshes,
        accessors=gltf_data.accessors,
        bufferViews=gltf_data.bufferViews,
        buffers=[pygltflib.Buffer(byteLength=len(gltf_data.binary_blobs))],
        materials=materials,
    )
    gltf.set_binary_blob(gltf_data.binary_blobs)

    # 保存
    gltf.save(filename)
    return gltf

実行

project = ifc_file.by_type("IfcProject")[0]
tree = IfcTreeStructure(project)
to_gltf(tree, "output.glb")

一応できたけどコードが気に入らないから?そのうち直す

kiyukakiyuka

IfcOpenShell 形状取得するの遅いな...少し大きめのモデルだと、geometry iterator 使っても1分くらいかかる。
内部的にはOCCT使っているはずだからPythonで動いているわけでもないしなんで?

https://blenderbim.org/docs-python/ifcopenshell-python/geometry_processing.html#geometry-iterator

iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count())
if iterator.initialize():
    while True:
        shape = iterator.get()
        matrix = shape.transformation.matrix.data
        faces = shape.geometry.faces
        edges = shape.geometry.edges
        verts = shape.geometry.verts
        materials = shape.geometry.materials
        material_ids = shape.geometry.material_ids
        # ... write code to process geometry here ...
        if not iterator.next():
            break

試しにIFC.jsで読み込んでみるとすぐに表示される。
速度気にするならIfcOpenShellは使わないかIFC.jsとのハイブリッドにしたほうがいいのかも?

https://docs.thatopen.com/Tutorials/FragmentIfcLoader

kiyukakiyuka

まず、公式のQuick setup のコードは動かない。
node main.js みたいに動かすなら以下で読み込める。

const WebIFC = require("web-ifc");
const fs = require("fs");

async function OpenIfc(filename) {
  const ifcData = fs.readFileSync(filename);
  await ifcapi.Init();
  return ifcapi.OpenModel(ifcData);
}

async function LoadFile(filename) {
  const modelID = await OpenIfc(filename);

  // なんやかんやここで処理する

  ifcapi.CloseModel(modelID);
}

const ifcapi = new WebIFC.IfcAPI();
LoadFile('model.ifc');
kiyukakiyuka

たぶんだけど const WebIFC = require("web-ifc/web-ifc-api.js"); でエラーになるのはpackage.jsonにこれがあるからだと思う。
つまり公式ドキュメントはコードの変更に追いついてないっぽい。

web-ifc/package.json
  "exports": {
    ".": {
      "require": "./web-ifc-api-node.js",
      "node": "./web-ifc-api-node.js",
      "import": "./web-ifc-api.js",
      "browser": "./web-ifc-api.js"
    }
  },

あんまりnodejs理解できてないんだよね...だからこのあたりは推測。
いつもはViteにおまかせしてるし...

kiyukakiyuka

ジオメトリの取得はこれで行けるはず。

  const meshes = ifcapi.LoadAllGeometry(modelID)
  const meshData = [];
  for(const mesh of meshes) {
    const placedGeometries = mesh.geometries;
    const size = placedGeometries.size();
    for (let i = 0; i < size; i++) {
        const placedGeometry = placedGeometries.get(i)
        const geometry = ifcapi.GetGeometry(modelID, placedGeometry.geometryExpressID);
        // 6つで一組のデータ: x, y, z, normalx, normaly, normalz
        const verts = ifcapi.GetVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
        // 3つで一組のデータ:頂点index 1, 2, 3
        const indices = ifcapi.GetIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize());

        // color RGBA: { x: 0.87, y: 0.75, z: 0.52, w: 1 }
        const color = placedGeometry.color
        // vertsは形状情報のみなので、4x4の変形行列を掛ける必要あり
        const transformation = placedGeometry.flatTransformation;

        meshData.push({
          verts: Array.from(verts),
          indices: Array.from(indices),
          color,
          transformation: Array.from(transformation),
        });
    }
  }
  fs.writeFile('hoge.json', JSON.stringify(meshData, null, 2), (err) => {
    if (err) throw err;
    console.log('The file has been saved!');
  });
kiyukakiyuka

glTFに出力したかったけど、なんかうまくいかないのでひとまずplyで。
...これ何で動くの?ifcapi.StreamAllMeshesって非同期だよね?

コード
async function exportply(modelID) {
  let indexoffset = 0;
  const vertex = [];
  const face = [];
  ifcapi.StreamAllMeshes(modelID, (mesh) => {
    const placedGeometries = mesh.geometries;
    const size = placedGeometries.size();

    const verts_list = [];
    const indices_list = [];
    const trans_list = [];
    const color_list = [];
    for (let i = 0; i < size; i++) {
      const placedGeometry = placedGeometries.get(i);
      const geometry = ifcapi.GetGeometry(
        modelID,
        placedGeometry.geometryExpressID
      );
      const verts = ifcapi.GetVertexArray(
        geometry.GetVertexData(),
        geometry.GetVertexDataSize()
      );
      const indices = ifcapi.GetIndexArray(
        geometry.GetIndexData(),
        geometry.GetIndexDataSize()
      );
      const flatTransformation = placedGeometry.flatTransformation;
      verts_list.push(verts);
      indices_list.push(indices);
      color_list.push(placedGeometry.color);
      trans_list.push(flatTransformation);
    }

    for (let k = 0; k < verts_list.length; k++) {
      if (color_list[k].w == 0) continue;
      const [r, g, b] = [
        Math.round(color_list[k].x * 255),
        Math.round(color_list[k].y * 255),
        Math.round(color_list[k].z * 255),
      ];
      for (let i = 0; i < verts_list[k].length; i += 6) {
        const [x, y, z] = verts_list[k].slice(i, i + 3);
        const point = [x, y, z, 1];
        // https://developer.mozilla.org/ja/docs/Web/API/WebGL_API/Matrix_math_for_the_web
        const [transX, transY, transZ] = multiplyMatrixAndPoint(
          trans_list[k],
          point
        );
        vertex.push([transX, transY, transZ, r, g, b]);
      }
      for (let i = 0; i < indices_list[k].length; i += 3) {
        face.push([
          indices_list[k][i] + indexoffset,
          indices_list[k][i + 1] + indexoffset,
          indices_list[k][i + 2] + indexoffset,
        ]);
      }
      indexoffset += verts_list[k].length / 6;
    }
  });

  // plyで保存
  const text = `ply
format ascii 1.0
element vertex ${vertex.length}
property double x
property double y
property double z
property uchar red
property uchar green
property uchar blue
element face ${face.length}
property list uchar uint vertex_indices
end_header
${vertex.map((v) => v.join(" ")).join("\n")}
${face.map((v) => "3 " + v.join(" ")).join("\n")}
`;
  fs.writeFileSync("a.ply", text, "utf8");
}

kiyukakiyuka

ジオメトリの読み込み速度、IfcOpenShellweb-ifcで全く違った。
すごい雑にしか計測してないけど、50MBくらいのファイル2つで試したら数十倍違う。

IfcOpenShell web-ifc
モデル1 30秒 1秒
モデル2 2分 2秒

あと、計測してないけど明らかにメモリ効率も web-ifcの方が良い。
手元に大きいデータがないから確認できないんだけど、IfcOpenShell だと操作できないんじゃないのこれ...?

実行したコード

IfcOpenShell
import numpy as np
import ifcopenshell.geom
from tqdm import tqdm

settings = ifcopenshell.geom.settings()
settings.set(settings.USE_WORLD_COORDS, True)
settings.set(settings.STRICT_TOLERANCE, True)
settings.set(settings.USE_ELEMENT_GUIDS, True)
settings.set(settings.APPLY_DEFAULT_MATERIALS, True)

def main(path):
    # IFCファイル読み込み
    ifc_file = ifcopenshell.open(path)

    # geometries = []
    for element in tqdm(ifc_file.by_type('IfcProduct')):
        if element.is_a('IfcOpeningElement') or element.is_a('IfcSpace'):
            continue
        if element.Representation is None:
            continue
        try:
            shape = ifcopenshell.geom.create_shape(settings, element)
            matrix = shape.transformation.matrix.data
            faces = shape.geometry.faces
            if len(faces) == 0:
                continue
            edges = shape.geometry.edges
            verts = shape.geometry.verts
            materials = shape.geometry.materials
            material_ids = shape.geometry.material_ids

            # 奥行きZに
            vertices = np.array(verts).reshape(-1, 3)[:, [0, 2, 1]]
            triangles = np.array(faces).reshape(-1, 3)[:, [0, 2, 1]]

            material_ids = np.array(material_ids)
        except:
            print(element)

if __name__ == '__main__':
    import time

    start = time.time()
    main("model.ifc")
    print(time.time() - start)
web-ifc
const fs = require("fs");
const WebIFC = require("web-ifc");

async function OpenIfc(filename) {
  const ifcData = fs.readFileSync(filename);
  await ifcapi.Init();
  return ifcapi.OpenModel(ifcData);
}

async function LoadFile(filename) {
  console.time('timer');
  const modelID = await OpenIfc(filename);

  console.time('read timer');
  await exportply(modelID);
  console.timeEnd('read timer');

  ifcapi.CloseModel(modelID);
  console.timeEnd('timer');
}

async function exportply(modelID) {
  let indexoffset = 0;
  const vertex = [];
  const face = [];
  ifcapi.StreamAllMeshes(modelID, (mesh) => {
    const placedGeometries = mesh.geometries;
    const size = placedGeometries.size();

    const verts_list = [];
    const indices_list = [];
    const trans_list = [];
    const color_list = [];
    for (let i = 0; i < size; i++) {
      const placedGeometry = placedGeometries.get(i);
      const geometry = ifcapi.GetGeometry(
        modelID,
        placedGeometry.geometryExpressID
      );
      const verts = ifcapi.GetVertexArray(
        geometry.GetVertexData(),
        geometry.GetVertexDataSize()
      );
      const indices = ifcapi.GetIndexArray(
        geometry.GetIndexData(),
        geometry.GetIndexDataSize()
      );
      const flatTransformation = placedGeometry.flatTransformation;
      verts_list.push(verts);
      indices_list.push(indices);
      color_list.push(placedGeometry.color);
      trans_list.push(flatTransformation);
    }

    for (let k = 0; k < verts_list.length; k++) {
      if (color_list[k].w == 0) continue;
      const [r, g, b] = [
        Math.round(color_list[k].x * 255),
        Math.round(color_list[k].y * 255),
        Math.round(color_list[k].z * 255),
      ];
      for (let i = 0; i < verts_list[k].length; i += 6) {
        const [x, y, z] = verts_list[k].slice(i, i + 3);
        const point = [x, y, z, 1];
        // https://developer.mozilla.org/ja/docs/Web/API/WebGL_API/Matrix_math_for_the_web
        const [transX, transY, transZ] = multiplyMatrixAndPoint(
          trans_list[k],
          point
        );
        vertex.push([transX, transY, transZ, r, g, b]);
      }
      for (let i = 0; i < indices_list[k].length; i += 3) {
        face.push([
          indices_list[k][i] + indexoffset,
          indices_list[k][i + 1] + indexoffset,
          indices_list[k][i + 2] + indexoffset,
        ]);
      }
      indexoffset += verts_list[k].length / 6;
    }
  });

  // plyで保存
  const text = `ply
format ascii 1.0
element vertex ${vertex.length}
property double x
property double y
property double z
property uchar red
property uchar green
property uchar blue
element face ${face.length}
property list uchar uint vertex_indices
end_header
${vertex.map((v) => v.join(" ")).join("\n")}
${face.map((v) => "3 " + v.join(" ")).join("\n")}
`;
  fs.writeFileSync("a.ply", text, "utf8");
}

// 点 • 行列
function multiplyMatrixAndPoint(matrix, point) {
  // 行列の各部分に、列 c、行 r の番号で単純な変数名を付けます
  let c0r0 = matrix[0],
    c1r0 = matrix[1],
    c2r0 = matrix[2],
    c3r0 = matrix[3];
  let c0r1 = matrix[4],
    c1r1 = matrix[5],
    c2r1 = matrix[6],
    c3r1 = matrix[7];
  let c0r2 = matrix[8],
    c1r2 = matrix[9],
    c2r2 = matrix[10],
    c3r2 = matrix[11];
  let c0r3 = matrix[12],
    c1r3 = matrix[13],
    c2r3 = matrix[14],
    c3r3 = matrix[15];

  // 次に、点にある単純な名前を設定します
  let x = point[0];
  let y = point[1];
  let z = point[2];
  let w = point[3];

  // 1番目の列の各部分に対して点を乗算し、次に合計します
  let resultX = x * c0r0 + y * c0r1 + z * c0r2 + w * c0r3;

  // 2番目の列の各部分に対して点を乗算し、次に合計します
  let resultY = x * c1r0 + y * c1r1 + z * c1r2 + w * c1r3;

  // 3番目の列の各部分に対して点を乗算し、次に合計します
  let resultZ = x * c2r0 + y * c2r1 + z * c2r2 + w * c2r3;

  // 4番目の列の各部分に対して点を乗算し、次に合計します
  let resultW = x * c3r0 + y * c3r1 + z * c3r2 + w * c3r3;

  return [resultX, resultY, resultZ, resultW];
}


const ifcapi = new WebIFC.IfcAPI();
LoadFile("model.ifc");