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 mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
@@ -36,7 +37,7 @@ import ifcopenshell.util.shape
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_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:
@@ -167,7 +168,7 @@ class MaterialCreator:
if -1 in self.mesh["ios_material_ids"]:
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)
def resolve_all_stylable_representation_items(
@@ -1030,7 +1031,7 @@ class IfcImporter:
self,
element: ifcopenshell.entity_instance,
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:
try:
if hasattr(shape, "geometry"):
@@ -1046,32 +1047,25 @@ class IfcImporter:
old_mesh.name = mesh_name + ".old"
mesh = bpy.data.meshes.new(mesh_name)
verts = ifcopenshell.util.shape.get_vertices(geometry)
if cartesian_point_offset is False:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
elif cartesian_point_offset is not None:
verts_array = np.array(geometry.verts)
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()
verts -= cartesian_point_offset
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = (
f"{cartesian_point_offset[0]},{cartesian_point_offset[1]},{cartesian_point_offset[2]}"
)
elif geometry.verts and tool.Loader.is_point_far_away(
(geometry.verts[0], geometry.verts[1], geometry.verts[2]), is_meters=True
):
elif verts.size and tool.Loader.is_point_far_away(verts[0], is_meters=True):
# Shift geometry close to the origin based off that first vert it found
verts_array = np.array(geometry.verts)
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
offset = verts[0]
verts -= offset
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:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
return tool.Loader.convert_geometry_to_mesh(
@@ -1846,7 +1846,7 @@ class LoadLinkedProject(bpy.types.Operator):
max_slot_index += 1
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
total_faces = len(faces)
+3 -6
View File
@@ -35,6 +35,7 @@ import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import ifcopenshell.util.shape_builder
import ifcopenshell.util.system
import ifcopenshell.util.unit
@@ -1756,13 +1757,9 @@ class Geometry(bonsai.core.tool.Geometry):
obj.matrix_world = rep_obj.matrix_world @ position
else:
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:
verts_array = np.array(geometry.verts)
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
verts -= cartesian_point_offset
tool.Loader.convert_geometry_to_mesh(geometry, obj.data, verts=verts)
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 numpy as np
import numpy.typing as npt
from ifcopenshell.util.shape_builder import np_to_4d
from mathutils import Vector, Matrix
from pathlib import Path
from typing import Union, Any, Optional
@@ -634,7 +635,7 @@ class Loader(bonsai.core.tool.Loader):
limit = cls.settings.distance_limit
limit = limit if is_meters else (limit / cls.unit_scale)
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
def is_element_far_away(cls, element: ifcopenshell.entity_instance) -> bool:
@@ -879,11 +880,12 @@ class Loader(bonsai.core.tool.Loader):
if not shape:
continue
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):
# Arbitrary origins should be to the nearest millimeter.
# 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
@classmethod
@@ -960,14 +962,19 @@ class Loader(bonsai.core.tool.Loader):
cls,
geometry: ifcopenshell.geom.ShapeType,
mesh: bpy.types.Mesh,
verts: Optional[list[float]] = None,
verts: Optional[npt.NDArray[np.float64]] = None,
*,
load_indexed_maps=True,
) -> bpy.types.Mesh:
"""
:param verts: Numpy array of shape (n, 3).
"""
if verts is None:
verts = geometry.verts
if geometry.faces:
num_vertices = len(verts) // 3
verts = ifcopenshell.util.shape.get_vertices(geometry)
faces = ifcopenshell.util.shape.get_faces(geometry)
total_faces: int
if total_faces := faces.shape[0]:
num_vertices: int = verts.shape[0]
# See bug 3546
# 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.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.vertices.foreach_set("co", verts.ravel().astype("f"))
is_triangulated = True
num_vertex_indices = faces.size
if is_triangulated:
total_faces = len(geometry.faces)
num_vertex_indices = len(geometry.faces)
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
loop_start = np.arange(0, num_vertex_indices, 3, dtype="I")
loop_total = np.full(total_faces, 3, dtype="I")
use_smooth = np.zeros(num_vertex_indices, dtype="?")
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry.faces)
mesh.polygons.add(num_loops)
mesh.loops.foreach_set("vertex_index", faces.ravel().astype("I"))
mesh.polygons.add(total_faces)
mesh.polygons.foreach_set("loop_start", loop_start)
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:
# TODO: optimize using correct numpy array types.
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]
vertex_index = np.concatenate(faces_array)
use_smooth = np.zeros(num_vertex_indices, dtype="?")
mesh.loops.add(len(vertex_index))
mesh.loops.foreach_set("vertex_index", vertex_index)
mesh.polygons.add(len(loop_start))
mesh.polygons.foreach_set("loop_start", loop_start)
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()
@@ -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_material_ids", "FACE", "INT", geometry.material_ids)
else:
e = geometry.edges
v = verts
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, [])
edges = ifcopenshell.util.shape.get_edges(geometry)
mesh.from_pydata(verts.tolist(), edges.tolist(), [])
# TODO: remove error handling after we update build in Bonsai.
try:
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_material_ids", "EDGE", "INT", geometry.material_ids)
mesh["ios_materials"] = [m.instance_id() for m in geometry.materials]
mesh["ios_material_ids"] = geometry.material_ids
mesh["ios_materials"] = [m.instance_id() for m in ifcopenshell.util.shape.get_shape_material_styles(geometry)]
mesh["ios_material_ids"] = ifcopenshell.util.shape.get_faces_material_style_ids(geometry).tolist()
return mesh
@classmethod
+1 -1
View File
@@ -614,7 +614,7 @@ class TestApplyIfcMaterialChanges(NewFile):
tool.Geometry._reload_representation(obj)
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)
return buffer