GPv3: Hard Eraser tool

Implementation of the hard eraser for grease pencil.
The tool "cuts" the strokes, meaning it removes points that are inside the eraser's radius, while adding points at intersections between the eraser and the strokes.

Note that this does not implement the "Stroke" and "Dissolve" mode of the eraser yet.

Pull Request: https://projects.blender.org/blender/blender/pulls/110063
This commit is contained in:
Amelie
2023-07-20 13:56:23 +02:00
committed by Falk David
parent b08b6984d0
commit 924aa0ef07
7 changed files with 603 additions and 0 deletions

View File

@@ -1741,6 +1741,25 @@ class _defs_paint_grease_pencil:
icon="brush.gpencil_draw.draw",
data_block='DRAW',
)
@ToolDef.from_fn
def erase():
def draw_settings(context, layout, _tool):
paint = context.tool_settings.gpencil_paint
brush = paint.brush
if not brush:
return
layout.prop(brush.gpencil_settings, "eraser_mode", expand=True)
if brush.gpencil_settings.eraser_mode == "HARD":
layout.prop(brush.gpencil_settings, "use_keep_caps_eraser")
layout.prop(brush.gpencil_settings, "use_active_layer_only")
return dict(
idname="builtin_brush.Erase",
label="Erase",
icon="brush.gpencil_draw.erase",
data_block='ERASE',
draw_settings=draw_settings,
)
class _defs_image_generic:
@@ -3105,6 +3124,7 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel):
_defs_view3d_generic.cursor,
None,
_defs_paint_grease_pencil.draw,
_defs_paint_grease_pencil.erase,
],
'PAINT_GPENCIL': [
_defs_view3d_generic.cursor,

View File

@@ -39,6 +39,7 @@ set(SRC
curves_sculpt_smooth.cc
curves_sculpt_snake_hook.cc
grease_pencil_draw_ops.cc
grease_pencil_erase.cc
grease_pencil_paint.cc
paint_canvas.cc
paint_cursor.cc

View File

@@ -49,6 +49,9 @@ static bool start_brush_operation(bContext &C,
/* FIXME: Somehow store the unique_ptr in the PaintStroke. */
operation = greasepencil::new_paint_operation().release();
break;
case GPAINT_TOOL_ERASE:
operation = greasepencil::new_erase_operation().release();
break;
}
if (operation) {

View File

@@ -0,0 +1,562 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#include "BLI_array.hh"
#include "BLI_array_utils.hh"
#include "BLI_index_mask.hh"
#include "BLI_math_geom.h"
#include "BLI_task.hh"
#include "BKE_brush.h"
#include "BKE_context.h"
#include "BKE_crazyspace.hh"
#include "BKE_curves.hh"
#include "BKE_curves_utils.hh"
#include "BKE_grease_pencil.h"
#include "BKE_grease_pencil.hh"
#include "BKE_scene.h"
#include "DEG_depsgraph_query.h"
#include "DNA_brush_enums.h"
#include "ED_view3d.h"
#include "WM_api.h"
#include "WM_types.h"
#include "grease_pencil_intern.hh"
namespace blender::ed::sculpt_paint::greasepencil {
class EraseOperation : public GreasePencilStrokeOperation {
public:
~EraseOperation() override {}
void on_stroke_begin(const bContext &C, const InputSample &start_sample) override;
void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override;
void on_stroke_done(const bContext &C) override;
bool keep_caps = false;
float radius = 50.0f;
eGP_BrushEraserMode eraser_mode = GP_BRUSH_ERASER_HARD;
bool active_layer_only = false;
};
/**
* 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{};
EraseOperationExecutor(const bContext & /*C*/) {}
/**
* Computes the intersection between the eraser tool and a 2D segment.
*
* \param point: coordinates of the first point in the segment.
* \param point_after: coordinates of the second point in the segment.
*
* \param r_mu0: output factor of the first intersection if it exists, otherwise (-1).
* \param r_mu1: output factor of the second intersection if it exists, otherwise (-1).
*
* \returns total number of intersections lying inside the segment (ie whose factor is in [0,1]).
*
* Note that the eraser is represented as a circle, and thus there can be only 0, 1 or 2
* intersections with a segment.
*/
int intersections_with_segment(const float2 &point,
const float2 &point_after,
float &r_mu0,
float &r_mu1) const
{
/* Compute the intersection points. */
float2 inter0{};
float2 inter1{};
const int nb_inter = isect_line_sphere_v2(
point, point_after, this->mouse_position, this->eraser_radius, inter0, inter1);
/* Retrieve the line factor from the coordinates of the intersection points. */
const auto compute_intersection_parameter =
[](const float2 p0, const float2 p1, const float2 inter) {
const float mu = (math::length(inter - p0) / math::length(p1 - p0));
const float sign_mu = (math::dot(inter - p0, p1 - p0) < 0) ? -1.0 : 1.0;
return sign_mu * mu;
};
r_mu0 = (nb_inter > 0) ? compute_intersection_parameter(point, point_after, inter0) : -1.0;
r_mu1 = (nb_inter > 1) ? compute_intersection_parameter(point, point_after, inter1) : -1.0;
/* Sort intersections by line factor. */
if ((nb_inter > 1) && (r_mu0 > r_mu1)) {
std::swap(r_mu0, r_mu1);
}
/* Return the number of intersections that actually lies within the segment. */
return int(IN_RANGE(r_mu0, 0, 1)) + int(IN_RANGE(r_mu1, 0, 1));
}
/**
* Compute intersections between the eraser and the input Curves Geometry.
*
* \param screen_space_positions: input parameter containing the 2D positions of the geometry in
* screen space.
*
* \param r_nb_intersections: output parameter filled with the number of intersections
* per-segment. Should be the size of the source point range.
* \param r_intersections_factors: output parameter filled with the factors of the potential
* intersections with each segment. Should be the size of the source point range.
* \returns total number of intersections found.
*
* Note that for the two output arrays the last element may contain intersections if the
* corresponding curve is cyclic.
*/
int intersections_with_curves(const blender::bke::CurvesGeometry &src,
const Array<float2> &screen_space_positions,
Array<int> &r_nb_intersections,
Array<float2> &r_intersections_factors) const
{
using namespace blender::bke;
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
const VArray<bool> src_cyclic = src.cyclic();
threading::parallel_for(src.curves_range(), 256, [&](const IndexRange src_curves) {
for (const int src_curve : src_curves) {
const IndexRange src_curve_points = src_points_by_curve[src_curve];
threading::parallel_for(
src_curve_points.drop_back(1), 512, [&](const IndexRange src_points) {
for (int src_point : src_points) {
float mu0;
float mu1;
r_nb_intersections[src_point] = intersections_with_segment(
screen_space_positions[src_point],
screen_space_positions[src_point + 1],
mu0,
mu1);
r_intersections_factors[src_point] = float2(mu0, mu1);
}
});
if (src_cyclic[src_curve]) {
/* If the curve is cyclic, we need to check for the closing segment. */
const int src_last_point = src_curve_points.last();
float mu0;
float mu1;
r_nb_intersections[src_last_point] = intersections_with_segment(
screen_space_positions[src_last_point],
screen_space_positions[src_curve_points.first()],
mu0,
mu1);
r_intersections_factors[src_last_point] = float2(mu0, mu1);
}
}
});
/* Compute total number of intersections. */
int total_intersections = 0;
for (const int src_point : src.points_range()) {
total_intersections += r_nb_intersections[src_point];
}
return total_intersections;
}
/**
* Checks if a point is inside the eraser or not.
*/
inline bool contains_point(const float2 &point) const
{
return (math::distance_squared(point, this->mouse_position) <=
this->eraser_radius * this->eraser_radius);
}
/**
* Checks if each point is inside the eraser or not.
*
* \param screen_space_positions: input parameter containing the 2D positions of the geometry in
* screen space.
* \param points_range: ranges of points to check.
*
* \param r_point_inside: output parameter filled with booleans : true if the point is inside the
* eraser, false otherwise.
* \returns total number of inside points.
*
* Note that for the two output arrays the last element may contain intersections if the
* corresponding curve is cyclic.
*/
int compute_points_inside(const Array<float2> &screen_space_positions,
const IndexRange points_range,
Array<bool> &r_point_inside) const
{
/* Check if points are inside the eraser. */
threading::parallel_for(points_range, 1024, [&](const IndexRange src_points) {
for (const int src_point : src_points) {
const float2 pos_view = screen_space_positions[src_point];
r_point_inside[src_point] = contains_point(pos_view);
}
});
/* Compute total number of points inside the eraser. */
int total_points_inside = 0;
for (const int src_point : points_range) {
total_points_inside += r_point_inside[src_point] ? 1 : 0;
}
return total_points_inside;
}
bool hard_eraser(const blender::bke::CurvesGeometry &src,
const Array<float2> &screen_space_positions,
blender::bke::CurvesGeometry &dst,
const bool keep_caps) const
{
using namespace blender::bke;
const VArray<bool> src_cyclic = src.cyclic();
const int src_points_num = src.points_num();
const int src_curves_num = src.curves_num();
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
/* Compute intersections between the eraser and the curves in the source domain. */
Array<int> nb_intersections(src_points_num, 0);
Array<float2> src_intersections_parameters(src_points_num);
const int total_intersections = intersections_with_curves(
src, screen_space_positions, nb_intersections, src_intersections_parameters);
/* Check if points are inside the eraser. */
Array<bool> is_point_inside(src_points_num, false);
const int total_points_inside = compute_points_inside(
screen_space_positions, src.points_range(), is_point_inside);
/* Total number of points in the destination :
* - intersections with the eraser are added,
* - points that are inside the erase are removed.
*/
const int dst_points_num = src_points_num + total_intersections - total_points_inside;
if ((total_intersections == 0) && (total_points_inside == 0)) {
/* Return early if nothing to change. */
return false;
}
if (dst_points_num == 0) {
/* Return early if no points left. */
dst.resize(0, 0);
return true;
}
/* Set the intersection parameters in the destination domain : a pair of int and float numbers
* for which the integer is the index of the corresponding segment in the source curves, and
* the float part is the (0,1) factor representing its position in the segment.
*/
Array<std::pair<int, float>> dst_points_parameters(dst_points_num);
Array<bool> is_cut(dst_points_num, false);
Array<int> src_pivot_point(src_curves_num, -1);
Array<int> dst_interm_curves_offsets(src_curves_num + 1, 0);
int dst_point = -1;
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) {
if (!is_point_inside[src_point]) {
/* Add a point from the source : the factor is only the index in the source. */
dst_points_parameters[++dst_point] = {src_point, 0.0};
}
if (nb_intersections[src_point] > 0) {
float mu0 = src_intersections_parameters[src_point].x;
float mu1 = src_intersections_parameters[src_point].y;
if (IN_RANGE(mu0, 0, 1)) {
/* Add an intersection with the eraser and mark it as a cut. */
dst_points_parameters[++dst_point] = {src_point, mu0};
is_cut[dst_point] = true;
}
if (IN_RANGE(mu1, 0, 1)) {
/* Add an intersection with the eraser and mark it as a cut. */
dst_points_parameters[++dst_point] = {src_point, mu1};
is_cut[dst_point] = true;
}
/* For cyclic curves, mark the pivot point as the last intersection with the eraser
* that starts a new segment in the destination.
*/
if (src_cyclic[src_curve] &&
(is_point_inside[src_point] || (nb_intersections[src_point] == 2))) {
src_pivot_point[src_curve] = dst_point;
}
}
}
/* We store intermediate curve offsets represent an intermediate state of the destination
* curves before cutting the curves at eraser's intersection. Thus, it contains the same
* number of curves than in the source, but the offsets are different, because points may
* have been added or removed. */
dst_interm_curves_offsets[src_curve + 1] = dst_point + 1;
}
/* Cyclic curves. */
Array<bool> src_now_cyclic(src_curves_num);
threading::parallel_for(src.curves_range(), 4096, [&](const IndexRange src_curves) {
for (const int src_curve : src_curves) {
const int pivot_point = src_pivot_point[src_curve];
if (pivot_point == -1) {
/* Either the curve was not cyclic or it wasn't cut : no need to change it. */
src_now_cyclic[src_curve] = src_cyclic[src_curve];
continue;
}
/* A cyclic curve was cut :
* - this curve is not cyclic anymore,
* - and we have to shift points to keep the closing segment.
*/
src_now_cyclic[src_curve] = false;
const int dst_interm_first = dst_interm_curves_offsets[src_curve];
const int dst_interm_last = dst_interm_curves_offsets[src_curve + 1];
std::rotate(dst_points_parameters.begin() + dst_interm_first,
dst_points_parameters.begin() + pivot_point,
dst_points_parameters.begin() + dst_interm_last);
std::rotate(is_cut.begin() + dst_interm_first,
is_cut.begin() + pivot_point,
is_cut.begin() + dst_interm_last);
}
});
/* Compute the destination curve offsets. */
Vector<int> dst_curves_offset;
Vector<int> dst_to_src_curve;
dst_curves_offset.append(0);
for (int src_curve : src.curves_range()) {
const IndexRange dst_points(dst_interm_curves_offsets[src_curve],
dst_interm_curves_offsets[src_curve + 1] -
dst_interm_curves_offsets[src_curve]);
int length_of_current = 0;
for (int dst_point : dst_points) {
const int src_point = dst_points_parameters[dst_point].first;
if ((length_of_current > 0) && is_cut[dst_point] && is_point_inside[src_point]) {
/* This is the new first point of a curve. */
dst_curves_offset.append(dst_point);
dst_to_src_curve.append(src_curve);
length_of_current = 0;
}
++length_of_current;
}
if (length_of_current != 0) {
/* End of a source curve. */
dst_curves_offset.append(dst_points.one_after_last());
dst_to_src_curve.append(src_curve);
}
}
const int dst_curves_num = dst_curves_offset.size() - 1;
/* Create the new curves geometry. */
dst.resize(dst_points_num, dst_curves_num);
array_utils::copy(dst_curves_offset.as_span(), dst.offsets_for_write());
/* Attributes. */
const bke::AttributeAccessor src_attributes = src.attributes();
bke::MutableAttributeAccessor dst_attributes = dst.attributes_for_write();
const AnonymousAttributePropagationInfo propagation_info{};
/* Copy curves attributes. */
for (bke::AttributeTransferData &attribute : bke::retrieve_attributes_for_transfer(
src_attributes, dst_attributes, ATTR_DOMAIN_MASK_CURVE, propagation_info, {"cyclic"}))
{
bke::attribute_math::gather(attribute.src, dst_to_src_curve, attribute.dst.span);
attribute.dst.finish();
}
array_utils::gather(
src_now_cyclic.as_span(), dst_to_src_curve.as_span(), dst.cyclic_for_write());
/* Display intersections with flat caps. */
const OffsetIndices<int> dst_points_by_curve = dst.points_by_curve();
if (!keep_caps) {
SpanAttributeWriter<int8_t> dst_start_caps =
dst_attributes.lookup_or_add_for_write_span<int8_t>("start_cap", ATTR_DOMAIN_CURVE);
SpanAttributeWriter<int8_t> dst_end_caps =
dst_attributes.lookup_or_add_for_write_span<int8_t>("end_cap", ATTR_DOMAIN_CURVE);
threading::parallel_for(dst.curves_range(), 4096, [&](const IndexRange dst_curves) {
for (const int dst_curve : dst_curves) {
const IndexRange dst_curve_points = dst_points_by_curve[dst_curve];
if (is_cut[dst_curve_points.first()]) {
dst_start_caps.span[dst_curve] = GP_STROKE_CAP_TYPE_FLAT;
}
if (is_cut[dst_curve_points.last()]) {
dst_end_caps.span[dst_curve] = GP_STROKE_CAP_TYPE_FLAT;
}
}
});
dst_start_caps.finish();
dst_end_caps.finish();
}
/* Copy/Interpolate point attributes. */
for (bke::AttributeTransferData &attribute : bke::retrieve_attributes_for_transfer(
src_attributes, dst_attributes, ATTR_DOMAIN_MASK_POINT, propagation_info))
{
bke::attribute_math::convert_to_static_type(attribute.dst.span.type(), [&](auto dummy) {
using T = decltype(dummy);
auto src_attr = attribute.src.typed<T>();
auto dst_attr = attribute.dst.span.typed<T>();
threading::parallel_for(dst.curves_range(), 512, [&](const IndexRange dst_curves) {
for (const int dst_curve : dst_curves) {
const IndexRange dst_curve_points = dst_points_by_curve[dst_curve];
const int src_curve = dst_to_src_curve[dst_curve];
const IndexRange src_curve_points = src_points_by_curve[src_curve];
threading::parallel_for(dst_curve_points, 4096, [&](const IndexRange dst_points) {
for (const int dst_point : dst_points) {
const int src_point = dst_points_parameters[dst_point].first;
if (!is_cut[dst_point]) {
dst_attr[dst_point] = src_attr[src_point];
continue;
}
const float src_pt_factor = dst_points_parameters[dst_point].second;
/* Compute the endpoint of the segment in the source domain.
* Note that if this is the closing segment of a cyclic curve, then the
* endpoint of the segment in the first point of the curve. */
const int src_next_point = (src_point == src_curve_points.last()) ?
src_curve_points.first() :
(src_point + 1);
dst_attr[dst_point] = bke::attribute_math::mix2<T>(
src_pt_factor, src_attr[src_point], src_attr[src_next_point]);
}
});
}
});
attribute.dst.finish();
});
}
return true;
}
void execute(EraseOperation &self, const bContext &C, const InputSample &extension_sample)
{
using namespace blender::bke::greasepencil;
Scene *scene = CTX_data_scene(&C);
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(&C);
ARegion *region = CTX_wm_region(&C);
Object *obact = CTX_data_active_object(&C);
Object *ob_eval = DEG_get_evaluated_object(depsgraph, obact);
/* Get the tool's data. */
this->mouse_position = extension_sample.mouse_position;
this->eraser_radius = extension_sample.pressure * self.radius;
/* Get the grease pencil drawing. */
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(obact->data);
bool changed = false;
const auto execute_eraser_on_drawing = [&](int drawing_index, Drawing &drawing) {
const bke::CurvesGeometry &src = drawing.strokes();
/* Evaluated geometry. */
bke::crazyspace::GeometryDeformation deformation =
bke::crazyspace::get_evaluated_grease_pencil_drawing_deformation(
ob_eval, *obact, drawing_index);
/* Compute screen space positions. */
Array<float2> screen_space_positions(src.points_num());
threading::parallel_for(src.points_range(), 4096, [&](const IndexRange src_points) {
for (const int src_point : src_points) {
ED_view3d_project_float_global(region,
deformation.positions[src_point],
screen_space_positions[src_point],
V3D_PROJ_TEST_NOP);
}
});
/* Erasing operator. */
bke::CurvesGeometry dst;
bool erased = false;
switch (self.eraser_mode) {
case GP_BRUSH_ERASER_STROKE:
// To be implemented
return;
case GP_BRUSH_ERASER_HARD:
erased = hard_eraser(src, screen_space_positions, dst, self.keep_caps);
break;
case GP_BRUSH_ERASER_SOFT:
// To be implemented
return;
}
if (erased) {
/* Set the new geometry. */
drawing.geometry.wrap() = std::move(dst);
drawing.tag_topology_changed();
changed = true;
}
};
if (self.active_layer_only) {
/* Erase only on the drawing at the current frame of the active layer. */
const Layer *active_layer = grease_pencil.get_active_layer();
Drawing *drawing = grease_pencil.get_editable_drawing_at(active_layer, scene->r.cfra);
if (drawing == nullptr) {
return;
}
execute_eraser_on_drawing(active_layer->drawing_index_at(scene->r.cfra), *drawing);
}
else {
/* Erase on all editable drawings. */
grease_pencil.foreach_editable_drawing(scene->r.cfra, execute_eraser_on_drawing);
}
if (changed) {
DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GEOM | ND_DATA, &grease_pencil);
}
}
};
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);
this->radius = BKE_brush_size_get(scene, brush);
if (brush->gpencil_settings) {
this->eraser_mode = eGP_BrushEraserMode(brush->gpencil_settings->eraser_mode);
this->keep_caps = ((brush->gpencil_settings->flag & GP_BRUSH_ERASER_KEEP_CAPS) != 0);
this->active_layer_only = ((brush->gpencil_settings->flag & GP_BRUSH_ACTIVE_LAYER_ONLY) != 0);
}
}
void EraseOperation::on_stroke_extended(const bContext &C, const InputSample &extension_sample)
{
EraseOperationExecutor executor{C};
executor.execute(*this, C, extension_sample);
}
void EraseOperation::on_stroke_done(const bContext & /*C*/) {}
std::unique_ptr<GreasePencilStrokeOperation> new_erase_operation()
{
return std::make_unique<EraseOperation>();
}
} // namespace blender::ed::sculpt_paint::greasepencil

View File

@@ -26,6 +26,7 @@ class GreasePencilStrokeOperation {
namespace greasepencil {
std::unique_ptr<GreasePencilStrokeOperation> new_paint_operation();
std::unique_ptr<GreasePencilStrokeOperation> new_erase_operation();
} // namespace greasepencil

View File

@@ -96,6 +96,11 @@ typedef enum eGPDbrush_Flag {
GP_BRUSH_OUTLINE_STROKE = (1 << 17),
/* Collide with stroke. */
GP_BRUSH_FILL_STROKE_COLLIDE = (1 << 18),
/* Keep the caps as they are when erasing. Otherwise flatten the caps. */
GP_BRUSH_ERASER_KEEP_CAPS = (1 << 19),
/* Affect only the drawing in the active layer. Otherwise affect all editable drawings in the
object. */
GP_BRUSH_ACTIVE_LAYER_ONLY = (1 << 20),
} eGPDbrush_Flag;
typedef enum eGPDbrush_Flag2 {

View File

@@ -2100,6 +2100,17 @@ static void rna_def_gpencil_options(BlenderRNA *brna)
RNA_def_property_boolean_sdna(prop, nullptr, "flag", GP_BRUSH_OCCLUDE_ERASER);
RNA_def_property_ui_text(prop, "Occlude Eraser", "Erase only strokes visible and not occluded");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
prop = RNA_def_property(srna, "use_keep_caps_eraser", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", GP_BRUSH_ERASER_KEEP_CAPS);
RNA_def_property_ui_text(
prop, "Keep caps", "Keep the caps as they are and don't flatten them when erasing");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
prop = RNA_def_property(srna, "use_active_layer_only", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", GP_BRUSH_ACTIVE_LAYER_ONLY);
RNA_def_property_ui_text(prop, "Active Layer", "Only edit the active layer of the object.");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
}
static void rna_def_curves_sculpt_options(BlenderRNA *brna)