Created
April 28, 2024 07:07
-
-
Save kiyuka829/32c0f65d9ecf95eff57802e370e15e6e to your computer and use it in GitHub Desktop.
Convert IFC to glTF
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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