Sculpt: Elastic Deform Brush

This patch implements the paper "Regularized Kelvinlets: Sculpting Brushes based on Fundamental Solutions of Elasticity" https://graphics.pixar.com/library/Kelvinlets/paper.pdf

It includes grab, biscale grab, triscale grab, scale and twist.
All deformation modes are accessible under the same tool. This helps to keep the code organized and it should not make any difference to the user when a better brush management system is implemented.

Reviewed By: brecht

Differential Revision: https://developer.blender.org/D5634
This commit is contained in:
Pablo Dobarro 2019-09-06 23:14:57 +02:00
parent 8127bbbe39
commit 70c1aaf59b
7 changed files with 349 additions and 11 deletions

View File

@ -369,6 +369,13 @@ class VIEW3D_PT_tools_brush(Panel, View3DPaintPanel):
row = col.row()
row.prop(brush, "normal_radius_factor", slider=True)
if brush.sculpt_tool == 'ELASTIC_DEFORM':
col.separator()
row = col.row()
row.prop(brush, "elastic_deform_type")
row = col.row()
row.prop(brush, "elastic_deform_compressibility", slider=True)
# topology_rake_factor
if (
capabilities.has_topology_rake and

View File

@ -901,6 +901,7 @@ void BKE_brush_sculpt_reset(Brush *br)
br->add_col[2] = 0.750000;
break;
case SCULPT_TOOL_GRAB:
case SCULPT_TOOL_ELASTIC_DEFORM:
case SCULPT_TOOL_SNAKE_HOOK:
case SCULPT_TOOL_THUMB:
br->size = 75;

View File

@ -3834,5 +3834,12 @@ void blo_do_versions_280(FileData *fd, Library *UNUSED(lib), Main *bmain)
}
}
}
/* Elatic deform brush */
for (Brush *br = bmain->brushes.first; br; br = br->id.next) {
if (br->ob_mode & OB_MODE_SCULPT && br->elastic_deform_compressibility == 0.0f) {
br->elastic_deform_compressibility = 0.5f;
}
}
}
}

View File

@ -221,6 +221,7 @@ static bool paint_tool_require_location(Brush *brush, ePaintMode mode)
case PAINT_MODE_SCULPT:
if (ELEM(brush->sculpt_tool,
SCULPT_TOOL_GRAB,
SCULPT_TOOL_ELASTIC_DEFORM,
SCULPT_TOOL_ROTATE,
SCULPT_TOOL_SNAKE_HOOK,
SCULPT_TOOL_THUMB)) {
@ -251,7 +252,11 @@ static bool paint_tool_require_inbetween_mouse_events(Brush *brush, ePaintMode m
{
switch (mode) {
case PAINT_MODE_SCULPT:
if (ELEM(brush->sculpt_tool, SCULPT_TOOL_GRAB, SCULPT_TOOL_ROTATE, SCULPT_TOOL_THUMB)) {
if (ELEM(brush->sculpt_tool,
SCULPT_TOOL_GRAB,
SCULPT_TOOL_ROTATE,
SCULPT_TOOL_THUMB,
SCULPT_TOOL_ELASTIC_DEFORM)) {
return false;
}
else {
@ -949,6 +954,7 @@ static bool sculpt_is_grab_tool(Brush *br)
{
return ELEM(br->sculpt_tool,
SCULPT_TOOL_GRAB,
SCULPT_TOOL_ELASTIC_DEFORM,
SCULPT_TOOL_THUMB,
SCULPT_TOOL_ROTATE,
SCULPT_TOOL_SNAKE_HOOK);

View File

@ -358,7 +358,8 @@ static bool sculpt_tool_needs_original(const char sculpt_tool)
SCULPT_TOOL_ROTATE,
SCULPT_TOOL_THUMB,
SCULPT_TOOL_LAYER,
SCULPT_TOOL_DRAW_SHARP);
SCULPT_TOOL_DRAW_SHARP,
SCULPT_TOOL_ELASTIC_DEFORM);
}
static bool sculpt_tool_is_proxy_used(const char sculpt_tool)
@ -388,6 +389,7 @@ static int sculpt_brush_needs_normal(const SculptSession *ss, const Brush *brush
SCULPT_TOOL_LAYER,
SCULPT_TOOL_NUDGE,
SCULPT_TOOL_ROTATE,
SCULPT_TOOL_ELASTIC_DEFORM,
SCULPT_TOOL_THUMB) ||
(brush->mtex.brush_map_mode == MTEX_MAP_MODE_AREA)) ||
@ -1445,6 +1447,9 @@ static float brush_strength(const Sculpt *sd,
case SCULPT_TOOL_ROTATE:
return alpha * pressure * feather;
case SCULPT_TOOL_ELASTIC_DEFORM:
return root_alpha * feather;
default:
return 0;
}
@ -2918,6 +2923,252 @@ static void do_grab_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode)
BLI_task_parallel_range(0, totnode, &data, do_grab_brush_task_cb_ex, &settings);
}
/* Regularized Kelvinlets: Sculpting Brushes based on Fundamental Solutions of Elasticity
* Pixar Technical Memo #17-03 */
typedef struct KelvinletParams {
float f;
float a;
float b;
float c;
float radius_scaled;
} KelvinletParams;
static int sculpt_kelvinlet_get_scale_iteration_count(eBrushElasticDeformType type)
{
if (type == BRUSH_ELASTIC_DEFORM_GRAB) {
return 1;
}
if (type == BRUSH_ELASTIC_DEFORM_GRAB_BISCALE) {
return 2;
}
if (type == BRUSH_ELASTIC_DEFORM_GRAB_TRISCALE) {
return 3;
}
return 0;
}
static void sculpt_kelvinet_integrate(void (*kelvinlet)(float disp[3],
const float vertex_co[3],
const float location[3],
float normal[3],
KelvinletParams *p),
float r_disp[3],
const float vertex_co[3],
const float location[3],
float normal[3],
KelvinletParams *p)
{
float k[4][3], k_it[4][3];
kelvinlet(k[0], vertex_co, location, normal, p);
copy_v3_v3(k_it[0], k[0]);
mul_v3_fl(k_it[0], 0.5f);
add_v3_v3v3(k_it[0], vertex_co, k_it[0]);
kelvinlet(k[1], k_it[0], location, normal, p);
copy_v3_v3(k_it[1], k[1]);
mul_v3_fl(k_it[1], 0.5f);
add_v3_v3v3(k_it[1], vertex_co, k_it[1]);
kelvinlet(k[2], k_it[1], location, normal, p);
copy_v3_v3(k_it[2], k[2]);
add_v3_v3v3(k_it[2], vertex_co, k_it[2]);
sub_v3_v3v3(k_it[2], k_it[2], location);
kelvinlet(k[3], k_it[2], location, normal, p);
copy_v3_v3(r_disp, k[0]);
madd_v3_v3fl(r_disp, k[1], 2);
madd_v3_v3fl(r_disp, k[2], 2);
add_v3_v3(r_disp, k[3]);
mul_v3_fl(r_disp, 1.0f / 6.0f);
}
/* Regularized Kelvinlets: Formula (16) */
static void sculpt_kelvinlet_scale(float disp[3],
const float vertex_co[3],
const float location[3],
float UNUSED(normal[3]),
KelvinletParams *p)
{
float r_v[3];
sub_v3_v3v3(r_v, vertex_co, location);
float r = len_v3(r_v);
float r_e = sqrtf(r * r + p->radius_scaled * p->radius_scaled);
float u = (2.0f * p->b - p->a) * ((1.0f / (r_e * r_e * r_e))) +
((3.0f * p->radius_scaled * p->radius_scaled) / (2.0f * r_e * r_e * r_e * r_e * r_e));
float fade = u * p->c;
mul_v3_v3fl(disp, r_v, fade * p->f);
}
/* Regularized Kelvinlets: Formula (15) */
static void sculpt_kelvinlet_twist(float disp[3],
const float vertex_co[3],
const float location[3],
float normal[3],
KelvinletParams *p)
{
float r_v[3], q_r[3];
sub_v3_v3v3(r_v, vertex_co, location);
float r = len_v3(r_v);
float r_e = sqrtf(r * r + p->radius_scaled * p->radius_scaled);
float u = -p->a * ((1.0f / (r_e * r_e * r_e))) +
((3.0f * p->radius_scaled * p->radius_scaled) / (2.0f * r_e * r_e * r_e * r_e * r_e));
float fade = u * p->c;
cross_v3_v3v3(q_r, normal, r_v);
mul_v3_v3fl(disp, q_r, fade * p->f);
}
static void do_elastic_deform_brush_task_cb_ex(void *__restrict userdata,
const int n,
const TaskParallelTLS *__restrict tls)
{
SculptThreadedTaskData *data = userdata;
SculptSession *ss = data->ob->sculpt;
const Brush *brush = data->brush;
const float *grab_delta = data->grab_delta;
const float *location = ss->cache->location;
PBVHVertexIter vd;
SculptOrigVertData orig_data;
float(*proxy)[3];
const float bstrength = ss->cache->bstrength;
sculpt_orig_vert_data_init(&orig_data, data->ob, data->nodes[n]);
proxy = BKE_pbvh_node_add_proxy(ss->pbvh, data->nodes[n])->co;
/* Maybe this can be exposed to the user */
float radius_e[3] = {1.0f, 2.0f, 2.0f};
float r_e[3];
float kvl[3];
float radius_scaled[3];
radius_scaled[0] = ss->cache->radius * radius_e[0];
radius_scaled[1] = radius_scaled[0] * radius_e[1];
radius_scaled[2] = radius_scaled[1] * radius_e[2];
float shear_modulus = 1.0f;
float poisson_ratio = brush->elastic_deform_compressibility;
float a = 1.0f / (4.0f * (float)M_PI * shear_modulus);
float b = a / (4.0f * (1.0f - poisson_ratio));
float c = 2 * (3.0f * a - 2.0f * b);
float dir;
if (ss->cache->mouse[0] > ss->cache->initial_mouse[0]) {
dir = 1.0f;
}
else {
dir = -1.0f;
}
if (brush->elastic_deform_type == BRUSH_ELASTIC_DEFORM_TWIST) {
int symm = ss->cache->mirror_symmetry_pass;
if (symm == 1 || symm == 2 || symm == 4 || symm == 7) {
dir = -dir;
}
}
BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE)
{
sculpt_orig_vert_data_update(&orig_data, &vd);
float fade, u, final_disp[3], weights[3];
float r = len_v3v3(location, orig_data.co);
KelvinletParams params;
params.a = a;
params.b = b;
params.c = c;
params.radius_scaled = radius_scaled[0];
int multi_scale_it = sculpt_kelvinlet_get_scale_iteration_count(brush->elastic_deform_type);
for (int it = 0; it < max_ii(1, multi_scale_it); it++) {
r_e[it] = sqrtf(r * r + radius_scaled[it] * radius_scaled[it]);
}
/* Regularized Kelvinlets: Formula (6) */
for (int s_it = 0; s_it < multi_scale_it; s_it++) {
kvl[s_it] = ((a - b) / r_e[s_it]) + ((b * r * r) / (r_e[s_it] * r_e[s_it] * r_e[s_it])) +
((a * radius_scaled[s_it] * radius_scaled[s_it]) /
(2.0f * r_e[s_it] * r_e[s_it] * r_e[s_it]));
}
switch (brush->elastic_deform_type) {
/* Regularized Kelvinlets: Multi-scale extrapolation. Formula (11) */
case BRUSH_ELASTIC_DEFORM_GRAB:
fade = kvl[0] * c;
mul_v3_v3fl(final_disp, grab_delta, fade * bstrength * 20.f);
break;
case BRUSH_ELASTIC_DEFORM_GRAB_BISCALE:
u = kvl[0] - kvl[1];
fade = u * c / ((1.0f / radius_scaled[0]) - (1.0f / radius_scaled[1]));
mul_v3_v3fl(final_disp, grab_delta, fade * bstrength * 20.0f);
break;
case BRUSH_ELASTIC_DEFORM_GRAB_TRISCALE:
weights[0] = 1.0f;
weights[1] = -(
(radius_scaled[2] * radius_scaled[2] - radius_scaled[0] * radius_scaled[0]) /
(radius_scaled[2] * radius_scaled[2] - radius_scaled[1] * radius_scaled[1]));
weights[2] = ((radius_scaled[1] * radius_scaled[1] - radius_scaled[0] * radius_scaled[0]) /
(radius_scaled[2] * radius_scaled[2] - radius_scaled[1] * radius_scaled[1]));
float u = weights[0] * kvl[0] + weights[1] * kvl[1] + weights[2] * kvl[2];
fade = u * c /
(weights[0] / radius_scaled[0] + weights[1] / radius_scaled[1] +
weights[2] / radius_scaled[2]);
mul_v3_v3fl(final_disp, grab_delta, fade * bstrength * 20.0f);
break;
case BRUSH_ELASTIC_DEFORM_SCALE:
params.f = len_v3(grab_delta) * dir * bstrength;
sculpt_kelvinet_integrate(sculpt_kelvinlet_scale,
final_disp,
orig_data.co,
location,
ss->cache->sculpt_normal_symm,
&params);
break;
case BRUSH_ELASTIC_DEFORM_TWIST:
params.f = len_v3(grab_delta) * dir * bstrength;
sculpt_kelvinet_integrate(sculpt_kelvinlet_twist,
final_disp,
orig_data.co,
location,
ss->cache->sculpt_normal_symm,
&params);
break;
}
if (vd.mask) {
mul_v3_fl(final_disp, 1.0f - *vd.mask);
}
copy_v3_v3(proxy[vd.i], final_disp);
if (vd.mvert) {
vd.mvert->flag |= ME_VERT_PBVH_UPDATE;
}
}
BKE_pbvh_vertex_iter_end;
}
static void do_elastic_deform_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode)
{
SculptSession *ss = ob->sculpt;
Brush *brush = BKE_paint_brush(&sd->paint);
float grab_delta[3];
copy_v3_v3(grab_delta, ss->cache->grab_delta_symmetry);
SculptThreadedTaskData data = {
.sd = sd,
.ob = ob,
.brush = brush,
.nodes = nodes,
.grab_delta = grab_delta,
};
TaskParallelSettings settings;
BLI_parallel_range_settings_defaults(&settings);
settings.use_threading = ((sd->flags & SCULPT_USE_OPENMP) && totnode > SCULPT_THREADED_LIMIT);
BLI_task_parallel_range(0, totnode, &data, do_elastic_deform_brush_task_cb_ex, &settings);
}
static void do_nudge_brush_task_cb_ex(void *__restrict userdata,
const int n,
const TaskParallelTLS *__restrict tls)
@ -4219,13 +4470,26 @@ static void do_brush_action(Sculpt *sd, Object *ob, Brush *brush, UnifiedPaintSe
{
SculptSession *ss = ob->sculpt;
int totnode;
PBVHNode **nodes;
/* Build a list of all nodes that are potentially within the brush's area of influence */
const bool use_original = sculpt_tool_needs_original(brush->sculpt_tool) ? true :
ss->cache->original;
const float radius_scale = 1.0f;
PBVHNode **nodes = sculpt_pbvh_gather_generic(
ob, sd, brush, use_original, radius_scale, &totnode);
/* These brushes need to update all nodes as they are not constrained by the brush radius */
if (brush->sculpt_tool == SCULPT_TOOL_ELASTIC_DEFORM) {
SculptSearchSphereData data = {
.ss = ss,
.sd = sd,
.radius_squared = FLT_MAX,
.original = true,
};
BKE_pbvh_search_gather(ss->pbvh, NULL, &data, &nodes, &totnode);
}
else {
const bool use_original = sculpt_tool_needs_original(brush->sculpt_tool) ? true :
ss->cache->original;
const float radius_scale = 1.0f;
nodes = sculpt_pbvh_gather_generic(ob, sd, brush, use_original, radius_scale, &totnode);
}
/* Only act if some verts are inside the brush area */
if (totnode) {
@ -4309,6 +4573,8 @@ static void do_brush_action(Sculpt *sd, Object *ob, Brush *brush, UnifiedPaintSe
break;
case SCULPT_TOOL_DRAW_SHARP:
do_draw_sharp_brush(sd, ob, nodes, totnode);
case SCULPT_TOOL_ELASTIC_DEFORM:
do_elastic_deform_brush(sd, ob, nodes, totnode);
break;
}
@ -4374,8 +4640,11 @@ static void sculpt_combine_proxies_task_cb(void *__restrict userdata,
Object *ob = data->ob;
/* these brushes start from original coordinates */
const bool use_orco = ELEM(
data->brush->sculpt_tool, SCULPT_TOOL_GRAB, SCULPT_TOOL_ROTATE, SCULPT_TOOL_THUMB);
const bool use_orco = ELEM(data->brush->sculpt_tool,
SCULPT_TOOL_GRAB,
SCULPT_TOOL_ROTATE,
SCULPT_TOOL_THUMB,
SCULPT_TOOL_ELASTIC_DEFORM);
PBVHVertexIter vd;
PBVHProxyNode *proxies;
@ -4817,6 +5086,8 @@ static const char *sculpt_tool_name(Sculpt *sd)
return "Simplify Brush";
case SCULPT_TOOL_DRAW_SHARP:
return "Draw Sharp Brush";
case SCULPT_TOOL_ELASTIC_DEFORM:
return "Elastic Deform Brush";
}
return "Sculpting";
@ -5055,6 +5326,7 @@ static void sculpt_update_brush_delta(UnifiedPaintSettings *ups, Object *ob, Bru
if (ELEM(tool,
SCULPT_TOOL_GRAB,
SCULPT_TOOL_ELASTIC_DEFORM,
SCULPT_TOOL_NUDGE,
SCULPT_TOOL_CLAY_STRIPS,
SCULPT_TOOL_SNAKE_HOOK,
@ -5078,6 +5350,7 @@ static void sculpt_update_brush_delta(UnifiedPaintSettings *ups, Object *ob, Bru
switch (tool) {
case SCULPT_TOOL_GRAB:
case SCULPT_TOOL_THUMB:
case SCULPT_TOOL_ELASTIC_DEFORM:
sub_v3_v3v3(delta, grab_location, cache->old_grab_location);
invert_m4_m4(imat, ob->obmat);
mul_mat3_m4_v3(imat, delta);
@ -5116,11 +5389,14 @@ static void sculpt_update_brush_delta(UnifiedPaintSettings *ups, Object *ob, Bru
if (tool == SCULPT_TOOL_GRAB) {
copy_v3_v3(cache->anchored_location, cache->true_location);
}
else if (tool == SCULPT_TOOL_ELASTIC_DEFORM) {
copy_v3_v3(cache->anchored_location, cache->true_location);
}
else if (tool == SCULPT_TOOL_THUMB) {
copy_v3_v3(cache->anchored_location, cache->orig_grab_location);
}
if (ELEM(tool, SCULPT_TOOL_GRAB, SCULPT_TOOL_THUMB)) {
if (ELEM(tool, SCULPT_TOOL_GRAB, SCULPT_TOOL_THUMB, SCULPT_TOOL_ELASTIC_DEFORM)) {
/* location stays the same for finding vertices in brush radius */
copy_v3_v3(cache->true_location, cache->orig_grab_location);
@ -5636,7 +5912,8 @@ static void sculpt_restore_mesh(Sculpt *sd, Object *ob)
/* Restore the mesh before continuing with anchored stroke */
if ((brush->flag & BRUSH_ANCHORED) ||
(brush->sculpt_tool == SCULPT_TOOL_GRAB &&
((brush->sculpt_tool == SCULPT_TOOL_GRAB ||
brush->sculpt_tool == SCULPT_TOOL_ELASTIC_DEFORM) &&
BKE_brush_use_size_pressure(ss->cache->vc->scene, brush)) ||
(brush->flag & BRUSH_DRAG_DOT)) {
paint_mesh_restore_co(sd, ob);

View File

@ -198,6 +198,14 @@ typedef enum eBrushCurvePreset {
BRUSH_CURVE_CONSTANT = 8,
} eBrushCurvePreset;
typedef enum eBrushElasticDeformType {
BRUSH_ELASTIC_DEFORM_GRAB = 0,
BRUSH_ELASTIC_DEFORM_GRAB_BISCALE = 1,
BRUSH_ELASTIC_DEFORM_GRAB_TRISCALE = 2,
BRUSH_ELASTIC_DEFORM_SCALE = 3,
BRUSH_ELASTIC_DEFORM_TWIST = 4,
} eBrushElasticDeformType;
typedef struct Brush {
ID id;
@ -305,6 +313,9 @@ typedef struct Brush {
int curve_preset;
int elastic_deform_type;
float elastic_deform_compressibility;
/* overlay */
int texture_overlay_alpha;
int mask_overlay_alpha;
@ -454,6 +465,7 @@ typedef enum eBrushSculptTool {
SCULPT_TOOL_CLAY_STRIPS = 18,
SCULPT_TOOL_MASK = 19,
SCULPT_TOOL_DRAW_SHARP = 20,
SCULPT_TOOL_ELASTIC_DEFORM = 21,
} eBrushSculptTool;
/* Brush.uv_sculpt_tool */
@ -488,6 +500,7 @@ typedef enum eBrushUVSculptTool {
SCULPT_TOOL_THUMB, \
SCULPT_TOOL_LAYER, \
SCULPT_TOOL_DRAW_SHARP, \
SCULPT_TOOL_ELASTIC_DEFORM, \
\
/* These brushes could handle dynamic topology, \
* but user feedback indicates it's better not to */ \

View File

@ -92,6 +92,7 @@ const EnumPropertyItem rna_enum_brush_sculpt_tool_items[] = {
{SCULPT_TOOL_SIMPLIFY, "SIMPLIFY", ICON_BRUSH_DATA, "Simplify", ""},
{SCULPT_TOOL_MASK, "MASK", ICON_BRUSH_MASK, "Mask", ""},
{SCULPT_TOOL_DRAW_SHARP, "DRAW_SHARP", ICON_BRUSH_SCULPT_DRAW, "Draw Sharp", ""},
{SCULPT_TOOL_ELASTIC_DEFORM, "ELASTIC_DEFORM", ICON_BRUSH_GRAB, "Elastic Deform", ""},
{0, NULL, 0, NULL, NULL},
};
@ -1595,6 +1596,19 @@ static void rna_def_brush(BlenderRNA *brna)
{0, NULL, 0, NULL, NULL},
};
static const EnumPropertyItem brush_elastic_deform_type_items[] = {
{BRUSH_ELASTIC_DEFORM_GRAB, "ELASTIC_DEFORM_GRAB", 0, "Grab", ""},
{BRUSH_ELASTIC_DEFORM_GRAB_BISCALE, "ELASTIC_DEFORM_GRAB_BISCALE", 0, "Bi-scale Grab", ""},
{BRUSH_ELASTIC_DEFORM_GRAB_TRISCALE,
"ELASTIC_DEFORM_GRAB_TRISCALE",
0,
"Tri-scale Grab",
""},
{BRUSH_ELASTIC_DEFORM_SCALE, "ELASTIC_DEFORM_SCALE", 0, "Scale", ""},
{BRUSH_ELASTIC_DEFORM_TWIST, "ELASTIC_DEFORM_TWIST", 0, "Twist", ""},
{0, NULL, 0, NULL, NULL},
};
srna = RNA_def_struct(brna, "Brush", "ID");
RNA_def_struct_ui_text(
srna, "Brush", "Brush data-block for storing brush settings for painting and sculpting");
@ -1675,6 +1689,11 @@ static void rna_def_brush(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Curve Preset", "");
RNA_def_property_update(prop, 0, "rna_Brush_update");
prop = RNA_def_property(srna, "elastic_deform_type", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_items(prop, brush_elastic_deform_type_items);
RNA_def_property_ui_text(prop, "Deformation", "Deformation type that is used in the brush");
RNA_def_property_update(prop, 0, "rna_Brush_update");
/* number values */
prop = RNA_def_property(srna, "size", PROP_INT, PROP_PIXEL);
RNA_def_property_int_funcs(prop, NULL, "rna_Brush_set_size", NULL);
@ -1810,6 +1829,14 @@ static void rna_def_brush(BlenderRNA *brna)
prop, "Normal Weight", "How much grab will pull vertexes out of surface during a grab");
RNA_def_property_update(prop, 0, "rna_Brush_update");
prop = RNA_def_property(srna, "elastic_deform_compressibility", PROP_FLOAT, PROP_NONE);
RNA_def_property_float_sdna(prop, NULL, "elastic_deform_compressibility");
RNA_def_property_range(prop, 0.0f, 1.0f);
RNA_def_property_ui_range(prop, 0.0f, 1.0f, 0.01f, 3);
RNA_def_property_ui_text(
prop, "Compressibility", "Material compressibility when simulating the elasticity");
RNA_def_property_update(prop, 0, "rna_Brush_update");
prop = RNA_def_property(srna, "rake_factor", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_float_sdna(prop, NULL, "rake_factor");
RNA_def_property_float_default(prop, 0);