GPv3: Soft mode for the Eraser tool #110310
@ -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':
|
||||
|
@ -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.
|
||||
|
@ -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<GreasePencilDrawing *> 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<float2> screen_space_positions,
|
||||
const int intersections_max_per_segment,
|
||||
MutableSpan<PointCircleSide> r_point_side,
|
||||
const Span<EraserRing> rings,
|
||||
MutableSpan<std::pair<int, PointCircleSide>> r_point_ring,
|
||||
MutableSpan<SegmentCircleIntersection> 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<int> src_points_by_curve = src.points_by_curve();
|
||||
const VArray<bool> 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<EraserRing> 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<PointCircleSide> src_point_side(src_points_num, PointCircleSide::Outside);
|
||||
Array<std::pair<int, PointCircleSide>> src_point_ring(src_points_num,
|
||||
{-1, PointCircleSide::Outside});
|
||||
Array<SegmentCircleIntersection> 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<Vector<ed::greasepencil::PointTransferData>> src_to_dst_points(src_points_num);
|
||||
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
|
||||
@ -410,7 +482,7 @@ struct EraseOperationExecutor {
|
||||
Vector<ed::greasepencil::PointTransferData> &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<EraserRing> 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<EraserRing> 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<bool> 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<bool> 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<float2> 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<EraserRing> 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.
|
||||
*/
|
||||
casey-bianco-davis marked this conversation as resolved
Outdated
|
||||
const int src_points_num = src.points_num();
|
||||
Array<std::pair<int, PointCircleSide>> src_point_ring(src_points_num,
|
||||
{-1, PointCircleSide::Outside});
|
||||
Array<SegmentCircleIntersection> 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<float> &src_opacity = *src.attributes().lookup_or_default<float>(
|
||||
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<Vector<ed::greasepencil::PointTransferData>> src_to_dst_points(src_points_num);
|
||||
const OffsetIndices<int> 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<ed::greasepencil::PointTransferData> &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<ed::greasepencil::PointTransferData> 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<float> dst_opacity =
|
||||
dst_attributes.lookup_or_add_for_write_span<float>(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<bool> dst_inserted = dst_attributes.lookup_or_add_for_write_span<bool>(
|
||||
"_eraser_inserted", bke::AttrDomain::Point);
|
||||
const OffsetIndices<int> &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<float2> 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<GreasePencil *>(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<bool> &point_was_inserted = *curves.attributes().lookup<bool>(
|
||||
"_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<float> opacities = drawing_->wrap().opacities();
|
||||
Span<float3> 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<bool> 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<GreasePencilStrokeOperation> new_erase_operation(const bool temp_eraser)
|
||||
|
Loading…
Reference in New Issue
Block a user
Maybe this could be split into a
CurvesGeometry
function that takes aIndexMask
and handles the point deleting and the topological changes, andsoft_eraser
only needs to handle the opacity.something like
Yes, that is one idea I have for refactoring the eraser in all modes. Right now, this PR is still pending because we would like to improve a bit the behavior of the soft eraser, so that it inserts points to respect at best the falloff and the radius of the brush tool.
The thing is, if we want to both insert and remove points, it would be best to do it in one pass, because we are going to re-build a new CurvesGeometry in both cases. So maybe this function would be more like
dst = insert_and_delete(src, points_to_delete, points_to_insert)
(or something similar).I agree with you that this is going to need a refactor, with the hard eraser mode as well.