mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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"]):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user