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
This commit is contained in:
Andrej730
2025-02-12 15:47:49 +05:00
parent cb95539cb0
commit 299b5bcf6b
5 changed files with 45 additions and 49 deletions
+10 -16
View File
@@ -24,6 +24,7 @@ import logging
import traceback import traceback
import mathutils import mathutils
import numpy as np import numpy as np
import numpy.typing as npt
import multiprocessing import multiprocessing
import ifcopenshell import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
@@ -36,7 +37,7 @@ import ifcopenshell.util.shape
import bonsai.tool as tool import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any from typing import Dict, Union, Optional, Any, Literal
class MaterialCreator: class MaterialCreator:
@@ -167,7 +168,7 @@ class MaterialCreator:
if -1 in self.mesh["ios_material_ids"]: if -1 in self.mesh["ios_material_ids"]:
material_to_slot[-1] = get_empty_slot_index() material_to_slot[-1] = get_empty_slot_index()
material_index = [material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]] material_index = np.array([material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]], dtype="I")
self.mesh.polygons.foreach_set("material_index", material_index) self.mesh.polygons.foreach_set("material_index", material_index)
def resolve_all_stylable_representation_items( def resolve_all_stylable_representation_items(
@@ -1030,7 +1031,7 @@ class IfcImporter:
self, self,
element: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType], shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
cartesian_point_offset=None, cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> bpy.types.Mesh: ) -> bpy.types.Mesh:
try: try:
if hasattr(shape, "geometry"): if hasattr(shape, "geometry"):
@@ -1046,32 +1047,25 @@ class IfcImporter:
old_mesh.name = mesh_name + ".old" old_mesh.name = mesh_name + ".old"
mesh = bpy.data.meshes.new(mesh_name) mesh = bpy.data.meshes.new(mesh_name)
verts = ifcopenshell.util.shape.get_vertices(geometry)
if cartesian_point_offset is False: if cartesian_point_offset is False:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False mesh["has_cartesian_point_offset"] = False
elif cartesian_point_offset is not None: elif cartesian_point_offset is not None:
verts_array = np.array(geometry.verts) verts -= cartesian_point_offset
offset = np.array([-cartesian_point_offset[0], -cartesian_point_offset[1], -cartesian_point_offset[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
mesh["has_cartesian_point_offset"] = True mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = ( mesh["cartesian_point_offset"] = (
f"{cartesian_point_offset[0]},{cartesian_point_offset[1]},{cartesian_point_offset[2]}" f"{cartesian_point_offset[0]},{cartesian_point_offset[1]},{cartesian_point_offset[2]}"
) )
elif geometry.verts and tool.Loader.is_point_far_away( elif verts.size and tool.Loader.is_point_far_away(verts[0], is_meters=True):
(geometry.verts[0], geometry.verts[1], geometry.verts[2]), is_meters=True
):
# Shift geometry close to the origin based off that first vert it found # Shift geometry close to the origin based off that first vert it found
verts_array = np.array(geometry.verts) verts_array = np.array(geometry.verts)
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]]) offset = verts[0]
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3) verts -= offset
verts = offset_verts.tolist()
mesh["has_cartesian_point_offset"] = True mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = f"{geometry.verts[0]},{geometry.verts[1]},{geometry.verts[2]}" mesh["cartesian_point_offset"] = f"{offset[0]},{offset[1]},{offset[2]}"
else: else:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False mesh["has_cartesian_point_offset"] = False
return tool.Loader.convert_geometry_to_mesh( return tool.Loader.convert_geometry_to_mesh(
@@ -1846,7 +1846,7 @@ class LoadLinkedProject(bpy.types.Operator):
max_slot_index += 1 max_slot_index += 1
material_to_slot[i] = slot_index material_to_slot[i] = slot_index
material_index = [(material_to_slot[i] if i != -1 else 0) for i in material_ids] material_index = np.array([(material_to_slot[i] if i != -1 else 0) for i in material_ids], dtype="I")
num_vertices = len(verts) // 3 num_vertices = len(verts) // 3
total_faces = len(faces) total_faces = len(faces)
+3 -6
View File
@@ -35,6 +35,7 @@ import ifcopenshell.guid
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement import ifcopenshell.util.placement
import ifcopenshell.util.representation import ifcopenshell.util.representation
import ifcopenshell.util.shape
import ifcopenshell.util.shape_builder import ifcopenshell.util.shape_builder
import ifcopenshell.util.system import ifcopenshell.util.system
import ifcopenshell.util.unit import ifcopenshell.util.unit
@@ -1756,13 +1757,9 @@ class Geometry(bonsai.core.tool.Geometry):
obj.matrix_world = rep_obj.matrix_world @ position obj.matrix_world = rep_obj.matrix_world @ position
else: else:
geometry = tool.Loader.create_generic_shape(item) geometry = tool.Loader.create_generic_shape(item)
verts = ifcopenshell.util.shape.get_vertices(geometry)
if (cartesian_point_offset := cls.get_cartesian_point_offset(rep_obj)) is not None: if (cartesian_point_offset := cls.get_cartesian_point_offset(rep_obj)) is not None:
verts_array = np.array(geometry.verts) verts -= cartesian_point_offset
offset = np.array([-cartesian_point_offset[0], -cartesian_point_offset[1], -cartesian_point_offset[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
else:
verts = geometry.verts
tool.Loader.convert_geometry_to_mesh(geometry, obj.data, verts=verts) tool.Loader.convert_geometry_to_mesh(geometry, obj.data, verts=verts)
if ios_materials := list(obj.data["ios_materials"]): if ios_materials := list(obj.data["ios_materials"]):
+30 -25
View File
@@ -35,6 +35,7 @@ import bonsai.tool as tool
import bonsai.bim.import_ifc import bonsai.bim.import_ifc
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
from ifcopenshell.util.shape_builder import np_to_4d
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from pathlib import Path from pathlib import Path
from typing import Union, Any, Optional from typing import Union, Any, Optional
@@ -634,7 +635,7 @@ class Loader(bonsai.core.tool.Loader):
limit = cls.settings.distance_limit limit = cls.settings.distance_limit
limit = limit if is_meters else (limit / cls.unit_scale) limit = limit if is_meters else (limit / cls.unit_scale)
coords = getattr(point, "Coordinates", point) coords = getattr(point, "Coordinates", point)
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit return any(abs(c) > limit for c in coords)
@classmethod @classmethod
def is_element_far_away(cls, element: ifcopenshell.entity_instance) -> bool: def is_element_far_away(cls, element: ifcopenshell.entity_instance) -> bool:
@@ -879,11 +880,12 @@ class Loader(bonsai.core.tool.Loader):
if not shape: if not shape:
continue continue
mat = ifcopenshell.util.shape.get_shape_matrix(shape) mat = ifcopenshell.util.shape.get_shape_matrix(shape)
point = mat @ np.array((shape.geometry.verts[0], shape.geometry.verts[1], shape.geometry.verts[2], 1.0)) verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
point = (mat @ np_to_4d(verts[0]))[:3]
if cls.is_point_far_away(point, is_meters=True): if cls.is_point_far_away(point, is_meters=True):
# Arbitrary origins should be to the nearest millimeter. # Arbitrary origins should be to the nearest millimeter.
# Anything more precise is just ridiculous from a practical surveying perspective. # Anything more precise is just ridiculous from a practical surveying perspective.
return [round(float(p), 3) / cls.unit_scale for p in point[:3]] return np.array([round(float(p), 3) / cls.unit_scale for p in point])
break break
@classmethod @classmethod
@@ -960,14 +962,19 @@ class Loader(bonsai.core.tool.Loader):
cls, cls,
geometry: ifcopenshell.geom.ShapeType, geometry: ifcopenshell.geom.ShapeType,
mesh: bpy.types.Mesh, mesh: bpy.types.Mesh,
verts: Optional[list[float]] = None, verts: Optional[npt.NDArray[np.float64]] = None,
*, *,
load_indexed_maps=True, load_indexed_maps=True,
) -> bpy.types.Mesh: ) -> bpy.types.Mesh:
"""
:param verts: Numpy array of shape (n, 3).
"""
if verts is None: if verts is None:
verts = geometry.verts verts = ifcopenshell.util.shape.get_vertices(geometry)
if geometry.faces: faces = ifcopenshell.util.shape.get_faces(geometry)
num_vertices = len(verts) // 3 total_faces: int
if total_faces := faces.shape[0]:
num_vertices: int = verts.shape[0]
# See bug 3546 # See bug 3546
# ios_edges holds true edges that aren't triangulated. # ios_edges holds true edges that aren't triangulated.
@@ -978,34 +985,35 @@ class Loader(bonsai.core.tool.Loader):
mesh["ios_item_ids"] = ios_item_ids mesh["ios_item_ids"] = ios_item_ids
mesh.vertices.add(num_vertices) mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts) mesh.vertices.foreach_set("co", verts.ravel().astype("f"))
is_triangulated = True is_triangulated = True
num_vertex_indices = faces.size
if is_triangulated: if is_triangulated:
total_faces = len(geometry.faces) loop_start = np.arange(0, num_vertex_indices, 3, dtype="I")
num_vertex_indices = len(geometry.faces) loop_total = np.full(total_faces, 3, dtype="I")
loop_start = range(0, total_faces, 3) use_smooth = np.zeros(num_vertex_indices, dtype="?")
num_loops = total_faces // 3
loop_total = [3] * num_loops
mesh.loops.add(num_vertex_indices) mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry.faces) mesh.loops.foreach_set("vertex_index", faces.ravel().astype("I"))
mesh.polygons.add(num_loops) mesh.polygons.add(total_faces)
mesh.polygons.foreach_set("loop_start", loop_start) mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total) mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces) mesh.polygons.foreach_set("use_smooth", use_smooth)
else: else:
# TODO: optimize using correct numpy array types.
faces_array = np.array(geometry.faces, dtype=object) faces_array = np.array(geometry.faces, dtype=object)
loop_total = tuple(len(face) for face in faces_array) loop_total = np.array(tuple(len(face) for face in faces_array), dtype="I")
loop_start = np.cumsum((0,) + loop_total)[:-1] loop_start = np.cumsum((0,) + loop_total)[:-1]
vertex_index = np.concatenate(faces_array) vertex_index = np.concatenate(faces_array)
use_smooth = np.zeros(num_vertex_indices, dtype="?")
mesh.loops.add(len(vertex_index)) mesh.loops.add(len(vertex_index))
mesh.loops.foreach_set("vertex_index", vertex_index) mesh.loops.foreach_set("vertex_index", vertex_index)
mesh.polygons.add(len(loop_start)) mesh.polygons.add(len(loop_start))
mesh.polygons.foreach_set("loop_start", loop_start) mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total) mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * len(geometry.faces)) mesh.polygons.foreach_set("use_smooth", use_smooth)
mesh.update() mesh.update()
@@ -1020,11 +1028,8 @@ class Loader(bonsai.core.tool.Loader):
tool.Blender.Attribute.fill_attribute(mesh, "ios_item_ids", "FACE", "INT", ios_item_ids) tool.Blender.Attribute.fill_attribute(mesh, "ios_item_ids", "FACE", "INT", ios_item_ids)
tool.Blender.Attribute.fill_attribute(mesh, "ios_material_ids", "FACE", "INT", geometry.material_ids) tool.Blender.Attribute.fill_attribute(mesh, "ios_material_ids", "FACE", "INT", geometry.material_ids)
else: else:
e = geometry.edges edges = ifcopenshell.util.shape.get_edges(geometry)
v = verts mesh.from_pydata(verts.tolist(), edges.tolist(), [])
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
mesh.from_pydata(vertices, edges, [])
# TODO: remove error handling after we update build in Bonsai. # TODO: remove error handling after we update build in Bonsai.
try: try:
edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist() edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist()
@@ -1034,8 +1039,8 @@ class Loader(bonsai.core.tool.Loader):
tool.Blender.Attribute.fill_attribute(mesh, "ios_edges_item_ids", "EDGE", "INT", edges_item_ids) tool.Blender.Attribute.fill_attribute(mesh, "ios_edges_item_ids", "EDGE", "INT", edges_item_ids)
tool.Blender.Attribute.fill_attribute(mesh, "ios_material_ids", "EDGE", "INT", geometry.material_ids) tool.Blender.Attribute.fill_attribute(mesh, "ios_material_ids", "EDGE", "INT", geometry.material_ids)
mesh["ios_materials"] = [m.instance_id() for m in geometry.materials] mesh["ios_materials"] = [m.instance_id() for m in ifcopenshell.util.shape.get_shape_material_styles(geometry)]
mesh["ios_material_ids"] = geometry.material_ids mesh["ios_material_ids"] = ifcopenshell.util.shape.get_faces_material_style_ids(geometry).tolist()
return mesh return mesh
@classmethod @classmethod
+1 -1
View File
@@ -614,7 +614,7 @@ class TestApplyIfcMaterialChanges(NewFile):
tool.Geometry._reload_representation(obj) tool.Geometry._reload_representation(obj)
def get_material_indices(mesh: bpy.types.Mesh) -> np.ndarray: def get_material_indices(mesh: bpy.types.Mesh) -> np.ndarray:
buffer = np.empty(len(mesh.polygons), dtype=np.int32) buffer = np.empty(len(mesh.polygons), dtype="I")
mesh.polygons.foreach_get("material_index", buffer) mesh.polygons.foreach_get("material_index", buffer)
return buffer return buffer