From 5f8311f596a4ddb8fc03337f8bea07831116cd89 Mon Sep 17 00:00:00 2001 From: Toby Yang Date: Wed, 3 Sep 2025 05:34:38 +0200 Subject: [PATCH] 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 Pull Request: https://projects.blender.org/blender/blender/pulls/144428 --- .../startup/bl_ui/properties_paint_common.py | 9 ++++ .../blender/blenkernel/BKE_blender_version.h | 2 +- source/blender/blenkernel/intern/brush.cc | 42 +++++++++++++++++++ .../blenloader/intern/versioning_500.cc | 15 +++++++ .../editors/sculpt_paint/curves_sculpt_ops.cc | 5 ++- .../sculpt_paint/paint_image_ops_paint.cc | 2 + .../editors/sculpt_paint/paint_stroke.cc | 7 +++- .../editors/sculpt_paint/paint_vertex.cc | 13 ++++-- source/blender/editors/sculpt_paint/sculpt.cc | 14 ++++--- source/blender/makesdna/DNA_brush_types.h | 4 ++ source/blender/makesrna/intern/rna_brush.cc | 24 +++++++++++ 11 files changed, 124 insertions(+), 13 deletions(-) diff --git a/scripts/startup/bl_ui/properties_paint_common.py b/scripts/startup/bl_ui/properties_paint_common.py index 1a9afdbfa2c..40f234ab9a6 100644 --- a/scripts/startup/bl_ui/properties_paint_common.py +++ b/scripts/startup/bl_ui/properties_paint_common.py @@ -562,6 +562,9 @@ class StrokePanel(BrushPanel): row.prop(brush, "jitter_absolute") row.prop(brush, "use_pressure_jitter", toggle=True, text="") 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() UnifiedPaintPanel.prop_unified( @@ -1156,6 +1159,9 @@ def brush_shared_settings(layout, context, brush, popover=False): text="Size", 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: layout.row().prop(size_owner, "use_locked_size", expand=True) layout.separator() @@ -1171,6 +1177,9 @@ def brush_shared_settings(layout, context, brush, popover=False): pressure_name=pressure_name, 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() if direction: diff --git a/source/blender/blenkernel/BKE_blender_version.h b/source/blender/blenkernel/BKE_blender_version.h index ee809635e9f..b4402ba27a7 100644 --- a/source/blender/blenkernel/BKE_blender_version.h +++ b/source/blender/blenkernel/BKE_blender_version.h @@ -27,7 +27,7 @@ /* Blender file format 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 * version. Older Blender versions will test this and cancel loading the file, showing a warning to diff --git a/source/blender/blenkernel/intern/brush.cc b/source/blender/blenkernel/intern/brush.cc index ca6f67ea985..3bf183184dc 100644 --- a/source/blender/blenkernel/intern/brush.cc +++ b/source/blender/blenkernel/intern/brush.cc @@ -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_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) { brush_dst->gpencil_settings = MEM_dupallocN( __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_value); + BKE_curvemapping_free(brush->curve_size); + BKE_curvemapping_free(brush->curve_strength); + BKE_curvemapping_free(brush->curve_jitter); + if (brush->gpencil_settings != nullptr) { BKE_curvemapping_free(brush->gpencil_settings->curve_sensitivity); 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); } + 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) { 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(); } + 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 */ BLO_read_struct(reader, BrushGpencilSettings, &brush->gpencil_settings); if (brush->gpencil_settings != nullptr) { diff --git a/source/blender/blenloader/intern/versioning_500.cc b/source/blender/blenloader/intern/versioning_500.cc index e36c040c285..4bb13919665 100644 --- a/source/blender/blenloader/intern/versioning_500.cc +++ b/source/blender/blenloader/intern/versioning_500.cc @@ -53,6 +53,7 @@ #include "BKE_node.hh" #include "BKE_node_legacy_types.hh" #include "BKE_node_runtime.hh" +#include "BKE_paint.hh" #include "BKE_pointcache.h" #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 * code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check. diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc b/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc index ec089c55459..e6bdbc246f1 100644 --- a/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc +++ b/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc @@ -14,6 +14,7 @@ #include "BKE_attribute.hh" #include "BKE_brush.hh" #include "BKE_bvhutils.hh" +#include "BKE_colortools.hh" #include "BKE_context.hh" #include "BKE_curves.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) { 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; } @@ -106,7 +107,7 @@ float brush_radius_get(const Paint &paint, float brush_strength_factor(const Brush &brush, const StrokeExtension &stroke_extension) { 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; } diff --git a/source/blender/editors/sculpt_paint/paint_image_ops_paint.cc b/source/blender/editors/sculpt_paint/paint_image_ops_paint.cc index 165744ecdd6..b605e641c47 100644 --- a/source/blender/editors/sculpt_paint/paint_image_ops_paint.cc +++ b/source/blender/editors/sculpt_paint/paint_image_ops_paint.cc @@ -15,6 +15,7 @@ #include "BLI_math_vector.h" #include "BKE_brush.hh" +#include "BKE_colortools.hh" #include "BKE_context.hh" #include "BKE_layer.hh" #include "BKE_paint.hh" @@ -363,6 +364,7 @@ static void paint_stroke_update_step(bContext *C, } 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)); } else { diff --git a/source/blender/editors/sculpt_paint/paint_stroke.cc b/source/blender/editors/sculpt_paint/paint_stroke.cc index 5e98abeab9b..39bd49d779e 100644 --- a/source/blender/editors/sculpt_paint/paint_stroke.cc +++ b/source/blender/editors/sculpt_paint/paint_stroke.cc @@ -341,6 +341,9 @@ bool paint_brush_update(bContext *C, copy_v2_v2(paint_runtime.tex_mouse, mouse); copy_v2_v2(paint_runtime.mask_tex_mouse, mouse); 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; } @@ -348,7 +351,7 @@ bool paint_brush_update(bContext *C, if (paint_supports_dynamic_size(brush, mode)) { copy_v2_v2(paint_runtime.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 */ @@ -533,7 +536,7 @@ void paint_stroke_jitter_pos(const PaintStroke &stroke, float factor = stroke.zoom_2d; 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); diff --git a/source/blender/editors/sculpt_paint/paint_vertex.cc b/source/blender/editors/sculpt_paint/paint_vertex.cc index 75f5e01739b..1d3b6d80935 100644 --- a/source/blender/editors/sculpt_paint/paint_vertex.cc +++ b/source/blender/editors/sculpt_paint/paint_vertex.cc @@ -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)) { - cache->radius = cache->initial_radius * cache->pressure; + cache->radius = cache->initial_radius * + BKE_curvemapping_evaluateF(brush.curve_size, 0, cache->pressure); } else { cache->radius = cache->initial_radius; @@ -558,9 +559,15 @@ void get_brush_alpha_data(const SculptSession &ss, float *r_brush_alpha_pressure) { *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_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) diff --git a/source/blender/editors/sculpt_paint/sculpt.cc b/source/blender/editors/sculpt_paint/sculpt.cc index a5b786c84e3..71f3a162e41 100644 --- a/source/blender/editors/sculpt_paint/sculpt.cc +++ b/source/blender/editors/sculpt_paint/sculpt.cc @@ -2183,7 +2183,9 @@ static float brush_strength(const Sculpt &sd, /* Primary strength input; square it to make lower values more sensitive. */ const float root_alpha = BKE_brush_alpha_get(&sd.paint, &brush); 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; /* Spacing is integer percentage of radius, divide by 50 to get * normalized diameter. */ @@ -4080,17 +4082,19 @@ static float brush_dynamic_size_get(const Brush &brush, const StrokeCache &cache, float initial_size) { + const float pressure_eval = BKE_curvemapping_evaluateF(brush.curve_size, 0, cache.pressure); switch (brush.sculpt_brush_type) { 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: - 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: { 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: - return initial_size * cache.pressure; + return initial_size * pressure_eval; } } diff --git a/source/blender/makesdna/DNA_brush_types.h b/source/blender/makesdna/DNA_brush_types.h index fcc5a51b24f..3950347422a 100644 --- a/source/blender/makesdna/DNA_brush_types.h +++ b/source/blender/makesdna/DNA_brush_types.h @@ -228,6 +228,10 @@ typedef struct Brush { struct CurveMapping *curve_rand_saturation; struct CurveMapping *curve_rand_value; + struct CurveMapping *curve_size; + struct CurveMapping *curve_strength; + struct CurveMapping *curve_jitter; + /** Opacity. */ float alpha; /** Hardness */ diff --git a/source/blender/makesrna/intern/rna_brush.cc b/source/blender/makesrna/intern/rna_brush.cc index 8608a763c90..5d79c6a3d40 100644 --- a/source/blender/makesrna/intern/rna_brush.cc +++ b/source/blender/makesrna/intern/rna_brush.cc @@ -2813,6 +2813,30 @@ static void rna_def_brush(BlenderRNA *brna) RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); 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); RNA_def_property_range(prop, 10, 200); RNA_def_property_ui_text(