IFCをglTFに変換する
IFCをglTFに変換するなんて簡単にできるだろ!!!!と思って挑んだら結構難しかったのでメモします。
IFC.jsとかBlenderBIMで読み込んで出力すればいいんじゃないかって?まあ、直接変換する方法もあれば便利かなって
最初、IfcOpenShell使えば変換できるのかと思って公式ページを見たら、BlenderBIM Add-on
使ってね、って書いてあって直接変換はできないようでした。
https://blenderbim.org/docs-python/ifcconvert.html
↓
リンクがなくなっていたので更新。
もともとifcopenshell
のドキュメントは存在しなくて、BlenderBIM
のドキュメントにifcopenshell
のことが書いてあったのだけれど、正式に?作られたっぽい。
なんでできないのかと思って調べたら、おそらくOCCTのバグっぽい?
下記リンクはFreeCADの内容だけど、IfcOpenShellもOCCTを使用しているので、同じ理由なのではないかと推測。
単純に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使うと色々操作できて、モデルの階層構造とかも見れる。
せっかくならモデルの階層構造もつけたいよね?
gltfについてはこのあたりを見つつ。pygltflib
はgltfの構造をある程度理解していないと使いこなせないので、ちょっとたいへん。
わからないところは、Babylon.js のPlayground で簡単なモデル作って、gltf出力して、その中のデータ見て、ってしてた。Inspectorがなかったら結構きつかった。Inspectorは神です。
モデルの階層構造のイメージ
BlenderBIMで表示するとこうなる
階層構造を取得するだけなら難しくない
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出力用にどうこうするのが、非常にめんどくさい。
階層構造取得の ifcopenshell.util.element.get_decomposition
だけど、ソースコード見てみたら
ContainsElements
, IsDecomposedBy
だけじゃなくって HasOpenings
, HasFillings
, IsNestedBy
も取得してた。
IsDecomposedBy
は IfcProject > IfcSite > IfcBuilding > IfcBuildingStorey > IfcSpace
とかで使用する。つまり空間から空間への紐づけ(だと思う)。
ContainsElements
は IfcBuildingStorey > 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)
そんなわけでこうです。
コード
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")
一応できたけどコードが気に入らないから?そのうち直す
IfcOpenShell
形状取得するの遅いな...少し大きめのモデルだと、geometry iterator 使っても1分くらいかかる。
内部的にはOCCT使っているはずだからPythonで動いているわけでもないしなんで?
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
とのハイブリッドにしたほうがいいのかも?
一旦記事にはしたけど、速度とかいろいろ気になるので続けます。
web-ifc
のほうが読み込みが速そうなのでちょっと試す。
公式のドキュメントはまだ整備されていないので、ちょっと大変です。
まず、公式の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');
たぶんだけど const WebIFC = require("web-ifc/web-ifc-api.js");
でエラーになるのは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におまかせしてるし...
ジオメトリの取得はこれで行けるはず。
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!');
});
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");
}
ジオメトリの読み込み速度、IfcOpenShell
とweb-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");