WIP: Sculpt: cleanup sculpt attribute API #106920

Closed
Joseph Eagar wants to merge 7 commits from temp-sculpt-attr-api into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
7 changed files with 168 additions and 104 deletions

View File

@ -499,6 +499,7 @@ typedef struct SculptAttributeParams {
*/
int permanent : 1; /* Cannot be combined with simple_array. */
int stroke_only : 1; /* Release layer at end of struct */
bool read_only : 1;
} SculptAttributeParams;
typedef struct SculptAttribute {
@ -780,11 +781,18 @@ SculptAttribute *BKE_sculpt_attribute_ensure(struct Object *ob,
const char *name,
const SculptAttributeParams *params);
/* Returns nullptr if attribute does not exist. */
/* Returns nullptr if attribute does not exist. params may be nullptr. */
SculptAttribute *BKE_sculpt_attribute_get(struct Object *ob,
eAttrDomain domain,
eCustomDataType proptype,
const char *name);
const char *name,
const SculptAttributeParams *params);
/* Release this attribute handle (does not destroying the attribute itself).
* Any pointers inside SculptSession.attrs that point to attr will be
* null'd.
*/
void BKE_sculpt_attribute_release(struct Object *ob, SculptAttribute *attr);
bool BKE_sculpt_attribute_exists(struct Object *ob,
eAttrDomain domain,
@ -799,48 +807,6 @@ void BKE_sculpt_attribute_destroy_temporary_all(struct Object *ob);
/* Destroy attributes that were marked as stroke only in SculptAttributeParams. */
void BKE_sculpt_attributes_destroy_temporary_stroke(struct Object *ob);
BLI_INLINE void *BKE_sculpt_vertex_attr_get(const PBVHVertRef vertex, const SculptAttribute *attr)
{
if (attr->data) {
char *p = (char *)attr->data;
int idx = (int)vertex.i;
if (attr->data_for_bmesh) {
BMElem *v = (BMElem *)vertex.i;
idx = v->head.index;
}
return p + attr->elem_size * (int)idx;
}
else {
BMElem *v = (BMElem *)vertex.i;
return BM_ELEM_CD_GET_VOID_P(v, attr->bmesh_cd_offset);
}
return NULL;
}
BLI_INLINE void *BKE_sculpt_face_attr_get(const PBVHFaceRef vertex, const SculptAttribute *attr)
{
if (attr->data) {
char *p = (char *)attr->data;
int idx = (int)vertex.i;
if (attr->data_for_bmesh) {
BMElem *v = (BMElem *)vertex.i;
idx = v->head.index;
}
return p + attr->elem_size * (int)idx;
}
else {
BMElem *v = (BMElem *)vertex.i;
return BM_ELEM_CD_GET_VOID_P(v, attr->bmesh_cd_offset);
}
return NULL;
}
/**
* Create new color layer on object if it doesn't have one and if experimental feature set has
* sculpt vertex color enabled. Returns truth if new layer has been added, false otherwise.
@ -943,4 +909,83 @@ struct CurveMapping *BKE_sculpt_default_cavity_curve(void);
#ifdef __cplusplus

I think it might be clearer to avoid Tptr and just use T * in the code below.

I think it might be clearer to avoid `Tptr` and just use `T *` in the code below.
}
namespace blender::bke::paint {
Review

Could avoid the temporary variable here and return the pointer inside the if statement.

Could avoid the temporary variable here and return the pointer inside the if statement.
/* Base implementation for vertex_attr_*** and face_attr_*** methods.
* Returns a pointer to the attribute data (as defined by attr) for elem.
*/
template<typename T, typename ElemRef = PBVHVertRef>
static T *elem_attr_ptr(const ElemRef elem, const SculptAttribute *attr)
{
void *ptr = nullptr;
if (attr->data) {
char *p = (char *)attr->data;
int idx = (int)elem.i;
if (attr->data_for_bmesh) {
BMElem *e = (BMElem *)elem.i;
idx = e->head.index;
}
ptr = p + attr->elem_size * (int)idx;
}
else {
BMElem *v = (BMElem *)elem.i;
ptr = BM_ELEM_CD_GET_VOID_P(v, attr->bmesh_cd_offset);
}
return static_cast<T *>(ptr);
}

I think exposing the pointer where a value is stored might not work well with different attribute storage methods in the future, and exposes the internal of the system a bit more than necessary. (For example, virtual arrays don't provide that ability, not that we would use them here exactly). The alternative is using a get/set combo when that's actually necessary.

I think exposing the pointer where a value is stored might not work well with different attribute storage methods in the future, and exposes the internal of the system a bit more than necessary. (For example, virtual arrays don't provide that ability, not that we would use them here exactly). The alternative is using a `get`/`set` combo when that's actually necessary.
/*
* Get a pointer to attribute data at vertex.
*
* Example: float *persistent_co = vertex_attr_ptr<float>(vertex, ss->attrs.persistent_co);
*/
template<typename T>
static T *vertex_attr_ptr(const PBVHVertRef vertex, const SculptAttribute *attr)
{
return elem_attr_ptr<T, PBVHVertRef>(vertex, attr);
}
/*
* Get attribute data at vertex.
*
* Example: float weight = vertex_attr_get<float>(vertex, ss->attrs.automasking_factor);
*/
template<typename T>
static T vertex_attr_get(const PBVHVertRef vertex, const SculptAttribute *attr)
{
return *vertex_attr_ptr<T>(vertex, attr);
}
/*
* Set attribute data at vertex.
*
* vertex_attr_set<float>(vertex, ss->attrs.automasking_factor, 1.0f);
*/
template<typename T>
static void vertex_attr_set(const PBVHVertRef vertex, const SculptAttribute *attr, T data)
{
*vertex_attr_ptr<T>(vertex, attr) = data;
}
template<typename T> static T *face_attr_ptr(const PBVHFaceRef face, const SculptAttribute *attr)
{
return elem_attr_ptr<T, PBVHFaceRef>(face, attr);
}
template<typename T> static T face_attr_get(const PBVHFaceRef face, const SculptAttribute *attr)
{
return *face_attr_ptr<T>(face, attr);
}
template<typename T>
static void face_attr_set(const PBVHFaceRef face, const SculptAttribute *attr, T data)
{
*face_attr_ptr<T>(face, attr) = data;
}
} // namespace blender::bke::paint
#endif

View File

@ -1652,11 +1652,11 @@ static void sculpt_update_persistent_base(Object *ob)
SculptSession *ss = ob->sculpt;
ss->attrs.persistent_co = BKE_sculpt_attribute_get(
ob, ATTR_DOMAIN_POINT, CD_PROP_FLOAT3, SCULPT_ATTRIBUTE_NAME(persistent_co));
ob, ATTR_DOMAIN_POINT, CD_PROP_FLOAT3, SCULPT_ATTRIBUTE_NAME(persistent_co), nullptr);
ss->attrs.persistent_no = BKE_sculpt_attribute_get(
ob, ATTR_DOMAIN_POINT, CD_PROP_FLOAT3, SCULPT_ATTRIBUTE_NAME(persistent_no));
ob, ATTR_DOMAIN_POINT, CD_PROP_FLOAT3, SCULPT_ATTRIBUTE_NAME(persistent_no), nullptr);
ss->attrs.persistent_disp = BKE_sculpt_attribute_get(
ob, ATTR_DOMAIN_POINT, CD_PROP_FLOAT, SCULPT_ATTRIBUTE_NAME(persistent_disp));
ob, ATTR_DOMAIN_POINT, CD_PROP_FLOAT, SCULPT_ATTRIBUTE_NAME(persistent_disp), nullptr);
}
static void sculpt_update_object(
@ -2574,7 +2574,9 @@ static bool sculpt_attr_update(Object *ob, SculptAttribute *attr)
attr->bmesh_cd_offset = cdata->layers[layer_index].offset;
}
else {
attr->data = cdata->layers[layer_index].data;
attr->data = attr->params.read_only ?
cdata->layers[layer_index].data :
CustomData_get_layer_for_write(cdata, attr->proptype, elem_num);
}
}
}
@ -2650,21 +2652,14 @@ static SculptAttribute *sculpt_alloc_attr(SculptSession *ss)
SculptAttribute *BKE_sculpt_attribute_get(struct Object *ob,
eAttrDomain domain,
eCustomDataType proptype,
const char *name)
const char *name,
const SculptAttributeParams *params)
{
SculptSession *ss = ob->sculpt;
/* See if attribute is cached in ss->temp_attributes. */
SculptAttribute *attr = sculpt_get_cached_layer(ss, domain, proptype, name);
if (attr) {
if (sculpt_attr_update(ob, attr)) {
sculpt_attribute_update_refs(ob);
}
return attr;
}
/* Does attribute exist in CustomData layout? */
CustomData *cdata = sculpt_get_cdata(ob, domain);
if (cdata) {
@ -2687,7 +2682,14 @@ SculptAttribute *BKE_sculpt_attribute_get(struct Object *ob,
attr = sculpt_alloc_attr(ss);
attr->used = true;
if (params) {
attr->params = *params;
}
else {
attr->params = {};
}
attr->params.permanent = !(cdata->layers[index].flag & CD_FLAG_TEMPORARY);
attr->domain = domain;
attr->proptype = proptype;
attr->data = cdata->layers[index].data;
@ -2697,10 +2699,17 @@ SculptAttribute *BKE_sculpt_attribute_get(struct Object *ob,
attr->elem_size = CustomData_get_elem_size(attr->layer);
STRNCPY_UTF8(attr->name, name);
return attr;
}
}
if (attr) {
if (sculpt_attr_update(ob, attr)) {
sculpt_attribute_update_refs(ob);
}
return attr;
}
return nullptr;
}
@ -2713,7 +2722,7 @@ static SculptAttribute *sculpt_attribute_ensure_ex(Object *ob,
bool flat_array_for_bmesh)
{
SculptSession *ss = ob->sculpt;
SculptAttribute *attr = BKE_sculpt_attribute_get(ob, domain, proptype, name);
SculptAttribute *attr = BKE_sculpt_attribute_get(ob, domain, proptype, name, params);
if (attr) {
sculpt_attr_update(ob, attr);
@ -2837,12 +2846,9 @@ void BKE_sculpt_attribute_destroy_temporary_all(Object *ob)
}
}
bool BKE_sculpt_attribute_destroy(Object *ob, SculptAttribute *attr)
void BKE_sculpt_attribute_release(struct Object *ob, SculptAttribute *attr)
{
SculptSession *ss = ob->sculpt;
eAttrDomain domain = attr->domain;
BLI_assert(attr->used);
/* Remove from convenience pointer struct. */
SculptAttribute **ptrs = (SculptAttribute **)&ss->attrs;
@ -2854,7 +2860,18 @@ bool BKE_sculpt_attribute_destroy(Object *ob, SculptAttribute *attr)
}
}
/* Remove from internal temp_attributes array. */
attr->used = false;
attr->data = nullptr;
}
bool BKE_sculpt_attribute_destroy(Object *ob, SculptAttribute *attr)
{
SculptSession *ss = ob->sculpt;
eAttrDomain domain = attr->domain;
BLI_assert(attr->used);
/* Remove any duplicates from internal temp_attributes array. */
for (int i = 0; i < SCULPT_MAX_ATTRIBUTES; i++) {
SculptAttribute *attr2 = ss->temp_attributes + i;
@ -2901,12 +2918,12 @@ bool BKE_sculpt_attribute_destroy(Object *ob, SculptAttribute *attr)
if (layer_i != 0) {
CustomData_free_layer(cdata, attr->proptype, totelem, layer_i);
}
sculpt_attribute_update_refs(ob);
}
attr->data = nullptr;
attr->used = false;
BKE_sculpt_attribute_release(ob, attr);
/* Make sure the remaining SculptAttributes are valid. */
sculpt_attribute_update_refs(ob);
return true;
}

View File

@ -210,7 +210,7 @@ void SCULPT_vertex_normal_get(SculptSession *ss, PBVHVertRef vertex, float no[3]
const float *SCULPT_vertex_persistent_co_get(SculptSession *ss, PBVHVertRef vertex)
{
if (ss->attrs.persistent_co) {
return (const float *)SCULPT_vertex_attr_get(vertex, ss->attrs.persistent_co);
return blender::bke::paint::vertex_attr_ptr<const float>(vertex, ss->attrs.persistent_co);
}
return SCULPT_vertex_co_get(ss, vertex);
@ -258,7 +258,7 @@ void SCULPT_vertex_limit_surface_get(SculptSession *ss, PBVHVertRef vertex, floa
void SCULPT_vertex_persistent_normal_get(SculptSession *ss, PBVHVertRef vertex, float no[3])
{
if (ss->attrs.persistent_no) {
copy_v3_v3(no, (float *)SCULPT_vertex_attr_get(vertex, ss->attrs.persistent_no));
copy_v3_v3(no, blender::bke::paint::vertex_attr_ptr<float>(vertex, ss->attrs.persistent_no));
return;
}
SCULPT_vertex_normal_get(ss, vertex, no);
@ -6354,7 +6354,7 @@ void SCULPT_face_set_set(SculptSession *ss, PBVHFaceRef face, int fset)
int SCULPT_vertex_island_get(SculptSession *ss, PBVHVertRef vertex)
{
if (ss->attrs.topology_island_key) {
return *static_cast<uint8_t *>(SCULPT_vertex_attr_get(vertex, ss->attrs.topology_island_key));
return blender::bke::paint::vertex_attr_get<uint8_t>(vertex, ss->attrs.topology_island_key);
}
return -1;
@ -6401,8 +6401,8 @@ void SCULPT_topology_islands_ensure(Object *ob)
PBVHVertRef vertex2 = stack.pop_last();
SculptVertexNeighborIter ni;
*static_cast<uint8_t *>(
SCULPT_vertex_attr_get(vertex2, ss->attrs.topology_island_key)) = island_nr;
blender::bke::paint::vertex_attr_set<uint8_t>(
vertex2, ss->attrs.topology_island_key, island_nr);
SCULPT_VERTEX_NEIGHBORS_ITER_BEGIN (ss, vertex2, ni) {
if (visit.add(ni.vertex) && SCULPT_vertex_any_face_visible_get(ss, ni.vertex)) {

View File

@ -266,10 +266,10 @@ static float automasking_view_occlusion_factor(AutomaskingCache *automasking,
uchar stroke_id,
AutomaskingNodeData * /*automask_data*/)
{
char f = *(char *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_occlusion);
char f = blender::bke::paint::vertex_attr_get<char>(vertex, ss->attrs.automasking_occlusion);
if (stroke_id != automasking->current_stroke_id) {

Can this use vertex_attr_get instead of *vertex_attr_ptr?

Same with a few places below.

Can this use `vertex_attr_get` instead of `*vertex_attr_ptr`? Same with a few places below.
f = *(char *)SCULPT_vertex_attr_get(
f = *blender::bke::paint::vertex_attr_ptr<char>(
vertex,
ss->attrs.automasking_occlusion) = SCULPT_vertex_is_occluded(ss, vertex, true) ? 2 : 1;
}
@ -284,8 +284,8 @@ static float automasking_factor_end(SculptSession *ss,
float value)
{
if (ss->attrs.automasking_stroke_id) {
*(uchar *)SCULPT_vertex_attr_get(
vertex, ss->attrs.automasking_stroke_id) = automasking->current_stroke_id;
blender::bke::paint::vertex_attr_set<uchar>(
vertex, ss->attrs.automasking_stroke_id, automasking->current_stroke_id);
}
return value;
@ -445,7 +445,7 @@ static void sculpt_calc_blurred_cavity(SculptSession *ss,
factor_sum = sculpt_cavity_calc_factor(automasking, factor_sum);
*(float *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_cavity) = factor_sum;
blender::bke::paint::vertex_attr_set<float>(vertex, ss->attrs.automasking_cavity, factor_sum);
}
int SCULPT_automasking_settings_hash(Object *ob, AutomaskingCache *automasking)
@ -499,13 +499,14 @@ static float sculpt_automasking_cavity_factor(AutomaskingCache *automasking,
SculptSession *ss,
PBVHVertRef vertex)
{
uchar stroke_id = *(uchar *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_stroke_id);
uchar stroke_id = blender::bke::paint::vertex_attr_get<uchar>(vertex,
ss->attrs.automasking_stroke_id);
if (stroke_id != automasking->current_stroke_id) {
sculpt_calc_blurred_cavity(ss, automasking, automasking->settings.cavity_blur_steps, vertex);
}
float factor = *(float *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_cavity);
float factor = blender::bke::paint::vertex_attr_get<float>(vertex, ss->attrs.automasking_cavity);
bool inverted = automasking->settings.flags & BRUSH_AUTOMASKING_CAVITY_INVERTED;
if ((automasking->settings.flags & BRUSH_AUTOMASKING_CAVITY_ALL) &&
@ -542,7 +543,7 @@ float SCULPT_automasking_factor_get(AutomaskingCache *automasking,
* automasking information can't be computed in real time per vertex and needs to be
* initialized for the whole mesh when the stroke starts. */
if (ss->attrs.automasking_factor) {
float factor = *(float *)SCULPT_vertex_attr_get(vert, ss->attrs.automasking_factor);
float factor = blender::bke::paint::vertex_attr_get<float>(vert, ss->attrs.automasking_factor);
if (automasking->settings.flags & BRUSH_AUTOMASKING_CAVITY_ALL) {
factor *= sculpt_automasking_cavity_factor(automasking, ss, vert);
@ -551,9 +552,9 @@ float SCULPT_automasking_factor_get(AutomaskingCache *automasking,
return automasking_factor_end(ss, automasking, vert, factor * mask);
}
uchar stroke_id = ss->attrs.automasking_stroke_id ?
*(uchar *)SCULPT_vertex_attr_get(vert, ss->attrs.automasking_stroke_id) :
-1;
uchar stroke_id = ss->attrs.automasking_stroke_id ? blender::bke::paint::vertex_attr_get<uchar>(
vert, ss->attrs.automasking_stroke_id) :
-1;
bool do_occlusion = (automasking->settings.flags &
(BRUSH_AUTOMASKING_VIEW_OCCLUSION | BRUSH_AUTOMASKING_VIEW_NORMAL)) ==
@ -627,8 +628,8 @@ static bool automask_floodfill_cb(
{
AutomaskFloodFillData *data = (AutomaskFloodFillData *)userdata;
*(float *)SCULPT_vertex_attr_get(to_v, ss->attrs.automasking_factor) = 1.0f;
*(float *)SCULPT_vertex_attr_get(from_v, ss->attrs.automasking_factor) = 1.0f;
blender::bke::paint::vertex_attr_set<float>(to_v, ss->attrs.automasking_factor, 1.0f);
blender::bke::paint::vertex_attr_set<float>(from_v, ss->attrs.automasking_factor, 1.0f);
return (!data->use_radius ||
SCULPT_is_vertex_inside_brush_radius_symm(
SCULPT_vertex_co_get(ss, to_v), data->location, data->radius, data->symm));
@ -648,7 +649,7 @@ static void SCULPT_topology_automasking_init(Sculpt *sd, Object *ob)
for (int i : IndexRange(totvert)) {
PBVHVertRef vertex = BKE_pbvh_index_to_vertex(ss->pbvh, i);
(*(float *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_factor)) = 0.0f;
blender::bke::paint::vertex_attr_set<float>(vertex, ss->attrs.automasking_factor, 0.0f);
}
/* Flood fill automask to connected vertices. Limited to vertices inside
@ -689,7 +690,7 @@ static void sculpt_face_sets_automasking_init(Sculpt *sd, Object *ob)
PBVHVertRef vertex = BKE_pbvh_index_to_vertex(ss->pbvh, i);
if (!SCULPT_vertex_has_face_set(ss, vertex, active_face_set)) {
*(float *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_factor) = 0.0f;
blender::bke::paint::vertex_attr_set<float>(vertex, ss->attrs.automasking_factor, 0.0f);
}
}
}
@ -754,7 +755,7 @@ static void SCULPT_boundary_automasking_init(Object *ob,
const float p = 1.0f - (float(edge_distance[i]) / float(propagation_steps));
const float edge_boundary_automask = pow2f(p);
*(float *)SCULPT_vertex_attr_get(
*blender::bke::paint::vertex_attr_ptr<float>(
vertex, ss->attrs.automasking_factor) *= (1.0f - edge_boundary_automask);
}
@ -800,7 +801,7 @@ static void sculpt_normal_occlusion_automasking_fill(AutomaskingCache *automaski
for (int i = 0; i < totvert; i++) {
PBVHVertRef vertex = BKE_pbvh_index_to_vertex(ss->pbvh, i);
float f = *(float *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_factor);
float f = blender::bke::paint::vertex_attr_get<float>(vertex, ss->attrs.automasking_factor);
if (int(mode) & BRUSH_AUTOMASKING_VIEW_NORMAL) {
if (int(mode) & BRUSH_AUTOMASKING_VIEW_OCCLUSION) {
@ -811,10 +812,11 @@ static void sculpt_normal_occlusion_automasking_fill(AutomaskingCache *automaski
}
if (ss->attrs.automasking_stroke_id) {
*(uchar *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_stroke_id) = ss->stroke_id;
blender::bke::paint::vertex_attr_set<uchar>(
vertex, ss->attrs.automasking_stroke_id, ss->stroke_id);
}
*(float *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_factor) = f;
blender::bke::paint::vertex_attr_set<float>(vertex, ss->attrs.automasking_factor, f);
}
}
@ -936,7 +938,8 @@ AutomaskingCache *SCULPT_automasking_cache_init(Sculpt *sd, Brush *brush, Object
for (int i : IndexRange(totvert)) {
PBVHVertRef vertex = BKE_pbvh_index_to_vertex(ss->pbvh, i);
(*(float *)SCULPT_vertex_attr_get(vertex, ss->attrs.automasking_factor)) = initial_value;
blender::bke::paint::vertex_attr_set<float>(
vertex, ss->attrs.automasking_factor, initial_value);
}
const int boundary_propagation_steps = brush ?

View File

@ -1550,7 +1550,8 @@ static void do_layer_brush_task_cb_ex(void *__restrict userdata,
const int vi = vd.index;
float *disp_factor;
if (use_persistent_base) {
disp_factor = (float *)SCULPT_vertex_attr_get(vd.vertex, ss->attrs.persistent_disp);
disp_factor = blender::bke::paint::vertex_attr_ptr<float>(vd.vertex,
ss->attrs.persistent_disp);
}
else {
disp_factor = &ss->cache->layer_displacement_factor[vi];

View File

@ -1913,8 +1913,3 @@ void SCULPT_topology_islands_invalidate(SculptSession *ss);
int SCULPT_vertex_island_get(SculptSession *ss, PBVHVertRef vertex);
/** \} */
/* Make SCULPT_ alias to a few blenkernel sculpt methods. */
#define SCULPT_vertex_attr_get BKE_sculpt_vertex_attr_get
#define SCULPT_face_attr_get BKE_sculpt_face_attr_get

View File

@ -11,6 +11,7 @@
#include "BLI_ghash.h"
#include "BLI_gsqueue.h"
#include "BLI_math.h"
#include "BLI_math_vector_types.hh"
#include "BLI_task.h"
#include "BLI_utildefines.h"
@ -71,6 +72,8 @@
#include <cstdlib>
#include <cstring>
using blender::float3;
/* Reset the copy of the mesh that is being sculpted on (currently just for the layer brush). */
static int sculpt_set_persistent_base_exec(bContext *C, wmOperator * /*op*/)
@ -101,11 +104,11 @@ static int sculpt_set_persistent_base_exec(bContext *C, wmOperator * /*op*/)
for (int i = 0; i < totvert; i++) {
PBVHVertRef vertex = BKE_pbvh_index_to_vertex(ss->pbvh, i);
copy_v3_v3((float *)SCULPT_vertex_attr_get(vertex, ss->attrs.persistent_co),
SCULPT_vertex_co_get(ss, vertex));
blender::bke::paint::vertex_attr_set<float3>(
vertex, ss->attrs.persistent_co, SCULPT_vertex_co_get(ss, vertex));
SCULPT_vertex_normal_get(
ss, vertex, (float *)SCULPT_vertex_attr_get(vertex, ss->attrs.persistent_no));
(*(float *)SCULPT_vertex_attr_get(vertex, ss->attrs.persistent_disp)) = 0.0f;
ss, vertex, blender::bke::paint::vertex_attr_ptr<float>(vertex, ss->attrs.persistent_no));
blender::bke::paint::vertex_attr_set<float>(vertex, ss->attrs.persistent_disp, 0.0f);
}
return OPERATOR_FINISHED;