From 9ffb277e0147fb3f37be44279aa630ebe478d97e Mon Sep 17 00:00:00 2001 From: Falk David Date: Mon, 24 Jun 2024 16:32:13 +0200 Subject: [PATCH] GPv3: Draw Tool: Jitter option This adds the `Jitter` draw tool option in the randomize panel. To make this work in combination with the active smoothing, the jitter of the positions is applied after active smoothing as an effect on top. This means that the active smooth will not smooth the jittered points. In addition, it is also now allowed to raise the `Jitter` factor above 1 for a more extreme effect. Note: The jittering worked a bit differently in GPv2 (probably because of a bug). In GPv3 we compute the cotangent using the smoothed stroke direction (over time), which is a lot more acurate then using the previous and current position for the direction. Pull Request: https://projects.blender.org/blender/blender/pulls/123680 --- .../sculpt_paint/grease_pencil_paint.cc | 312 +++++++++++------- source/blender/makesrna/intern/rna_brush.cc | 5 +- 2 files changed, 191 insertions(+), 126 deletions(-) diff --git a/source/blender/editors/sculpt_paint/grease_pencil_paint.cc b/source/blender/editors/sculpt_paint/grease_pencil_paint.cc index 270ec170f1a..6e0b8ae7c18 100644 --- a/source/blender/editors/sculpt_paint/grease_pencil_paint.cc +++ b/source/blender/editors/sculpt_paint/grease_pencil_paint.cc @@ -18,6 +18,7 @@ #include "BLI_math_base.hh" #include "BLI_math_color.h" #include "BLI_math_geom.h" +#include "BLI_rand.hh" #include "DEG_depsgraph_query.hh" @@ -125,15 +126,114 @@ static void morph_points_to_curve(Span src, Span target, Mutable dst.last() = src.last(); } +/** + * Creates a new curve with one point at the beginning or end. + * \note Does not initialize the new curve or points. + */ +static void create_blank_curve(bke::CurvesGeometry &curves, const bool on_back) +{ + if (!on_back) { + const int num_old_points = curves.points_num(); + curves.resize(curves.points_num() + 1, curves.curves_num() + 1); + curves.offsets_for_write().last(1) = num_old_points; + return; + } + + curves.resize(curves.points_num() + 1, curves.curves_num() + 1); + MutableSpan offsets = curves.offsets_for_write(); + offsets.first() = 0; + + /* Loop through backwards to not overwrite the data. */ + for (int i = curves.curves_num() - 2; i >= 0; i--) { + offsets[i + 1] = offsets[i] + 1; + } + + bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); + + attributes.for_all( + [&](const bke::AttributeIDRef &id, const bke::AttributeMetaData /*meta_data*/) { + bke::GSpanAttributeWriter dst = attributes.lookup_for_write_span(id); + + GMutableSpan attribute_data = dst.span; + + bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) { + using T = decltype(dummy); + MutableSpan span_data = attribute_data.typed(); + + /* Loop through backwards to not overwrite the data. */ + for (int i = span_data.size() - 2; i >= 0; i--) { + span_data[i + 1] = span_data[i]; + } + }); + dst.finish(); + return true; + }); +} + +/** + * Extends the first or last curve by `new_points_num` number of points. + * \note Does not initialize the new points. + */ +static void extend_curve(bke::CurvesGeometry &curves, const bool on_back, const int new_points_num) +{ + if (!on_back) { + curves.resize(curves.points_num() + new_points_num, curves.curves_num()); + curves.offsets_for_write().last() = curves.points_num(); + return; + } + + const int last_active_point = curves.points_by_curve()[0].last(); + + curves.resize(curves.points_num() + new_points_num, curves.curves_num()); + MutableSpan offsets = curves.offsets_for_write(); + + for (const int src_curve : curves.curves_range().drop_front(1)) { + offsets[src_curve] = offsets[src_curve] + new_points_num; + } + offsets.last() = curves.points_num(); + + bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); + + attributes.for_all([&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) { + if (meta_data.domain != bke::AttrDomain::Point) { + return true; + } + + bke::GSpanAttributeWriter dst = attributes.lookup_for_write_span(id); + GMutableSpan attribute_data = dst.span; + + bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) { + using T = decltype(dummy); + MutableSpan span_data = attribute_data.typed(); + + /* Loop through backwards to not overwrite the data. */ + for (int i = (span_data.size() - 1) - new_points_num; i >= last_active_point; i--) { + span_data[i + new_points_num] = span_data[i]; + } + }); + dst.finish(); + return true; + }); + + curves.tag_topology_changed(); +} + class PaintOperation : public GreasePencilStrokeOperation { private: /* Screen space coordinates from input samples. */ Vector screen_space_coords_orig_; + /* Temporary vector of curve fitted screen space coordinates per input sample from the active - * smoothing window. */ + * smoothing window. The length of this depends on `active_smooth_start_index_`. */ Vector> screen_space_curve_fitted_coords_; + /* Temporary vector of screen space offsets */ + Vector screen_space_jitter_offsets_; + /* Screen space coordinates after smoothing. */ Vector screen_space_smoothed_coords_; + /* Screen space coordinates after smoothing and jittering. */ + Vector screen_space_final_coords_; + /* The start index of the smoothing window. */ int active_smooth_start_index_ = 0; blender::float4x2 texture_space_ = float4x2::identity(); @@ -141,8 +241,10 @@ class PaintOperation : public GreasePencilStrokeOperation { /* Helper class to project screen space coordinates to 3d. */ ed::greasepencil::DrawingPlacement placement_; - /* Angle factor smoothed over time. */ - float smoothed_angle_factor_ = 1.0f; + /* Direction the pen is moving in smoothed over time. */ + float2 smoothed_pen_direction_ = float2(0.0f); + + RandomNumberGenerator rng; friend struct PaintOperationExecutor; @@ -167,6 +269,8 @@ struct PaintOperationExecutor { std::optional fill_color_; float softness_; + bool use_settings_random_; + bke::greasepencil::Drawing *drawing_; PaintOperationExecutor(const bContext &C) @@ -179,6 +283,7 @@ struct PaintOperationExecutor { brush_ = BKE_paint_brush(paint); settings_ = brush_->gpencil_settings; + use_settings_random_ = (settings_->flag & GP_BRUSH_GROUP_RANDOM) != 0; const bool use_vertex_color = (scene_->toolsettings->gp_paint->mode == GPPAINT_FLAG_USE_VERTEXCOLOR); if (use_vertex_color) { @@ -203,100 +308,6 @@ struct PaintOperationExecutor { BLI_assert(drawing_ != nullptr); } - /** - * Creates a new curve with one point at the beginning or end. - * \note Does not initialize the new curve or points. - */ - static void create_blank_curve(bke::CurvesGeometry &curves, const bool on_back) - { - if (!on_back) { - const int num_old_points = curves.points_num(); - curves.resize(curves.points_num() + 1, curves.curves_num() + 1); - curves.offsets_for_write().last(1) = num_old_points; - return; - } - - curves.resize(curves.points_num() + 1, curves.curves_num() + 1); - MutableSpan offsets = curves.offsets_for_write(); - offsets.first() = 0; - - /* Loop through backwards to not overwrite the data. */ - for (int i = curves.curves_num() - 2; i >= 0; i--) { - offsets[i + 1] = offsets[i] + 1; - } - - bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); - - attributes.for_all( - [&](const bke::AttributeIDRef &id, const bke::AttributeMetaData /*meta_data*/) { - bke::GSpanAttributeWriter dst = attributes.lookup_for_write_span(id); - - GMutableSpan attribute_data = dst.span; - - bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) { - using T = decltype(dummy); - MutableSpan span_data = attribute_data.typed(); - - /* Loop through backwards to not overwrite the data. */ - for (int i = span_data.size() - 2; i >= 0; i--) { - span_data[i + 1] = span_data[i]; - } - }); - dst.finish(); - return true; - }); - } - - /** - * Extends the first or last curve by `new_points_num` number of points. - * \note Does not initialize the new points. - */ - static void extend_curve(bke::CurvesGeometry &curves, - const bool on_back, - const int new_points_num) - { - if (!on_back) { - curves.resize(curves.points_num() + new_points_num, curves.curves_num()); - curves.offsets_for_write().last() = curves.points_num(); - return; - } - - const int last_active_point = curves.points_by_curve()[0].last(); - - curves.resize(curves.points_num() + new_points_num, curves.curves_num()); - MutableSpan offsets = curves.offsets_for_write(); - - for (const int src_curve : curves.curves_range().drop_front(1)) { - offsets[src_curve] = offsets[src_curve] + new_points_num; - } - offsets.last() = curves.points_num(); - - bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); - - attributes.for_all([&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) { - if (meta_data.domain != bke::AttrDomain::Point) { - return true; - } - - bke::GSpanAttributeWriter dst = attributes.lookup_for_write_span(id); - GMutableSpan attribute_data = dst.span; - - bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) { - using T = decltype(dummy); - MutableSpan span_data = attribute_data.typed(); - - /* Loop through backwards to not overwrite the data. */ - for (int i = (span_data.size() - 1) - new_points_num; i >= last_active_point; i--) { - span_data[i + new_points_num] = span_data[i]; - } - }); - dst.finish(); - return true; - }); - - curves.tag_topology_changed(); - } - /* Attributes that are defined explicitly and should not be copied from original geometry. */ Set skipped_attribute_ids(const bke::AttrDomain domain) const { @@ -352,7 +363,9 @@ struct PaintOperationExecutor { self.screen_space_coords_orig_.append(start_coords); self.screen_space_curve_fitted_coords_.append(Vector({start_coords})); + self.screen_space_jitter_offsets_.append(float2(0.0f)); self.screen_space_smoothed_coords_.append(start_coords); + self.screen_space_final_coords_.append(start_coords); /* Resize the curves geometry so there is one more curve with a single point. */ bke::CurvesGeometry &curves = drawing_->strokes_for_write(); @@ -419,9 +432,7 @@ struct PaintOperationExecutor { drawing_->tag_topology_changed(); } - void active_smoothing(PaintOperation &self, - const IndexRange smooth_window, - MutableSpan curve_positions) + void active_smoothing(PaintOperation &self, const IndexRange smooth_window) { const Span coords_to_smooth = self.screen_space_coords_orig_.as_span().slice( smooth_window); @@ -468,7 +479,6 @@ struct PaintOperationExecutor { MutableSpan window_coords = self.screen_space_smoothed_coords_.as_mutable_span().slice( smooth_window); - MutableSpan positions_slice = curve_positions.slice(smooth_window); const float converging_threshold_px = 0.1f; bool stop_counting_converged = false; int num_converged = 0; @@ -492,7 +502,6 @@ struct PaintOperationExecutor { /* Update the positions in the current cache. */ window_coords[window_i] = new_pos; - positions_slice[window_i] = self.placement_.project(new_pos); } /* Remove all the converged points from the active window and shrink the window accordingly. */ @@ -502,15 +511,48 @@ struct PaintOperationExecutor { } } + void active_jitter(PaintOperation &self, + const int new_points_num, + const float brush_radius_px, + const float pressure, + const IndexRange active_window, + MutableSpan curve_positions) + { + float jitter_factor = 1.0f; + if (settings_->flag & GP_BRUSH_USE_JITTER_PRESSURE) { + jitter_factor = BKE_curvemapping_evaluateF(settings_->curve_jitter, 0, pressure); + } + const float2 tangent = math::normalize(self.smoothed_pen_direction_); + const float2 cotangent = float2(-tangent.y, tangent.x); + for ([[maybe_unused]] const int _ : IndexRange(new_points_num)) { + const float rand = self.rng.get_float() * 2.0f - 1.0f; + const float factor = rand * settings_->draw_jitter * jitter_factor; + self.screen_space_jitter_offsets_.append(cotangent * factor * brush_radius_px); + } + const Span jitter_slice = self.screen_space_jitter_offsets_.as_mutable_span().slice( + active_window); + MutableSpan smoothed_coords = + self.screen_space_smoothed_coords_.as_mutable_span().slice(active_window); + MutableSpan final_coords = self.screen_space_final_coords_.as_mutable_span().slice( + active_window); + MutableSpan positions_slice = curve_positions.slice(active_window); + for (const int64_t window_i : active_window.index_range()) { + final_coords[window_i] = smoothed_coords[window_i] + jitter_slice[window_i]; + positions_slice[window_i] = self.placement_.project(final_coords[window_i]); + } + } + void process_extension_sample(PaintOperation &self, const bContext &C, const InputSample &extension_sample) { - const float2 coords = extension_sample.mouse_position; + Scene *scene = CTX_data_scene(&C); const RegionView3D *rv3d = CTX_wm_region_view3d(&C); const ARegion *region = CTX_wm_region(&C); + const bool on_back = (scene->toolsettings->gpencil_flags & GP_TOOL_FLAG_PAINT_ONBACK) != 0; - const float3 position = self.placement_.project(coords); + const float2 coords = extension_sample.mouse_position; + float3 position = self.placement_.project(coords); float radius = ed::greasepencil::radius_from_input_sample(rv3d, region, brush_, @@ -518,17 +560,18 @@ struct PaintOperationExecutor { position, self.placement_.to_world_space(), settings_); + const float brush_radius_px = brush_radius_to_pixel_radius( + rv3d, brush_, math::transform_point(self.placement_.to_world_space(), position)); const float opacity = ed::greasepencil::opacity_from_input_sample( extension_sample.pressure, brush_, settings_); - Scene *scene = CTX_data_scene(&C); - const bool on_back = (scene->toolsettings->gpencil_flags & GP_TOOL_FLAG_PAINT_ONBACK) != 0; bke::CurvesGeometry &curves = drawing_->strokes_for_write(); + OffsetIndices points_by_curve = curves.points_by_curve(); bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); const int active_curve = on_back ? curves.curves_range().first() : curves.curves_range().last(); - const IndexRange curve_points = curves.points_by_curve()[active_curve]; + const IndexRange curve_points = points_by_curve[active_curve]; const int last_active_point = curve_points.last(); const float2 prev_coords = self.screen_space_coords_orig_.last(); @@ -536,29 +579,31 @@ struct PaintOperationExecutor { const float prev_opacity = drawing_->opacities()[last_active_point]; const ColorGeometry4f prev_vertex_color = drawing_->vertex_colors()[last_active_point]; + /* Use the vector from the previous to the next point and then interpolate it with the previous + * direction to get a smoothed value over time. */ + self.smoothed_pen_direction_ = math::interpolate( + self.smoothed_pen_direction_, coords - self.screen_space_coords_orig_.last(), 0.1f); + /* Approximate brush with non-circular shape by changing the radius based on the angle. */ if (settings_->draw_angle_factor > 0.0f) { const float angle = settings_->draw_angle; const float2 angle_vec = float2(math::cos(angle), math::sin(angle)); - const float2 vec = coords - self.screen_space_coords_orig_.last(); /* `angle_factor` is the angle to the horizontal line in screen space. */ - const float angle_factor = 1.0f - math::abs(math::dot(angle_vec, math::normalize(vec))); - /* Smooth the angle factor over time. */ - self.smoothed_angle_factor_ = math::interpolate( - self.smoothed_angle_factor_, angle_factor, 0.1f); + const float angle_factor = + 1.0f - math::abs(math::dot(angle_vec, math::normalize(self.smoothed_pen_direction_))); /* Influence is controlled by `draw_angle_factor`. */ const float radius_factor = math::interpolate( - 1.0f, self.smoothed_angle_factor_, settings_->draw_angle_factor); + 1.0f, angle_factor, settings_->draw_angle_factor); radius *= radius_factor; } /* Overwrite last point if it's very close. */ - const IndexRange points_range = curves.points_by_curve()[curves.curves_range().last()]; - const bool is_first_sample = (points_range.size() == 1); + const bool is_first_sample = (curve_points.size() == 1); constexpr float point_override_threshold_px = 2.0f; - if (math::distance(coords, prev_coords) < point_override_threshold_px) { + const float distance_px = math::distance(coords, prev_coords); + if (distance_px < point_override_threshold_px) { /* Don't move the first point of the stroke. */ if (!is_first_sample) { curves.positions_for_write()[last_active_point] = position; @@ -568,22 +613,19 @@ struct PaintOperationExecutor { return; } - /* If the next sample is far away, we subdivide the segment to add more points. */ - const float distance_px = math::distance(coords, prev_coords); - const float brush_radius_px = brush_radius_to_pixel_radius( - rv3d, brush_, math::transform_point(self.placement_.to_world_space(), position)); /* Clamp the number of points within a pixel in screen space. */ constexpr int max_points_per_pixel = 4; /* The value `brush_->spacing` is a percentage of the brush radius in pixels. */ const float max_spacing_px = math::max((float(brush_->spacing) / 100.0f) * float(brush_radius_px), 1.0f / float(max_points_per_pixel)); + /* If the next sample is far away, we subdivide the segment to add more points. */ const int new_points_num = (distance_px > max_spacing_px) ? int(math::floor(distance_px / max_spacing_px)) : 1; - /* Resize the curves geometry. */ extend_curve(curves, on_back, new_points_num); + /* Subdivide stroke in new_points. */ const IndexRange new_points = curves.points_by_curve()[active_curve].take_back(new_points_num); Array new_screen_space_coords(new_points_num); @@ -604,6 +646,7 @@ struct PaintOperationExecutor { /* Update screen space buffers with new points. */ self.screen_space_coords_orig_.extend(new_screen_space_coords); self.screen_space_smoothed_coords_.extend(new_screen_space_coords); + self.screen_space_final_coords_.extend(new_screen_space_coords); for (float2 new_position : new_screen_space_coords) { self.screen_space_curve_fitted_coords_.append(Vector({new_position})); } @@ -617,8 +660,29 @@ struct PaintOperationExecutor { } else { /* Active smoothing is done in a window at the end of the new stroke. */ - this->active_smoothing( - self, smooth_window, positions.slice(curves.points_by_curve()[active_curve])); + this->active_smoothing(self, smooth_window); + } + + MutableSpan curve_positions = positions.slice(curves.points_by_curve()[active_curve]); + if (use_settings_random_ && settings_->draw_jitter > 0.0f) { + this->active_jitter(self, + new_points_num, + brush_radius_px, + extension_sample.pressure, + smooth_window, + curve_positions); + } + else { + MutableSpan smoothed_coords = + self.screen_space_smoothed_coords_.as_mutable_span().slice(smooth_window); + MutableSpan final_coords = self.screen_space_final_coords_.as_mutable_span().slice( + smooth_window); + /* Not jitter, so we just copy the positions over. */ + final_coords.copy_from(smoothed_coords); + MutableSpan curve_positions_slice = curve_positions.slice(smooth_window); + for (const int64_t window_i : smooth_window.index_range()) { + curve_positions_slice[window_i] = self.placement_.project(final_coords[window_i]); + } } /* Initialize the rest of the attributes with default values. */ @@ -953,7 +1017,7 @@ void PaintOperation::on_stroke_done(const bContext &C) } if (settings->simplify_px > 0.0f) { simplify_stroke(drawing, - this->screen_space_smoothed_coords_.as_span().drop_back(num_points_removed), + this->screen_space_final_coords_.as_span().drop_back(num_points_removed), settings->simplify_px, active_curve); } diff --git a/source/blender/makesrna/intern/rna_brush.cc b/source/blender/makesrna/intern/rna_brush.cc index b5b770aa50f..9edd0ab541d 100644 --- a/source/blender/makesrna/intern/rna_brush.cc +++ b/source/blender/makesrna/intern/rna_brush.cc @@ -1490,8 +1490,9 @@ static void rna_def_gpencil_options(BlenderRNA *brna) /* Jitter factor for new strokes */ prop = RNA_def_property(srna, "pen_jitter", PROP_FLOAT, PROP_FACTOR); RNA_def_property_float_sdna(prop, nullptr, "draw_jitter"); - RNA_def_property_range(prop, 0.0f, 1.0f); - RNA_def_property_ui_text(prop, "Jitter", "Jitter factor for new strokes"); + RNA_def_property_range(prop, 0.0f, 100.0f); + RNA_def_property_ui_range(prop, 0.0f, 1.0f, 0.001, 3); + RNA_def_property_ui_text(prop, "Jitter", "Jitter factor of brush radius for new strokes"); RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_BRUSH); RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); RNA_def_property_update(prop, NC_GPENCIL | ND_DATA, nullptr);