Paint: Add pressure mapping curves for size, jitter, and strength

This commit adds support for users to customize individual pressure
mapping curves for size, strength, and position jitter on a per-brush
basis to improve expression when using drawing tablets. This feature is
already implemented in Grease Pencil, and this PR adds the same
functionality to Texture/Image paint, Vertex/Weight paint, and Sculpt
modes.

The UI and functionality are the same as those in Grease Pencil.

Co-authored-by: Sean Kim <SeanCTKim@protonmail.com>
Pull Request: https://projects.blender.org/blender/blender/pulls/144428
This commit is contained in:
Toby Yang
2025-09-03 05:34:38 +02:00
committed by Sean Kim
parent 848ced8fe6
commit 5f8311f596
11 changed files with 124 additions and 13 deletions

View File

@@ -562,6 +562,9 @@ class StrokePanel(BrushPanel):
row.prop(brush, "jitter_absolute") row.prop(brush, "jitter_absolute")
row.prop(brush, "use_pressure_jitter", toggle=True, text="") row.prop(brush, "use_pressure_jitter", toggle=True, text="")
col.row().prop(brush, "jitter_unit", expand=True) col.row().prop(brush, "jitter_unit", expand=True)
# Pen pressure mapping curve for Jitter.
if brush.use_pressure_jitter and self.is_popover is False:
col.template_curve_mapping(brush, "curve_jitter", brush=True, use_negative_slope=True)
col.separator() col.separator()
UnifiedPaintPanel.prop_unified( UnifiedPaintPanel.prop_unified(
@@ -1156,6 +1159,9 @@ def brush_shared_settings(layout, context, brush, popover=False):
text="Size", text="Size",
slider=True, slider=True,
) )
if mode in {'PAINT_TEXTURE', 'PAINT_2D', 'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT', 'SCULPT_CURVES'}:
if brush.use_pressure_size:
layout.template_curve_mapping(brush, "curve_size", brush=True, use_negative_slope=True)
if size_mode: if size_mode:
layout.row().prop(size_owner, "use_locked_size", expand=True) layout.row().prop(size_owner, "use_locked_size", expand=True)
layout.separator() layout.separator()
@@ -1171,6 +1177,9 @@ def brush_shared_settings(layout, context, brush, popover=False):
pressure_name=pressure_name, pressure_name=pressure_name,
slider=True, slider=True,
) )
if mode in {'PAINT_TEXTURE', 'PAINT_2D', 'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT', 'SCULPT_CURVES'}:
if brush.use_pressure_strength:
layout.template_curve_mapping(brush, "curve_strength", brush=True, use_negative_slope=True)
layout.separator() layout.separator()
if direction: if direction:

View File

@@ -27,7 +27,7 @@
/* Blender file format version. */ /* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION #define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 71 #define BLENDER_FILE_SUBVERSION 72
/* Minimum Blender version that supports reading file written with the current /* Minimum Blender version that supports reading file written with the current
* version. Older Blender versions will test this and cancel loading the file, showing a warning to * version. Older Blender versions will test this and cancel loading the file, showing a warning to

View File

@@ -88,6 +88,10 @@ static void brush_copy_data(Main * /*bmain*/,
brush_dst->curve_rand_saturation = BKE_curvemapping_copy(brush_src->curve_rand_saturation); brush_dst->curve_rand_saturation = BKE_curvemapping_copy(brush_src->curve_rand_saturation);
brush_dst->curve_rand_value = BKE_curvemapping_copy(brush_src->curve_rand_value); brush_dst->curve_rand_value = BKE_curvemapping_copy(brush_src->curve_rand_value);
brush_dst->curve_size = BKE_curvemapping_copy(brush_src->curve_size);
brush_dst->curve_strength = BKE_curvemapping_copy(brush_src->curve_strength);
brush_dst->curve_jitter = BKE_curvemapping_copy(brush_src->curve_jitter);
if (brush_src->gpencil_settings != nullptr) { if (brush_src->gpencil_settings != nullptr) {
brush_dst->gpencil_settings = MEM_dupallocN<BrushGpencilSettings>( brush_dst->gpencil_settings = MEM_dupallocN<BrushGpencilSettings>(
__func__, *(brush_src->gpencil_settings)); __func__, *(brush_src->gpencil_settings));
@@ -132,6 +136,10 @@ static void brush_free_data(ID *id)
BKE_curvemapping_free(brush->curve_rand_saturation); BKE_curvemapping_free(brush->curve_rand_saturation);
BKE_curvemapping_free(brush->curve_rand_value); BKE_curvemapping_free(brush->curve_rand_value);
BKE_curvemapping_free(brush->curve_size);
BKE_curvemapping_free(brush->curve_strength);
BKE_curvemapping_free(brush->curve_jitter);
if (brush->gpencil_settings != nullptr) { if (brush->gpencil_settings != nullptr) {
BKE_curvemapping_free(brush->gpencil_settings->curve_sensitivity); BKE_curvemapping_free(brush->gpencil_settings->curve_sensitivity);
BKE_curvemapping_free(brush->gpencil_settings->curve_strength); BKE_curvemapping_free(brush->gpencil_settings->curve_strength);
@@ -232,6 +240,16 @@ static void brush_blend_write(BlendWriter *writer, ID *id, const void *id_addres
BKE_curvemapping_blend_write(writer, brush->curve_rand_value); BKE_curvemapping_blend_write(writer, brush->curve_rand_value);
} }
if (brush->curve_size) {
BKE_curvemapping_blend_write(writer, brush->curve_size);
}
if (brush->curve_strength) {
BKE_curvemapping_blend_write(writer, brush->curve_strength);
}
if (brush->curve_jitter) {
BKE_curvemapping_blend_write(writer, brush->curve_jitter);
}
if (brush->gpencil_settings) { if (brush->gpencil_settings) {
BLO_write_struct(writer, BrushGpencilSettings, brush->gpencil_settings); BLO_write_struct(writer, BrushGpencilSettings, brush->gpencil_settings);
@@ -322,6 +340,30 @@ static void brush_blend_read_data(BlendDataReader *reader, ID *id)
brush->curve_rand_value = BKE_paint_default_curve(); brush->curve_rand_value = BKE_paint_default_curve();
} }
BLO_read_struct(reader, CurveMapping, &brush->curve_size);
if (brush->curve_size) {
BKE_curvemapping_blend_read(reader, brush->curve_size);
}
else {
brush->curve_size = BKE_paint_default_curve();
}
BLO_read_struct(reader, CurveMapping, &brush->curve_strength);
if (brush->curve_strength) {
BKE_curvemapping_blend_read(reader, brush->curve_strength);
}
else {
brush->curve_strength = BKE_paint_default_curve();
}
BLO_read_struct(reader, CurveMapping, &brush->curve_jitter);
if (brush->curve_jitter) {
BKE_curvemapping_blend_read(reader, brush->curve_jitter);
}
else {
brush->curve_jitter = BKE_paint_default_curve();
}
/* grease pencil */ /* grease pencil */
BLO_read_struct(reader, BrushGpencilSettings, &brush->gpencil_settings); BLO_read_struct(reader, BrushGpencilSettings, &brush->gpencil_settings);
if (brush->gpencil_settings != nullptr) { if (brush->gpencil_settings != nullptr) {

View File

@@ -53,6 +53,7 @@
#include "BKE_node.hh" #include "BKE_node.hh"
#include "BKE_node_legacy_types.hh" #include "BKE_node_legacy_types.hh"
#include "BKE_node_runtime.hh" #include "BKE_node_runtime.hh"
#include "BKE_paint.hh"
#include "BKE_pointcache.h" #include "BKE_pointcache.h"
#include "BKE_report.hh" #include "BKE_report.hh"
@@ -2998,6 +2999,20 @@ void blo_do_versions_500(FileData *fd, Library * /*lib*/, Main *bmain)
} }
} }
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 500, 72)) {
LISTBASE_FOREACH (Brush *, brush, &bmain->brushes) {
if (brush->curve_size == nullptr) {
brush->curve_size = BKE_paint_default_curve();
}
if (brush->curve_strength == nullptr) {
brush->curve_strength = BKE_paint_default_curve();
}
if (brush->curve_jitter == nullptr) {
brush->curve_jitter = BKE_paint_default_curve();
}
}
}
/** /**
* Always bump subversion in BKE_blender_version.h when adding versioning * Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check. * code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check.

View File

@@ -14,6 +14,7 @@
#include "BKE_attribute.hh" #include "BKE_attribute.hh"
#include "BKE_brush.hh" #include "BKE_brush.hh"
#include "BKE_bvhutils.hh" #include "BKE_bvhutils.hh"
#include "BKE_colortools.hh"
#include "BKE_context.hh" #include "BKE_context.hh"
#include "BKE_curves.hh" #include "BKE_curves.hh"
#include "BKE_modifier.hh" #include "BKE_modifier.hh"
@@ -91,7 +92,7 @@ bool curves_sculpt_poll_view3d(bContext *C)
float brush_radius_factor(const Brush &brush, const StrokeExtension &stroke_extension) float brush_radius_factor(const Brush &brush, const StrokeExtension &stroke_extension)
{ {
if (BKE_brush_use_size_pressure(&brush)) { if (BKE_brush_use_size_pressure(&brush)) {
return stroke_extension.pressure; return BKE_curvemapping_evaluateF(brush.curve_size, 0, stroke_extension.pressure);
} }
return 1.0f; return 1.0f;
} }
@@ -106,7 +107,7 @@ float brush_radius_get(const Paint &paint,
float brush_strength_factor(const Brush &brush, const StrokeExtension &stroke_extension) float brush_strength_factor(const Brush &brush, const StrokeExtension &stroke_extension)
{ {
if (BKE_brush_use_alpha_pressure(&brush)) { if (BKE_brush_use_alpha_pressure(&brush)) {
return stroke_extension.pressure; return BKE_curvemapping_evaluateF(brush.curve_strength, 0, stroke_extension.pressure);
} }
return 1.0f; return 1.0f;
} }

View File

@@ -15,6 +15,7 @@
#include "BLI_math_vector.h" #include "BLI_math_vector.h"
#include "BKE_brush.hh" #include "BKE_brush.hh"
#include "BKE_colortools.hh"
#include "BKE_context.hh" #include "BKE_context.hh"
#include "BKE_layer.hh" #include "BKE_layer.hh"
#include "BKE_paint.hh" #include "BKE_paint.hh"
@@ -363,6 +364,7 @@ static void paint_stroke_update_step(bContext *C,
} }
if (BKE_brush_use_alpha_pressure(brush)) { if (BKE_brush_use_alpha_pressure(brush)) {
pressure = BKE_curvemapping_evaluateF(brush->curve_strength, 0, pressure);
BKE_brush_alpha_set(paint, brush, max_ff(0.0f, startalpha * pressure * alphafac)); BKE_brush_alpha_set(paint, brush, max_ff(0.0f, startalpha * pressure * alphafac));
} }
else { else {

View File

@@ -341,6 +341,9 @@ bool paint_brush_update(bContext *C,
copy_v2_v2(paint_runtime.tex_mouse, mouse); copy_v2_v2(paint_runtime.tex_mouse, mouse);
copy_v2_v2(paint_runtime.mask_tex_mouse, mouse); copy_v2_v2(paint_runtime.mask_tex_mouse, mouse);
stroke->cached_size_pressure = pressure; stroke->cached_size_pressure = pressure;
BKE_curvemapping_init(brush.curve_size);
BKE_curvemapping_init(brush.curve_strength);
BKE_curvemapping_init(brush.curve_jitter);
stroke->brush_init = true; stroke->brush_init = true;
} }
@@ -348,7 +351,7 @@ bool paint_brush_update(bContext *C,
if (paint_supports_dynamic_size(brush, mode)) { if (paint_supports_dynamic_size(brush, mode)) {
copy_v2_v2(paint_runtime.tex_mouse, mouse); copy_v2_v2(paint_runtime.tex_mouse, mouse);
copy_v2_v2(paint_runtime.mask_tex_mouse, mouse); copy_v2_v2(paint_runtime.mask_tex_mouse, mouse);
stroke->cached_size_pressure = pressure; stroke->cached_size_pressure = BKE_curvemapping_evaluateF(brush.curve_size, 0, pressure);
} }
/* Truly temporary data that isn't stored in properties */ /* Truly temporary data that isn't stored in properties */
@@ -533,7 +536,7 @@ void paint_stroke_jitter_pos(const PaintStroke &stroke,
float factor = stroke.zoom_2d; float factor = stroke.zoom_2d;
if (brush.flag & BRUSH_JITTER_PRESSURE) { if (brush.flag & BRUSH_JITTER_PRESSURE) {
factor *= pressure; factor *= BKE_curvemapping_evaluateF(brush.curve_jitter, 0, pressure);
} }
BKE_brush_jitter_pos(*stroke.paint, brush, mval, r_mouse_out); BKE_brush_jitter_pos(*stroke.paint, brush, mval, r_mouse_out);

View File

@@ -537,7 +537,8 @@ void update_cache_variants(bContext *C, VPaint &vp, Object &ob, PointerRNA *ptr)
} }
if (BKE_brush_use_size_pressure(&brush) && paint_supports_dynamic_size(brush, paint_mode)) { if (BKE_brush_use_size_pressure(&brush) && paint_supports_dynamic_size(brush, paint_mode)) {
cache->radius = cache->initial_radius * cache->pressure; cache->radius = cache->initial_radius *
BKE_curvemapping_evaluateF(brush.curve_size, 0, cache->pressure);
} }
else { else {
cache->radius = cache->initial_radius; cache->radius = cache->initial_radius;
@@ -558,9 +559,15 @@ void get_brush_alpha_data(const SculptSession &ss,
float *r_brush_alpha_pressure) float *r_brush_alpha_pressure)
{ {
*r_brush_size_pressure = BKE_brush_radius_get(&paint, &brush) * *r_brush_size_pressure = BKE_brush_radius_get(&paint, &brush) *
(BKE_brush_use_size_pressure(&brush) ? ss.cache->pressure : 1.0f); (BKE_brush_use_size_pressure(&brush) ?
BKE_curvemapping_evaluateF(
brush.curve_size, 0, ss.cache->pressure) :
1.0f);
*r_brush_alpha_value = BKE_brush_alpha_get(&paint, &brush); *r_brush_alpha_value = BKE_brush_alpha_get(&paint, &brush);
*r_brush_alpha_pressure = (BKE_brush_use_alpha_pressure(&brush) ? ss.cache->pressure : 1.0f); *r_brush_alpha_pressure = BKE_brush_use_alpha_pressure(&brush) ?
BKE_curvemapping_evaluateF(
brush.curve_strength, 0, ss.cache->pressure) :
1.0f;
} }
void last_stroke_update(const float location[3], Paint &paint) void last_stroke_update(const float location[3], Paint &paint)

View File

@@ -2183,7 +2183,9 @@ static float brush_strength(const Sculpt &sd,
/* Primary strength input; square it to make lower values more sensitive. */ /* Primary strength input; square it to make lower values more sensitive. */
const float root_alpha = BKE_brush_alpha_get(&sd.paint, &brush); const float root_alpha = BKE_brush_alpha_get(&sd.paint, &brush);
const float alpha = root_alpha * root_alpha; const float alpha = root_alpha * root_alpha;
const float pressure = BKE_brush_use_alpha_pressure(&brush) ? cache.pressure : 1.0f; const float pressure = BKE_brush_use_alpha_pressure(&brush) ?
BKE_curvemapping_evaluateF(brush.curve_strength, 0, cache.pressure) :
1.0f;
float overlap = paint_runtime.overlap_factor; float overlap = paint_runtime.overlap_factor;
/* Spacing is integer percentage of radius, divide by 50 to get /* Spacing is integer percentage of radius, divide by 50 to get
* normalized diameter. */ * normalized diameter. */
@@ -4080,17 +4082,19 @@ static float brush_dynamic_size_get(const Brush &brush,
const StrokeCache &cache, const StrokeCache &cache,
float initial_size) float initial_size)
{ {
const float pressure_eval = BKE_curvemapping_evaluateF(brush.curve_size, 0, cache.pressure);
switch (brush.sculpt_brush_type) { switch (brush.sculpt_brush_type) {
case SCULPT_BRUSH_TYPE_CLAY: case SCULPT_BRUSH_TYPE_CLAY:
return max_ff(initial_size * 0.20f, initial_size * pow3f(cache.pressure)); return max_ff(initial_size * 0.20f, initial_size * pow3f(pressure_eval));
case SCULPT_BRUSH_TYPE_CLAY_STRIPS: case SCULPT_BRUSH_TYPE_CLAY_STRIPS:
return max_ff(initial_size * 0.30f, initial_size * powf(cache.pressure, 1.5f)); return max_ff(initial_size * 0.30f, initial_size * powf(pressure_eval, 1.5f));
case SCULPT_BRUSH_TYPE_CLAY_THUMB: { case SCULPT_BRUSH_TYPE_CLAY_THUMB: {
float clay_stabilized_pressure = brushes::clay_thumb_get_stabilized_pressure(cache); float clay_stabilized_pressure = brushes::clay_thumb_get_stabilized_pressure(cache);
return initial_size * clay_stabilized_pressure; return initial_size *
BKE_curvemapping_evaluateF(brush.curve_size, 0, clay_stabilized_pressure);
} }
default: default:
return initial_size * cache.pressure; return initial_size * pressure_eval;
} }
} }

View File

@@ -228,6 +228,10 @@ typedef struct Brush {
struct CurveMapping *curve_rand_saturation; struct CurveMapping *curve_rand_saturation;
struct CurveMapping *curve_rand_value; struct CurveMapping *curve_rand_value;
struct CurveMapping *curve_size;
struct CurveMapping *curve_strength;
struct CurveMapping *curve_jitter;
/** Opacity. */ /** Opacity. */
float alpha; float alpha;
/** Hardness */ /** Hardness */

View File

@@ -2813,6 +2813,30 @@ static void rna_def_brush(BlenderRNA *brna)
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_update(prop, 0, "rna_Brush_update"); RNA_def_property_update(prop, 0, "rna_Brush_update");
prop = RNA_def_property(srna, "curve_size", PROP_POINTER, PROP_NONE);
RNA_def_property_pointer_sdna(prop, nullptr, "curve_size");
RNA_def_property_struct_type(prop, "CurveMapping");
RNA_def_property_ui_text(
prop, "Pressure Size Mapping", "Curve used to map pressure to brush size");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_update(prop, 0, "rna_Brush_update");
prop = RNA_def_property(srna, "curve_strength", PROP_POINTER, PROP_NONE);
RNA_def_property_pointer_sdna(prop, nullptr, "curve_strength");
RNA_def_property_struct_type(prop, "CurveMapping");
RNA_def_property_ui_text(
prop, "Pressure Strength Mapping", "Curve used to map pressure to brush strength");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_update(prop, 0, "rna_Brush_update");
prop = RNA_def_property(srna, "curve_jitter", PROP_POINTER, PROP_NONE);
RNA_def_property_pointer_sdna(prop, nullptr, "curve_jitter");
RNA_def_property_struct_type(prop, "CurveMapping");
RNA_def_property_ui_text(
prop, "Pressure Jitter Mapping", "Curve used to map pressure to brush jitter");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_update(prop, 0, "rna_Brush_update");
prop = RNA_def_property(srna, "smooth_stroke_radius", PROP_INT, PROP_PIXEL); prop = RNA_def_property(srna, "smooth_stroke_radius", PROP_INT, PROP_PIXEL);
RNA_def_property_range(prop, 10, 200); RNA_def_property_range(prop, 10, 200);
RNA_def_property_ui_text( RNA_def_property_ui_text(