diff --git a/scripts/startup/bl_ui/properties_paint_common.py b/scripts/startup/bl_ui/properties_paint_common.py index fa6adcd879c..0b0d49b110e 100644 --- a/scripts/startup/bl_ui/properties_paint_common.py +++ b/scripts/startup/bl_ui/properties_paint_common.py @@ -152,6 +152,8 @@ class UnifiedPaintPanel: return tool_settings.gpencil_weight_paint elif mode == 'VERTEX_GPENCIL': return tool_settings.gpencil_vertex_paint + elif mode == 'PAINT_GREASE_PENCIL': + return tool_settings.gpencil_paint elif mode == 'SCULPT_CURVES': return tool_settings.curves_sculpt elif mode == 'PAINT_GREASE_PENCIL': @@ -1637,7 +1639,7 @@ def brush_basic_grease_pencil_paint_settings(layout, context, brush, *, compact= layout.use_property_split = use_property_split_prev elif grease_pencil_tool == 'ERASE': layout.prop(gp_settings, "eraser_mode", expand=True) - if gp_settings.eraser_mode == 'HARD': + if gp_settings.eraser_mode in {'HARD', 'SOFT'}: layout.prop(gp_settings, "use_keep_caps_eraser") layout.prop(gp_settings, "use_active_layer_only") elif grease_pencil_tool == 'TINT': diff --git a/source/blender/editors/include/ED_grease_pencil.hh b/source/blender/editors/include/ED_grease_pencil.hh index 153c33318ec..797e556c21b 100644 --- a/source/blender/editors/include/ED_grease_pencil.hh +++ b/source/blender/editors/include/ED_grease_pencil.hh @@ -434,6 +434,11 @@ struct PointTransferData { float factor; bool is_src_point; bool is_cut; + /* Additional attributes changes that can be stored to be used after a call to + * compute_topology_change. + * Note that they won't be automatically updated in the destination's attributes. + */ + float opacity; /** * Source point is the last of the curve. diff --git a/source/blender/editors/sculpt_paint/grease_pencil_erase.cc b/source/blender/editors/sculpt_paint/grease_pencil_erase.cc index 39a74993578..4d7f3c265d7 100644 --- a/source/blender/editors/sculpt_paint/grease_pencil_erase.cc +++ b/source/blender/editors/sculpt_paint/grease_pencil_erase.cc @@ -7,6 +7,7 @@ #include "BLI_array.hh" #include "BLI_array_utils.hh" #include "BLI_index_mask.hh" +#include "BLI_math_base.hh" #include "BLI_math_geom.h" #include "BLI_task.hh" @@ -50,24 +51,60 @@ class EraseOperation : public GreasePencilStrokeOperation { bool keep_caps_ = false; float radius_ = 50.0f; + float strength_ = 0.1f; eGP_BrushEraserMode eraser_mode_ = GP_BRUSH_ERASER_HARD; bool active_layer_only_ = false; + + Set affected_drawings_; }; +struct SegmentCircleIntersection { + /* Position of the intersection in the segment. + * Note: we use a value > 1.0f as initial value so that sorting intersection by increasing + * factor can directly put the invalid ones at the end. */ + float factor = 2.0f; + + /* True if the intersection corresponds to an inside/outside transition with respect to the + * circle, false if it corresponds to an outside/inside transition. */ + bool inside_outside_intersection = false; + + int ring_index = -1; + + /* An intersection is considered valid if it lies inside of the segment, i.e. + * if its factor is in (0,1). */ + bool is_valid() const + { + return IN_RANGE(factor, 0.0f, 1.0f); + } +}; +enum class PointCircleSide { Outside, OutsideInsideBoundary, InsideOutsideBoundary, Inside }; + /** * Utility class that actually executes the update when the stroke is updated. That's useful * because it avoids passing a very large number of parameters between functions. */ struct EraseOperationExecutor { - float2 mouse_position{}; - float eraser_radius{}; + float2 mouse_position; + float eraser_radius; + float eraser_strength; + Brush *brush_; - int2 mouse_position_pixels{}; - int64_t eraser_squared_radius_pixels{}; + int2 mouse_position_pixels; + int64_t eraser_squared_radius_pixels; + + /* Threshold below which points are considered as transparent and thus shall be removed. */ + static constexpr float opacity_threshold = 0.05f; EraseOperationExecutor(const bContext & /*C*/) {} + struct EraserRing { + float radius; + int64_t squared_radius; + float opacity; + bool hard_erase{false}; + }; + /** * Computes the intersections between a 2D line segment and a circle with integer values. * @@ -105,38 +142,21 @@ struct EraseOperationExecutor { if (i == 0) { /* One intersection. */ const float mu0_f = -b / (2.0f * a); - r_mu0 = round_fl_to_int(mu0_f * segment_length); + r_mu0 = math::round(mu0_f * segment_length); return 1; } /* Two intersections. */ - const float i_sqrt = sqrtf(float(i)); + const float i_sqrt = math::sqrt(float(i)); const float mu0_f = (-b + i_sqrt) / (2.0f * a); const float mu1_f = (-b - i_sqrt) / (2.0f * a); - r_mu0 = round_fl_to_int(mu0_f * segment_length); - r_mu1 = round_fl_to_int(mu1_f * segment_length); + r_mu0 = math::round(mu0_f * segment_length); + r_mu1 = math::round(mu1_f * segment_length); return 2; } - struct SegmentCircleIntersection { - /* Position of the intersection in the segment. */ - float factor = -1.0f; - - /* True if the intersection corresponds to an inside/outside transition with respect to the - * circle, false if it corresponds to an outside/inside transition. */ - bool inside_outside_intersection = false; - - /* An intersection is considered valid if it lies inside of the segment, i.e. - * if its factor is in (0,1). */ - bool is_valid() const - { - return IN_RANGE(factor, 0.0f, 1.0f); - } - }; - enum class PointCircleSide { Outside, OutsideInsideBoundary, InsideOutsideBoundary, Inside }; - /** * Computes the intersection between the eraser tool and a 2D segment, using integer values. * Also computes if the endpoints of the segment lie inside/outside, or in the boundary of the @@ -271,11 +291,12 @@ struct EraseOperationExecutor { int curves_intersections_and_points_sides( const bke::CurvesGeometry &src, const Span screen_space_positions, - const int intersections_max_per_segment, - MutableSpan r_point_side, + const Span rings, + MutableSpan> r_point_ring, MutableSpan r_intersections) const { - + /* Each ring can generate zero, one or two intersections per segment. */ + const int intersections_max_per_segment = rings.size() * 2; const OffsetIndices src_points_by_curve = src.points_by_curve(); const VArray src_cyclic = src.cyclic(); @@ -294,41 +315,79 @@ struct EraseOperationExecutor { if (src_curve_points.size() == 1) { /* One-point stroke : just check if the point is inside the eraser. */ - const int src_point = src_curve_points.first(); - const int64_t squared_distance = math::distance_squared( - this->mouse_position_pixels, screen_space_positions_pixel[src_point]); + int ring_index = 0; + for (const EraserRing &eraser_point : rings) { + const int src_point = src_curve_points.first(); + const int64_t squared_distance = math::distance_squared( + this->mouse_position_pixels, screen_space_positions_pixel[src_point]); - /* NOTE: We don't account for boundaries here, since we are not going to split any - * curve. */ - r_point_side[src_point] = (squared_distance <= this->eraser_squared_radius_pixels) ? - PointCircleSide::Inside : - PointCircleSide::Outside; + /* NOTE: We don't account for boundaries here, since we are not going to split any + * curve. */ + if ((r_point_ring[src_point].first == -1) && + (squared_distance <= eraser_point.squared_radius)) + { + r_point_ring[src_point] = {ring_index, PointCircleSide::Inside}; + } + ++ring_index; + } continue; } for (const int src_point : src_curve_points.drop_back(1)) { - SegmentCircleIntersection inter0; - SegmentCircleIntersection inter1; + int ring_index = 0; + int intersection_offset = src_point * intersections_max_per_segment - 1; - const int8_t nb_inter = segment_intersections_and_points_sides( - screen_space_positions_pixel[src_point], - screen_space_positions_pixel[src_point + 1], - this->eraser_squared_radius_pixels, - inter0.factor, - inter1.factor, - r_point_side[src_point], - r_point_side[src_point + 1]); + for (const EraserRing &eraser_point : rings) { + SegmentCircleIntersection inter0; + SegmentCircleIntersection inter1; - if (nb_inter > 0) { - const int intersection_offset = src_point * intersections_max_per_segment; + inter0.ring_index = ring_index; + inter1.ring_index = ring_index; - inter0.inside_outside_intersection = (inter0.factor > inter1.factor); - r_intersections[intersection_offset + 0] = inter0; + PointCircleSide point_side; + PointCircleSide point_after_side; - if (nb_inter > 1) { - inter1.inside_outside_intersection = true; - r_intersections[intersection_offset + 1] = inter1; + const int8_t nb_inter = segment_intersections_and_points_sides( + screen_space_positions_pixel[src_point], + screen_space_positions_pixel[src_point + 1], + eraser_point.squared_radius, + inter0.factor, + inter1.factor, + point_side, + point_after_side); + + /* The point belongs in the ring of the smallest radius circle it is in. + * Since our rings are stored in increasing radius order, it corresponds to the first + * ring that contains the point. We only include the InsideOutside boundary of the + * ring, that is why we do not check for OutsideInsideBoundary. + */ + if ((r_point_ring[src_point].first == -1) && + ELEM(point_side, PointCircleSide::Inside, PointCircleSide::InsideOutsideBoundary)) + { + r_point_ring[src_point] = {ring_index, point_side}; } + + if (src_point + 1 == src_curve_points.last()) { + if ((r_point_ring[src_point + 1].first == -1) && + ELEM(point_after_side, + PointCircleSide::Inside, + PointCircleSide::InsideOutsideBoundary)) + { + r_point_ring[src_point + 1] = {ring_index, point_after_side}; + } + } + + if (nb_inter > 0) { + inter0.inside_outside_intersection = (inter0.factor > inter1.factor); + r_intersections[++intersection_offset] = inter0; + + if (nb_inter > 1) { + inter1.inside_outside_intersection = true; + r_intersections[++intersection_offset] = inter1; + } + } + + ++ring_index; } } @@ -336,29 +395,42 @@ struct EraseOperationExecutor { /* If the curve is cyclic, we need to check for the closing segment. */ const int src_last_point = src_curve_points.last(); const int src_first_point = src_curve_points.first(); + int ring_index = 0; + int intersection_offset = src_last_point * intersections_max_per_segment - 1; - SegmentCircleIntersection inter0; - SegmentCircleIntersection inter1; + for (const EraserRing &eraser_point : rings) { + SegmentCircleIntersection inter0; + SegmentCircleIntersection inter1; - const int8_t nb_inter = segment_intersections_and_points_sides( - screen_space_positions_pixel[src_last_point], - screen_space_positions_pixel[src_first_point], - this->eraser_squared_radius_pixels, - inter0.factor, - inter1.factor, - r_point_side[src_last_point], - r_point_side[src_first_point]); + inter0.ring_index = ring_index; + inter1.ring_index = ring_index; - if (nb_inter > 0) { - const int intersection_offset = src_last_point * intersections_max_per_segment; + PointCircleSide point_side; + PointCircleSide point_after_side; - inter0.inside_outside_intersection = (inter0.factor > inter1.factor); - r_intersections[intersection_offset + 0] = inter0; + const int8_t nb_inter = segment_intersections_and_points_sides( + screen_space_positions_pixel[src_last_point], + screen_space_positions_pixel[src_first_point], + eraser_point.squared_radius, + inter0.factor, + inter1.factor, + point_side, + point_after_side); - if (nb_inter > 1) { - inter1.inside_outside_intersection = true; - r_intersections[intersection_offset + 1] = inter1; + /* Note : we don't need to set the point side here, since it was already set by the + * former loop. */ + + if (nb_inter > 0) { + inter0.inside_outside_intersection = (inter0.factor > inter1.factor); + r_intersections[++intersection_offset] = inter0; + + if (nb_inter > 1) { + inter1.inside_outside_intersection = true; + r_intersections[++intersection_offset] = inter1; + } } + + ++ring_index; } } } @@ -389,17 +461,17 @@ struct EraseOperationExecutor { /* For the hard erase, we compute with a circle, so there can only be a maximum of two * intersection per segment. */ - const int intersections_max_per_segment = 2; + const Vector eraser_rings( + 1, {this->eraser_radius, this->eraser_squared_radius_pixels, 0.0f, true}); + const int intersections_max_per_segment = eraser_rings.size() * 2; /* Compute intersections between the eraser and the curves in the source domain. */ - Array src_point_side(src_points_num, PointCircleSide::Outside); + Array> src_point_ring(src_points_num, + {-1, PointCircleSide::Outside}); Array src_intersections(src_points_num * intersections_max_per_segment); - curves_intersections_and_points_sides(src, - screen_space_positions, - intersections_max_per_segment, - src_point_side, - src_intersections); + curves_intersections_and_points_sides( + src, screen_space_positions, eraser_rings, src_point_ring, src_intersections); Array> src_to_dst_points(src_points_num); const OffsetIndices src_points_by_curve = src.points_by_curve(); @@ -410,7 +482,7 @@ struct EraseOperationExecutor { Vector &dst_points = src_to_dst_points[src_point]; const int src_next_point = (src_point == src_points.last()) ? src_points.first() : (src_point + 1); - const PointCircleSide point_side = src_point_side[src_point]; + const PointCircleSide point_side = src_point_ring[src_point].second; /* Add the source point only if it does not lie inside of the eraser. */ if (point_side != PointCircleSide::Inside) { @@ -445,6 +517,265 @@ struct EraseOperationExecutor { return true; } + Vector compute_piecewise_linear_falloff() const + { + /* The changes in opacity implied by the soft eraser are described by a falloff curve + * mapping. Abscissa of the curve is the normalized distance to the brush, and ordinate of + * the curve is the strength of the eraser. + * + * To apply this falloff as precisely as possible, we compute a set of "rings" to the brush, + * meaning a set of samples in the curve mapping in between which the strength of the eraser + * is applied linearly. + * + * In other words, we compute a minimal set of samples that describe the falloff curve as a + * polyline. */ + + /* First, distance-based sampling with a small pixel distance. + * The samples are stored in increasing radius order. */ + const int step_pixels = 2; + int nb_samples = math::round(this->eraser_radius / step_pixels); + Vector eraser_rings(nb_samples); + for (const int sample_index : eraser_rings.index_range()) { + const int64_t sampled_distance = (sample_index + 1) * step_pixels; + + EraserRing &ring = eraser_rings[sample_index]; + ring.radius = sampled_distance; + ring.squared_radius = sampled_distance * sampled_distance; + ring.opacity = 1.0 - this->eraser_strength * + BKE_brush_curve_strength( + this->brush_, float(sampled_distance), this->eraser_radius); + } + + /* Then, prune samples that are under the opacity threshold. */ + Array prune_sample(nb_samples, false); + for (const int sample_index : eraser_rings.index_range()) { + EraserRing &sample = eraser_rings[sample_index]; + + if (sample_index == nb_samples - 1) { + /* If this is the last samples, we need to keep it at the same position (it corresponds + * to the brush overall radius). It is a cut if the opacity is under the threshold.*/ + sample.hard_erase = (sample.opacity < opacity_threshold); + continue; + } + + EraserRing next_sample = eraser_rings[sample_index + 1]; + + /* If both samples are under the threshold, prune it ! + * If none of them are under the threshold, leave them as they are. + */ + if ((sample.opacity < opacity_threshold) == (next_sample.opacity < opacity_threshold)) { + prune_sample[sample_index] = (sample.opacity < opacity_threshold); + continue; + } + + /* Otherwise, shift the sample to the spot where the opacity is exactly at the threshold. + * This way we don't remove larger opacity values in-between the samples. */ + const EraserRing &sample_after = eraser_rings[sample_index + 1]; + + const float t = (opacity_threshold - sample.opacity) / + (sample_after.opacity - sample.opacity); + + const int64_t radius = math::round( + math::interpolate(float(sample.radius), float(sample_after.radius), t)); + + sample.radius = radius; + sample.squared_radius = radius * radius; + sample.opacity = opacity_threshold; + sample.hard_erase = !(next_sample.opacity < opacity_threshold); + } + + for (const int rev_sample_index : eraser_rings.index_range()) { + const int sample_index = nb_samples - rev_sample_index - 1; + if (prune_sample[sample_index]) { + eraser_rings.remove(sample_index); + } + } + + /* Finally, simplify the array to have a minimal set of samples. */ + nb_samples = eraser_rings.size(); + + const auto opacity_distance = [&](int64_t first_index, int64_t last_index, int64_t index) { + /* Distance function for the simplification algorithm. + * It is computed as the difference in opacity that may result from removing the + * samples inside the range. */ + const EraserRing &sample_first = eraser_rings[first_index]; + const EraserRing &sample_last = eraser_rings[last_index]; + const EraserRing &sample = eraser_rings[index]; + + /* If we were to remove the samples between sample_first and sample_last, then the opacity + * at sample.radius would be a linear interpolation between the opacities in the endpoints + * of the range, with a parameter depending on the distance between radii. That is what we + * are computing here. */ + const float t = (sample.radius - sample_first.radius) / + (sample_last.radius - sample_first.radius); + const float linear_opacity = math::interpolate(sample_first.opacity, sample_last.opacity, t); + + return math::abs(sample.opacity - linear_opacity); + }; + Array simplify_sample(nb_samples, false); + const float distance_threshold = 0.1f; + ed::greasepencil::ramer_douglas_peucker_simplify( + eraser_rings.index_range(), distance_threshold, opacity_distance, simplify_sample); + + for (const int rev_sample_index : eraser_rings.index_range()) { + const int sample_index = nb_samples - rev_sample_index - 1; + if (simplify_sample[sample_index]) { + eraser_rings.remove(sample_index); + } + } + + return eraser_rings; + } + + /** + * The soft eraser decreases the opacity of the points it hits. + * The new opacity is computed as a minimum between the current opacity and + * a falloff function of the distance of the point to the center of the eraser. + * If the opacity of a point falls below a threshold, then the point is removed from the + * curves. + */ + bool soft_eraser(const blender::bke::CurvesGeometry &src, + const Span screen_space_positions, + blender::bke::CurvesGeometry &dst, + const bool keep_caps) + { + using namespace blender::bke; + const std::string opacity_attr = "opacity"; + + /* The soft eraser changes the opacity of the strokes underneath it using a curve falloff. We + * sample this curve to get a set of rings in the brush. */ + const Vector eraser_rings = compute_piecewise_linear_falloff(); + const int intersections_max_per_segment = eraser_rings.size() * 2; + + /* Compute intersections between the source curves geometry and all the rings of the eraser. + */ + const int src_points_num = src.points_num(); + Array> src_point_ring(src_points_num, + {-1, PointCircleSide::Outside}); + Array src_intersections(src_points_num * + intersections_max_per_segment); + curves_intersections_and_points_sides( + src, screen_space_positions, eraser_rings, src_point_ring, src_intersections); + + /* Function to get the resulting opacity at a specific point in the source. */ + const VArray &src_opacity = *src.attributes().lookup_or_default( + opacity_attr, bke::AttrDomain::Point, 1.0f); + const auto compute_opacity = [&](const int src_point) { + const float distance = math::distance(screen_space_positions[src_point], + this->mouse_position); + const float brush_strength = this->eraser_strength * + BKE_brush_curve_strength( + this->brush_, distance, this->eraser_radius); + return math::clamp(src_opacity[src_point] - brush_strength, 0.0f, 1.0f); + }; + + /* Compute the map of points in the destination. + * For each point in the source, we create a vector of destination points. Destination points + * can either be directly a point of the source, or a point inside a segment of the source. A + * destination point can also carry the role of a "cut", meaning it is going to be the first + * point of a new splitted curve in the destination. */ + Array> src_to_dst_points(src_points_num); + const OffsetIndices src_points_by_curve = src.points_by_curve(); + for (const int src_curve : src.curves_range()) { + const IndexRange src_points = src_points_by_curve[src_curve]; + + for (const int src_point : src_points) { + Vector &dst_points = src_to_dst_points[src_point]; + const int src_next_point = (src_point == src_points.last()) ? src_points.first() : + (src_point + 1); + + /* Get the ring into which the source point lies. + * If the point is completely outside of the eraser, then the index is (-1). */ + const int point_ring = src_point_ring[src_point].first; + const bool ring_is_cut = (point_ring != -1) && eraser_rings[point_ring].hard_erase; + const PointCircleSide point_side = src_point_ring[src_point].second; + + const bool point_is_cut = ring_is_cut && + (point_side == PointCircleSide::InsideOutsideBoundary); + const bool remove_point = ring_is_cut && (point_side == PointCircleSide::Inside); + if (!remove_point) { + dst_points.append( + {src_point, src_next_point, 0.0f, true, point_is_cut, compute_opacity(src_point)}); + } + + const IndexRange src_point_intersections(src_point * intersections_max_per_segment, + intersections_max_per_segment); + + std::sort(src_intersections.begin() + src_point_intersections.first(), + src_intersections.begin() + src_point_intersections.last() + 1, + [](SegmentCircleIntersection a, SegmentCircleIntersection b) { + return a.factor < b.factor; + }); + + /* Add all intersections with the rings. */ + for (const SegmentCircleIntersection &intersection : + src_intersections.as_span().slice(src_point_intersections)) + { + if (!intersection.is_valid()) { + /* Stop at the first non valid intersection. */ + break; + } + + const EraserRing &ring = eraser_rings[intersection.ring_index]; + const bool is_cut = intersection.inside_outside_intersection && ring.hard_erase; + const float initial_opacity = math::interpolate( + src_opacity[src_point], src_opacity[src_next_point], intersection.factor); + + const float opacity = math::max(0.0f, math::min(initial_opacity, ring.opacity)); + + /* Avoid the accumulation of multiple cuts. */ + if (is_cut && !dst_points.is_empty() && dst_points.last().is_cut) { + dst_points.remove_last(); + } + + dst_points.append( + {src_point, src_next_point, intersection.factor, false, is_cut, opacity}); + } + } + } + + const Array dst_points = compute_topology_change( + src, dst, src_to_dst_points, keep_caps); + + /* Set opacity. */ + bke::MutableAttributeAccessor dst_attributes = dst.attributes_for_write(); + const bke::AnonymousAttributePropagationInfo propagation_info{}; + + bke::SpanAttributeWriter dst_opacity = + dst_attributes.lookup_or_add_for_write_span(opacity_attr, bke::AttrDomain::Point); + threading::parallel_for(dst.points_range(), 4096, [&](const IndexRange dst_points_range) { + for (const int dst_point_index : dst_points_range) { + const ed::greasepencil::PointTransferData &dst_point = dst_points[dst_point_index]; + dst_opacity.span[dst_point_index] = dst_point.opacity; + } + }); + dst_opacity.finish(); + + SpanAttributeWriter dst_inserted = dst_attributes.lookup_or_add_for_write_span( + "_eraser_inserted", bke::AttrDomain::Point); + const OffsetIndices &dst_points_by_curve = dst.points_by_curve(); + threading::parallel_for(dst.curves_range(), 4096, [&](const IndexRange dst_curves_range) { + for (const int dst_curve : dst_curves_range) { + IndexRange dst_points_range = dst_points_by_curve[dst_curve]; + + dst_inserted.span[dst_points_range.first()] = false; + dst_inserted.span[dst_points_range.last()] = false; + + if (dst_points_range.size() < 3) { + continue; + } + + for (const int dst_point_index : dst_points_range.drop_back(1).drop_front(1)) { + const ed::greasepencil::PointTransferData &dst_point = dst_points[dst_point_index]; + dst_inserted.span[dst_point_index] |= !dst_point.is_src_point; + } + } + }); + dst_inserted.finish(); + + return true; + } + bool stroke_eraser(const bke::CurvesGeometry &src, const Span screen_space_positions, bke::CurvesGeometry &dst) const @@ -516,10 +847,17 @@ struct EraseOperationExecutor { /* Get the tool's data. */ this->mouse_position = extension_sample.mouse_position; this->eraser_radius = self.radius_; + this->eraser_strength = self.strength_; + if (BKE_brush_use_size_pressure(brush)) { this->eraser_radius *= BKE_curvemapping_evaluateF( brush->gpencil_settings->curve_strength, 0, extension_sample.pressure); } + if (BKE_brush_use_alpha_pressure(brush)) { + this->eraser_strength *= BKE_curvemapping_evaluateF( + brush->gpencil_settings->curve_strength, 0, extension_sample.pressure); + } + this->brush_ = brush; this->mouse_position_pixels = int2(round_fl_to_int(this->mouse_position.x), round_fl_to_int(this->mouse_position.y)); @@ -564,8 +902,7 @@ struct EraseOperationExecutor { erased = hard_eraser(src, screen_space_positions, dst, self.keep_caps_); break; case GP_BRUSH_ERASER_SOFT: - /* TODO: Implement the soft eraser mode. */ - erased = hard_eraser(src, screen_space_positions, dst, self.keep_caps_); + erased = soft_eraser(src, screen_space_positions, dst, self.keep_caps_); break; } @@ -574,6 +911,7 @@ struct EraseOperationExecutor { drawing.geometry.wrap() = std::move(dst); drawing.tag_topology_changed(); changed = true; + self.affected_drawings_.add(&drawing); } }; @@ -611,6 +949,7 @@ struct EraseOperationExecutor { void EraseOperation::on_stroke_begin(const bContext &C, const InputSample &start_sample) { + Scene *scene = CTX_data_scene(&C); Paint *paint = BKE_paint_get_active_from_context(&C); Brush *brush = BKE_paint_brush(paint); @@ -621,7 +960,6 @@ void EraseOperation::on_stroke_begin(const bContext &C, const InputSample &start ARegion *region = CTX_wm_region(&C); View3D *view3d = CTX_wm_view3d(&C); RegionView3D *rv3d = CTX_wm_region_view3d(&C); - Scene *scene = CTX_data_scene(&C); Object *object = CTX_data_active_object(&C); Object *eval_object = DEG_get_evaluated_object(depsgraph, object); GreasePencil *grease_pencil = static_cast(object->data); @@ -659,11 +997,13 @@ void EraseOperation::on_stroke_begin(const bContext &C, const InputSample &start } BLI_assert(brush->gpencil_settings != nullptr); + BKE_curvemapping_init(brush->curve); BKE_curvemapping_init(brush->gpencil_settings->curve_strength); eraser_mode_ = eGP_BrushEraserMode(brush->gpencil_settings->eraser_mode); keep_caps_ = ((brush->gpencil_settings->flag & GP_BRUSH_ERASER_KEEP_CAPS) != 0); active_layer_only_ = ((brush->gpencil_settings->flag & GP_BRUSH_ACTIVE_LAYER_ONLY) != 0); + strength_ = brush->alpha; } void EraseOperation::on_stroke_extended(const bContext &C, const InputSample &extension_sample) @@ -682,6 +1022,57 @@ void EraseOperation::on_stroke_done(const bContext &C) grease_pencil.runtime->temp_use_eraser = false; grease_pencil.runtime->temp_eraser_size = 0.0f; } + + /* Epsilon used for simplify. */ + const float epsilon = 0.01f; + for (GreasePencilDrawing *drawing_ : affected_drawings_) { + blender::bke::CurvesGeometry &curves = drawing_->geometry.wrap(); + + /* Simplify in between the ranges of inserted points. */ + const VArray &point_was_inserted = *curves.attributes().lookup( + "_eraser_inserted", bke::AttrDomain::Point); + if (point_was_inserted.is_empty()) { + continue; + } + IndexMaskMemory mem_inserted; + IndexMask inserted_points = IndexMask::from_bools(point_was_inserted, mem_inserted); + + /* Distance function for the simplification algorithm. + * It is computed as the difference in opacity that may result from removing the + * samples inside the range. */ + VArray opacities = drawing_->wrap().opacities(); + Span positions = curves.positions(); + const auto opacity_distance = [&](int64_t first_index, int64_t last_index, int64_t index) { + const float3 &s0 = positions[first_index]; + const float3 &s1 = positions[last_index]; + const float segment_length = math::distance(s0, s1); + if (segment_length < 1e-6) { + return 0.0f; + } + const float t = math::distance(s0, positions[index]) / segment_length; + const float linear_opacity = math::interpolate( + opacities[first_index], opacities[last_index], t); + return math::abs(opacities[index] - linear_opacity); + }; + + Array remove_points(curves.points_num(), false); + inserted_points.foreach_range([&](const IndexRange &range) { + IndexRange range_to_simplify(range.one_before_start(), range.size() + 2); + ed::greasepencil::ramer_douglas_peucker_simplify( + range_to_simplify, epsilon, opacity_distance, remove_points); + }); + + /* Remove the points. */ + IndexMaskMemory mem_remove; + IndexMask points_to_remove = IndexMask::from_bools(remove_points, mem_remove); + + curves.remove_points(points_to_remove, {}); + drawing_->wrap().tag_topology_changed(); + + curves.attributes_for_write().remove("_eraser_inserted"); + } + + affected_drawings_.clear(); } std::unique_ptr new_erase_operation(const bool temp_eraser)