Sculpt: Improve node tools performance for simpler changes
Currently node tools always adds a dependency graph update tag. In sculpt mode this causes the paint BVH to be rebuilt, which causes a complete rebuild of the sculpt mode draw data. Both are quite expensive relative to most other operations. Also, node tools currenly always uses the "geometry" sculpt undo type, which causes its own depsgraph update tag. Arguably a depsgraph geometry reevaluation shouldn't cause a rebuild of the BVH and draw data, but that's a limitation that's out of scope for now. Most tools in sculpt mode avoid adding a depsgraph tag when they don't change mesh topology for this reason. This PR gives node tools the ability to check if the output mesh has a different topology than the input. When the topology is the same, we can use one of the specialized sculpt undo types for positions, masks, or face sets. Though when more than one of these attributes changes, we're still forced to still use the geometry undo type because sculpt undo steps can only handle a single type of change. In the end this results in much better performance for most simple node tools that just deform the mesh or change masks or face sets. Pull Request: https://projects.blender.org/blender/blender/pulls/133842
This commit is contained in:
@@ -168,6 +168,41 @@ static void find_socket_log_contexts(const Main &bmain,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class adds a user to shared mesh data, requiring modifications of the mesh to reallocate
|
||||
* the data and its sharing info. This allows tracking which data is modified without having to
|
||||
* explicitly compare it.
|
||||
*/
|
||||
class MeshState {
|
||||
VectorSet<const ImplicitSharingInfo *> sharing_infos_;
|
||||
|
||||
public:
|
||||
MeshState(const Mesh &mesh)
|
||||
{
|
||||
if (mesh.runtime->face_offsets_sharing_info) {
|
||||
this->freeze_shared_state(*mesh.runtime->face_offsets_sharing_info);
|
||||
}
|
||||
mesh.attributes().foreach_attribute([&](const bke::AttributeIter &iter) {
|
||||
const bke::GAttributeReader attribute = iter.get();
|
||||
this->freeze_shared_state(*attribute.sharing_info);
|
||||
});
|
||||
}
|
||||
|
||||
void freeze_shared_state(const ImplicitSharingInfo &sharing_info)
|
||||
{
|
||||
if (sharing_infos_.add(&sharing_info)) {
|
||||
sharing_info.add_user();
|
||||
}
|
||||
}
|
||||
|
||||
~MeshState()
|
||||
{
|
||||
for (const ImplicitSharingInfo *sharing_info : sharing_infos_) {
|
||||
sharing_info->remove_user_and_delete_if_last();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Geometry nodes currently requires working on "evaluated" data-blocks (rather than "original"
|
||||
* data-blocks that are part of a #Main data-base). This could change in the future, but for now,
|
||||
@@ -175,7 +210,8 @@ static void find_socket_log_contexts(const Main &bmain,
|
||||
* sharing lets us avoid copying attribute data though.
|
||||
*/
|
||||
static bke::GeometrySet get_original_geometry_eval_copy(Object &object,
|
||||
nodes::GeoNodesOperatorData &operator_data)
|
||||
nodes::GeoNodesOperatorData &operator_data,
|
||||
Vector<MeshState> &orig_mesh_states)
|
||||
{
|
||||
switch (object.type) {
|
||||
case OB_CURVES: {
|
||||
@@ -199,15 +235,22 @@ static bke::GeometrySet get_original_geometry_eval_copy(Object &object,
|
||||
BKE_id_free(nullptr, mesh_copy);
|
||||
return bke::GeometrySet::from_mesh(final_copy);
|
||||
}
|
||||
return bke::GeometrySet::from_mesh(BKE_mesh_copy_for_eval(*mesh));
|
||||
Mesh *mesh_copy = BKE_mesh_copy_for_eval(*mesh);
|
||||
orig_mesh_states.append_as(*mesh_copy);
|
||||
return bke::GeometrySet::from_mesh(mesh_copy);
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
static void store_result_geometry(
|
||||
const wmOperator &op, Main &bmain, Scene &scene, Object &object, bke::GeometrySet geometry)
|
||||
static void store_result_geometry(const wmOperator &op,
|
||||
const Depsgraph &depsgraph,
|
||||
Main &bmain,
|
||||
Scene &scene,
|
||||
Object &object,
|
||||
const RegionView3D *rv3d,
|
||||
bke::GeometrySet geometry)
|
||||
{
|
||||
geometry.ensure_owns_direct_data();
|
||||
switch (object.type) {
|
||||
@@ -224,6 +267,7 @@ static void store_result_geometry(
|
||||
|
||||
curves.geometry.wrap() = std::move(new_curves->geometry.wrap());
|
||||
BKE_object_material_from_eval_data(&bmain, &object, &new_curves->id);
|
||||
DEG_id_tag_update(&curves.id, ID_RECALC_GEOMETRY);
|
||||
break;
|
||||
}
|
||||
case OB_POINTCLOUD: {
|
||||
@@ -241,6 +285,7 @@ static void store_result_geometry(
|
||||
|
||||
BKE_object_material_from_eval_data(&bmain, &object, &new_points->id);
|
||||
BKE_pointcloud_nomain_to_pointcloud(new_points, &points);
|
||||
DEG_id_tag_update(&points.id, ID_RECALC_GEOMETRY);
|
||||
break;
|
||||
}
|
||||
case OB_MESH: {
|
||||
@@ -248,37 +293,33 @@ static void store_result_geometry(
|
||||
|
||||
const bool has_shape_keys = mesh.key != nullptr;
|
||||
|
||||
if (object.mode == OB_MODE_SCULPT) {
|
||||
sculpt_paint::undo::geometry_begin(scene, object, &op);
|
||||
}
|
||||
|
||||
Mesh *new_mesh = geometry.get_component_for_write<bke::MeshComponent>().release();
|
||||
if (!new_mesh) {
|
||||
BKE_mesh_clear_geometry(&mesh);
|
||||
}
|
||||
else {
|
||||
if (new_mesh) {
|
||||
/* Anonymous attributes shouldn't be available on the applied geometry. */
|
||||
new_mesh->attributes_for_write().remove_anonymous();
|
||||
|
||||
BKE_object_material_from_eval_data(&bmain, &object, &new_mesh->id);
|
||||
if (object.mode == OB_MODE_EDIT) {
|
||||
EDBM_mesh_make_from_mesh(&object, new_mesh, scene.toolsettings->selectmode, true);
|
||||
BKE_editmesh_looptris_and_normals_calc(mesh.runtime->edit_mesh.get());
|
||||
BKE_id_free(nullptr, new_mesh);
|
||||
}
|
||||
else {
|
||||
BKE_mesh_nomain_to_mesh(new_mesh, &mesh, &object);
|
||||
}
|
||||
}
|
||||
else {
|
||||
new_mesh = BKE_mesh_new_nomain(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
if (object.mode == OB_MODE_SCULPT) {
|
||||
sculpt_paint::store_mesh_from_eval(op, scene, depsgraph, rv3d, object, new_mesh);
|
||||
}
|
||||
else if (object.mode == OB_MODE_EDIT) {
|
||||
EDBM_mesh_make_from_mesh(&object, new_mesh, scene.toolsettings->selectmode, true);
|
||||
BKE_editmesh_looptris_and_normals_calc(mesh.runtime->edit_mesh.get());
|
||||
BKE_id_free(nullptr, new_mesh);
|
||||
DEG_id_tag_update(&mesh.id, ID_RECALC_GEOMETRY);
|
||||
}
|
||||
else {
|
||||
BKE_mesh_nomain_to_mesh(new_mesh, &mesh, &object);
|
||||
DEG_id_tag_update(&mesh.id, ID_RECALC_GEOMETRY);
|
||||
}
|
||||
|
||||
if (has_shape_keys && !mesh.key) {
|
||||
BKE_report(op.reports, RPT_WARNING, "Mesh shape key data removed");
|
||||
}
|
||||
|
||||
if (object.mode == OB_MODE_SCULPT) {
|
||||
sculpt_paint::undo::geometry_end(object);
|
||||
BKE_sculptsession_free_pbvh(object);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -539,6 +580,10 @@ static int run_node_group_exec(bContext *C, wmOperator *op)
|
||||
eval_log.node_group_name = node_tree->id.name + 2;
|
||||
find_socket_log_contexts(*bmain, socket_log_contexts);
|
||||
|
||||
/* May be null if operator called from outside 3D view context. */
|
||||
const RegionView3D *rv3d = CTX_wm_region_view3d(C);
|
||||
Vector<MeshState> orig_mesh_states;
|
||||
|
||||
for (Object *object : objects) {
|
||||
nodes::GeoNodesOperatorData operator_eval_data{};
|
||||
operator_eval_data.mode = mode;
|
||||
@@ -564,14 +609,14 @@ static int run_node_group_exec(bContext *C, wmOperator *op)
|
||||
call_data.socket_log_contexts = &socket_log_contexts;
|
||||
}
|
||||
|
||||
bke::GeometrySet geometry_orig = get_original_geometry_eval_copy(*object, operator_eval_data);
|
||||
bke::GeometrySet geometry_orig = get_original_geometry_eval_copy(
|
||||
*object, operator_eval_data, orig_mesh_states);
|
||||
|
||||
bke::GeometrySet new_geometry = nodes::execute_geometry_nodes_on_geometry(
|
||||
*node_tree, properties, compute_context, call_data, std::move(geometry_orig));
|
||||
|
||||
store_result_geometry(*op, *bmain, *scene, *object, std::move(new_geometry));
|
||||
|
||||
DEG_id_tag_update(static_cast<ID *>(object->data), ID_RECALC_GEOMETRY);
|
||||
store_result_geometry(
|
||||
*op, *depsgraph_active, *bmain, *scene, *object, rv3d, std::move(new_geometry));
|
||||
WM_event_add_notifier(C, NC_GEOM | ND_DATA, object->data);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ struct Depsgraph;
|
||||
struct Main;
|
||||
struct Mesh;
|
||||
struct Object;
|
||||
struct RegionView3D;
|
||||
struct ReportList;
|
||||
struct Scene;
|
||||
struct UndoType;
|
||||
@@ -91,4 +92,19 @@ int active_update_and_get(bContext *C, Object &ob, const float mval_fl[2]);
|
||||
*/
|
||||
bool object_active_color_fill(Object &ob, const float fill_color[4], bool only_selected);
|
||||
|
||||
/**
|
||||
* Fully replace the sculpt mesh with a mesh outside of #Main. This implements various checks to
|
||||
* avoid pushing full geometry-type undo steps when possible, allowing for better performance.
|
||||
*
|
||||
* \warning To avoid false negatives when detecting mesh changes, it is critical that the caller
|
||||
* adds an owner to the attribute data arrays before modifying the original object's mesh. This
|
||||
* allows constant time checks for whether the mesh has changed.
|
||||
*/
|
||||
void store_mesh_from_eval(const wmOperator &op,
|
||||
const Scene &scene,
|
||||
const Depsgraph &depsgraph,
|
||||
const RegionView3D *rv3d,
|
||||
Object &object,
|
||||
Mesh *new_mesh);
|
||||
|
||||
} // namespace blender::ed::sculpt_paint
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#include "BKE_image.hh"
|
||||
#include "BKE_key.hh"
|
||||
#include "BKE_layer.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_mesh.hh"
|
||||
#include "BKE_modifier.hh"
|
||||
#include "BKE_multires.hh"
|
||||
@@ -5169,6 +5170,51 @@ static void restore_from_undo_step_if_necessary(const Depsgraph &depsgraph,
|
||||
|
||||
namespace blender::ed::sculpt_paint {
|
||||
|
||||
static void tag_mesh_positions_changed(Object &object, const bool use_pbvh_draw)
|
||||
{
|
||||
Mesh &mesh = *static_cast<Mesh *>(object.data);
|
||||
|
||||
/* Various operations inside sculpt mode can cause either the #MeshRuntimeData or the entire
|
||||
* Mesh to be changed (e.g. Undoing the very first operation after opening a file, performing
|
||||
* remesh, etc).
|
||||
*
|
||||
* This isn't an ideal fix for the core issue here, but to mitigate the drastic performance
|
||||
* falloff, we refreeze the cache before we do any operation that would tag this runtime
|
||||
* cache as dirty.
|
||||
*
|
||||
* See #130636. */
|
||||
if (!mesh.runtime->corner_tris_cache.frozen) {
|
||||
mesh.runtime->corner_tris_cache.freeze();
|
||||
}
|
||||
|
||||
/* Updating mesh positions without marking caches dirty is generally not good, but since
|
||||
* sculpt mode has special requirements and is expected to have sole ownership of the mesh it
|
||||
* modifies, it's generally okay. */
|
||||
if (use_pbvh_draw) {
|
||||
/* When drawing from bke::pbvh::Tree is used, vertex and face normals are updated
|
||||
* later in #bke::pbvh::update_normals. However, we update the mesh's bounds eagerly here
|
||||
* since they are trivial to access from the bke::pbvh::Tree. Updating the
|
||||
* object's evaluated geometry bounding box is necessary because sculpt strokes don't cause
|
||||
* an object reevaluation. */
|
||||
mesh.tag_positions_changed_no_normals();
|
||||
/* Sculpt mode does not use or recalculate face corner normals, so they are cleared. */
|
||||
mesh.runtime->corner_normals_cache.tag_dirty();
|
||||
}
|
||||
else {
|
||||
/* Drawing happens from the modifier stack evaluation result.
|
||||
* Tag both coordinates and normals as modified, as both needed for proper drawing and the
|
||||
* modifier stack is not guaranteed to tag normals for update. */
|
||||
mesh.tag_positions_changed();
|
||||
}
|
||||
|
||||
if (const bke::pbvh::Tree *pbvh = bke::object::pbvh_get(object)) {
|
||||
mesh.bounds_set_eager(bke::pbvh::bounds_get(*pbvh));
|
||||
if (object.runtime->bounds_eval) {
|
||||
object.runtime->bounds_eval = mesh.bounds_min_max();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void flush_update_step(bContext *C, UpdateType update_type)
|
||||
{
|
||||
Depsgraph &depsgraph = *CTX_data_depsgraph_pointer(C);
|
||||
@@ -5177,7 +5223,6 @@ void flush_update_step(bContext *C, UpdateType update_type)
|
||||
ARegion ®ion = *CTX_wm_region(C);
|
||||
MultiresModifierData *mmd = ss.multires.modifier;
|
||||
RegionView3D *rv3d = CTX_wm_region_view3d(C);
|
||||
Mesh *mesh = static_cast<Mesh *>(ob.data);
|
||||
|
||||
bke::pbvh::Tree &pbvh = *bke::object::pbvh_get(ob);
|
||||
|
||||
@@ -5236,44 +5281,7 @@ void flush_update_step(bContext *C, UpdateType update_type)
|
||||
|
||||
if (update_type == UpdateType::Position && !ss.shapekey_active) {
|
||||
if (pbvh.type() == bke::pbvh::Type::Mesh) {
|
||||
/* Various operations inside sculpt mode can cause either the #MeshRuntimeData or the entire
|
||||
* Mesh to be changed (e.g. Undoing the very first operation after opening a file, performing
|
||||
* remesh, etc).
|
||||
*
|
||||
* This isn't an ideal fix for the core issue here, but to mitigate the drastic performance
|
||||
* falloff, we refreeze the cache before we do any operation that would tag this runtime
|
||||
* cache as dirty.
|
||||
*
|
||||
* See #130636.
|
||||
*/
|
||||
if (!mesh->runtime->corner_tris_cache.frozen) {
|
||||
mesh->runtime->corner_tris_cache.freeze();
|
||||
}
|
||||
|
||||
/* Updating mesh positions without marking caches dirty is generally not good, but since
|
||||
* sculpt mode has special requirements and is expected to have sole ownership of the mesh it
|
||||
* modifies, it's generally okay. */
|
||||
if (use_pbvh_draw) {
|
||||
/* When drawing from bke::pbvh::Tree is used, vertex and face normals are updated
|
||||
* later in #bke::pbvh::update_normals. However, we update the mesh's bounds eagerly here
|
||||
* since they are trivial to access from the bke::pbvh::Tree. Updating the
|
||||
* object's evaluated geometry bounding box is necessary because sculpt strokes don't cause
|
||||
* an object reevaluation. */
|
||||
mesh->tag_positions_changed_no_normals();
|
||||
/* Sculpt mode does not use or recalculate face corner normals, so they are cleared. */
|
||||
mesh->runtime->corner_normals_cache.tag_dirty();
|
||||
}
|
||||
else {
|
||||
/* Drawing happens from the modifier stack evaluation result.
|
||||
* Tag both coordinates and normals as modified, as both needed for proper drawing and the
|
||||
* modifier stack is not guaranteed to tag normals for update. */
|
||||
mesh->tag_positions_changed();
|
||||
}
|
||||
|
||||
mesh->bounds_set_eager(bke::pbvh::bounds_get(pbvh));
|
||||
if (ob.runtime->bounds_eval) {
|
||||
ob.runtime->bounds_eval = mesh->bounds_min_max();
|
||||
}
|
||||
tag_mesh_positions_changed(ob, use_pbvh_draw);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5348,6 +5356,180 @@ void flush_update_done(const bContext *C, Object &ob, UpdateType update_type)
|
||||
}
|
||||
}
|
||||
|
||||
/* Replace an entire attribute using implicit sharing to avoid copies when possible. */
|
||||
static void replace_attribute(const bke::AttributeAccessor src_attributes,
|
||||
const StringRef name,
|
||||
const bke::AttrDomain domain,
|
||||
const eCustomDataType data_type,
|
||||
bke::MutableAttributeAccessor dst_attributes)
|
||||
{
|
||||
dst_attributes.remove(name);
|
||||
bke::GAttributeReader src = src_attributes.lookup(name, domain, data_type);
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
if (src.sharing_info && src.varray.is_span()) {
|
||||
const bke::AttributeInitShared init(src.varray.get_internal_span().data(), *src.sharing_info);
|
||||
dst_attributes.add(name, domain, data_type, init);
|
||||
}
|
||||
else {
|
||||
const bke::AttributeInitVArray init(*src);
|
||||
dst_attributes.add(name, domain, data_type, init);
|
||||
}
|
||||
}
|
||||
|
||||
static bool attribute_matches(const bke::AttributeAccessor a,
|
||||
const bke::AttributeAccessor b,
|
||||
const StringRef name)
|
||||
{
|
||||
const bke::GAttributeReader a_attr = a.lookup(name);
|
||||
const bke::GAttributeReader b_attr = b.lookup(name);
|
||||
if (!a_attr.sharing_info || !b_attr.sharing_info) {
|
||||
return false;
|
||||
}
|
||||
return a_attr.sharing_info == b_attr.sharing_info;
|
||||
}
|
||||
|
||||
static bool topology_matches(const Mesh &a, const Mesh &b)
|
||||
{
|
||||
if (a.verts_num != b.verts_num || a.edges_num != b.edges_num || a.faces_num != b.faces_num ||
|
||||
a.corners_num != b.corners_num)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (a.runtime->face_offsets_sharing_info != b.runtime->face_offsets_sharing_info) {
|
||||
return false;
|
||||
}
|
||||
const bke::AttributeAccessor a_attributes = a.attributes();
|
||||
const bke::AttributeAccessor b_attributes = b.attributes();
|
||||
if (!attribute_matches(a_attributes, b_attributes, ".edge_verts") ||
|
||||
!attribute_matches(a_attributes, b_attributes, ".corner_vert") ||
|
||||
!attribute_matches(a_attributes, b_attributes, ".corner_edge"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void store_sculpt_entire_mesh(const wmOperator &op,
|
||||
const Scene &scene,
|
||||
Object &object,
|
||||
Mesh *new_mesh)
|
||||
{
|
||||
Mesh &mesh = *static_cast<Mesh *>(object.data);
|
||||
sculpt_paint::undo::geometry_begin(scene, object, &op);
|
||||
BKE_mesh_nomain_to_mesh(new_mesh, &mesh, &object);
|
||||
sculpt_paint::undo::geometry_end(object);
|
||||
BKE_sculptsession_free_pbvh(object);
|
||||
}
|
||||
|
||||
void store_mesh_from_eval(const wmOperator &op,
|
||||
const Scene &scene,
|
||||
const Depsgraph &depsgraph,
|
||||
const RegionView3D *rv3d,
|
||||
Object &object,
|
||||
Mesh *new_mesh)
|
||||
{
|
||||
Mesh &mesh = *static_cast<Mesh *>(object.data);
|
||||
const bool changed_topology = !topology_matches(mesh, *new_mesh);
|
||||
const bool use_pbvh_draw = BKE_sculptsession_use_pbvh_draw(&object, rv3d);
|
||||
|
||||
if (changed_topology) {
|
||||
store_sculpt_entire_mesh(op, scene, object, new_mesh);
|
||||
}
|
||||
else {
|
||||
/* Detect attributes present in the new mesh which no longer match the original. */
|
||||
VectorSet<StringRef> changed_attributes;
|
||||
new_mesh->attributes().foreach_attribute([&](const bke::AttributeIter &iter) {
|
||||
if (ELEM(iter.name, ".edge_verts", ".corner_vert", ".corner_edge")) {
|
||||
return;
|
||||
}
|
||||
const bke::GAttributeReader attribute = iter.get();
|
||||
if (attribute_matches(new_mesh->attributes(), mesh.attributes(), iter.name)) {
|
||||
return;
|
||||
}
|
||||
changed_attributes.add(iter.name);
|
||||
});
|
||||
/* Detect attributes that were removed in the new mesh. */
|
||||
mesh.attributes().foreach_attribute([&](const bke::AttributeIter &iter) {
|
||||
if (!new_mesh->attributes().contains(iter.name)) {
|
||||
changed_attributes.add(iter.name);
|
||||
}
|
||||
});
|
||||
|
||||
/* Try to use the few specialized sculpt undo types that result in better performance, mainly
|
||||
* because redo avoids clearing the BVH, but also because some other updates can be skipped. */
|
||||
bke::pbvh::Tree &pbvh = *bke::object::pbvh_get(object);
|
||||
IndexMaskMemory memory;
|
||||
const IndexMask leaf_nodes = bke::pbvh::all_leaf_nodes(pbvh, memory);
|
||||
if (changed_attributes.as_span() == Span<StringRef>{"position"}) {
|
||||
undo::push_begin(scene, object, &op);
|
||||
undo::push_nodes(depsgraph, object, leaf_nodes, undo::Type::Position);
|
||||
undo::push_end(object);
|
||||
CustomData_free_layer_named(&mesh.vert_data, "position", mesh.verts_num);
|
||||
mesh.attributes_for_write().remove("position");
|
||||
const bke::AttributeReader position = new_mesh->attributes().lookup<float3>("position");
|
||||
if (position.sharing_info) {
|
||||
/* Use lower level API to add the position attribute to avoid copying the array and to
|
||||
* allow using #tag_positions_changed_no_normals instead of #tag_positions_changed (which
|
||||
* would be called by the attribute API). */
|
||||
CustomData_add_layer_named_with_data(
|
||||
&mesh.vert_data,
|
||||
CD_PROP_FLOAT3,
|
||||
const_cast<float3 *>(position.varray.get_internal_span().data()),
|
||||
mesh.verts_num,
|
||||
"position",
|
||||
position.sharing_info);
|
||||
}
|
||||
else {
|
||||
mesh.vert_positions_for_write().copy_from(VArraySpan(*position));
|
||||
}
|
||||
|
||||
pbvh.tag_positions_changed(leaf_nodes);
|
||||
pbvh.update_bounds(depsgraph, object);
|
||||
tag_mesh_positions_changed(object, use_pbvh_draw);
|
||||
BKE_mesh_copy_parameters(&mesh, new_mesh);
|
||||
BKE_id_free(nullptr, new_mesh);
|
||||
}
|
||||
else if (changed_attributes.as_span() == Span<StringRef>{".sculpt_mask"}) {
|
||||
undo::push_begin(scene, object, &op);
|
||||
undo::push_nodes(depsgraph, object, leaf_nodes, undo::Type::Mask);
|
||||
undo::push_end(object);
|
||||
replace_attribute(new_mesh->attributes(),
|
||||
".sculpt_mask",
|
||||
bke::AttrDomain::Point,
|
||||
CD_PROP_FLOAT,
|
||||
mesh.attributes_for_write());
|
||||
pbvh.tag_masks_changed(leaf_nodes);
|
||||
BKE_mesh_copy_parameters(&mesh, new_mesh);
|
||||
BKE_id_free(nullptr, new_mesh);
|
||||
}
|
||||
else if (changed_attributes.as_span() == Span<StringRef>{".sculpt_face_set"}) {
|
||||
undo::push_begin(scene, object, &op);
|
||||
undo::push_nodes(depsgraph, object, leaf_nodes, undo::Type::FaceSet);
|
||||
undo::push_end(object);
|
||||
replace_attribute(new_mesh->attributes(),
|
||||
".sculpt_face_set",
|
||||
bke::AttrDomain::Face,
|
||||
CD_PROP_INT32,
|
||||
mesh.attributes_for_write());
|
||||
pbvh.tag_face_sets_changed(leaf_nodes);
|
||||
BKE_mesh_copy_parameters(&mesh, new_mesh);
|
||||
BKE_id_free(nullptr, new_mesh);
|
||||
}
|
||||
else {
|
||||
/* Non-geometry-type sculpt undo steps can only handle a single change at a time. When
|
||||
* multiple attributes or attributes that don't have their own undo type are changed, we're
|
||||
* forced to fall back to the slower geometry undo type. */
|
||||
store_sculpt_entire_mesh(op, scene, object, new_mesh);
|
||||
}
|
||||
}
|
||||
DEG_id_tag_update(&mesh.id, ID_RECALC_SHADING);
|
||||
if (!use_pbvh_draw) {
|
||||
DEG_id_tag_update(&mesh.id, ID_RECALC_GEOMETRY);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::ed::sculpt_paint
|
||||
|
||||
/* Returns whether the mouse/stylus is over the mesh (1)
|
||||
|
||||
Reference in New Issue
Block a user