diff --git a/source/blender/editors/sculpt_paint/CMakeLists.txt b/source/blender/editors/sculpt_paint/CMakeLists.txt index a48234feeb0..55ad3df86f7 100644 --- a/source/blender/editors/sculpt_paint/CMakeLists.txt +++ b/source/blender/editors/sculpt_paint/CMakeLists.txt @@ -130,6 +130,7 @@ set(SRC brushes/draw_sharp.cc brushes/draw.cc brushes/elastic_deform.cc + brushes/enhance_details.cc brushes/fill.cc brushes/flatten.cc brushes/grab.cc diff --git a/source/blender/editors/sculpt_paint/brushes/enhance_details.cc b/source/blender/editors/sculpt_paint/brushes/enhance_details.cc new file mode 100644 index 00000000000..52222f587b2 --- /dev/null +++ b/source/blender/editors/sculpt_paint/brushes/enhance_details.cc @@ -0,0 +1,380 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "editors/sculpt_paint/brushes/types.hh" + +#include "DNA_brush_types.h" +#include "DNA_mesh_types.h" +#include "DNA_object_types.h" +#include "DNA_scene_types.h" + +#include "BKE_key.hh" +#include "BKE_mesh.hh" +#include "BKE_paint.hh" +#include "BKE_pbvh.hh" +#include "BKE_subdiv_ccg.hh" + +#include "BLI_array.hh" +#include "BLI_array_utils.hh" +#include "BLI_enumerable_thread_specific.hh" +#include "BLI_math_vector.hh" +#include "BLI_task.hh" + +#include "editors/sculpt_paint/mesh_brush_common.hh" +#include "editors/sculpt_paint/sculpt_intern.hh" + +namespace blender::ed::sculpt_paint { + +inline namespace enhance_details_cc { + +struct LocalData { + Vector positions; + Vector new_positions; + Vector factors; + Vector distances; + Vector> vert_neighbors; + Vector translations; +}; + +static void calc_faces(const Sculpt &sd, + const Brush &brush, + const Span positions_eval, + const Span vert_normals, + const Span all_translations, + const float strength, + const PBVHNode &node, + Object &object, + LocalData &tls, + const MutableSpan positions_orig) +{ + SculptSession &ss = *object.sculpt; + const StrokeCache &cache = *ss.cache; + Mesh &mesh = *static_cast(object.data); + + const Span verts = bke::pbvh::node_unique_verts(node); + + tls.factors.reinitialize(verts.size()); + const MutableSpan factors = tls.factors; + fill_factor_from_hide_and_mask(mesh, verts, factors); + filter_region_clip_factors(ss, positions_eval, verts, factors); + if (brush.flag & BRUSH_FRONTFACE) { + calc_front_face(cache.view_normal, vert_normals, verts, factors); + } + + tls.distances.reinitialize(verts.size()); + const MutableSpan distances = tls.distances; + calc_brush_distances( + ss, positions_eval, verts, eBrushFalloffShape(brush.falloff_shape), distances); + filter_distances_with_radius(cache.radius, distances, factors); + apply_hardness_to_distances(cache, distances); + calc_brush_strength_factors(cache, brush, distances, factors); + + if (cache.automasking) { + auto_mask::calc_vert_factors(object, *cache.automasking, node, verts, factors); + } + + calc_brush_texture_factors(ss, brush, positions_eval, verts, factors); + + scale_factors(factors, strength); + + tls.translations.reinitialize(verts.size()); + const MutableSpan translations = tls.translations; + array_utils::gather(all_translations, verts, translations); + scale_translations(translations, factors); + + write_translations(sd, object, positions_eval, verts, translations, positions_orig); +} + +static void calc_grids(const Sculpt &sd, + Object &object, + const Brush &brush, + const Span all_translations, + const float strength, + const PBVHNode &node, + LocalData &tls) +{ + SculptSession &ss = *object.sculpt; + const StrokeCache &cache = *ss.cache; + SubdivCCG &subdiv_ccg = *ss.subdiv_ccg; + const CCGKey key = BKE_subdiv_ccg_key_top_level(subdiv_ccg); + + const Span grids = bke::pbvh::node_grid_indices(node); + const int grid_verts_num = grids.size() * key.grid_area; + + tls.positions.reinitialize(grid_verts_num); + const MutableSpan positions = tls.positions; + gather_grids_positions(subdiv_ccg, grids, positions); + + tls.factors.reinitialize(grid_verts_num); + const MutableSpan factors = tls.factors; + fill_factor_from_hide_and_mask(subdiv_ccg, grids, factors); + filter_region_clip_factors(ss, positions, factors); + if (brush.flag & BRUSH_FRONTFACE) { + calc_front_face(cache.view_normal, subdiv_ccg, grids, factors); + } + + tls.distances.reinitialize(grid_verts_num); + const MutableSpan distances = tls.distances; + calc_brush_distances(ss, positions, eBrushFalloffShape(brush.falloff_shape), distances); + filter_distances_with_radius(cache.radius, distances, factors); + apply_hardness_to_distances(cache, distances); + calc_brush_strength_factors(cache, brush, distances, factors); + + if (cache.automasking) { + auto_mask::calc_grids_factors(object, *cache.automasking, node, grids, factors); + } + + calc_brush_texture_factors(ss, brush, positions, factors); + + scale_factors(factors, strength); + + tls.translations.reinitialize(grid_verts_num); + const MutableSpan translations = tls.translations; + gather_data_grids(subdiv_ccg, all_translations, grids, translations); + scale_translations(translations, factors); + + clip_and_lock_translations(sd, ss, positions, translations); + apply_translations(translations, grids, subdiv_ccg); +} + +static void calc_bmesh(const Sculpt &sd, + Object &object, + const Brush &brush, + const Span all_translations, + const float strength, + PBVHNode &node, + LocalData &tls) +{ + SculptSession &ss = *object.sculpt; + const StrokeCache &cache = *ss.cache; + + const Set &verts = BKE_pbvh_bmesh_node_unique_verts(&node); + + tls.positions.reinitialize(verts.size()); + const MutableSpan positions = tls.positions; + gather_bmesh_positions(verts, positions); + + tls.factors.reinitialize(verts.size()); + const MutableSpan factors = tls.factors; + fill_factor_from_hide_and_mask(*ss.bm, verts, factors); + filter_region_clip_factors(ss, positions, factors); + if (brush.flag & BRUSH_FRONTFACE) { + calc_front_face(cache.view_normal, verts, factors); + } + + tls.distances.reinitialize(verts.size()); + const MutableSpan distances = tls.distances; + calc_brush_distances(ss, positions, eBrushFalloffShape(brush.falloff_shape), distances); + filter_distances_with_radius(cache.radius, distances, factors); + apply_hardness_to_distances(cache, distances); + calc_brush_strength_factors(cache, brush, distances, factors); + + if (cache.automasking) { + auto_mask::calc_vert_factors(object, *cache.automasking, node, verts, factors); + } + + calc_brush_texture_factors(ss, brush, positions, factors); + + scale_factors(factors, strength); + + tls.translations.reinitialize(verts.size()); + const MutableSpan translations = tls.translations; + gather_data_vert_bmesh(all_translations, verts, translations); + scale_translations(translations, factors); + + clip_and_lock_translations(sd, ss, positions, translations); + apply_translations(translations, verts); +} + +static void calc_translations_faces(const Span vert_positions, + const OffsetIndices faces, + const Span corner_verts, + const GroupedSpan vert_to_face_map, + const PBVHNode &node, + LocalData &tls, + const MutableSpan all_translations) +{ + const Span verts = bke::pbvh::node_unique_verts(node); + + tls.vert_neighbors.reinitialize(verts.size()); + const MutableSpan> neighbors = tls.vert_neighbors; + calc_vert_neighbors(faces, corner_verts, vert_to_face_map, {}, verts, neighbors); + + tls.new_positions.reinitialize(verts.size()); + const MutableSpan new_positions = tls.new_positions; + smooth::neighbor_position_average_mesh(vert_positions, verts, neighbors, new_positions); + + tls.translations.reinitialize(verts.size()); + const MutableSpan translations = tls.translations; + translations_from_new_positions(new_positions, verts, vert_positions, translations); + array_utils::scatter(translations.as_span(), verts, all_translations); +} + +static void calc_translations_grids(const SubdivCCG &subdiv_ccg, + const PBVHNode &node, + LocalData &tls, + const MutableSpan all_translations) +{ + const CCGKey key = BKE_subdiv_ccg_key_top_level(subdiv_ccg); + + const Span grids = bke::pbvh::node_grid_indices(node); + const int grid_verts_num = grids.size() * key.grid_area; + + tls.positions.reinitialize(grid_verts_num); + const MutableSpan positions = tls.positions; + gather_grids_positions(subdiv_ccg, grids, positions); + + tls.new_positions.reinitialize(grid_verts_num); + const MutableSpan new_positions = tls.new_positions; + smooth::neighbor_position_average_grids(subdiv_ccg, grids, new_positions); + + tls.translations.reinitialize(grid_verts_num); + const MutableSpan translations = tls.translations; + translations_from_new_positions(new_positions, positions, translations); + scatter_data_grids(subdiv_ccg, translations, grids, all_translations); +} + +static void calc_translations_bmesh(PBVHNode &node, + LocalData &tls, + const MutableSpan all_translations) +{ + const Set &verts = BKE_pbvh_bmesh_node_unique_verts(&node); + + tls.positions.reinitialize(verts.size()); + const MutableSpan positions = tls.positions; + gather_bmesh_positions(verts, positions); + + tls.new_positions.reinitialize(verts.size()); + const MutableSpan new_positions = tls.new_positions; + smooth::neighbor_position_average_bmesh(verts, new_positions); + + tls.translations.reinitialize(verts.size()); + const MutableSpan translations = tls.translations; + translations_from_new_positions(new_positions, positions, translations); + scatter_data_vert_bmesh(translations.as_span(), verts, all_translations); +} + +/** + * The brush uses translations calculated at the beginning of the stroke. They can't be calculated + * dynamically because changing positions will influence neighboring translations. However we can + * reduce the cost in some cases by skipping initializing values for vertices in hidden or masked + * nodes. + */ +static void precalc_translations(Object &object, const MutableSpan translations) +{ + SculptSession &ss = *object.sculpt; + PBVH &pbvh = *ss.pbvh; + + Vector effective_nodes = bke::pbvh::search_gather( + pbvh, [&](PBVHNode &node) { return !node_fully_masked_or_hidden(node); }); + + threading::EnumerableThreadSpecific all_tls; + switch (BKE_pbvh_type(pbvh)) { + case PBVH_FACES: { + Mesh &mesh = *static_cast(object.data); + const Span positions_eval = BKE_pbvh_get_vert_positions(pbvh); + const OffsetIndices faces = mesh.faces(); + const Span corner_verts = mesh.corner_verts(); + threading::parallel_for(effective_nodes.index_range(), 1, [&](const IndexRange range) { + LocalData &tls = all_tls.local(); + threading::isolate_task([&]() { + for (const int i : range) { + calc_translations_faces(positions_eval, + faces, + corner_verts, + ss.vert_to_face_map, + *effective_nodes[i], + tls, + translations); + } + }); + }); + break; + } + case PBVH_GRIDS: { + SubdivCCG &subdiv_ccg = *ss.subdiv_ccg; + threading::parallel_for(effective_nodes.index_range(), 1, [&](const IndexRange range) { + LocalData &tls = all_tls.local(); + for (const int i : range) { + calc_translations_grids(subdiv_ccg, *effective_nodes[i], tls, translations); + } + }); + break; + } + case PBVH_BMESH: + BM_mesh_elem_index_ensure(ss.bm, BM_VERT); + BM_mesh_elem_table_ensure(ss.bm, BM_VERT); + threading::parallel_for(effective_nodes.index_range(), 1, [&](const IndexRange range) { + LocalData &tls = all_tls.local(); + for (const int i : range) { + calc_translations_bmesh(*effective_nodes[i], tls, translations); + } + }); + break; + } +} + +} // namespace enhance_details_cc + +void do_enhance_details_brush(const Sculpt &sd, Object &object, Span nodes) +{ + SculptSession &ss = *object.sculpt; + const Brush &brush = *BKE_paint_brush_for_read(&sd.paint); + PBVH &pbvh = *ss.pbvh; + + if (SCULPT_stroke_is_first_brush_step(*ss.cache)) { + ss.cache->detail_directions.reinitialize(SCULPT_vertex_count_get(ss)); + precalc_translations(object, ss.cache->detail_directions); + } + + const float strength = std::clamp(ss.cache->bstrength, -1.0f, 1.0f); + MutableSpan translations = ss.cache->detail_directions; + + threading::EnumerableThreadSpecific all_tls; + switch (BKE_pbvh_type(pbvh)) { + case PBVH_FACES: { + Mesh &mesh = *static_cast(object.data); + const Span positions_eval = BKE_pbvh_get_vert_positions(pbvh); + const Span vert_normals = BKE_pbvh_get_vert_normals(pbvh); + MutableSpan positions_orig = mesh.vert_positions_for_write(); + threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) { + LocalData &tls = all_tls.local(); + threading::isolate_task([&]() { + for (const int i : range) { + calc_faces(sd, + brush, + positions_eval, + vert_normals, + translations, + strength, + *nodes[i], + object, + tls, + positions_orig); + BKE_pbvh_node_mark_positions_update(nodes[i]); + } + }); + }); + break; + } + case PBVH_GRIDS: + threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) { + LocalData &tls = all_tls.local(); + for (const int i : range) { + calc_grids(sd, object, brush, translations, strength, *nodes[i], tls); + } + }); + break; + case PBVH_BMESH: + threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) { + LocalData &tls = all_tls.local(); + for (const int i : range) { + calc_bmesh(sd, object, brush, translations, strength, *nodes[i], tls); + } + }); + break; + } +} + +} // namespace blender::ed::sculpt_paint diff --git a/source/blender/editors/sculpt_paint/brushes/types.hh b/source/blender/editors/sculpt_paint/brushes/types.hh index 8d243849aea..a1da458be10 100644 --- a/source/blender/editors/sculpt_paint/brushes/types.hh +++ b/source/blender/editors/sculpt_paint/brushes/types.hh @@ -31,6 +31,7 @@ void do_draw_brush(const Sculpt &sd, Object &object, Span nodes); void do_draw_vector_displacement_brush(const Sculpt &sd, Object &object, Span nodes); void do_draw_sharp_brush(const Sculpt &sd, Object &object, Span nodes); void do_elastic_deform_brush(const Sculpt &sd, Object &object, Span nodes); +void do_enhance_details_brush(const Sculpt &sd, Object &object, Span nodes); void do_fill_brush(const Sculpt &sd, Object &object, Span nodes); void do_flatten_brush(const Sculpt &sd, Object &ob, Span nodes); void do_grab_brush(const Sculpt &sd, Object &ob, Span nodes); diff --git a/source/blender/editors/sculpt_paint/mesh_brush_common.hh b/source/blender/editors/sculpt_paint/mesh_brush_common.hh index f2643052c9d..aff4ba75e0a 100644 --- a/source/blender/editors/sculpt_paint/mesh_brush_common.hh +++ b/source/blender/editors/sculpt_paint/mesh_brush_common.hh @@ -96,6 +96,24 @@ void gather_grids_normals(const SubdivCCG &subdiv_ccg, MutableSpan normals); void gather_bmesh_normals(const Set &verts, MutableSpan normals); +/** Gather data from an array aligned with all geometry vertices. */ +void gather_data_grids(const SubdivCCG &subdiv_ccg, + Span src, + Span grids, + MutableSpan node_data); +void gather_data_vert_bmesh(Span src, + const Set &verts, + MutableSpan node_data); + +/** Scatter data from an array of the node's data to the referenced geometry vertices. */ +void scatter_data_grids(const SubdivCCG &subdiv_ccg, + Span node_data, + Span grids, + MutableSpan dst); +void scatter_data_vert_bmesh(Span node_data, + const Set &verts, + MutableSpan dst); + /** * Calculate initial influence factors based on vertex visibility. */ diff --git a/source/blender/editors/sculpt_paint/sculpt.cc b/source/blender/editors/sculpt_paint/sculpt.cc index 47ecce08753..d405101ed0e 100644 --- a/source/blender/editors/sculpt_paint/sculpt.cc +++ b/source/blender/editors/sculpt_paint/sculpt.cc @@ -3938,7 +3938,7 @@ static void do_brush_action(const Scene &scene, * enhance brush in the middle of the stroke. */ if (ss.cache->bstrength < 0.0f) { /* Invert mode, intensify details. */ - smooth::enhance_details_brush(sd, ob, nodes); + do_enhance_details_brush(sd, ob, nodes); } else { do_smooth_brush(sd, ob, nodes, std::clamp(ss.cache->bstrength, 0.0f, 1.0f)); @@ -6724,6 +6724,62 @@ void gather_bmesh_normals(const Set &verts, const MutableSpan src, + const Span grids, + const MutableSpan node_data) +{ + const CCGKey key = BKE_subdiv_ccg_key_top_level(subdiv_ccg); + BLI_assert(grids.size() * key.grid_area == node_data.size()); + + for (const int i : grids.index_range()) { + const int node_start = i * key.grid_area; + const int grids_start = grids[i] * key.grid_area; + node_data.slice(node_start, key.grid_area).copy_from(src.slice(grids_start, key.grid_area)); + } +} + +void gather_data_vert_bmesh(const Span src, + const Set &verts, + const MutableSpan node_data) +{ + BLI_assert(verts.size() == node_data.size()); + + int i = 0; + for (const BMVert *vert : verts) { + node_data[i] = src[BM_elem_index_get(vert)]; + i++; + } +} + +void scatter_data_grids(const SubdivCCG &subdiv_ccg, + const Span node_data, + const Span grids, + const MutableSpan dst) +{ + const CCGKey key = BKE_subdiv_ccg_key_top_level(subdiv_ccg); + BLI_assert(grids.size() * key.grid_area == node_data.size()); + + for (const int i : grids.index_range()) { + const int node_start = i * key.grid_area; + const int grids_start = grids[i] * key.grid_area; + dst.slice(grids_start, key.grid_area).copy_from(node_data.slice(node_start, key.grid_area)); + } +} + +void scatter_data_vert_bmesh(const Span node_data, + const Set &verts, + const MutableSpan dst) +{ + BLI_assert(verts.size() == node_data.size()); + + int i = 0; + for (const BMVert *vert : verts) { + dst[BM_elem_index_get(vert)] = node_data[i]; + i++; + } +} + void fill_factor_from_hide(const Mesh &mesh, const Span verts, const MutableSpan r_factors) diff --git a/source/blender/editors/sculpt_paint/sculpt_intern.hh b/source/blender/editors/sculpt_paint/sculpt_intern.hh index 5b6be2b5b65..7425c4cc51c 100644 --- a/source/blender/editors/sculpt_paint/sculpt_intern.hh +++ b/source/blender/editors/sculpt_paint/sculpt_intern.hh @@ -1565,6 +1565,9 @@ float4 neighbor_color_average(SculptSession &ss, */ float3 neighbor_coords_average_interior(const SculptSession &ss, PBVHVertRef vertex); +void neighbor_position_average_grids(const SubdivCCG &subdiv_ccg, + Span grids, + MutableSpan new_positions); void neighbor_position_average_interior_grids(OffsetIndices faces, Span corner_verts, BitSpan boundary_verts, @@ -1572,6 +1575,8 @@ void neighbor_position_average_interior_grids(OffsetIndices faces, Span grids, MutableSpan new_positions); +void neighbor_position_average_bmesh(const Set &verts, + MutableSpan new_positions); void neighbor_position_average_interior_bmesh(const Set &verts, MutableSpan new_positions); @@ -1580,7 +1585,6 @@ void neighbor_position_average_mesh(Span positions, Span> vert_neighbors, MutableSpan new_positions); -void enhance_details_brush(const Sculpt &sd, Object &ob, Span nodes); /* Surface Smooth Brush. */ diff --git a/source/blender/editors/sculpt_paint/sculpt_smooth.cc b/source/blender/editors/sculpt_paint/sculpt_smooth.cc index a44a8ed5de6..6f79feee581 100644 --- a/source/blender/editors/sculpt_paint/sculpt_smooth.cc +++ b/source/blender/editors/sculpt_paint/sculpt_smooth.cc @@ -67,6 +67,40 @@ static float3 average_positions(const CCGKey &key, return result; } +void neighbor_position_average_grids(const SubdivCCG &subdiv_ccg, + const Span grids, + const MutableSpan new_positions) +{ + const CCGKey key = BKE_subdiv_ccg_key_top_level(subdiv_ccg); + const Span elems = subdiv_ccg.grids; + + BLI_assert(grids.size() * key.grid_area == new_positions.size()); + + for (const int i : grids.index_range()) { + const int grid = grids[i]; + const int node_verts_start = i * key.grid_area; + + /* TODO: This loop could be optimized in the future by skipping unnecessary logic for + * non-boundary grid vertices. */ + for (const int y : IndexRange(key.grid_size)) { + for (const int x : IndexRange(key.grid_size)) { + const int offset = CCG_grid_xy_to_index(key.grid_size, x, y); + const int node_vert_index = node_verts_start + offset; + + SubdivCCGCoord coord{}; + coord.grid_index = grid; + coord.x = x; + coord.y = y; + + SubdivCCGNeighbors neighbors; + BKE_subdiv_ccg_neighbor_coords_get(subdiv_ccg, coord, false, neighbors); + + new_positions[node_vert_index] = average_positions(key, elems, neighbors.coords); + } + } + } +} + void neighbor_position_average_interior_grids(const OffsetIndices faces, const Span corner_verts, const BitSpan boundary_verts, @@ -136,6 +170,21 @@ static float3 average_positions(const Span verts) return result; } +void neighbor_position_average_bmesh(const Set &verts, + const MutableSpan new_positions) +{ + BLI_assert(verts.size() == new_positions.size()); + Vector neighbor_data; + + int i = 0; + for (BMVert *vert : verts) { + neighbor_data.clear(); + const Span neighbors = vert_neighbors_get_bmesh(*vert, neighbor_data); + new_positions[i] = average_positions(neighbors); + i++; + } +} + void neighbor_position_average_interior_bmesh(const Set &verts, const MutableSpan new_positions) { @@ -326,77 +375,6 @@ float4 neighbor_color_average(SculptSession &ss, faces, corner_verts, vert_to_face_map, color_attribute, color_domain, vert); } -static void do_enhance_details_brush_task(Object &ob, - const Sculpt &sd, - const Brush &brush, - PBVHNode *node) -{ - SculptSession &ss = *ob.sculpt; - - PBVHVertexIter vd; - - float bstrength = ss.cache->bstrength; - CLAMP(bstrength, -1.0f, 1.0f); - - SculptBrushTest test; - SculptBrushTestFn sculpt_brush_test_sq_fn = SCULPT_brush_test_init_with_falloff_shape( - ss, test, brush.falloff_shape); - - const int thread_id = BLI_task_parallel_thread_id(nullptr); - auto_mask::NodeData automask_data = auto_mask::node_begin( - ob, ss.cache->automasking.get(), *node); - - BKE_pbvh_vertex_iter_begin (*ss.pbvh, node, vd, PBVH_ITER_UNIQUE) { - if (!sculpt_brush_test_sq_fn(test, vd.co)) { - continue; - } - - auto_mask::node_update(automask_data, vd); - - const float fade = bstrength * SCULPT_brush_strength_factor(ss, - brush, - vd.co, - sqrtf(test.dist), - vd.no, - vd.fno, - vd.mask, - vd.vertex, - thread_id, - &automask_data); - - float disp[3]; - madd_v3_v3v3fl(disp, vd.co, ss.cache->detail_directions[vd.index], fade); - SCULPT_clip(sd, ss, vd.co, disp); - } - BKE_pbvh_vertex_iter_end; -} - -void enhance_details_brush(const Sculpt &sd, Object &ob, Span nodes) -{ - SculptSession &ss = *ob.sculpt; - const Brush &brush = *BKE_paint_brush_for_read(&sd.paint); - - SCULPT_vertex_random_access_ensure(ss); - SCULPT_boundary_info_ensure(ob); - - if (SCULPT_stroke_is_first_brush_step(*ss.cache)) { - const int totvert = SCULPT_vertex_count_get(ss); - ss.cache->detail_directions.reinitialize(totvert); - - for (int i = 0; i < totvert; i++) { - PBVHVertRef vertex = BKE_pbvh_index_to_vertex(*ss.pbvh, i); - const float3 avg = neighbor_coords_average(ss, vertex); - sub_v3_v3v3(ss.cache->detail_directions[i], avg, SCULPT_vertex_co_get(ss, vertex)); - } - } - - threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) { - for (const int i : range) { - do_enhance_details_brush_task(ob, sd, brush, nodes[i]); - } - }); -} - /* HC Smooth Algorithm. */ /* From: Improved Laplacian Smoothing of Noisy Surface Meshes */