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
This commit is contained in:
Namit Bhutani
2025-07-04 20:02:37 +02:00
committed by Hans Goudey
parent a60ec16858
commit e5db240434
13 changed files with 694 additions and 16 deletions

View File

@@ -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