Curves: initial brush implementations for curves sculpt mode

The main goal here is to add the boilerplate code to make it possible
to add the actual sculpt tools more easily. Both brush implementations
added by this patch are meant to be prototypes which will be removed
or refined in the coming weeks.

Ref T95773.

Differential Revision: https://developer.blender.org/D14180
This commit is contained in:
Jacques Lucke 2022-02-23 16:56:27 +01:00
parent 120f16fa1f
commit 226f0c4fef
Notes: blender-bot 2023-02-14 08:24:03 +01:00
Referenced by commit d3a1e9cbb9, Geometry Nodes: Multi-thread creation of selection from field
Referenced by issue #95773, Prototype a grab brush for the hair curves sculpt mode.
8 changed files with 382 additions and 16 deletions

View File

@ -8,13 +8,13 @@
* General operations for brushes.
*/
#include "DNA_color_types.h"
#include "DNA_object_enums.h"
#ifdef __cplusplus
extern "C" {
#endif
enum eCurveMappingPreset;
struct Brush;
struct ImBuf;
struct ImagePool;

View File

@ -0,0 +1,60 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup bli
*
* This is separate from `BLI_index_mask.hh` because it includes headers just `IndexMask` shouldn't
* depend on.
*/
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_index_mask.hh"
#include "BLI_task.hh"
#include "BLI_vector.hh"
namespace blender::index_mask_ops {
namespace detail {
IndexMask find_indices_based_on_predicate__merge(
IndexMask indices_to_check,
threading::EnumerableThreadSpecific<Vector<Vector<int64_t>>> &sub_masks,
Vector<int64_t> &r_indices);
} // namespace detail
/**
* Evaluate the #predicate for all indices in #indices_to_check and return a mask that contains all
* indices where the predicate was true.
*
* #r_indices indices is only used if necessary.
*/
template<typename Predicate>
inline IndexMask find_indices_based_on_predicate(const IndexMask indices_to_check,
const int64_t parallel_grain_size,
Vector<int64_t> &r_indices,
const Predicate &predicate)
{
/* Evaluate predicate in parallel. Since the size of the final mask is not known yet, many
* smaller vectors have to be filled with all indices where the predicate is true. Those smaller
* vectors are joined afterwards. */
threading::EnumerableThreadSpecific<Vector<Vector<int64_t>>> sub_masks;
threading::parallel_for(
indices_to_check.index_range(), parallel_grain_size, [&](const IndexRange range) {
const IndexMask sub_mask = indices_to_check.slice(range);
Vector<int64_t> masked_indices;
for (const int64_t i : sub_mask) {
if (predicate(i)) {
masked_indices.append(i);
}
}
if (!masked_indices.is_empty()) {
sub_masks.local().append(std::move(masked_indices));
}
});
/* This part doesn't have to be in the header. */
return detail::find_indices_based_on_predicate__merge(indices_to_check, sub_masks, r_indices);
}
} // namespace blender::index_mask_ops

View File

@ -303,6 +303,9 @@ float dist_squared_to_projected_aabb_simple(const float projmat[4][4],
const float bbmin[3],
const float bbmax[3]);
/** Returns the distance between two 2D line segments. */
float dist_seg_seg_v2(const float a1[3], const float a2[3], const float b1[3], const float b2[3]);
float closest_to_ray_v3(float r_close[3],
const float p[3],
const float ray_orig[3],

View File

@ -203,6 +203,7 @@ set(SRC
BLI_heap.h
BLI_heap_simple.h
BLI_index_mask.hh
BLI_index_mask_ops.hh
BLI_index_range.hh
BLI_inplace_priority_queue.hh
BLI_iterator.h

View File

@ -1,6 +1,7 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_index_mask.hh"
#include "BLI_index_mask_ops.hh"
namespace blender {
@ -126,3 +127,70 @@ Vector<IndexRange> IndexMask::extract_ranges_invert(const IndexRange full_range,
}
} // namespace blender
namespace blender::index_mask_ops::detail {
IndexMask find_indices_based_on_predicate__merge(
IndexMask indices_to_check,
threading::EnumerableThreadSpecific<Vector<Vector<int64_t>>> &sub_masks,
Vector<int64_t> &r_indices)
{
/* Gather vectors that have been generated by possibly multiple threads. */
Vector<Vector<int64_t> *> all_vectors;
int64_t result_mask_size = 0;
for (Vector<Vector<int64_t>> &local_sub_masks : sub_masks) {
for (Vector<int64_t> &sub_mask : local_sub_masks) {
all_vectors.append(&sub_mask);
result_mask_size += sub_mask.size();
}
}
if (all_vectors.is_empty()) {
/* Special case when the predicate was false for all elements. */
return {};
}
if (result_mask_size == indices_to_check.size()) {
/* Special case when the predicate was true for all elements. */
return indices_to_check;
}
if (all_vectors.size() == 1) {
/* Special case when all indices for which the predicate is true happen to be in a single
* vector. */
r_indices = std::move(*all_vectors[0]);
return r_indices.as_span();
}
/* Indices in separate vectors don't overlap. So it is ok to sort the vectors just by looking at
* the first element. */
std::sort(all_vectors.begin(),
all_vectors.end(),
[](const Vector<int64_t> *a, const Vector<int64_t> *b) { return (*a)[0] < (*b)[0]; });
/* Precompute the offsets for the individual vectors, so that the indices can be copied into the
* final vector in parallel. */
Vector<int64_t> offsets;
offsets.reserve(all_vectors.size() + 1);
offsets.append(0);
for (Vector<int64_t> *vector : all_vectors) {
offsets.append(offsets.last() + vector->size());
}
r_indices.resize(result_mask_size);
/* Fill the final index mask in parallel again. */
threading::parallel_for(all_vectors.index_range(), 100, [&](const IndexRange all_vectors_range) {
for (const int64_t vector_index : all_vectors_range) {
Vector<int64_t> &vector = *all_vectors[vector_index];
const int64_t offset = offsets[vector_index];
threading::parallel_for(vector.index_range(), 1024, [&](const IndexRange range) {
initialized_copy_n(vector.data() + range.start(),
range.size(),
r_indices.data() + offset + range.start());
});
}
});
return r_indices.as_span();
}
} // namespace blender::index_mask_ops::detail

View File

@ -903,6 +903,18 @@ float dist_squared_to_projected_aabb_simple(const float projmat[4][4],
/** \} */
float dist_seg_seg_v2(const float a1[3], const float a2[3], const float b1[3], const float b2[3])
{
if (isect_seg_seg_v2_simple(a1, a2, b1, b2)) {
return 0.0f;
}
const float d1 = dist_squared_to_line_segment_v2(a1, b1, b2);
const float d2 = dist_squared_to_line_segment_v2(a2, b1, b2);
const float d3 = dist_squared_to_line_segment_v2(b1, a1, a2);
const float d4 = dist_squared_to_line_segment_v2(b2, a1, a2);
return sqrtf(min_ffff(d1, d2, d3, d4));
}
void closest_on_tri_to_point_v3(
float r[3], const float p[3], const float v1[3], const float v2[3], const float v3[3])
{

View File

@ -9,6 +9,7 @@ set(INC
../../bmesh
../../depsgraph
../../draw
../../functions
../../gpu
../../imbuf
../../makesdna
@ -78,5 +79,16 @@ set(LIB
bf_blenlib
)
if(WITH_TBB)
list(APPEND INC_SYS
${TBB_INCLUDE_DIRS}
)
add_definitions(-DWITH_TBB)
if(WIN32)
# TBB includes Windows.h which will define min/max macros
# that will collide with the stl versions.
add_definitions(-DNOMINMAX)
endif()
endif()
blender_add_lib(bf_editor_sculpt_paint "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")

View File

@ -2,7 +2,9 @@
#include "BLI_utildefines.h"
#include "BKE_brush.h"
#include "BKE_context.h"
#include "BKE_curves.hh"
#include "BKE_paint.h"
#include "WM_api.h"
@ -10,6 +12,19 @@
#include "ED_curves_sculpt.h"
#include "ED_object.h"
#include "ED_screen.h"
#include "ED_view3d.h"
#include "DEG_depsgraph.h"
#include "DNA_brush_types.h"
#include "DNA_curves_types.h"
#include "DNA_screen_types.h"
#include "RNA_access.h"
#include "BLI_index_mask_ops.hh"
#include "BLI_math_vector.hh"
#include "curves_sculpt_intern.h"
#include "paint_intern.h"
@ -35,14 +50,178 @@ bool CURVES_SCULPT_mode_poll_view3d(bContext *C)
return true;
}
/** \} */
namespace blender::ed::sculpt_paint {
/** \} */
using blender::bke::CurvesGeometry;
/* -------------------------------------------------------------------- */
/** \name * SCULPT_CURVES_OT_brush_stroke
* \{ */
struct StrokeExtension {
bool is_first;
float2 mouse_position;
};
/**
* Base class for stroke based operations in curves sculpt mode.
*/
class CurvesSculptStrokeOperation {
public:
virtual ~CurvesSculptStrokeOperation() = default;
virtual void on_stroke_extended(bContext *C, const StrokeExtension &stroke_extension) = 0;
};
class DeleteOperation : public CurvesSculptStrokeOperation {
private:
float2 last_mouse_position_;
public:
void on_stroke_extended(bContext *C, const StrokeExtension &stroke_extension)
{
Scene &scene = *CTX_data_scene(C);
Object &object = *CTX_data_active_object(C);
ARegion *region = CTX_wm_region(C);
RegionView3D *rv3d = CTX_wm_region_view3d(C);
CurvesSculpt &curves_sculpt = *scene.toolsettings->curves_sculpt;
Brush &brush = *BKE_paint_brush(&curves_sculpt.paint);
const float brush_radius = BKE_brush_size_get(&scene, &brush);
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d, &object, projection.values);
Curves &curves_id = *static_cast<Curves *>(object.data);
CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
MutableSpan<float3> positions = curves.positions();
const float2 mouse_start = stroke_extension.is_first ? stroke_extension.mouse_position :
last_mouse_position_;
const float2 mouse_end = stroke_extension.mouse_position;
/* Find indices of curves that have to be removed. */
Vector<int64_t> indices;
const IndexMask curves_to_remove = index_mask_ops::find_indices_based_on_predicate(
curves.curves_range(), 512, indices, [&](const int curve_i) {
const IndexRange point_range = curves.range_for_curve(curve_i);
for (const int segment_i : IndexRange(point_range.size() - 1)) {
const float3 pos1 = positions[point_range[segment_i]];
const float3 pos2 = positions[point_range[segment_i + 1]];
float2 pos1_proj, pos2_proj;
ED_view3d_project_float_v2_m4(region, pos1, pos1_proj, projection.values);
ED_view3d_project_float_v2_m4(region, pos2, pos2_proj, projection.values);
const float dist = dist_seg_seg_v2(pos1_proj, pos2_proj, mouse_start, mouse_end);
if (dist <= brush_radius) {
return true;
}
}
return false;
});
/* Just reset positions instead of actually removing the curves. This is just a prototype. */
threading::parallel_for(curves_to_remove.index_range(), 512, [&](const IndexRange range) {
for (const int curve_i : curves_to_remove.slice(range)) {
for (const int point_i : curves.range_for_curve(curve_i)) {
positions[point_i] = {0.0f, 0.0f, 0.0f};
}
}
});
curves.tag_positions_changed();
DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY);
ED_region_tag_redraw(region);
last_mouse_position_ = stroke_extension.mouse_position;
}
};
class MoveOperation : public CurvesSculptStrokeOperation {
private:
Vector<int64_t> points_to_move_indices_;
IndexMask points_to_move_;
float2 last_mouse_position_;
public:
void on_stroke_extended(bContext *C, const StrokeExtension &stroke_extension)
{
Scene &scene = *CTX_data_scene(C);
Object &object = *CTX_data_active_object(C);
ARegion *region = CTX_wm_region(C);
View3D *v3d = CTX_wm_view3d(C);
RegionView3D *rv3d = CTX_wm_region_view3d(C);
CurvesSculpt &curves_sculpt = *scene.toolsettings->curves_sculpt;
Brush &brush = *BKE_paint_brush(&curves_sculpt.paint);
const float brush_radius = BKE_brush_size_get(&scene, &brush);
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d, &object, projection.values);
Curves &curves_id = *static_cast<Curves *>(object.data);
CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
MutableSpan<float3> positions = curves.positions();
if (stroke_extension.is_first) {
/* Find point indices to move. */
points_to_move_ = index_mask_ops::find_indices_based_on_predicate(
curves.points_range(), 512, points_to_move_indices_, [&](const int64_t point_i) {
const float3 position = positions[point_i];
float2 screen_position;
ED_view3d_project_float_v2_m4(region, position, screen_position, projection.values);
const float distance = len_v2v2(screen_position, stroke_extension.mouse_position);
return distance <= brush_radius;
});
}
else {
/* Move points based on mouse movement. */
const float2 mouse_diff = stroke_extension.mouse_position - last_mouse_position_;
threading::parallel_for(points_to_move_.index_range(), 512, [&](const IndexRange range) {
for (const int point_i : points_to_move_.slice(range)) {
const float3 old_position = positions[point_i];
float2 old_position_screen;
ED_view3d_project_float_v2_m4(
region, old_position, old_position_screen, projection.values);
const float2 new_position_screen = old_position_screen + mouse_diff;
float3 new_position;
ED_view3d_win_to_3d(v3d, region, old_position, new_position_screen, new_position);
positions[point_i] = new_position;
}
});
curves.tag_positions_changed();
DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY);
ED_region_tag_redraw(region);
}
last_mouse_position_ = stroke_extension.mouse_position;
}
};
static std::unique_ptr<CurvesSculptStrokeOperation> start_brush_operation(bContext *C,
wmOperator *UNUSED(op))
{
Scene &scene = *CTX_data_scene(C);
CurvesSculpt &curves_sculpt = *scene.toolsettings->curves_sculpt;
Brush &brush = *BKE_paint_brush(&curves_sculpt.paint);
switch (brush.curves_sculpt_tool) {
case CURVES_SCULPT_TOOL_TEST1:
return std::make_unique<MoveOperation>();
case CURVES_SCULPT_TOOL_TEST2:
return std::make_unique<DeleteOperation>();
}
BLI_assert_unreachable();
return {};
}
struct SculptCurvesBrushStrokeData {
std::unique_ptr<CurvesSculptStrokeOperation> operation;
PaintStroke *stroke;
};
static bool stroke_get_location(bContext *C, float out[3], const float mouse[2])
{
out[0] = mouse[0];
@ -60,10 +239,24 @@ static bool stroke_test_start(bContext *C, struct wmOperator *op, const float mo
static void stroke_update_step(bContext *C,
wmOperator *op,
PaintStroke *stroke,
PointerRNA *itemptr)
PaintStroke *UNUSED(stroke),
PointerRNA *stroke_element)
{
UNUSED_VARS(C, op, stroke, itemptr);
SculptCurvesBrushStrokeData *op_data = static_cast<SculptCurvesBrushStrokeData *>(
op->customdata);
StrokeExtension stroke_extension;
RNA_float_get_array(stroke_element, "mouse", stroke_extension.mouse_position);
if (!op_data->operation) {
stroke_extension.is_first = true;
op_data->operation = start_brush_operation(C, op);
}
else {
stroke_extension.is_first = false;
}
op_data->operation->on_stroke_extended(C, stroke_extension);
}
static void stroke_done(const bContext *C, PaintStroke *stroke)
@ -73,15 +266,23 @@ static void stroke_done(const bContext *C, PaintStroke *stroke)
static int sculpt_curves_stroke_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
PaintStroke *stroke = paint_stroke_new(C,
op,
stroke_get_location,
stroke_test_start,
stroke_update_step,
nullptr,
stroke_done,
event->type);
op->customdata = stroke;
SculptCurvesBrushStrokeData *op_data = MEM_new<SculptCurvesBrushStrokeData>(__func__);
op_data->stroke = paint_stroke_new(C,
op,
stroke_get_location,
stroke_test_start,
stroke_update_step,
nullptr,
stroke_done,
event->type);
op->customdata = op_data;
int return_value = op->type->modal(C, op, event);
if (return_value == OPERATOR_FINISHED) {
paint_stroke_free(C, op, op_data->stroke);
MEM_delete(op_data);
return OPERATOR_FINISHED;
}
WM_event_add_modal_handler(C, op);
return OPERATOR_RUNNING_MODAL;
@ -89,12 +290,21 @@ static int sculpt_curves_stroke_invoke(bContext *C, wmOperator *op, const wmEven
static int sculpt_curves_stroke_modal(bContext *C, wmOperator *op, const wmEvent *event)
{
return paint_stroke_modal(C, op, event, static_cast<PaintStroke *>(op->customdata));
SculptCurvesBrushStrokeData *op_data = static_cast<SculptCurvesBrushStrokeData *>(
op->customdata);
int return_value = paint_stroke_modal(C, op, event, op_data->stroke);
if (ELEM(return_value, OPERATOR_FINISHED, OPERATOR_CANCELLED)) {
MEM_delete(op_data);
}
return return_value;
}
static void sculpt_curves_stroke_cancel(bContext *C, wmOperator *op)
{
paint_stroke_cancel(C, op, static_cast<PaintStroke *>(op->customdata));
SculptCurvesBrushStrokeData *op_data = static_cast<SculptCurvesBrushStrokeData *>(
op->customdata);
paint_stroke_cancel(C, op, op_data->stroke);
MEM_delete(op_data);
}
static void SCULPT_CURVES_OT_brush_stroke(struct wmOperatorType *ot)