From 5fdf7deb57e809dea2d7aeff3a9036bca0d5aa51 Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Thu, 22 Aug 2024 21:00:25 +0200 Subject: [PATCH] Sculpt: Remove duplicate BVH tree positions storage When there is a deform modifier or shape keys, currently the evaluated position array is copied to a duplicate array on the sculpt PBVH tree. Historically most code has used this array directly, so the code has been a bit confusing, but conceptually this is just supposed to be the evaluated positions. Instead of editing the BVH tree position array while sculpting, we can edit the arrays on the evaluated mesh directly-- or the original mesh when there are no deform modifiers. Removing this array reduces memory usage and plays better with implicit sharing since we don't need to unshare the attribute just to store a mutable copy. The remaining non-ideal part is sculpt mode's use of a specialized crazy-space function `BKE_crazyspace_build_sculpt` for building the evaluated position array rather than using the general system for retrieving deformed mesh data, `BKE_object_get_mesh_deform_eval`. This should be replaced at some point. This change makes clear a few simplifications for sculpt normals recomputation, because we can store the normals for all cases as a shared cache, unifying the code paths for original and deformed normals. Part of #118145. Pull Request: https://projects.blender.org/blender/blender/pulls/126382 --- source/blender/blenkernel/BKE_paint.hh | 8 + source/blender/blenkernel/BKE_pbvh_api.hh | 22 +- source/blender/blenkernel/intern/paint.cc | 20 +- source/blender/blenkernel/intern/pbvh.cc | 264 +++++++++++------- .../editors/sculpt_paint/mesh_brush_common.hh | 11 - source/blender/editors/sculpt_paint/sculpt.cc | 21 +- 6 files changed, 183 insertions(+), 163 deletions(-) diff --git a/source/blender/blenkernel/BKE_paint.hh b/source/blender/blenkernel/BKE_paint.hh index b05529da27f..ab7e9a5f80a 100644 --- a/source/blender/blenkernel/BKE_paint.hh +++ b/source/blender/blenkernel/BKE_paint.hh @@ -18,6 +18,7 @@ #include "BLI_offset_indices.hh" #include "BLI_ordered_edge.hh" #include "BLI_set.hh" +#include "BLI_shared_cache.hh" #include "BLI_utility_mixins.hh" #include "DNA_brush_enums.h" @@ -470,6 +471,13 @@ struct SculptSession : blender::NonCopyable, blender::NonMovable { /* Crazy-space deformation matrices. */ blender::Array deform_imats; + /** + * Normals corresponding to the #deform_cos evaluated/deform positions. Stored as a #SharedCache + * for consistency with mesh caches in #MeshRuntime::vert_normals_cache. + */ + blender::SharedCache> vert_normals_deform; + blender::SharedCache> face_normals_deform; + /* Pool for texture evaluations. */ ImagePool *tex_pool = nullptr; diff --git a/source/blender/blenkernel/BKE_pbvh_api.hh b/source/blender/blenkernel/BKE_pbvh_api.hh index 0d660fbd434..5846fb0526f 100644 --- a/source/blender/blenkernel/BKE_pbvh_api.hh +++ b/source/blender/blenkernel/BKE_pbvh_api.hh @@ -183,23 +183,9 @@ class Tree { /* Memory backing for Node.prim_indices. */ Array prim_indices_; - /** Local array used when not sculpting base mesh positions directly. */ - Array vert_positions_deformed_; - /** Local array used when not sculpting base mesh positions directly. */ - Array vert_normals_deformed_; - /** Local array used when not sculpting base mesh positions directly. */ - Array face_normals_deformed_; - - MutableSpan vert_positions_; - Span vert_normals_; - Span face_normals_; - /* Grid Data */ SubdivCCG *subdiv_ccg_ = nullptr; - /* flag are verts/faces deformed */ - bool deformed_ = false; - float planes_[6][4]; int num_planes_; @@ -261,7 +247,6 @@ namespace blender::bke::pbvh { * Do a full rebuild with on Mesh data structure. */ std::unique_ptr build_mesh(Mesh *mesh); -void update_mesh_pointers(Tree &pbvh, Mesh *mesh); /** * Do a full rebuild with on Grids data structure. */ @@ -515,7 +500,6 @@ void BKE_pbvh_subdiv_cgg_set(blender::bke::pbvh::Tree &pbvh, SubdivCCG *subdiv_c void BKE_pbvh_vert_coords_apply(blender::bke::pbvh::Tree &pbvh, blender::Span vert_positions); -bool BKE_pbvh_is_deformed(const blender::bke::pbvh::Tree &pbvh); void BKE_pbvh_node_get_bm_orco_data(blender::bke::pbvh::Node *node, int (**r_orco_tris)[3], @@ -530,7 +514,7 @@ namespace blender::bke::pbvh { * topology-changing operations. If there are no deform modifiers, this returns the original mesh's * vertex positions. */ -Span vert_positions_eval(const Depsgraph &depsgraph, const Object &object); +Span vert_positions_eval(const Depsgraph &depsgraph, const Object &object_orig); Span vert_positions_eval_from_eval(const Object &object_eval); /** @@ -539,13 +523,13 @@ Span vert_positions_eval_from_eval(const Object &object_eval); * they are used for drawing and we don't run a full dependency graph update whenever they are * changed. */ -MutableSpan vert_positions_eval_for_write(const Depsgraph &depsgraph, Object &object); +MutableSpan vert_positions_eval_for_write(const Depsgraph &depsgraph, Object &object_orig); /** * Return the vertex normals corresponding the the positions from #vert_positions_eval. This may be * a reference to the normals cache on the original mesh. */ -Span vert_normals_eval(const Depsgraph &depsgraph, const Object &object); +Span vert_normals_eval(const Depsgraph &depsgraph, const Object &object_orig); Span vert_normals_eval_from_eval(const Object &object_eval); } // namespace blender::bke::pbvh diff --git a/source/blender/blenkernel/intern/paint.cc b/source/blender/blenkernel/intern/paint.cc index 0dee816f51c..c58b90b4ebe 100644 --- a/source/blender/blenkernel/intern/paint.cc +++ b/source/blender/blenkernel/intern/paint.cc @@ -2052,16 +2052,12 @@ static void sculpt_update_object(Depsgraph *depsgraph, /* if pbvh is deformed, key block is already applied to it */ if (ss.shapekey_active) { - bool pbvh_deformed = BKE_pbvh_is_deformed(*ss.pbvh); - if (!pbvh_deformed || ss.deform_cos.is_empty()) { + if (ss.deform_cos.is_empty()) { const Span key_data(static_cast(ss.shapekey_active->data), mesh_orig->verts_num); if (key_data.data() != nullptr) { - if (!pbvh_deformed) { - /* apply shape keys coordinates to pbvh::Tree */ - BKE_pbvh_vert_coords_apply(*ss.pbvh, key_data); - } + BKE_pbvh_vert_coords_apply(*ss.pbvh, key_data); if (ss.deform_cos.is_empty()) { ss.deform_cos = key_data; } @@ -2390,18 +2386,6 @@ blender::bke::pbvh::Tree *BKE_sculpt_object_pbvh_ensure(Depsgraph *depsgraph, Ob } if (ob->sculpt->pbvh) { - /* NOTE: It is possible that pointers to grids or other geometry data changed. Need to update - * those pointers. */ - const pbvh::Type pbvh_type = ob->sculpt->pbvh->type(); - switch (pbvh_type) { - case pbvh::Type::Mesh: - pbvh::update_mesh_pointers(*ob->sculpt->pbvh, BKE_object_get_original_mesh(ob)); - break; - case pbvh::Type::Grids: - case pbvh::Type::BMesh: - break; - } - return ob->sculpt->pbvh.get(); } diff --git a/source/blender/blenkernel/intern/pbvh.cc b/source/blender/blenkernel/intern/pbvh.cc index 2c31c196a2a..2b72df8fcad 100644 --- a/source/blender/blenkernel/intern/pbvh.cc +++ b/source/blender/blenkernel/intern/pbvh.cc @@ -35,6 +35,7 @@ #include "BKE_ccg.hh" #include "BKE_mesh.hh" #include "BKE_mesh_mapping.hh" +#include "BKE_object.hh" #include "BKE_paint.hh" #include "BKE_pbvh_api.hh" #include "BKE_subdiv_ccg.hh" @@ -341,23 +342,9 @@ static void build_nodes_recursive_mesh(const Span corner_verts, nodes); } -void update_mesh_pointers(Tree &pbvh, Mesh *mesh) -{ - BLI_assert(pbvh.type() == Type::Mesh); - if (!pbvh.deformed_) { - /* Deformed data not matching the original mesh are owned directly by the - * Tree, and are set separately by #BKE_pbvh_vert_coords_apply. */ - pbvh.vert_positions_ = mesh->vert_positions_for_write(); - pbvh.vert_normals_ = mesh->vert_normals(); - pbvh.face_normals_ = mesh->face_normals(); - } -} - std::unique_ptr build_mesh(Mesh *mesh) { std::unique_ptr pbvh = std::make_unique(Type::Mesh); - update_mesh_pointers(*pbvh, mesh); - MutableSpan vert_positions = mesh->vert_positions_for_write(); const Span corner_verts = mesh->corner_verts(); const Span corner_tris = mesh->corner_tris(); @@ -846,6 +833,79 @@ static bool update_search(Node *node, const int flag) return true; } +/** + * Logic used to test whether to use the evaluated mesh for positions. + * \todo A deeper test of equality of topology array pointers would be better. This is kept for now + * to avoid changing logic during a refactor. + */ +static bool mesh_topology_count_matches(const Mesh &a, const Mesh &b) +{ + return a.faces_num == b.faces_num && a.corners_num == b.corners_num && + a.verts_num == b.verts_num; +} + +static const SharedCache> &vert_normals_cache_eval(const Object &object_orig, + const Object &object_eval) +{ + const SculptSession &ss = *object_orig.sculpt; + const Mesh &mesh_orig = *static_cast(object_orig.data); + BLI_assert(object_orig.sculpt->pbvh->type() == Type::Mesh); + if (object_orig.mode & (OB_MODE_VERTEX_PAINT | OB_MODE_WEIGHT_PAINT)) { + if (const Mesh *mesh_eval = BKE_object_get_evaluated_mesh_no_subsurf(&object_eval)) { + if (mesh_topology_count_matches(*mesh_eval, mesh_orig)) { + return mesh_eval->runtime->vert_normals_cache; + } + } + if (const Mesh *mesh_eval = BKE_object_get_mesh_deform_eval(&object_eval)) { + return mesh_eval->runtime->vert_normals_cache; + } + } + + if (!ss.deform_cos.is_empty()) { + BLI_assert(ss.deform_cos.size() == mesh_orig.verts_num); + return ss.vert_normals_deform; + } + + return mesh_orig.runtime->vert_normals_cache; +} +static SharedCache> &vert_normals_cache_eval_for_write(Object &object_orig, + Object &object_eval) +{ + return const_cast> &>( + vert_normals_cache_eval(object_orig, object_eval)); +} + +static const SharedCache> &face_normals_cache_eval(const Object &object_orig, + const Object &object_eval) +{ + const SculptSession &ss = *object_orig.sculpt; + const Mesh &mesh_orig = *static_cast(object_orig.data); + BLI_assert(object_orig.sculpt->pbvh->type() == Type::Mesh); + if (object_orig.mode & (OB_MODE_VERTEX_PAINT | OB_MODE_WEIGHT_PAINT)) { + if (const Mesh *mesh_eval = BKE_object_get_evaluated_mesh_no_subsurf(&object_eval)) { + if (mesh_topology_count_matches(*mesh_eval, mesh_orig)) { + return mesh_eval->runtime->face_normals_cache; + } + } + if (const Mesh *mesh_eval = BKE_object_get_mesh_deform_eval(&object_eval)) { + return mesh_eval->runtime->face_normals_cache; + } + } + + if (!ss.deform_cos.is_empty()) { + BLI_assert(ss.deform_cos.size() == mesh_orig.verts_num); + return ss.face_normals_deform; + } + + return mesh_orig.runtime->face_normals_cache; +} +static SharedCache> &face_normals_cache_eval_for_write(Object &object_orig, + Object &object_eval) +{ + return const_cast> &>( + face_normals_cache_eval(object_orig, object_eval)); +} + static void normals_calc_faces(const Span positions, const OffsetIndices faces, const Span corner_verts, @@ -940,13 +1000,17 @@ static void update_normals_mesh(Object &object_orig, Object &object_eval, Span(object_orig.data); - Tree &pbvh = *object_orig.sculpt->pbvh; const Span positions = bke::pbvh::vert_positions_eval_from_eval(object_eval); const OffsetIndices faces = mesh.faces(); const Span corner_verts = mesh.corner_verts(); const Span tri_faces = mesh.corner_tri_faces(); const GroupedSpan vert_to_face_map = mesh.vert_to_face_map(); + SharedCache> &vert_normals_cache = vert_normals_cache_eval_for_write(object_orig, + object_eval); + SharedCache> &face_normals_cache = face_normals_cache_eval_for_write(object_orig, + object_eval); + VectorSet boundary_faces; for (const Node *node : nodes) { for (const int vert : node->vert_indices_.as_span().drop_front(node->unique_verts_num_)) { @@ -954,35 +1018,21 @@ static void update_normals_mesh(Object &object_orig, Object &object_eval, Spanface_normals_cache.is_dirty()) { - mesh.face_normals(); - } - if (mesh.runtime->vert_normals_cache.is_dirty()) { - mesh.vert_normals(); - } - } - VectorSet boundary_verts; + threading::parallel_invoke( [&]() { - if (pbvh.deformed_) { - calc_node_face_normals( - positions, faces, corner_verts, tri_faces, nodes, pbvh.face_normals_deformed_); - calc_boundary_face_normals( - positions, faces, corner_verts, boundary_faces, pbvh.face_normals_deformed_); + if (face_normals_cache.is_dirty()) { + face_normals_cache.ensure([&](Vector &r_data) { + r_data.resize(faces.size()); + bke::mesh::normals_calc_faces(positions, faces, corner_verts, r_data); + }); } else { - mesh.runtime->face_normals_cache.update([&](Vector &r_data) { + face_normals_cache.update([&](Vector &r_data) { calc_node_face_normals(positions, faces, corner_verts, tri_faces, nodes, r_data); calc_boundary_face_normals(positions, faces, corner_verts, boundary_faces, r_data); }); - /* #SharedCache::update() reallocates cached vectors if they were shared initially. */ - pbvh.face_normals_ = mesh.runtime->face_normals_cache.data(); } }, [&]() { @@ -992,19 +1042,20 @@ static void update_normals_mesh(Object &object_orig, Object &object_eval, Span face_normals = face_normals_cache.data(); - if (pbvh.deformed_) { - calc_node_vert_normals( - vert_to_face_map, pbvh.face_normals_, nodes, pbvh.vert_normals_deformed_); - calc_boundary_vert_normals( - vert_to_face_map, pbvh.face_normals_, boundary_verts, pbvh.vert_normals_deformed_); + if (vert_normals_cache.is_dirty()) { + vert_normals_cache.ensure([&](Vector &r_data) { + r_data.resize(positions.size()); + mesh::normals_calc_verts( + positions, faces, corner_verts, vert_to_face_map, face_normals, r_data); + }); } else { - mesh.runtime->vert_normals_cache.update([&](Vector &r_data) { - calc_node_vert_normals(vert_to_face_map, pbvh.face_normals_, nodes, r_data); - calc_boundary_vert_normals(vert_to_face_map, pbvh.face_normals_, boundary_verts, r_data); + vert_normals_cache.update([&](Vector &r_data) { + calc_node_vert_normals(vert_to_face_map, face_normals, nodes, r_data); + calc_boundary_vert_normals(vert_to_face_map, face_normals, boundary_verts, r_data); }); - pbvh.vert_normals_ = mesh.runtime->vert_normals_cache.data(); } for (Node *node : nodes) { @@ -2473,7 +2524,8 @@ static blender::draw::pbvh::PBVH_GPU_Args pbvh_draw_args_init(const Object &obje args.corner_edges = mesh_eval.corner_edges(); args.corner_tris = mesh_eval.corner_tris(); args.vert_normals = blender::bke::pbvh::vert_normals_eval_from_eval(object_eval); - args.face_normals = pbvh.face_normals_; + args.face_normals = + blender::bke::pbvh::face_normals_cache_eval(object_orig, object_eval).data(); args.hide_poly = *mesh_orig.attributes().lookup(".hide_poly", blender::bke::AttrDomain::Face); @@ -2668,46 +2720,11 @@ void BKE_pbvh_vert_coords_apply(blender::bke::pbvh::Tree &pbvh, const blender::Span vert_positions) { using namespace blender::bke::pbvh; - - if (!pbvh.deformed_) { - if (!pbvh.vert_positions_.is_empty()) { - /* When the Tree is deformed, it creates a separate vertex position array - * that it owns directly. Conceptually these copies often aren't and often adds extra - * indirection, but: - * - Sculpting shape keys, the deformations are flushed back to the keys as a separate step. - * - Sculpting on a deformed mesh, deformations are also flushed to original positions - * separately. - * - The Tree currently always assumes we want to change positions, and - * has no way to avoid calculating normals if it's only used for painting, for example. */ - pbvh.vert_positions_deformed_ = pbvh.vert_positions_.as_span(); - pbvh.vert_positions_ = pbvh.vert_positions_deformed_; - - pbvh.vert_normals_deformed_ = pbvh.vert_normals_; - pbvh.vert_normals_ = pbvh.vert_normals_deformed_; - - pbvh.face_normals_deformed_ = pbvh.face_normals_; - pbvh.face_normals_ = pbvh.face_normals_deformed_; - - pbvh.deformed_ = true; - } + for (Node &node : pbvh.nodes_) { + BKE_pbvh_node_mark_positions_update(node); } - - if (!pbvh.vert_positions_.is_empty()) { - blender::MutableSpan positions = pbvh.vert_positions_; - positions.copy_from(vert_positions); - - for (Node &node : pbvh.nodes_) { - BKE_pbvh_node_mark_positions_update(node); - } - - update_bounds_mesh(vert_positions, pbvh); - store_bounds_orig(pbvh); - } -} - -bool BKE_pbvh_is_deformed(const blender::bke::pbvh::Tree &pbvh) -{ - return pbvh.deformed_; + update_bounds_mesh(vert_positions, pbvh); + store_bounds_orig(pbvh); } namespace blender::bke::pbvh { @@ -2728,38 +2745,87 @@ void get_frustum_planes(const Tree &pbvh, PBVHFrustumPlanes *planes) } } -Span vert_positions_eval(const Depsgraph & /*depsgraph*/, const Object &object) +static Span vert_positions_eval(const Object &object_orig, const Object &object_eval) { - BLI_assert(object.sculpt->pbvh->type() == Type::Mesh); - return object.sculpt->pbvh->vert_positions_; + const SculptSession &ss = *object_orig.sculpt; + const Mesh &mesh_orig = *static_cast(object_orig.data); + BLI_assert(object_orig.sculpt->pbvh->type() == Type::Mesh); + if (object_orig.mode & (OB_MODE_VERTEX_PAINT | OB_MODE_WEIGHT_PAINT)) { + if (const Mesh *mesh_eval = BKE_object_get_evaluated_mesh_no_subsurf(&object_eval)) { + if (mesh_topology_count_matches(*mesh_eval, mesh_orig)) { + return mesh_eval->vert_positions(); + } + } + if (const Mesh *mesh_eval = BKE_object_get_mesh_deform_eval(&object_eval)) { + return mesh_eval->vert_positions(); + } + } + + if (!ss.deform_cos.is_empty()) { + BLI_assert(ss.deform_cos.size() == mesh_orig.verts_num); + return ss.deform_cos; + } + + return mesh_orig.vert_positions(); +} +static MutableSpan vert_positions_eval_for_write(Object &object_orig, Object &object_eval) +{ + SculptSession &ss = *object_orig.sculpt; + Mesh &mesh_orig = *static_cast(object_orig.data); + BLI_assert(object_orig.sculpt->pbvh->type() == Type::Mesh); + if (object_orig.mode & (OB_MODE_VERTEX_PAINT | OB_MODE_WEIGHT_PAINT)) { + if (const Mesh *mesh_eval = BKE_object_get_evaluated_mesh_no_subsurf(&object_eval)) { + if (mesh_topology_count_matches(*mesh_eval, mesh_orig)) { + Mesh *mesh_eval_mut = const_cast(mesh_eval); + return mesh_eval_mut->vert_positions_for_write(); + } + } + if (const Mesh *mesh_eval = BKE_object_get_mesh_deform_eval(&object_eval)) { + Mesh *mesh_eval_mut = const_cast(mesh_eval); + return mesh_eval_mut->vert_positions_for_write(); + } + } + + if (!ss.deform_cos.is_empty()) { + BLI_assert(ss.deform_cos.size() == mesh_orig.verts_num); + return ss.deform_cos; + } + + return mesh_orig.vert_positions_for_write(); +} + +Span vert_positions_eval(const Depsgraph &depsgraph, const Object &object_orig) +{ + const Object &object_eval = *DEG_get_evaluated_object(&depsgraph, + &const_cast(object_orig)); + return vert_positions_eval(object_orig, object_eval); } Span vert_positions_eval_from_eval(const Object &object_eval) { BLI_assert(!DEG_is_original_object(&object_eval)); - Object &object_orig = *DEG_get_original_object(&const_cast(object_eval)); - BLI_assert(object_orig.sculpt->pbvh->type() == Type::Mesh); - return object_orig.sculpt->pbvh->vert_positions_; + const Object &object_orig = *DEG_get_original_object(&const_cast(object_eval)); + return vert_positions_eval(object_orig, object_eval); } -MutableSpan vert_positions_eval_for_write(const Depsgraph & /*depsgraph*/, Object &object) +MutableSpan vert_positions_eval_for_write(const Depsgraph &depsgraph, Object &object_orig) { - BLI_assert(object.sculpt->pbvh->type() == Type::Mesh); - return object.sculpt->pbvh->vert_positions_; + Object &object_eval = *DEG_get_evaluated_object(&depsgraph, &object_orig); + return vert_positions_eval_for_write(object_orig, object_eval); } -Span vert_normals_eval(const Depsgraph & /*depsgraph*/, const Object &object) +Span vert_normals_eval(const Depsgraph &depsgraph, const Object &object_orig) { - BLI_assert(object.sculpt->pbvh->type() == Type::Mesh); - return object.sculpt->pbvh->vert_normals_; + const Object &object_eval = *DEG_get_evaluated_object(&depsgraph, + &const_cast(object_orig)); + return vert_normals_cache_eval(object_orig, object_eval).data(); } Span vert_normals_eval_from_eval(const Object &object_eval) { BLI_assert(!DEG_is_original_object(&object_eval)); Object &object_orig = *DEG_get_original_object(&const_cast(object_eval)); - BLI_assert(object_orig.sculpt->pbvh->type() == Type::Mesh); - return object_orig.sculpt->pbvh->vert_normals_; + return vert_normals_cache_eval(object_orig, object_eval).data(); } } // namespace blender::bke::pbvh diff --git a/source/blender/editors/sculpt_paint/mesh_brush_common.hh b/source/blender/editors/sculpt_paint/mesh_brush_common.hh index 2d1d191221b..02d1fbe4244 100644 --- a/source/blender/editors/sculpt_paint/mesh_brush_common.hh +++ b/source/blender/editors/sculpt_paint/mesh_brush_common.hh @@ -430,17 +430,6 @@ void update_shape_keys(Object &object, Span translations, Span positions_orig); -/** - * Currently the pbvh::Tree owns its own copy of deformed positions that needs to be updated to - * stay in sync with brush deformations. - * \todo This should be removed one the pbvh::Tree no longer stores this copy of deformed - * positions. - */ -void apply_translations_to_pbvh(const Depsgraph &depsgraph, - Object &object, - Span verts, - Span translations); - /** * Write the new translated positions to the original mesh, taking into account inverse * deformation from modifiers, axis locking, and clipping. Flush the deformation to shape keys as diff --git a/source/blender/editors/sculpt_paint/sculpt.cc b/source/blender/editors/sculpt_paint/sculpt.cc index 7c3ec366be5..67602b4bda2 100644 --- a/source/blender/editors/sculpt_paint/sculpt.cc +++ b/source/blender/editors/sculpt_paint/sculpt.cc @@ -7209,21 +7209,6 @@ void update_shape_keys(Object &object, } } -void apply_translations_to_pbvh(const Depsgraph &depsgraph, - Object &object, - const Span verts, - const Span translations) -{ - if (!BKE_pbvh_is_deformed(*object.sculpt->pbvh)) { - return; - } - MutableSpan pbvh_positions = bke::pbvh::vert_positions_eval_for_write(depsgraph, object); - for (const int i : verts.index_range()) { - const int vert = verts[i]; - pbvh_positions[vert] += translations[i]; - } -} - void write_translations(const Depsgraph &depsgraph, const Sculpt &sd, Object &object, @@ -7236,7 +7221,11 @@ void write_translations(const Depsgraph &depsgraph, clip_and_lock_translations(sd, ss, positions_eval, verts, translations); - apply_translations_to_pbvh(depsgraph, object, verts, translations); + MutableSpan positions_eval_mut = bke::pbvh::vert_positions_eval_for_write(depsgraph, + object); + if (positions_eval_mut.data() != positions_orig.data()) { + apply_translations(translations, verts, positions_eval_mut); + } if (!ss.deform_imats.is_empty()) { apply_crazyspace_to_translations(ss.deform_imats, verts, translations);