Skip to content

Instantly share code, notes, and snippets.

@kiyuka829
Created April 28, 2024 07:07
Show Gist options
  • Save kiyuka829/32c0f65d9ecf95eff57802e370e15e6e to your computer and use it in GitHub Desktop.
Save kiyuka829/32c0f65d9ecf95eff57802e370e15e6e to your computer and use it in GitHub Desktop.
Convert IFC to glTF
from types import SimpleNamespace
from functools import partial
import pygltflib
import numpy as np
import ifcopenshell
import ifcopenshell.geom
from tqdm.auto import tqdm
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)
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):
# マテリアル
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,
alphaCutoff=None,
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)
return gltf
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.element_count = 0
self.explore_element_count(element)
self.bar = tqdm(total=self.element_count)
self.explore_element(self.tree)
self.bar.close()
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_count(self, element):
self.element_count += 1
for child_element in self.get_child_elements(element):
self.explore_element_count(child_element)
def explore_element(self, node, level=0):
self.bar.update(1)
# ジオメトリを取得する
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}"
)
if __name__ == "__main__":
ifc_file = ifcopenshell.open("model.ifc")
project = ifc_file.by_type("IfcProject")[0]
tree = IfcTreeStructure(project)
gltf = to_gltf(tree)
gltf.save("model.glb")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment