Skip to content

Commit 299b5bc

Browse files
committed
Optimize loading meshes using numpy arrays
1) avoid calling geometry.xxx multiple times as each time it creates a new copy 2) use numpy arrays with correct data type, so Blender could perform buffer copy significantly reducing overhead
1 parent cb95539 commit 299b5bc

5 files changed

Lines changed: 45 additions & 49 deletions

File tree

src/bonsai/bonsai/bim/import_ifc.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import traceback
2525
import mathutils
2626
import numpy as np
27+
import numpy.typing as npt
2728
import multiprocessing
2829
import ifcopenshell
2930
import ifcopenshell.geom
@@ -36,7 +37,7 @@
3637
import bonsai.tool as tool
3738
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
3839
from bonsai.tool.loader import OBJECT_DATA_TYPE
39-
from typing import Dict, Union, Optional, Any
40+
from typing import Dict, Union, Optional, Any, Literal
4041

4142

4243
class MaterialCreator:
@@ -167,7 +168,7 @@ def get_empty_slot_index() -> int:
167168
if -1 in self.mesh["ios_material_ids"]:
168169
material_to_slot[-1] = get_empty_slot_index()
169170

170-
material_index = [material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]]
171+
material_index = np.array([material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]], dtype="I")
171172
self.mesh.polygons.foreach_set("material_index", material_index)
172173

173174
def resolve_all_stylable_representation_items(
@@ -1030,7 +1031,7 @@ def create_mesh(
10301031
self,
10311032
element: ifcopenshell.entity_instance,
10321033
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
1033-
cartesian_point_offset=None,
1034+
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
10341035
) -> bpy.types.Mesh:
10351036
try:
10361037
if hasattr(shape, "geometry"):
@@ -1046,32 +1047,25 @@ def create_mesh(
10461047
old_mesh.name = mesh_name + ".old"
10471048
mesh = bpy.data.meshes.new(mesh_name)
10481049

1050+
verts = ifcopenshell.util.shape.get_vertices(geometry)
10491051
if cartesian_point_offset is False:
1050-
verts = geometry.verts
10511052
mesh["has_cartesian_point_offset"] = False
10521053
elif cartesian_point_offset is not None:
1053-
verts_array = np.array(geometry.verts)
1054-
offset = np.array([-cartesian_point_offset[0], -cartesian_point_offset[1], -cartesian_point_offset[2]])
1055-
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
1056-
verts = offset_verts.tolist()
1054+
verts -= cartesian_point_offset
10571055

10581056
mesh["has_cartesian_point_offset"] = True
10591057
mesh["cartesian_point_offset"] = (
10601058
f"{cartesian_point_offset[0]},{cartesian_point_offset[1]},{cartesian_point_offset[2]}"
10611059
)
1062-
elif geometry.verts and tool.Loader.is_point_far_away(
1063-
(geometry.verts[0], geometry.verts[1], geometry.verts[2]), is_meters=True
1064-
):
1060+
elif verts.size and tool.Loader.is_point_far_away(verts[0], is_meters=True):
10651061
# Shift geometry close to the origin based off that first vert it found
10661062
verts_array = np.array(geometry.verts)
1067-
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]])
1068-
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
1069-
verts = offset_verts.tolist()
1063+
offset = verts[0]
1064+
verts -= offset
10701065

10711066
mesh["has_cartesian_point_offset"] = True
1072-
mesh["cartesian_point_offset"] = f"{geometry.verts[0]},{geometry.verts[1]},{geometry.verts[2]}"
1067+
mesh["cartesian_point_offset"] = f"{offset[0]},{offset[1]},{offset[2]}"
10731068
else:
1074-
verts = geometry.verts
10751069
mesh["has_cartesian_point_offset"] = False
10761070

10771071
return tool.Loader.convert_geometry_to_mesh(

src/bonsai/bonsai/bim/module/project/operator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1846,7 +1846,7 @@ def process_occurrence(self, shape: ShapeElementType) -> None:
18461846
max_slot_index += 1
18471847
material_to_slot[i] = slot_index
18481848

1849-
material_index = [(material_to_slot[i] if i != -1 else 0) for i in material_ids]
1849+
material_index = np.array([(material_to_slot[i] if i != -1 else 0) for i in material_ids], dtype="I")
18501850

18511851
num_vertices = len(verts) // 3
18521852
total_faces = len(faces)

src/bonsai/bonsai/tool/geometry.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import ifcopenshell.util.element
3636
import ifcopenshell.util.placement
3737
import ifcopenshell.util.representation
38+
import ifcopenshell.util.shape
3839
import ifcopenshell.util.shape_builder
3940
import ifcopenshell.util.system
4041
import ifcopenshell.util.unit
@@ -1756,13 +1757,9 @@ def import_item(cls, obj: bpy.types.Object) -> None:
17561757
obj.matrix_world = rep_obj.matrix_world @ position
17571758
else:
17581759
geometry = tool.Loader.create_generic_shape(item)
1760+
verts = ifcopenshell.util.shape.get_vertices(geometry)
17591761
if (cartesian_point_offset := cls.get_cartesian_point_offset(rep_obj)) is not None:
1760-
verts_array = np.array(geometry.verts)
1761-
offset = np.array([-cartesian_point_offset[0], -cartesian_point_offset[1], -cartesian_point_offset[2]])
1762-
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
1763-
verts = offset_verts.tolist()
1764-
else:
1765-
verts = geometry.verts
1762+
verts -= cartesian_point_offset
17661763
tool.Loader.convert_geometry_to_mesh(geometry, obj.data, verts=verts)
17671764

17681765
if ios_materials := list(obj.data["ios_materials"]):

src/bonsai/bonsai/tool/loader.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import bonsai.bim.import_ifc
3636
import numpy as np
3737
import numpy.typing as npt
38+
from ifcopenshell.util.shape_builder import np_to_4d
3839
from mathutils import Vector, Matrix
3940
from pathlib import Path
4041
from typing import Union, Any, Optional
@@ -634,7 +635,7 @@ def is_point_far_away(
634635
limit = cls.settings.distance_limit
635636
limit = limit if is_meters else (limit / cls.unit_scale)
636637
coords = getattr(point, "Coordinates", point)
637-
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
638+
return any(abs(c) > limit for c in coords)
638639

639640
@classmethod
640641
def is_element_far_away(cls, element: ifcopenshell.entity_instance) -> bool:
@@ -879,11 +880,12 @@ def get_offset_point(cls, ifc_file: ifcopenshell.file) -> Union[npt.NDArray[np.f
879880
if not shape:
880881
continue
881882
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
882-
point = mat @ np.array((shape.geometry.verts[0], shape.geometry.verts[1], shape.geometry.verts[2], 1.0))
883+
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
884+
point = (mat @ np_to_4d(verts[0]))[:3]
883885
if cls.is_point_far_away(point, is_meters=True):
884886
# Arbitrary origins should be to the nearest millimeter.
885887
# Anything more precise is just ridiculous from a practical surveying perspective.
886-
return [round(float(p), 3) / cls.unit_scale for p in point[:3]]
888+
return np.array([round(float(p), 3) / cls.unit_scale for p in point])
887889
break
888890

889891
@classmethod
@@ -960,14 +962,19 @@ def convert_geometry_to_mesh(
960962
cls,
961963
geometry: ifcopenshell.geom.ShapeType,
962964
mesh: bpy.types.Mesh,
963-
verts: Optional[list[float]] = None,
965+
verts: Optional[npt.NDArray[np.float64]] = None,
964966
*,
965967
load_indexed_maps=True,
966968
) -> bpy.types.Mesh:
969+
"""
970+
:param verts: Numpy array of shape (n, 3).
971+
"""
967972
if verts is None:
968-
verts = geometry.verts
969-
if geometry.faces:
970-
num_vertices = len(verts) // 3
973+
verts = ifcopenshell.util.shape.get_vertices(geometry)
974+
faces = ifcopenshell.util.shape.get_faces(geometry)
975+
total_faces: int
976+
if total_faces := faces.shape[0]:
977+
num_vertices: int = verts.shape[0]
971978

972979
# See bug 3546
973980
# ios_edges holds true edges that aren't triangulated.
@@ -978,34 +985,35 @@ def convert_geometry_to_mesh(
978985
mesh["ios_item_ids"] = ios_item_ids
979986

980987
mesh.vertices.add(num_vertices)
981-
mesh.vertices.foreach_set("co", verts)
988+
mesh.vertices.foreach_set("co", verts.ravel().astype("f"))
982989

983990
is_triangulated = True
991+
num_vertex_indices = faces.size
984992
if is_triangulated:
985-
total_faces = len(geometry.faces)
986-
num_vertex_indices = len(geometry.faces)
987-
loop_start = range(0, total_faces, 3)
988-
num_loops = total_faces // 3
989-
loop_total = [3] * num_loops
993+
loop_start = np.arange(0, num_vertex_indices, 3, dtype="I")
994+
loop_total = np.full(total_faces, 3, dtype="I")
995+
use_smooth = np.zeros(num_vertex_indices, dtype="?")
990996

991997
mesh.loops.add(num_vertex_indices)
992-
mesh.loops.foreach_set("vertex_index", geometry.faces)
993-
mesh.polygons.add(num_loops)
998+
mesh.loops.foreach_set("vertex_index", faces.ravel().astype("I"))
999+
mesh.polygons.add(total_faces)
9941000
mesh.polygons.foreach_set("loop_start", loop_start)
9951001
mesh.polygons.foreach_set("loop_total", loop_total)
996-
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
1002+
mesh.polygons.foreach_set("use_smooth", use_smooth)
9971003
else:
1004+
# TODO: optimize using correct numpy array types.
9981005
faces_array = np.array(geometry.faces, dtype=object)
999-
loop_total = tuple(len(face) for face in faces_array)
1006+
loop_total = np.array(tuple(len(face) for face in faces_array), dtype="I")
10001007
loop_start = np.cumsum((0,) + loop_total)[:-1]
10011008
vertex_index = np.concatenate(faces_array)
1009+
use_smooth = np.zeros(num_vertex_indices, dtype="?")
10021010

10031011
mesh.loops.add(len(vertex_index))
10041012
mesh.loops.foreach_set("vertex_index", vertex_index)
10051013
mesh.polygons.add(len(loop_start))
10061014
mesh.polygons.foreach_set("loop_start", loop_start)
10071015
mesh.polygons.foreach_set("loop_total", loop_total)
1008-
mesh.polygons.foreach_set("use_smooth", [0] * len(geometry.faces))
1016+
mesh.polygons.foreach_set("use_smooth", use_smooth)
10091017

10101018
mesh.update()
10111019

@@ -1020,11 +1028,8 @@ def convert_geometry_to_mesh(
10201028
tool.Blender.Attribute.fill_attribute(mesh, "ios_item_ids", "FACE", "INT", ios_item_ids)
10211029
tool.Blender.Attribute.fill_attribute(mesh, "ios_material_ids", "FACE", "INT", geometry.material_ids)
10221030
else:
1023-
e = geometry.edges
1024-
v = verts
1025-
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
1026-
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
1027-
mesh.from_pydata(vertices, edges, [])
1031+
edges = ifcopenshell.util.shape.get_edges(geometry)
1032+
mesh.from_pydata(verts.tolist(), edges.tolist(), [])
10281033
# TODO: remove error handling after we update build in Bonsai.
10291034
try:
10301035
edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist()
@@ -1034,8 +1039,8 @@ def convert_geometry_to_mesh(
10341039
tool.Blender.Attribute.fill_attribute(mesh, "ios_edges_item_ids", "EDGE", "INT", edges_item_ids)
10351040
tool.Blender.Attribute.fill_attribute(mesh, "ios_material_ids", "EDGE", "INT", geometry.material_ids)
10361041

1037-
mesh["ios_materials"] = [m.instance_id() for m in geometry.materials]
1038-
mesh["ios_material_ids"] = geometry.material_ids
1042+
mesh["ios_materials"] = [m.instance_id() for m in ifcopenshell.util.shape.get_shape_material_styles(geometry)]
1043+
mesh["ios_material_ids"] = ifcopenshell.util.shape.get_faces_material_style_ids(geometry).tolist()
10391044
return mesh
10401045

10411046
@classmethod

src/bonsai/test/tool/test_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ def test_assign_material_to_representation_that_has_2_items_and_1_item_has_a_sty
614614
tool.Geometry._reload_representation(obj)
615615

616616
def get_material_indices(mesh: bpy.types.Mesh) -> np.ndarray:
617-
buffer = np.empty(len(mesh.polygons), dtype=np.int32)
617+
buffer = np.empty(len(mesh.polygons), dtype="I")
618618
mesh.polygons.foreach_get("material_index", buffer)
619619
return buffer
620620

0 commit comments

Comments
 (0)