From e5db24043403e33cf170e987425c949712841b56 Mon Sep 17 00:00:00 2001 From: Namit Bhutani Date: Fri, 4 Jul 2025 20:02:37 +0200 Subject: [PATCH] Mesh: Spatial Reordering for Sculpt Speed Improvements **Problem Description** Blender's current mesh data layout often lacks spatial coherence, causing performance bottlenecks during BVH construction for sculpting and painting operations. Each time a BVH is built, the system must recompute spatial partitioning and vertex groupings from scratch, leading to redundant calculations and suboptimal memory access patterns. **Proposed Solution** This patch implements pre-computed spatial organization of mesh data through a new `mesh_apply_spatial_organization()` function that: - Reorders vertices and faces based on spatial locality using recursive spatial partitioning. - Stores pre-computed MeshGroup hierarchies in MeshRuntime for reuse. - Enables the BVH system to bypass expensive spatial computation when pre-organized data is available. This approach separates the expensive spatial computation from more frequent BVH rebuilds, providing sustained performance improvements across multiple sculpting operations. **Limitations** - Requires manual invocation (occurs automatically only during remesh operations). - Additional memory overhead for storing MeshGroup metadata. - One-time computational cost during initial organization. - Spatial group references are not yet stored in files. **User Interface** The feature is accessible via a new "Reorder Mesh Spatially" operator in the Mesh Data Properties panel under the Geometry Data section. Users can invoke it manually when needed, or it will be applied automatically during quadriflow and voxel remesh operations. The operator provides feedback confirming successful spatial reordering. Pull Request: https://projects.blender.org/blender/blender/pulls/139536 --- scripts/startup/bl_ui/properties_data_mesh.py | 2 +- source/blender/blenkernel/BKE_mesh.hh | 1 + source/blender/blenkernel/BKE_mesh_types.hh | 25 ++ source/blender/blenkernel/BKE_paint_bvh.hh | 22 ++ source/blender/blenkernel/intern/mesh.cc | 363 ++++++++++++++++++ .../blender/blenkernel/intern/mesh_runtime.cc | 1 + source/blender/blenkernel/intern/pbvh.cc | 100 ++++- source/blender/editors/mesh/mesh_intern.hh | 1 + source/blender/editors/mesh/mesh_ops.cc | 1 + source/blender/editors/mesh/meshtools.cc | 58 +++ .../blender/editors/object/object_remesh.cc | 6 +- tests/performance/tests/sculpt.py | 61 ++- tests/python/bl_reorder.py | 69 ++++ 13 files changed, 694 insertions(+), 16 deletions(-) create mode 100644 tests/python/bl_reorder.py diff --git a/scripts/startup/bl_ui/properties_data_mesh.py b/scripts/startup/bl_ui/properties_data_mesh.py index 673f569f41a..f98d3ee62fb 100644 --- a/scripts/startup/bl_ui/properties_data_mesh.py +++ b/scripts/startup/bl_ui/properties_data_mesh.py @@ -421,7 +421,7 @@ class DATA_PT_customdata(MeshButtonsPanel, Panel): col.operator("mesh.customdata_mask_clear", icon='X') col.operator("mesh.customdata_skin_clear", icon='X') - + col.operator("mesh.reorder_vertices_spatial") if me.has_custom_normals: col.operator("mesh.customdata_custom_splitnormals_clear", icon='X') else: diff --git a/source/blender/blenkernel/BKE_mesh.hh b/source/blender/blenkernel/BKE_mesh.hh index 119166e94fe..cd838d032ca 100644 --- a/source/blender/blenkernel/BKE_mesh.hh +++ b/source/blender/blenkernel/BKE_mesh.hh @@ -425,6 +425,7 @@ void mesh_data_update(Depsgraph &depsgraph, /** Remove strings referring to attributes if they no longer exist. */ void mesh_remove_invalid_attribute_strings(Mesh &mesh); +void mesh_apply_spatial_organization(Mesh &mesh); const AttributeAccessorFunctions &mesh_attribute_accessor_functions(); } // namespace blender::bke diff --git a/source/blender/blenkernel/BKE_mesh_types.hh b/source/blender/blenkernel/BKE_mesh_types.hh index d733a0dfdaa..32d5ce5b620 100644 --- a/source/blender/blenkernel/BKE_mesh_types.hh +++ b/source/blender/blenkernel/BKE_mesh_types.hh @@ -129,6 +129,24 @@ struct TrianglesCache { void tag_dirty(); }; +struct MeshGroup { + /** Range of unique vertices in reordered mesh. */ + IndexRange unique_verts; + /** Range of all faces in reordered mesh. */ + IndexRange faces; + /** + * Indices of vertices that are shared with other groups in reordered mesh. + * This is empty if all vertices in the group are unique. + */ + Array shared_verts; + /** Parent node index (-1 for root). */ + int parent; + /** Children node offset (empty for leaf nodes). */ + int children_offset; + /** Number of corners in each group, calculated from number of faces. */ + int corners_count; +}; + struct MeshRuntime { /** * "Evaluated" mesh owned by this mesh. Used for objects which don't have effective modifiers, so @@ -193,6 +211,13 @@ struct MeshRuntime { /** Needed in case we need to lazily initialize the mesh. */ CustomData_MeshMasks cd_mask_extra = {}; + /** + * Pre-computed groups of vertices and faces for a mesh's BVH (Bounding Volume Hierarchy) nodes, + * used to quickly access the node data for the BVH. Used to avoid recomputing the offsets every + * time the BVH is built. + */ + std::unique_ptr> spatial_groups; + /** * Grids representation for multi-resolution sculpting. When this is set, the mesh data * corresponds to the unsubdivided base mesh; it is conceptually replaced with the limited diff --git a/source/blender/blenkernel/BKE_paint_bvh.hh b/source/blender/blenkernel/BKE_paint_bvh.hh index e88cb04fa8f..5a7ff5356d7 100644 --- a/source/blender/blenkernel/BKE_paint_bvh.hh +++ b/source/blender/blenkernel/BKE_paint_bvh.hh @@ -17,6 +17,7 @@ #include "BLI_bounds_types.hh" #include "BLI_function_ref.hh" #include "BLI_index_mask_fwd.hh" +#include "BLI_math_vector.hh" #include "BLI_math_vector_types.hh" #include "BLI_offset_indices.hh" #include "BLI_set.hh" @@ -329,6 +330,8 @@ class Tree { private: explicit Tree(Type type); + /** Build a BVH tree from pre-computed MeshGroup data. */ + static Tree from_spatially_organized_mesh(const Mesh &mesh); }; void build_pixels(const Depsgraph &depsgraph, Object &object, Image &image, ImageUser &image_user); @@ -344,6 +347,25 @@ void raycast(Tree &pbvh, const float3 &ray_normal, bool original); +inline Bounds calc_face_bounds(const Span vert_positions, + const Span face_verts) +{ + Bounds bounds{vert_positions[face_verts.first()]}; + for (const int vert : face_verts.slice(1, face_verts.size() - 1)) { + math::min_max(vert_positions[vert], bounds.min, bounds.max); + } + return bounds; +} + +int partition_along_axis(const Span face_centers, + MutableSpan faces, + const int axis, + const float middle); + +int partition_material_indices(const Span material_indices, MutableSpan faces); + +bool leaf_needs_material_split(const Span faces, const Span material_indices); + bool node_raycast_mesh(const MeshNode &node, Span node_positions, Span vert_positions, diff --git a/source/blender/blenkernel/intern/mesh.cc b/source/blender/blenkernel/intern/mesh.cc index b7cffeec047..2b22119da4a 100644 --- a/source/blender/blenkernel/intern/mesh.cc +++ b/source/blender/blenkernel/intern/mesh.cc @@ -20,6 +20,7 @@ #include "DNA_meshdata_types.h" #include "DNA_object_types.h" +#include "BLI_array_utils.hh" #include "BLI_bounds.hh" #include "BLI_hash.h" #include "BLI_implicit_sharing.hh" @@ -45,6 +46,7 @@ #include "BKE_anonymous_attribute_id.hh" #include "BKE_attribute.hh" #include "BKE_attribute_legacy_convert.hh" +#include "BKE_attribute_math.hh" #include "BKE_attribute_storage.hh" #include "BKE_attribute_storage_blend_write.hh" #include "BKE_bake_data_block_id.hh" @@ -66,12 +68,16 @@ #include "BKE_modifier.hh" #include "BKE_multires.hh" #include "BKE_object.hh" +#include "BKE_paint_bvh.hh" #include "DEG_depsgraph.hh" #include "DEG_depsgraph_query.hh" #include "BLO_read_write.hh" +/** Using STACK_FIXED_DEPTH to keep the implementation in line with pbvh.cc.*/ +#define STACK_FIXED_DEPTH 100 + using blender::float3; using blender::int2; using blender::MutableSpan; @@ -601,6 +607,363 @@ void mesh_remove_invalid_attribute_strings(Mesh &mesh) } } +static Bounds merge_bounds(const Bounds &a, const Bounds &b) +{ + return bounds::merge(a, b); +} + +static Bounds negative_bounds() +{ + return {float3(std::numeric_limits::max()), float3(std::numeric_limits::lowest())}; +} + +struct NonContiguousGroup { + Array unique_verts; + Array faces; + Array shared_verts; + int corner_count; + int parent; + int children_offset; +}; + +static void partition_faces_recursively(const Span face_centers, + MutableSpan face_indices, + Vector &groups, + int node_index, + int depth, + const std::optional> &bounds_precalc, + const Span material_indices, + int target_group_size) +{ + if (face_indices.size() <= target_group_size || depth >= STACK_FIXED_DEPTH - 1) { + if (!blender::bke::pbvh::leaf_needs_material_split(face_indices, material_indices)) { + groups[node_index].children_offset = 0; + groups[node_index].faces = Array(face_indices.size(), NoInitialization()); + std::copy(face_indices.begin(), face_indices.end(), groups[node_index].faces.begin()); + return; + } + } + + const int children_start = groups.size(); + groups[node_index].children_offset = children_start; + + groups.resize(groups.size() + 2); + groups[children_start].parent = node_index; + groups[children_start + 1].parent = node_index; + + int split; + if (!(face_indices.size() <= target_group_size || depth >= STACK_FIXED_DEPTH - 1)) { + Bounds bounds; + if (bounds_precalc) { + bounds = *bounds_precalc; + } + else { + bounds = threading::parallel_reduce( + face_indices.index_range(), + 1024, + negative_bounds(), + [&](const IndexRange range, Bounds value) { + for (const int face : face_indices.slice(range)) { + math::min_max(face_centers[face], value.min, value.max); + } + return value; + }, + merge_bounds); + } + const int axis = math::dominant_axis(bounds.max - bounds.min); + + split = blender::bke::pbvh::partition_along_axis( + face_centers, face_indices, axis, math::midpoint(bounds.min[axis], bounds.max[axis])); + } + else { + split = blender::bke::pbvh::partition_material_indices(material_indices, face_indices); + } + + partition_faces_recursively(face_centers, + face_indices.take_front(split), + groups, + children_start, + depth + 1, + std::nullopt, + material_indices, + target_group_size); + partition_faces_recursively(face_centers, + face_indices.drop_front(split), + groups, + children_start + 1, + depth + 1, + std::nullopt, + material_indices, + target_group_size); +} + +static void build_vertex_groups_for_leaves(const int verts_num, + const OffsetIndices faces, + const Span corner_verts, + Vector &groups) +{ + Vector leaf_indices; + for (const int i : groups.index_range()) { + if (groups[i].children_offset == 0 && !groups[i].faces.is_empty()) { + leaf_indices.append(i); + } + } + + Array> verts_per_leaf(leaf_indices.size(), NoInitialization()); + + threading::parallel_for(leaf_indices.index_range(), 8, [&](const IndexRange range) { + Set verts; + for (const int i : range) { + const int group_idx = leaf_indices[i]; + NonContiguousGroup &group = groups[group_idx]; + verts.clear(); + int corners_count = 0; + + for (const int face_index : group.faces) { + const IndexRange face = faces[face_index]; + verts.add_multiple(corner_verts.slice(face)); + corners_count += face.size(); + } + + new (&verts_per_leaf[i]) Array(verts.size()); + std::copy(verts.begin(), verts.end(), verts_per_leaf[i].begin()); + std::sort(verts_per_leaf[i].begin(), verts_per_leaf[i].end()); + group.corner_count = corners_count; + } + }); + + Vector owned_verts; + Vector shared_verts; + BitVector<> vert_used(verts_num); + + for (const int i : leaf_indices.index_range()) { + const int group_idx = leaf_indices[i]; + NonContiguousGroup &group = groups[group_idx]; + owned_verts.clear(); + shared_verts.clear(); + + for (const int vert : verts_per_leaf[i]) { + if (vert_used[vert]) { + shared_verts.append(vert); + } + else { + vert_used[vert].set(); + owned_verts.append(vert); + } + } + + if (!owned_verts.is_empty()) { + group.unique_verts = Array(owned_verts.size()); + std::copy(owned_verts.begin(), owned_verts.end(), group.unique_verts.begin()); + } + + if (!shared_verts.is_empty()) { + group.shared_verts = Array(shared_verts.size()); + std::copy(shared_verts.begin(), shared_verts.end(), group.shared_verts.begin()); + } + } +} + +static Vector compute_local_mesh_groups(Mesh &mesh) +{ + const Span vert_positions = mesh.vert_positions(); + const OffsetIndices faces = mesh.faces(); + const Span corner_verts = mesh.corner_verts(); + + if (faces.is_empty()) { + return {}; + } + + Array face_centers(faces.size()); + const Bounds bounds = threading::parallel_reduce( + faces.index_range(), + 1024, + negative_bounds(), + [&](const IndexRange range, const Bounds &init) { + Bounds current = init; + for (const int face : range) { + const Bounds bounds = blender::bke::pbvh::calc_face_bounds( + vert_positions, corner_verts.slice(faces[face])); + face_centers[face] = bounds.center(); + current = bounds::merge(current, bounds); + } + return current; + }, + merge_bounds); + + Array prim_face_indices(mesh.faces_num); + array_utils::fill_index_range(prim_face_indices); + + Vector groups; + groups.resize(1); + groups[0].parent = -1; + groups[0].children_offset = 0; + + const AttributeAccessor attributes = mesh.attributes(); + const VArraySpan material_index = *attributes.lookup("material_index", AttrDomain::Face); + + partition_faces_recursively( + face_centers, prim_face_indices, groups, 0, 0, bounds, material_index, 2500); + + build_vertex_groups_for_leaves(mesh.verts_num, faces, corner_verts, groups); + + return groups; +} + +void mesh_apply_spatial_organization(Mesh &mesh) +{ + Vector local_groups = compute_local_mesh_groups(mesh); + + Vector new_vert_order; + new_vert_order.reserve(mesh.verts_num); + + Vector new_face_order; + new_face_order.reserve(mesh.faces_num); + + BitVector<> added_verts(mesh.verts_num, false); + + Vector group_unique_offsets; + group_unique_offsets.reserve(local_groups.size() + 1); + group_unique_offsets.append(0); + + Vector group_face_offsets; + group_face_offsets.reserve(local_groups.size() + 1); + group_face_offsets.append(0); + + for (const int group_index : local_groups.index_range()) { + const NonContiguousGroup &local_group = local_groups[group_index]; + + for (const int vert_idx : local_group.unique_verts) { + if (!added_verts[vert_idx]) { + new_vert_order.append(vert_idx); + added_verts[vert_idx].set(); + } + } + group_unique_offsets.append(new_vert_order.size()); + + for (const int vert_idx : local_group.shared_verts) { + if (!added_verts[vert_idx]) { + new_vert_order.append(vert_idx); + added_verts[vert_idx].set(); + } + } + + for (const int face_idx : local_group.faces) { + new_face_order.append(face_idx); + } + group_face_offsets.append(new_face_order.size()); + } + + Array vert_reverse_map(mesh.verts_num); + for (const int i : IndexRange(mesh.verts_num)) { + vert_reverse_map[new_vert_order[i]] = i; + } + + MutableSpan edges = mesh.edges_for_write(); + for (int2 &edge : edges) { + edge.x = vert_reverse_map[edge.x]; + edge.y = vert_reverse_map[edge.y]; + } + + MutableSpan corner_verts = mesh.corner_verts_for_write(); + Array new_corner_verts(corner_verts.size()); + const OffsetIndices old_faces = mesh.faces(); + + int new_corner_idx = 0; + for (const int old_face_idx : new_face_order) { + const IndexRange face = old_faces[old_face_idx]; + for (const int corner : face) { + new_corner_verts[new_corner_idx] = vert_reverse_map[corner_verts[corner]]; + new_corner_idx++; + } + } + corner_verts.copy_from(new_corner_verts); + + MutableSpan face_offsets = mesh.face_offsets_for_write(); + MutableSpan face_sizes_view = face_offsets.take_front(new_face_order.size()); + gather_group_sizes(old_faces, new_face_order, face_sizes_view); + offset_indices::accumulate_counts_to_offsets(face_offsets); + + MutableAttributeAccessor attributes_for_write = mesh.attributes_for_write(); + attributes_for_write.foreach_attribute([&](const bke::AttributeIter &iter) { + if (iter.domain == bke::AttrDomain::Face) { + bke::GSpanAttributeWriter attribute = attributes_for_write.lookup_for_write_span(iter.name); + const CPPType &type = attribute.span.type(); + GArray<> new_values(type, new_face_order.size()); + bke::attribute_math::gather(attribute.span, new_face_order, new_values.as_mutable_span()); + attribute.span.copy_from(new_values.as_span()); + attribute.finish(); + } + else if (iter.domain == bke::AttrDomain::Point) { + bke::GSpanAttributeWriter attribute = attributes_for_write.lookup_for_write_span(iter.name); + const CPPType &type = attribute.span.type(); + GArray<> new_values(type, new_vert_order.size()); + bke::attribute_math::gather(attribute.span, new_vert_order, new_values.as_mutable_span()); + attribute.span.copy_from(new_values.as_span()); + attribute.finish(); + } + else if (iter.domain == bke::AttrDomain::Corner && iter.name != ".corner_vert") { + bke::GSpanAttributeWriter attribute = attributes_for_write.lookup_for_write_span(iter.name); + GMutableSpan attribute_data = attribute.span; + const CPPType &type = attribute_data.type(); + GArray<> new_values(type, attribute_data.size()); + + int new_corner_idx = 0; + for (const int old_face_idx : new_face_order) { + const IndexRange face = old_faces[old_face_idx]; + for (const int old_corner_idx : face) { + type.copy_construct(attribute_data[old_corner_idx], new_values[new_corner_idx]); + new_corner_idx++; + } + } + attribute_data.copy_from(new_values.as_span()); + attribute.finish(); + } + }); + + for (NonContiguousGroup &local_group : local_groups) { + for (int &vert_idx : local_group.unique_verts) { + vert_idx = vert_reverse_map[vert_idx]; + } + for (int &vert_idx : local_group.shared_verts) { + vert_idx = vert_reverse_map[vert_idx]; + } + } + + Array nodes(local_groups.size()); + + for (const int node_idx : local_groups.index_range()) { + const NonContiguousGroup &local_group = local_groups[node_idx]; + MeshGroup &node = nodes[node_idx]; + + node.parent = local_group.parent; + node.children_offset = local_group.children_offset; + node.corners_count = local_group.corner_count; + node.unique_verts = IndexRange(0, 0); + node.faces = IndexRange(0, 0); + if (local_group.children_offset == 0 && !local_group.faces.is_empty()) { + int unique_start = (node_idx == 0) ? 0 : group_unique_offsets[node_idx]; + int unique_end = group_unique_offsets[node_idx + 1]; + node.unique_verts = IndexRange(unique_start, unique_end - unique_start); + + int face_start = (node_idx == 0) ? 0 : group_face_offsets[node_idx]; + int face_end = group_face_offsets[node_idx + 1]; + node.faces = IndexRange(face_start, face_end - face_start); + + if (!local_group.shared_verts.is_empty()) { + node.shared_verts = Array(local_group.shared_verts.size()); + for (const int j : local_group.shared_verts.index_range()) { + node.shared_verts[j] = local_group.shared_verts[j]; + } + } + } + } + + mesh.tag_positions_changed(); + mesh.tag_topology_changed(); + mesh.runtime->spatial_groups = std::make_unique>(std::move(nodes)); +} + } // namespace blender::bke /** diff --git a/source/blender/blenkernel/intern/mesh_runtime.cc b/source/blender/blenkernel/intern/mesh_runtime.cc index 4778c929148..fe6c9638ab5 100644 --- a/source/blender/blenkernel/intern/mesh_runtime.cc +++ b/source/blender/blenkernel/intern/mesh_runtime.cc @@ -334,6 +334,7 @@ void BKE_mesh_runtime_clear_geometry(Mesh *mesh) mesh->runtime->max_material_index.tag_dirty(); mesh->runtime->subsurf_face_dot_tags.clear_and_shrink(); mesh->runtime->subsurf_optimal_display_edges.clear_and_shrink(); + mesh->runtime->spatial_groups.reset(); mesh->flag &= ~ME_NO_OVERLAPPING_TOPOLOGY; } diff --git a/source/blender/blenkernel/intern/pbvh.cc b/source/blender/blenkernel/intern/pbvh.cc index 408f26c7f92..e21f1e18480 100644 --- a/source/blender/blenkernel/intern/pbvh.cc +++ b/source/blender/blenkernel/intern/pbvh.cc @@ -58,10 +58,10 @@ static Bounds merge_bounds(const Bounds &a, const Bounds return bounds::merge(a, b); } -static int partition_along_axis(const Span face_centers, - MutableSpan faces, - const int axis, - const float middle) +int partition_along_axis(const Span face_centers, + MutableSpan faces, + const int axis, + const float middle) { const int *split = std::partition(faces.begin(), faces.end(), [&](const int face) { return face_centers[face][axis] >= middle; @@ -69,7 +69,7 @@ static int partition_along_axis(const Span face_centers, return split - faces.begin(); } -static int partition_material_indices(const Span material_indices, MutableSpan faces) +int partition_material_indices(const Span material_indices, MutableSpan faces) { const int first = material_indices[faces.first()]; const int *split = std::partition( @@ -130,7 +130,7 @@ BLI_NOINLINE static void build_mesh_leaf_nodes(const int verts_num, } } -static bool leaf_needs_material_split(const Span faces, const Span material_indices) +bool leaf_needs_material_split(const Span faces, const Span material_indices) { if (material_indices.is_empty()) { return false; @@ -220,14 +220,87 @@ static void build_nodes_recursive_mesh(const Span material_indices, nodes); } -inline Bounds calc_face_bounds(const Span vert_positions, - const Span face_verts) +Tree Tree::from_spatially_organized_mesh(const Mesh &mesh) { - Bounds bounds{vert_positions[face_verts.first()]}; - for (const int vert : face_verts.slice(1, face_verts.size() - 1)) { - math::min_max(vert_positions[vert], bounds.min, bounds.max); +#ifdef DEBUG_BUILD_TIME + SCOPED_TIMER_AVERAGED(__func__); +#endif + + Tree pbvh(Type::Mesh); + const Span vert_positions = mesh.vert_positions(); + + const Span &spatial_groups = *mesh.runtime->spatial_groups; + + if (spatial_groups.is_empty()) { + return pbvh; } - return bounds; + + Vector &nodes = std::get>(pbvh.nodes_); + nodes.resize(spatial_groups.size()); + + pbvh.prim_indices_.reinitialize(mesh.faces_num); + array_utils::fill_index_range(pbvh.prim_indices_); + + threading::parallel_for(nodes.index_range(), 8, [&](const IndexRange range) { + for (const int node_idx : range) { + MeshNode &pbvh_node = nodes[node_idx]; + pbvh_node.parent_ = spatial_groups[node_idx].parent; + + if (spatial_groups[node_idx].children_offset != 0) { + pbvh_node.children_offset_ = spatial_groups[node_idx].children_offset; + } + else { + pbvh_node.children_offset_ = 0; + pbvh_node.flag_ = Node::Leaf; + + const IndexRange face_range = spatial_groups[node_idx].faces; + const int face_count = face_range.size(); + + if (face_count > 0) { + pbvh_node.face_indices_ = Span(&pbvh.prim_indices_[face_range.start()], face_count); + + pbvh_node.corners_num_ = spatial_groups[node_idx].corners_count; + + pbvh_node.unique_verts_num_ = spatial_groups[node_idx].unique_verts.size(); + + pbvh_node.vert_indices_.reserve(spatial_groups[node_idx].unique_verts.size() + + spatial_groups[node_idx].shared_verts.size()); + + for (const int i : spatial_groups[node_idx].unique_verts.index_range()) { + const int vert_idx = spatial_groups[node_idx].unique_verts.start() + i; + pbvh_node.vert_indices_.add(vert_idx); + } + + for (const int vert_idx : spatial_groups[node_idx].shared_verts) { + pbvh_node.vert_indices_.add(vert_idx); + } + } + else { + pbvh_node.unique_verts_num_ = 0; + pbvh_node.corners_num_ = 0; + } + } + } + }); + + pbvh.tag_positions_changed(nodes.index_range()); + pbvh.update_bounds_mesh(vert_positions); + store_bounds_orig(pbvh); + + const AttributeAccessor attributes = mesh.attributes(); + const VArraySpan hide_vert = *attributes.lookup(".hide_vert", AttrDomain::Point); + + if (!hide_vert.is_empty()) { + threading::parallel_for(nodes.index_range(), 8, [&](const IndexRange range) { + for (const int i : range) { + node_update_visibility_mesh(hide_vert, nodes[i]); + } + }); + } + + update_mask_mesh(mesh, nodes.index_range(), pbvh); + + return pbvh; } Tree Tree::from_mesh(const Mesh &mesh) @@ -235,6 +308,9 @@ Tree Tree::from_mesh(const Mesh &mesh) #ifdef DEBUG_BUILD_TIME SCOPED_TIMER_AVERAGED(__func__); #endif + if (mesh.runtime->spatial_groups) { + return from_spatially_organized_mesh(mesh); + } Tree pbvh(Type::Mesh); const Span vert_positions = mesh.vert_positions(); const OffsetIndices faces = mesh.faces(); diff --git a/source/blender/editors/mesh/mesh_intern.hh b/source/blender/editors/mesh/mesh_intern.hh index a8b9dc7787e..a7a2b5c9d9f 100644 --- a/source/blender/editors/mesh/mesh_intern.hh +++ b/source/blender/editors/mesh/mesh_intern.hh @@ -321,3 +321,4 @@ void MESH_OT_customdata_skin_add(wmOperatorType *ot); void MESH_OT_customdata_skin_clear(wmOperatorType *ot); void MESH_OT_customdata_custom_splitnormals_add(wmOperatorType *ot); void MESH_OT_customdata_custom_splitnormals_clear(wmOperatorType *ot); +void MESH_OT_reorder_vertices_spatial(wmOperatorType *ot); diff --git a/source/blender/editors/mesh/mesh_ops.cc b/source/blender/editors/mesh/mesh_ops.cc index 261739fdcbd..ed72edac217 100644 --- a/source/blender/editors/mesh/mesh_ops.cc +++ b/source/blender/editors/mesh/mesh_ops.cc @@ -191,6 +191,7 @@ void ED_operatortypes_mesh() WM_operatortype_append(MESH_OT_smooth_normals); WM_operatortype_append(MESH_OT_mod_weighted_strength); WM_operatortype_append(MESH_OT_flip_quad_tessellation); + WM_operatortype_append(MESH_OT_reorder_vertices_spatial); } #if 0 /* UNUSED, remove? */ diff --git a/source/blender/editors/mesh/meshtools.cc b/source/blender/editors/mesh/meshtools.cc index 84c2862dde2..bc0cc6333c1 100644 --- a/source/blender/editors/mesh/meshtools.cc +++ b/source/blender/editors/mesh/meshtools.cc @@ -44,6 +44,8 @@ #include "BKE_multires.hh" #include "BKE_object.hh" #include "BKE_object_deform.h" +#include "BKE_paint.hh" +#include "BKE_paint_bvh.hh" #include "BKE_report.hh" #include "DEG_depsgraph.hh" @@ -54,10 +56,12 @@ #include "ED_mesh.hh" #include "ED_object.hh" +#include "ED_sculpt.hh" #include "ED_view3d.hh" #include "WM_api.hh" #include "WM_types.hh" +#include "mesh_intern.hh" using blender::float3; using blender::int2; @@ -1544,3 +1548,57 @@ void EDBM_mesh_elem_index_ensure_multi(const Span objects, const char BM_mesh_elem_index_ensure_ex(bm, htype, elem_offset); } } +static wmOperatorStatus mesh_reorder_vertices_spatial_exec(bContext *C, wmOperator *op) +{ + Object *ob = blender::ed::object::context_active_object(C); + + Mesh *mesh = static_cast(ob->data); + Scene *scene = CTX_data_scene(C); + + if (ob->mode == OB_MODE_SCULPT && mesh->flag & ME_SCULPT_DYNAMIC_TOPOLOGY) { + /* Dyntopo not supported. */ + BKE_report(op->reports, RPT_INFO, "Not supported in dynamic topology sculpting"); + return OPERATOR_CANCELLED; + } + + if (ob->mode == OB_MODE_SCULPT) { + blender::ed::sculpt_paint::undo::geometry_begin(*scene, *ob, op); + } + + blender::bke::mesh_apply_spatial_organization(*mesh); + + if (ob->mode == OB_MODE_SCULPT) { + blender::ed::sculpt_paint::undo::geometry_end(*ob); + } + + DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_OBJECT | ND_MODIFIER, ob); + + BKE_report(op->reports, RPT_INFO, "Mesh faces and vertices reordered spatially"); + + return OPERATOR_FINISHED; +} + +static bool mesh_reorder_vertices_spatial_poll(bContext *C) +{ + Object *ob = blender::ed::object::context_active_object(C); + if (!ob || ob->type != OB_MESH) { + return false; + } + + return true; +} + +void MESH_OT_reorder_vertices_spatial(wmOperatorType *ot) +{ + ot->name = "Reorder Mesh Spatially"; + ot->idname = "MESH_OT_reorder_vertices_spatial"; + ot->description = + "Reorder mesh faces and vertices based on their spatial position for better BVH building " + "and sculpting performance."; + + ot->exec = mesh_reorder_vertices_spatial_exec; + ot->poll = mesh_reorder_vertices_spatial_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; +} diff --git a/source/blender/editors/object/object_remesh.cc b/source/blender/editors/object/object_remesh.cc index d8d12d92124..514a5752143 100644 --- a/source/blender/editors/object/object_remesh.cc +++ b/source/blender/editors/object/object_remesh.cc @@ -165,7 +165,8 @@ static wmOperatorStatus voxel_remesh_exec(bContext *C, wmOperator *op) sculpt_paint::undo::geometry_end(*ob); BKE_sculptsession_free_pbvh(*ob); } - + /** Spatially organize the mesh after remesh.*/ + blender::bke::mesh_apply_spatial_organization(*static_cast(ob->data)); BKE_mesh_batch_cache_dirty_tag(static_cast(ob->data), BKE_MESH_BATCH_DIRTY_ALL); DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY); WM_event_add_notifier(C, NC_GEOM | ND_DATA, ob->data); @@ -914,7 +915,8 @@ static void quadriflow_start_job(void *customdata, wmJobWorkerStatus *worker_sta sculpt_paint::undo::geometry_end(*ob); BKE_sculptsession_free_pbvh(*ob); } - + /** Spatially organize the mesh after remesh.*/ + blender::bke::mesh_apply_spatial_organization(*static_cast(ob->data)); BKE_mesh_batch_cache_dirty_tag(static_cast(ob->data), BKE_MESH_BATCH_DIRTY_ALL); worker_status->do_update = true; diff --git a/tests/performance/tests/sculpt.py b/tests/performance/tests/sculpt.py index 7f1e337a417..5829ea5f93e 100644 --- a/tests/performance/tests/sculpt.py +++ b/tests/performance/tests/sculpt.py @@ -176,6 +176,8 @@ def _run_brush_test(args: dict): context_override = context.copy() set_view3d_context_override(context_override) with context.temp_override(**context_override): + if args.get('spatial_reorder', False): + bpy.ops.mesh.reorder_vertices_spatial() start = time.time() bpy.ops.sculpt.brush_stroke(stroke=generate_stroke(context_override), override_location=True) measurements.append(time.time() - start) @@ -208,6 +210,8 @@ def _run_bvh_test(args: dict): context_override = context.copy() set_view3d_context_override(context_override) with context.temp_override(**context_override): + if args.get('spatial_reorder', False): + bpy.ops.mesh.reorder_vertices_spatial() start = time.time() bpy.ops.sculpt.optimize() measurements.append(time.time() - start) @@ -268,6 +272,31 @@ class SculptBrushTest(api.Test): args = { 'mode': self.mode, 'brush_type': self.brush_type, + 'spatial_reorder': False, + } + + result, _ = env.run_in_blender(_run_brush_test, args, [self.filepath]) + + return {'time': result} + + +class SculptBrushAfterSpatialReorderingTest(api.Test): + def __init__(self, filepath: pathlib.Path, mode: SculptMode, brush_type: BrushType): + self.filepath = filepath + self.mode = mode + self.brush_type = brush_type + + def name(self): + return "{}_{}_{}".format(self.mode.name.lower(), self.brush_type.name.lower(), "after_reordering") + + def category(self): + return "sculpt" + + def run(self, env, _device_id): + args = { + 'mode': self.mode, + 'brush_type': self.brush_type, + 'spatial_reorder': True, } result, _ = env.run_in_blender(_run_brush_test, args, [self.filepath]) @@ -289,6 +318,29 @@ class SculptRebuildBVHTest(api.Test): def run(self, env, _device_id): args = { 'mode': self.mode, + 'spatial_reorder': False, + } + + result, _ = env.run_in_blender(_run_bvh_test, args, [self.filepath]) + + return {'time': result} + + +class SculptRebuildSpatialBVHTest(api.Test): + def __init__(self, filepath: pathlib.Path, mode: SculptMode): + self.filepath = filepath + self.mode = mode + + def name(self): + return "{}_spatial_rebuild_bvh".format(self.mode.name.lower()) + + def category(self): + return "sculpt" + + def run(self, env, _device_id): + args = { + 'mode': self.mode, + 'spatial_reorder': True, } result, _ = env.run_in_blender(_run_bvh_test, args, [self.filepath]) @@ -316,7 +368,14 @@ def generate(env): filepaths = env.find_blend_files('sculpt/*') # For now, we only expect there to ever be a single file to use as the basis for generating other brush tests assert len(filepaths) == 1 + brush_tests = [SculptBrushTest(filepaths[0], mode, brush_type) for mode in SculptMode for brush_type in BrushType] + brush_tests_after_reordering = [ + SculptBrushAfterSpatialReorderingTest( + filepaths[0], + SculptMode.MESH, + brush_type)for brush_type in BrushType] bvh_tests = [SculptRebuildBVHTest(filepaths[0], mode) for mode in SculptMode] + spatial_bvh_tests = [SculptRebuildSpatialBVHTest(filepaths[0], SculptMode.MESH)] subdivision_tests = [SculptMultiresSubdivideTest(filepaths[0])] - return brush_tests + bvh_tests + subdivision_tests + return brush_tests + brush_tests_after_reordering + bvh_tests + spatial_bvh_tests + subdivision_tests diff --git a/tests/python/bl_reorder.py b/tests/python/bl_reorder.py new file mode 100644 index 00000000000..2a9340a3724 --- /dev/null +++ b/tests/python/bl_reorder.py @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: 2022 Blender Authors +# +# SPDX-License-Identifier: Apache-2.0 + +# ./blender.bin --background --python tests/python/bl_pyapi_text.py -- --verbose +import bpy +import unittest + + +class TestMeshSpatialOrganization(unittest.TestCase): + + def setUp(self): + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + if bpy.context.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + def tearDown(self): + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + + def create_subdivided_plane(self, subdivisions): + bpy.ops.mesh.primitive_plane_add(size=2, location=(0, 0, 0)) + plane = bpy.context.active_object + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.subdivide(number_cuts=subdivisions, smoothness=0.0) + bpy.ops.object.mode_set(mode='OBJECT') + return plane + + def get_vertex_data(self, obj): + mesh = obj.data + vertices = [(v.co.x, v.co.y, v.co.z) for v in mesh.vertices] + return { + 'vertices': vertices, + 'vertex_count': len(vertices) + } + + def create_reference_mesh(self, source_obj): + """Create a reference copy of the mesh for comparison""" + bpy.context.view_layer.objects.active = source_obj + source_obj.select_set(True) + bpy.ops.object.duplicate() + reference_obj = bpy.context.active_object + reference_obj.name = "reference_mesh" + return reference_obj + + def test_spatial_organization_changes_vertex_order(self): + plane = self.create_subdivided_plane(subdivisions=50) + initial_data = self.get_vertex_data(plane) + bpy.ops.mesh.reorder_vertices_spatial() + final_data = self.get_vertex_data(plane) + self.assertEqual(initial_data['vertex_count'], final_data['vertex_count']) + vertices_changed = initial_data['vertices'] != final_data['vertices'] + self.assertTrue(vertices_changed) + + def test_spatial_organization_preserves_topology(self): + plane = self.create_subdivided_plane(subdivisions=50) + reference_plane = self.create_reference_mesh(plane) + bpy.ops.mesh.reorder_vertices_spatial() + + comparison_result = plane.data.unit_test_compare(mesh=reference_plane.data) + self.assertEqual(comparison_result, "The geometries are the same up to a change of indices") + + +if __name__ == '__main__': + import sys + sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []) + unittest.main()