mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-28 03:05:33 +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 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)
|
||||||
|
|||||||
@@ -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"]):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user