From 416aef4e13ccc30e82ecaa691f26af54dbd5ee7e Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Thu, 30 Jun 2022 15:09:13 +0200 Subject: Curves: New tools for curves sculpt mode. This commit contains various new features for curves sculpt mode that have been developed in parallel. * Selection: * Operator to select points/curves randomly. * Operator to select endpoints of curves. * Operator to grow/shrink an existing selection. * New Brushes: * Pinch: Moves points towards the brush center. * Smooth: Makes individual curves straight without changing the root or tip position. * Puff: Makes curves stand up, aligning them with the surface normal. * Density: Add or remove curves to achieve a certain density defined by a minimum distance value. * Slide: Move root points on the surface. Differential Revision: https://developer.blender.org/D15134 --- source/blender/blenkernel/intern/brush.c | 1 + source/blender/blenloader/intern/versioning_300.c | 8 + source/blender/editors/curves/CMakeLists.txt | 13 + source/blender/editors/curves/intern/curves_ops.cc | 27 +- source/blender/editors/include/ED_curves.h | 8 +- source/blender/editors/sculpt_paint/CMakeLists.txt | 7 +- .../editors/sculpt_paint/curves_sculpt_density.cc | 834 ++++++++++++++++++ .../editors/sculpt_paint/curves_sculpt_intern.hh | 2 +- .../editors/sculpt_paint/curves_sculpt_ops.cc | 964 ++++++++++++++++++++- .../editors/sculpt_paint/curves_sculpt_pinch.cc | 307 +++++++ .../editors/sculpt_paint/curves_sculpt_puff.cc | 393 +++++++++ .../editors/sculpt_paint/curves_sculpt_slide.cc | 310 +++++++ .../editors/sculpt_paint/curves_sculpt_smooth.cc | 259 ++++++ source/blender/editors/sculpt_paint/paint_stroke.c | 2 +- source/blender/makesdna/DNA_brush_enums.h | 11 + source/blender/makesdna/DNA_brush_types.h | 7 + source/blender/makesrna/intern/rna_brush.c | 42 + 17 files changed, 3181 insertions(+), 14 deletions(-) create mode 100644 source/blender/editors/sculpt_paint/curves_sculpt_density.cc create mode 100644 source/blender/editors/sculpt_paint/curves_sculpt_pinch.cc create mode 100644 source/blender/editors/sculpt_paint/curves_sculpt_puff.cc create mode 100644 source/blender/editors/sculpt_paint/curves_sculpt_slide.cc create mode 100644 source/blender/editors/sculpt_paint/curves_sculpt_smooth.cc (limited to 'source') diff --git a/source/blender/blenkernel/intern/brush.c b/source/blender/blenkernel/intern/brush.c index 1cda0e8a4bb..dd38af126fe 100644 --- a/source/blender/blenkernel/intern/brush.c +++ b/source/blender/blenkernel/intern/brush.c @@ -1562,6 +1562,7 @@ void BKE_brush_init_curves_sculpt_settings(Brush *brush) settings->points_per_curve = 8; settings->minimum_length = 0.01f; settings->curve_length = 0.3f; + settings->density_add_attempts = 100; } struct Brush *BKE_brush_first_search(struct Main *bmain, const eObjectMode ob_mode) diff --git a/source/blender/blenloader/intern/versioning_300.c b/source/blender/blenloader/intern/versioning_300.c index 68df560a389..35b1367ca1e 100644 --- a/source/blender/blenloader/intern/versioning_300.c +++ b/source/blender/blenloader/intern/versioning_300.c @@ -3243,5 +3243,13 @@ void blo_do_versions_300(FileData *fd, Library *UNUSED(lib), Main *bmain) */ { /* Keep this block, even when empty. */ + + /* Initialize brush curves sculpt settings. */ + LISTBASE_FOREACH (Brush *, brush, &bmain->brushes) { + if (brush->ob_mode != OB_MODE_SCULPT_CURVES) { + continue; + } + brush->curves_sculpt_settings->density_add_attempts = 100; + } } } diff --git a/source/blender/editors/curves/CMakeLists.txt b/source/blender/editors/curves/CMakeLists.txt index 3c31e8014ff..303d2fb71dc 100644 --- a/source/blender/editors/curves/CMakeLists.txt +++ b/source/blender/editors/curves/CMakeLists.txt @@ -8,6 +8,7 @@ set(INC ../../depsgraph ../../functions ../../geometry + ../../gpu ../../makesdna ../../makesrna ../../windowmanager @@ -27,5 +28,17 @@ 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_curves "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") add_dependencies(bf_editor_curves bf_rna) diff --git a/source/blender/editors/curves/intern/curves_ops.cc b/source/blender/editors/curves/intern/curves_ops.cc index 25bcba6cfb3..dd7edf66920 100644 --- a/source/blender/editors/curves/intern/curves_ops.cc +++ b/source/blender/editors/curves/intern/curves_ops.cc @@ -72,7 +72,7 @@ static bool object_has_editable_curves(const Main &bmain, const Object &object) return true; } -static VectorSet get_unique_editable_curves(const bContext &C) +VectorSet get_unique_editable_curves(const bContext &C) { VectorSet unique_curves; @@ -715,7 +715,7 @@ static void CURVES_OT_snap_curves_to_surface(wmOperatorType *ot) "How to find the point on the surface to attach to"); } -static bool selection_poll(bContext *C) +bool selection_operator_poll(bContext *C) { const Object *object = CTX_data_active_object(C); if (object == nullptr) { @@ -784,7 +784,7 @@ static void CURVES_OT_set_selection_domain(wmOperatorType *ot) ot->description = "Change the mode used for selection masking in curves sculpt mode"; ot->exec = set_selection_domain::curves_set_selection_domain_exec; - ot->poll = selection_poll; + ot->poll = selection_operator_poll; ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; @@ -820,13 +820,11 @@ static void CURVES_OT_disable_selection(wmOperatorType *ot) ot->description = "Disable the drawing of influence of selection in sculpt mode"; ot->exec = disable_selection::curves_disable_selection_exec; - ot->poll = selection_poll; + ot->poll = selection_operator_poll; ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } -namespace select_all { - static bool varray_contains_nonzero(const VArray &data) { bool contains_nonzero = false; @@ -841,6 +839,19 @@ static bool varray_contains_nonzero(const VArray &data) return contains_nonzero; } +bool has_anything_selected(const Curves &curves_id) +{ + const CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry); + switch (curves_id.selection_domain) { + case ATTR_DOMAIN_POINT: + return varray_contains_nonzero(curves.selection_point_float()); + case ATTR_DOMAIN_CURVE: + return varray_contains_nonzero(curves.selection_curve_float()); + } + BLI_assert_unreachable(); + return false; +} + static bool any_point_selected(const CurvesGeometry &curves) { return varray_contains_nonzero(curves.selection_point_float()); @@ -856,6 +867,8 @@ static bool any_point_selected(const Span curves_ids) return false; } +namespace select_all { + static void invert_selection(MutableSpan selection) { threading::parallel_for(selection.index_range(), 2048, [&](IndexRange range) { @@ -924,7 +937,7 @@ static void SCULPT_CURVES_OT_select_all(wmOperatorType *ot) ot->description = "(De)select all control points"; ot->exec = select_all::select_all_exec; - ot->poll = selection_poll; + ot->poll = selection_operator_poll; ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; diff --git a/source/blender/editors/include/ED_curves.h b/source/blender/editors/include/ED_curves.h index 9233b65b2ce..68e09fd1b12 100644 --- a/source/blender/editors/include/ED_curves.h +++ b/source/blender/editors/include/ED_curves.h @@ -6,6 +6,8 @@ #pragma once +struct bContext; + #ifdef __cplusplus extern "C" { #endif @@ -19,10 +21,14 @@ void ED_operatortypes_curves(void); #ifdef __cplusplus # include "BKE_curves.hh" +# include "BLI_vector_set.hh" namespace blender::ed::curves { bke::CurvesGeometry primitive_random_sphere(int curves_size, int points_per_curve); +bool selection_operator_poll(bContext *C); +bool has_anything_selected(const Curves &curves_id); +VectorSet get_unique_editable_curves(const bContext &C); -} +} // namespace blender::ed::curves #endif diff --git a/source/blender/editors/sculpt_paint/CMakeLists.txt b/source/blender/editors/sculpt_paint/CMakeLists.txt index 34247b4ef75..edb0f1cda4d 100644 --- a/source/blender/editors/sculpt_paint/CMakeLists.txt +++ b/source/blender/editors/sculpt_paint/CMakeLists.txt @@ -32,10 +32,15 @@ set(SRC curves_sculpt_brush.cc curves_sculpt_comb.cc curves_sculpt_delete.cc + curves_sculpt_density.cc curves_sculpt_grow_shrink.cc curves_sculpt_ops.cc - curves_sculpt_selection.cc + curves_sculpt_pinch.cc + curves_sculpt_puff.cc curves_sculpt_selection_paint.cc + curves_sculpt_selection.cc + curves_sculpt_slide.cc + curves_sculpt_smooth.cc curves_sculpt_snake_hook.cc paint_canvas.cc paint_cursor.c diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_density.cc b/source/blender/editors/sculpt_paint/curves_sculpt_density.cc new file mode 100644 index 00000000000..536c44ddd44 --- /dev/null +++ b/source/blender/editors/sculpt_paint/curves_sculpt_density.cc @@ -0,0 +1,834 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + +#include "BKE_brush.h" +#include "BKE_bvhutils.h" +#include "BKE_context.h" +#include "BKE_geometry_set.hh" +#include "BKE_mesh.h" +#include "BKE_mesh_runtime.h" +#include "BKE_mesh_sample.hh" + +#include "ED_screen.h" +#include "ED_view3d.h" + +#include "DEG_depsgraph.h" + +#include "BLI_index_mask_ops.hh" +#include "BLI_kdtree.h" +#include "BLI_rand.hh" +#include "BLI_task.hh" + +#include "PIL_time.h" + +#include "GEO_add_curves_on_mesh.hh" + +#include "DNA_brush_types.h" +#include "DNA_mesh_types.h" + +#include "WM_api.h" + +#include "curves_sculpt_intern.hh" + +namespace blender::ed::sculpt_paint { + +class DensityAddOperation : public CurvesSculptStrokeOperation { + private: + /** Used when some data should be interpolated from existing curves. */ + KDTree_3d *curve_roots_kdtree_ = nullptr; + int original_curve_num_ = 0; + + friend struct DensityAddOperationExecutor; + + public: + ~DensityAddOperation() override + { + if (curve_roots_kdtree_ != nullptr) { + BLI_kdtree_3d_free(curve_roots_kdtree_); + } + } + + void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override; +}; + +struct DensityAddOperationExecutor { + DensityAddOperation *self_ = nullptr; + CurvesSculptCommonContext ctx_; + + Object *object_ = nullptr; + Curves *curves_id_ = nullptr; + CurvesGeometry *curves_ = nullptr; + + Object *surface_ob_ = nullptr; + Mesh *surface_ = nullptr; + Span surface_looptris_; + Span corner_normals_su_; + VArray_Span surface_uv_map_; + + const CurvesSculpt *curves_sculpt_ = nullptr; + const Brush *brush_ = nullptr; + const BrushCurvesSculptSettings *brush_settings_ = nullptr; + + float brush_strength_; + float brush_radius_re_; + float2 brush_pos_re_; + + CurvesSculptTransforms transforms_; + + BVHTreeFromMesh surface_bvh_; + + DensityAddOperationExecutor(const bContext &C) : ctx_(C) + { + } + + void execute(DensityAddOperation &self, + const bContext &C, + const StrokeExtension &stroke_extension) + { + self_ = &self; + object_ = CTX_data_active_object(&C); + curves_id_ = static_cast(object_->data); + curves_ = &CurvesGeometry::wrap(curves_id_->geometry); + + if (stroke_extension.is_first) { + self_->original_curve_num_ = curves_->curves_num(); + } + + if (curves_id_->surface == nullptr || curves_id_->surface->type != OB_MESH) { + return; + } + + surface_ob_ = curves_id_->surface; + surface_ = static_cast(surface_ob_->data); + + surface_looptris_ = {BKE_mesh_runtime_looptri_ensure(surface_), + BKE_mesh_runtime_looptri_len(surface_)}; + + transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface); + + if (!CustomData_has_layer(&surface_->ldata, CD_NORMAL)) { + BKE_mesh_calc_normals_split(surface_); + } + corner_normals_su_ = { + reinterpret_cast(CustomData_get_layer(&surface_->ldata, CD_NORMAL)), + surface_->totloop}; + + curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt; + brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint); + brush_settings_ = brush_->curves_sculpt_settings; + brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension); + brush_radius_re_ = brush_radius_get(*ctx_.scene, *brush_, stroke_extension); + brush_pos_re_ = stroke_extension.mouse_position; + + const eBrushFalloffShape falloff_shape = static_cast( + brush_->falloff_shape); + + BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2); + BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh_); }); + + Vector new_bary_coords; + Vector new_looptri_indices; + Vector new_positions_cu; + const double time = PIL_check_seconds_timer() * 1000000.0; + RandomNumberGenerator rng{*(uint32_t *)(&time)}; + + /* Find potential new curve root points. */ + if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) { + this->sample_projected_with_symmetry( + rng, new_bary_coords, new_looptri_indices, new_positions_cu); + } + else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) { + this->sample_spherical_with_symmetry( + rng, new_bary_coords, new_looptri_indices, new_positions_cu); + } + else { + BLI_assert_unreachable(); + } + for (float3 &pos : new_positions_cu) { + pos = transforms_.surface_to_curves * pos; + } + + this->ensure_curve_roots_kdtree(); + + const int already_added_curves = curves_->curves_num() - self_->original_curve_num_; + KDTree_3d *new_roots_kdtree = BLI_kdtree_3d_new(already_added_curves + + new_positions_cu.size()); + BLI_SCOPED_DEFER([&]() { BLI_kdtree_3d_free(new_roots_kdtree); }); + + /* Used to tag all curves that are too close to existing curves or too close to other new + * curves. */ + Array new_curve_skipped(new_positions_cu.size(), false); + threading::parallel_invoke( + /* Build kdtree from root points created by the current stroke. */ + [&]() { + const Span positions_cu = curves_->positions(); + for (const int curve_i : curves_->curves_range().take_back(already_added_curves)) { + const float3 &root_pos_cu = positions_cu[curves_->offsets()[curve_i]]; + BLI_kdtree_3d_insert(new_roots_kdtree, curve_i, root_pos_cu); + } + for (const int new_i : new_positions_cu.index_range()) { + const int index_in_kdtree = curves_->curves_num() + new_i; + const float3 &root_pos_cu = new_positions_cu[new_i]; + BLI_kdtree_3d_insert(new_roots_kdtree, index_in_kdtree, root_pos_cu); + } + BLI_kdtree_3d_balance(new_roots_kdtree); + }, + /* Check which new root points are close to roots that existed before the current stroke + * started. */ + [&]() { + threading::parallel_for( + new_positions_cu.index_range(), 128, [&](const IndexRange range) { + for (const int new_i : range) { + const float3 &new_root_pos_cu = new_positions_cu[new_i]; + KDTreeNearest_3d nearest; + nearest.dist = FLT_MAX; + BLI_kdtree_3d_find_nearest( + self_->curve_roots_kdtree_, new_root_pos_cu, &nearest); + if (nearest.dist < brush_settings_->minimum_distance) { + new_curve_skipped[new_i] = true; + } + } + }); + }); + + /* Find new points that are too close too other new points. */ + for (const int new_i : new_positions_cu.index_range()) { + if (new_curve_skipped[new_i]) { + continue; + } + const float3 &root_pos_cu = new_positions_cu[new_i]; + BLI_kdtree_3d_range_search_cb_cpp( + new_roots_kdtree, + root_pos_cu, + brush_settings_->minimum_distance, + [&](const int other_i, const float *UNUSED(co), float UNUSED(dist_sq)) { + if (other_i < curves_->curves_num()) { + new_curve_skipped[new_i] = true; + return false; + } + const int other_new_i = other_i - curves_->curves_num(); + if (new_i == other_new_i) { + return true; + } + new_curve_skipped[other_new_i] = true; + return true; + }); + } + + /* Remove points that are too close to others. */ + for (int64_t i = new_positions_cu.size() - 1; i >= 0; i--) { + if (new_curve_skipped[i]) { + new_positions_cu.remove_and_reorder(i); + new_bary_coords.remove_and_reorder(i); + new_looptri_indices.remove_and_reorder(i); + } + } + + /* Find UV map. */ + VArray_Span surface_uv_map; + if (curves_id_->surface_uv_map != nullptr) { + MeshComponent surface_component; + surface_component.replace(surface_, GeometryOwnershipType::ReadOnly); + surface_uv_map = surface_component + .attribute_try_get_for_read(curves_id_->surface_uv_map, + ATTR_DOMAIN_CORNER) + .typed(); + } + + /* Find normals. */ + if (!CustomData_has_layer(&surface_->ldata, CD_NORMAL)) { + BKE_mesh_calc_normals_split(surface_); + } + const Span corner_normals_su = { + reinterpret_cast(CustomData_get_layer(&surface_->ldata, CD_NORMAL)), + surface_->totloop}; + + geometry::AddCurvesOnMeshInputs add_inputs; + add_inputs.root_positions_cu = new_positions_cu; + add_inputs.bary_coords = new_bary_coords; + add_inputs.looptri_indices = new_looptri_indices; + add_inputs.interpolate_length = brush_settings_->flag & + BRUSH_CURVES_SCULPT_FLAG_INTERPOLATE_LENGTH; + add_inputs.interpolate_shape = brush_settings_->flag & + BRUSH_CURVES_SCULPT_FLAG_INTERPOLATE_SHAPE; + add_inputs.interpolate_point_count = brush_settings_->flag & + BRUSH_CURVES_SCULPT_FLAG_INTERPOLATE_POINT_COUNT; + add_inputs.fallback_curve_length = brush_settings_->curve_length; + add_inputs.fallback_point_count = std::max(2, brush_settings_->points_per_curve); + add_inputs.surface = surface_; + add_inputs.surface_bvh = &surface_bvh_; + add_inputs.surface_looptris = surface_looptris_; + add_inputs.surface_uv_map = surface_uv_map; + add_inputs.corner_normals_su = corner_normals_su; + add_inputs.curves_to_surface_mat = transforms_.curves_to_surface; + add_inputs.surface_to_curves_normal_mat = transforms_.surface_to_curves_normal; + add_inputs.old_roots_kdtree = self_->curve_roots_kdtree_; + + geometry::add_curves_on_mesh(*curves_, add_inputs); + + DEG_id_tag_update(&curves_id_->id, ID_RECALC_GEOMETRY); + WM_main_add_notifier(NC_GEOM | ND_DATA, &curves_id_->id); + ED_region_tag_redraw(ctx_.region); + } + + void ensure_curve_roots_kdtree() + { + if (self_->curve_roots_kdtree_ == nullptr) { + self_->curve_roots_kdtree_ = BLI_kdtree_3d_new(curves_->curves_num()); + for (const int curve_i : curves_->curves_range()) { + const int root_point_i = curves_->offsets()[curve_i]; + const float3 &root_pos_cu = curves_->positions()[root_point_i]; + BLI_kdtree_3d_insert(self_->curve_roots_kdtree_, curve_i, root_pos_cu); + } + BLI_kdtree_3d_balance(self_->curve_roots_kdtree_); + } + } + + void sample_projected_with_symmetry(RandomNumberGenerator &rng, + Vector &r_bary_coords, + Vector &r_looptri_indices, + Vector &r_positions_su) + { + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + const float4x4 brush_transform_inv = brush_transform.inverted(); + const float4x4 transform = transforms_.curves_to_surface * brush_transform * + transforms_.world_to_curves; + const int new_points = bke::mesh_surface_sample::sample_surface_points_projected( + rng, + *surface_, + surface_bvh_, + brush_pos_re_, + brush_radius_re_, + [&](const float2 &pos_re, float3 &r_start_su, float3 &r_end_su) { + float3 start_wo, end_wo; + ED_view3d_win_to_segment_clipped( + ctx_.depsgraph, ctx_.region, ctx_.v3d, pos_re, start_wo, end_wo, true); + r_start_su = transform * start_wo; + r_end_su = transform * end_wo; + }, + true, + brush_settings_->density_add_attempts, + brush_settings_->density_add_attempts, + r_bary_coords, + r_looptri_indices, + r_positions_su); + + /* Remove some sampled points randomly based on the brush falloff and strength. */ + const int old_points = r_bary_coords.size() - new_points; + for (int i = r_bary_coords.size() - 1; i >= old_points; i--) { + const float3 pos_su = r_positions_su[i]; + const float3 pos_cu = brush_transform_inv * transforms_.surface_to_curves * pos_su; + float2 pos_re; + ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values); + const float dist_to_brush_re = math::distance(brush_pos_re_, pos_re); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_re, brush_radius_re_); + const float weight = brush_strength_ * radius_falloff; + if (rng.get_float() > weight) { + r_bary_coords.remove_and_reorder(i); + r_looptri_indices.remove_and_reorder(i); + r_positions_su.remove_and_reorder(i); + } + } + } + } + + void sample_spherical_with_symmetry(RandomNumberGenerator &rng, + Vector &r_bary_coords, + Vector &r_looptri_indices, + Vector &r_positions_su) + { + const std::optional brush_3d = sample_curves_surface_3d_brush(*ctx_.depsgraph, + *ctx_.region, + *ctx_.v3d, + transforms_, + surface_bvh_, + brush_pos_re_, + brush_radius_re_); + if (!brush_3d.has_value()) { + return; + } + + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + const float3 brush_pos_cu = brush_transform * brush_3d->position_cu; + const float3 brush_pos_su = transforms_.curves_to_surface * brush_pos_cu; + const float brush_radius_su = transform_brush_radius( + transforms_.curves_to_surface, brush_pos_cu, brush_3d->radius_cu); + const float brush_radius_sq_su = pow2f(brush_radius_su); + + Vector looptri_indices; + BLI_bvhtree_range_query_cpp( + *surface_bvh_.tree, + brush_pos_su, + brush_radius_su, + [&](const int index, const float3 &UNUSED(co), const float UNUSED(dist_sq)) { + looptri_indices.append(index); + }); + + const float brush_plane_area_su = M_PI * brush_radius_sq_su; + const float approximate_density_su = brush_settings_->density_add_attempts / + brush_plane_area_su; + + const int new_points = bke::mesh_surface_sample::sample_surface_points_spherical( + rng, + *surface_, + looptri_indices, + brush_pos_su, + brush_radius_su, + approximate_density_su, + r_bary_coords, + r_looptri_indices, + r_positions_su); + + /* Remove some sampled points randomly based on the brush falloff and strength. */ + const int old_points = r_bary_coords.size() - new_points; + for (int i = r_bary_coords.size() - 1; i >= old_points; i--) { + const float3 pos_su = r_positions_su[i]; + const float3 pos_cu = transforms_.surface_to_curves * pos_su; + const float dist_to_brush_cu = math::distance(pos_cu, brush_pos_cu); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_cu, brush_3d->radius_cu); + const float weight = brush_strength_ * radius_falloff; + if (rng.get_float() > weight) { + r_bary_coords.remove_and_reorder(i); + r_looptri_indices.remove_and_reorder(i); + r_positions_su.remove_and_reorder(i); + } + } + } + } +}; + +void DensityAddOperation::on_stroke_extended(const bContext &C, + const StrokeExtension &stroke_extension) +{ + DensityAddOperationExecutor executor{C}; + executor.execute(*this, C, stroke_extension); +} + +class DensitySubtractOperation : public CurvesSculptStrokeOperation { + private: + /** Only used when a 3D brush is used. */ + CurvesBrush3D brush_3d_; + + friend struct DensitySubtractOperationExecutor; + + public: + void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override; +}; + +/** + * 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 DensitySubtractOperationExecutor { + DensitySubtractOperation *self_ = nullptr; + CurvesSculptCommonContext ctx_; + + Object *object_ = nullptr; + Curves *curves_id_ = nullptr; + CurvesGeometry *curves_ = nullptr; + + Vector selected_curve_indices_; + IndexMask curve_selection_; + + Object *surface_ob_ = nullptr; + Mesh *surface_ = nullptr; + + const CurvesSculpt *curves_sculpt_ = nullptr; + const Brush *brush_ = nullptr; + float brush_radius_base_re_; + float brush_radius_factor_; + float brush_strength_; + float2 brush_pos_re_; + + float minimum_distance_; + + CurvesSculptTransforms transforms_; + BVHTreeFromMesh surface_bvh_; + + KDTree_3d *root_points_kdtree_; + + DensitySubtractOperationExecutor(const bContext &C) : ctx_(C) + { + } + + void execute(DensitySubtractOperation &self, + const bContext &C, + const StrokeExtension &stroke_extension) + { + self_ = &self; + + object_ = CTX_data_active_object(&C); + + curves_id_ = static_cast(object_->data); + curves_ = &CurvesGeometry::wrap(curves_id_->geometry); + if (curves_->curves_num() == 0) { + return; + } + + surface_ob_ = curves_id_->surface; + if (surface_ob_ == nullptr) { + return; + } + surface_ = static_cast(surface_ob_->data); + + curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt; + brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint); + brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_); + brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension); + brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension); + brush_pos_re_ = stroke_extension.mouse_position; + + minimum_distance_ = brush_->curves_sculpt_settings->minimum_distance; + + curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_); + + transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface); + const eBrushFalloffShape falloff_shape = static_cast( + brush_->falloff_shape); + BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2); + BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh_); }); + + const Span positions_cu = curves_->positions(); + + root_points_kdtree_ = BLI_kdtree_3d_new(curve_selection_.size()); + BLI_SCOPED_DEFER([&]() { BLI_kdtree_3d_free(root_points_kdtree_); }); + for (const int curve_i : curve_selection_) { + const int first_point_i = curves_->offsets()[curve_i]; + const float3 &pos_cu = positions_cu[first_point_i]; + BLI_kdtree_3d_insert(root_points_kdtree_, curve_i, pos_cu); + } + BLI_kdtree_3d_balance(root_points_kdtree_); + + /* Find all curves that should be deleted. */ + Array curves_to_delete(curves_->curves_num(), false); + if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) { + this->reduce_density_projected_with_symmetry(curves_to_delete); + } + else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) { + this->reduce_density_spherical_with_symmetry(curves_to_delete); + } + else { + BLI_assert_unreachable(); + } + + Vector indices; + const IndexMask mask = index_mask_ops::find_indices_based_on_predicate( + curves_->curves_range(), 4096, indices, [&](const int curve_i) { + return curves_to_delete[curve_i]; + }); + + curves_->remove_curves(mask); + + DEG_id_tag_update(&curves_id_->id, ID_RECALC_GEOMETRY); + WM_main_add_notifier(NC_GEOM | ND_DATA, &curves_id_->id); + ED_region_tag_redraw(ctx_.region); + } + + void reduce_density_projected_with_symmetry(MutableSpan curves_to_delete) + { + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + this->reduce_density_projected(brush_transform, curves_to_delete); + } + } + + void reduce_density_projected(const float4x4 &brush_transform, + MutableSpan curves_to_delete) + { + const Span positions_cu = curves_->positions(); + const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_; + const float brush_radius_sq_re = pow2f(brush_radius_re); + + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + + const Span offsets = curves_->offsets(); + + /* Randomly select the curves that are allowed to be removed, based on the brush radius and + * strength. */ + Array allow_remove_curve(curves_->curves_num(), false); + threading::parallel_for(curves_->curves_range(), 512, [&](const IndexRange range) { + RandomNumberGenerator rng((int)(PIL_check_seconds_timer() * 1000000.0)); + + for (const int curve_i : range) { + if (curves_to_delete[curve_i]) { + allow_remove_curve[curve_i] = true; + continue; + } + const int first_point_i = offsets[curve_i]; + const float3 pos_cu = brush_transform * positions_cu[first_point_i]; + + float2 pos_re; + ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values); + const float dist_to_brush_sq_re = math::distance_squared(brush_pos_re_, pos_re); + if (dist_to_brush_sq_re > brush_radius_sq_re) { + continue; + } + const float dist_to_brush_re = std::sqrt(dist_to_brush_sq_re); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_re, brush_radius_re); + const float weight = brush_strength_ * radius_falloff; + if (rng.get_float() < weight) { + allow_remove_curve[curve_i] = true; + } + } + }); + + /* Detect curves that are too close to other existing curves. */ + for (const int curve_i : curve_selection_) { + if (curves_to_delete[curve_i]) { + continue; + } + if (!allow_remove_curve[curve_i]) { + continue; + } + const int first_point_i = offsets[curve_i]; + const float3 orig_pos_cu = positions_cu[first_point_i]; + const float3 pos_cu = brush_transform * orig_pos_cu; + float2 pos_re; + ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values); + const float dist_to_brush_sq_re = math::distance_squared(brush_pos_re_, pos_re); + if (dist_to_brush_sq_re > brush_radius_sq_re) { + continue; + } + BLI_kdtree_3d_range_search_cb_cpp( + root_points_kdtree_, + orig_pos_cu, + minimum_distance_, + [&](const int other_curve_i, const float *UNUSED(co), float UNUSED(dist_sq)) { + if (other_curve_i == curve_i) { + return true; + } + if (allow_remove_curve[other_curve_i]) { + curves_to_delete[other_curve_i] = true; + } + return true; + }); + } + } + + void reduce_density_spherical_with_symmetry(MutableSpan curves_to_delete) + { + const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_; + const std::optional brush_3d = sample_curves_surface_3d_brush(*ctx_.depsgraph, + *ctx_.region, + *ctx_.v3d, + transforms_, + surface_bvh_, + brush_pos_re_, + brush_radius_re); + if (!brush_3d.has_value()) { + return; + } + + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + const float3 brush_pos_cu = brush_transform * brush_3d->position_cu; + this->reduce_density_spherical(brush_pos_cu, brush_3d->radius_cu, curves_to_delete); + } + } + + void reduce_density_spherical(const float3 &brush_pos_cu, + const float brush_radius_cu, + MutableSpan curves_to_delete) + { + const float brush_radius_sq_cu = pow2f(brush_radius_cu); + const Span positions_cu = curves_->positions(); + const Span offsets = curves_->offsets(); + + /* Randomly select the curves that are allowed to be removed, based on the brush radius and + * strength. */ + Array allow_remove_curve(curves_->curves_num(), false); + threading::parallel_for(curves_->curves_range(), 512, [&](const IndexRange range) { + RandomNumberGenerator rng((int)(PIL_check_seconds_timer() * 1000000.0)); + + for (const int curve_i : range) { + if (curves_to_delete[curve_i]) { + allow_remove_curve[curve_i] = true; + continue; + } + const int first_point_i = offsets[curve_i]; + const float3 pos_cu = positions_cu[first_point_i]; + + const float dist_to_brush_sq_cu = math::distance_squared(brush_pos_cu, pos_cu); + if (dist_to_brush_sq_cu > brush_radius_sq_cu) { + continue; + } + const float dist_to_brush_cu = std::sqrt(dist_to_brush_sq_cu); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_cu, brush_radius_cu); + const float weight = brush_strength_ * radius_falloff; + if (rng.get_float() < weight) { + allow_remove_curve[curve_i] = true; + } + } + }); + + /* Detect curves that are too close to other existing curves. */ + for (const int curve_i : curve_selection_) { + if (curves_to_delete[curve_i]) { + continue; + } + if (!allow_remove_curve[curve_i]) { + continue; + } + const int first_point_i = offsets[curve_i]; + const float3 &pos_cu = positions_cu[first_point_i]; + const float dist_to_brush_sq_cu = math::distance_squared(pos_cu, brush_pos_cu); + if (dist_to_brush_sq_cu > brush_radius_sq_cu) { + continue; + } + + BLI_kdtree_3d_range_search_cb_cpp( + root_points_kdtree_, + pos_cu, + minimum_distance_, + [&](const int other_curve_i, const float *UNUSED(co), float UNUSED(dist_sq)) { + if (other_curve_i == curve_i) { + return true; + } + if (allow_remove_curve[other_curve_i]) { + curves_to_delete[other_curve_i] = true; + } + return true; + }); + } + } +}; + +void DensitySubtractOperation::on_stroke_extended(const bContext &C, + const StrokeExtension &stroke_extension) +{ + DensitySubtractOperationExecutor executor{C}; + executor.execute(*this, C, stroke_extension); +} + +/** + * Detects whether the brush should be in Add or Subtract mode. + */ +static bool use_add_density_mode(const BrushStrokeMode brush_mode, + const bContext &C, + const StrokeExtension &stroke_start) +{ + const Scene &scene = *CTX_data_scene(&C); + const Brush &brush = *BKE_paint_brush_for_read(&scene.toolsettings->curves_sculpt->paint); + const eBrushCurvesSculptDensityMode density_mode = static_cast( + brush.curves_sculpt_settings->density_mode); + const bool use_invert = brush_mode == BRUSH_STROKE_INVERT; + + if (density_mode == BRUSH_CURVES_SCULPT_DENSITY_MODE_ADD) { + return !use_invert; + } + if (density_mode == BRUSH_CURVES_SCULPT_DENSITY_MODE_REMOVE) { + return use_invert; + } + + const Object &curves_ob = *CTX_data_active_object(&C); + const Curves &curves_id = *static_cast(curves_ob.data); + const CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry); + if (curves_id.surface == nullptr) { + /* The brush won't do anything in this case anyway. */ + return true; + } + if (curves.curves_num() <= 1) { + return true; + } + + const CurvesSculptTransforms transforms(curves_ob, curves_id.surface); + BVHTreeFromMesh surface_bvh; + BKE_bvhtree_from_mesh_get( + &surface_bvh, static_cast(curves_id.surface->data), BVHTREE_FROM_LOOPTRI, 2); + BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh); }); + + const Depsgraph &depsgraph = *CTX_data_depsgraph_pointer(&C); + const ARegion ®ion = *CTX_wm_region(&C); + const View3D &v3d = *CTX_wm_view3d(&C); + + const float2 brush_pos_re = stroke_start.mouse_position; + /* Reduce radius so that only an inner circle is used to determine the existing density. */ + const float brush_radius_re = BKE_brush_size_get(&scene, &brush) * 0.5f; + + /* Find the surface point under the brush. */ + const std::optional brush_3d = sample_curves_surface_3d_brush( + depsgraph, region, v3d, transforms, surface_bvh, brush_pos_re, brush_radius_re); + if (!brush_3d.has_value()) { + return true; + } + + const float3 brush_pos_cu = brush_3d->position_cu; + const float brush_radius_cu = brush_3d->radius_cu; + const float brush_radius_sq_cu = pow2f(brush_radius_cu); + + const Span offsets = curves.offsets(); + const Span positions_cu = curves.positions(); + + /* Compute distance from brush to curve roots. */ + Array> distances_sq_to_brush(curves.curves_num()); + threading::EnumerableThreadSpecific valid_curve_count_by_thread; + threading::parallel_for(curves.curves_range(), 512, [&](const IndexRange range) { + int &valid_curve_count = valid_curve_count_by_thread.local(); + for (const int curve_i : range) { + const int root_point_i = offsets[curve_i]; + const float3 &root_pos_cu = positions_cu[root_point_i]; + const float dist_sq_cu = math::distance_squared(root_pos_cu, brush_pos_cu); + if (dist_sq_cu < brush_radius_sq_cu) { + distances_sq_to_brush[curve_i] = {math::distance_squared(root_pos_cu, brush_pos_cu), + curve_i}; + valid_curve_count++; + } + else { + distances_sq_to_brush[curve_i] = {FLT_MAX, -1}; + } + } + }); + const int valid_curve_count = std::accumulate( + valid_curve_count_by_thread.begin(), valid_curve_count_by_thread.end(), 0); + + /* Find a couple of curves that are closest to the brush center. */ + const int check_curve_count = std::min(8, valid_curve_count); + std::partial_sort(distances_sq_to_brush.begin(), + distances_sq_to_brush.begin() + check_curve_count, + distances_sq_to_brush.end()); + + /* Compute the minimum pair-wise distance between the curve roots that are close to the brush + * center. */ + float min_dist_sq_cu = FLT_MAX; + for (const int i : IndexRange(check_curve_count)) { + const float3 &pos_i = positions_cu[offsets[distances_sq_to_brush[i].second]]; + for (int j = i + 1; j < check_curve_count; j++) { + const float3 &pos_j = positions_cu[offsets[distances_sq_to_brush[j].second]]; + const float dist_sq_cu = math::distance_squared(pos_i, pos_j); + math::min_inplace(min_dist_sq_cu, dist_sq_cu); + } + } + + const float min_dist_cu = std::sqrt(min_dist_sq_cu); + if (min_dist_cu > brush.curves_sculpt_settings->minimum_distance) { + return true; + } + + return false; +} + +std::unique_ptr new_density_operation( + const BrushStrokeMode brush_mode, const bContext &C, const StrokeExtension &stroke_start) +{ + if (use_add_density_mode(brush_mode, C, stroke_start)) { + return std::make_unique(); + } + return std::make_unique(); +} + +} // namespace blender::ed::sculpt_paint diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh b/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh index ad3871bee45..b26649e746b 100644 --- a/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh +++ b/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh @@ -66,7 +66,7 @@ std::unique_ptr new_pinch_operation(const BrushStro std::unique_ptr new_smooth_operation(); std::unique_ptr new_puff_operation(); std::unique_ptr new_density_operation( - const BrushStrokeMode brush_mode, const bContext &C); + const BrushStrokeMode brush_mode, const bContext &C, const StrokeExtension &stroke_start); std::unique_ptr new_slide_operation(); struct CurvesBrush3D { diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc b/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc index e6da2039433..739c39f6196 100644 --- a/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc +++ b/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc @@ -1,8 +1,12 @@ /* SPDX-License-Identifier: GPL-2.0-or-later */ +#include "BLI_kdtree.h" +#include "BLI_rand.hh" #include "BLI_utildefines.h" +#include "BLI_vector_set.hh" #include "BKE_brush.h" +#include "BKE_bvhutils.h" #include "BKE_context.h" #include "BKE_curves.hh" #include "BKE_paint.h" @@ -11,10 +15,12 @@ #include "WM_message.h" #include "WM_toolsystem.h" +#include "ED_curves.h" #include "ED_curves_sculpt.h" #include "ED_image.h" #include "ED_object.h" #include "ED_screen.h" +#include "ED_space_api.h" #include "ED_view3d.h" #include "DEG_depsgraph.h" @@ -24,11 +30,21 @@ #include "DNA_screen_types.h" #include "RNA_access.h" +#include "RNA_define.h" +#include "RNA_enum_types.h" #include "curves_sculpt_intern.h" #include "curves_sculpt_intern.hh" #include "paint_intern.h" +#include "UI_interface.h" +#include "UI_resources.h" + +#include "GPU_immediate.h" +#include "GPU_immediate_util.h" +#include "GPU_matrix.h" +#include "GPU_state.h" + /* -------------------------------------------------------------------- */ /** \name Poll Functions * \{ */ @@ -90,8 +106,8 @@ float brush_strength_get(const Scene &scene, return BKE_brush_alpha_get(&scene, &brush) * brush_strength_factor(brush, stroke_extension); } -static std::unique_ptr start_brush_operation(bContext &C, - wmOperator &op) +static std::unique_ptr start_brush_operation( + bContext &C, wmOperator &op, const StrokeExtension &stroke_start) { const BrushStrokeMode mode = static_cast(RNA_enum_get(op.ptr, "mode")); @@ -111,6 +127,16 @@ static std::unique_ptr start_brush_operation(bConte return new_grow_shrink_operation(mode, C); case CURVES_SCULPT_TOOL_SELECTION_PAINT: return new_selection_paint_operation(mode, C); + case CURVES_SCULPT_TOOL_PINCH: + return new_pinch_operation(mode, C); + case CURVES_SCULPT_TOOL_SMOOTH: + return new_smooth_operation(); + case CURVES_SCULPT_TOOL_PUFF: + return new_puff_operation(); + case CURVES_SCULPT_TOOL_DENSITY: + return new_density_operation(mode, C, stroke_start); + case CURVES_SCULPT_TOOL_SLIDE: + return new_slide_operation(); } BLI_assert_unreachable(); return {}; @@ -150,7 +176,7 @@ static void stroke_update_step(bContext *C, if (!op_data->operation) { stroke_extension.is_first = true; - op_data->operation = start_brush_operation(*C, *op); + op_data->operation = start_brush_operation(*C, *op, stroke_extension); } else { stroke_extension.is_first = false; @@ -316,6 +342,934 @@ static void CURVES_OT_sculptmode_toggle(wmOperatorType *ot) /** \} */ +namespace select_random { + +static int select_random_exec(bContext *C, wmOperator *op) +{ + VectorSet unique_curves = curves::get_unique_editable_curves(*C); + + const int seed = RNA_int_get(op->ptr, "seed"); + RandomNumberGenerator rng{static_cast(seed)}; + + const bool partial = RNA_boolean_get(op->ptr, "partial"); + const bool constant_per_curve = RNA_boolean_get(op->ptr, "constant_per_curve"); + const float probability = RNA_float_get(op->ptr, "probability"); + const float min_value = RNA_float_get(op->ptr, "min"); + const auto next_partial_random_value = [&]() { + return rng.get_float() * (1.0f - min_value) + min_value; + }; + const auto next_bool_random_value = [&]() { return rng.get_float() <= probability; }; + + for (Curves *curves_id : unique_curves) { + CurvesGeometry &curves = CurvesGeometry::wrap(curves_id->geometry); + const bool was_anything_selected = curves::has_anything_selected(*curves_id); + switch (curves_id->selection_domain) { + case ATTR_DOMAIN_POINT: { + MutableSpan selection = curves.selection_point_float_for_write(); + if (!was_anything_selected) { + selection.fill(1.0f); + } + if (partial) { + if (constant_per_curve) { + for (const int curve_i : curves.curves_range()) { + const float random_value = next_partial_random_value(); + const IndexRange points = curves.points_for_curve(curve_i); + for (const int point_i : points) { + selection[point_i] *= random_value; + } + } + } + else { + for (const int point_i : selection.index_range()) { + const float random_value = next_partial_random_value(); + selection[point_i] *= random_value; + } + } + } + else { + if (constant_per_curve) { + for (const int curve_i : curves.curves_range()) { + const bool random_value = next_bool_random_value(); + const IndexRange points = curves.points_for_curve(curve_i); + if (!random_value) { + selection.slice(points).fill(0.0f); + } + } + } + else { + for (const int point_i : selection.index_range()) { + const bool random_value = next_bool_random_value(); + if (!random_value) { + selection[point_i] = 0.0f; + } + } + } + } + break; + } + case ATTR_DOMAIN_CURVE: { + MutableSpan selection = curves.selection_curve_float_for_write(); + if (!was_anything_selected) { + selection.fill(1.0f); + } + if (partial) { + for (const int curve_i : curves.curves_range()) { + const float random_value = next_partial_random_value(); + selection[curve_i] *= random_value; + } + } + else { + for (const int curve_i : curves.curves_range()) { + const bool random_value = next_bool_random_value(); + if (!random_value) { + selection[curve_i] = 0.0f; + } + } + } + break; + } + } + MutableSpan selection = curves_id->selection_domain == ATTR_DOMAIN_POINT ? + curves.selection_point_float_for_write() : + curves.selection_curve_float_for_write(); + const bool was_any_selected = std::any_of( + selection.begin(), selection.end(), [](const float v) { return v > 0.0f; }); + if (was_any_selected) { + for (float &v : selection) { + v *= rng.get_float(); + } + } + else { + for (float &v : selection) { + v = rng.get_float(); + } + } + + /* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because it is handled as a generic + * attribute for now. */ + DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GEOM | ND_DATA, curves_id); + } + return OPERATOR_FINISHED; +} + +static void select_random_ui(bContext *UNUSED(C), wmOperator *op) +{ + uiLayout *layout = op->layout; + + uiItemR(layout, op->ptr, "seed", 0, nullptr, ICON_NONE); + uiItemR(layout, op->ptr, "constant_per_curve", 0, nullptr, ICON_NONE); + uiItemR(layout, op->ptr, "partial", 0, nullptr, ICON_NONE); + + if (RNA_boolean_get(op->ptr, "partial")) { + uiItemR(layout, op->ptr, "min", UI_ITEM_R_SLIDER, "Min", ICON_NONE); + } + else { + uiItemR(layout, op->ptr, "probability", UI_ITEM_R_SLIDER, "Probability", ICON_NONE); + } +} + +} // namespace select_random + +static void SCULPT_CURVES_OT_select_random(wmOperatorType *ot) +{ + ot->name = "Select Random"; + ot->idname = __func__; + ot->description = "Randomizes existing selection or create new random selection"; + + ot->exec = select_random::select_random_exec; + ot->poll = curves::selection_operator_poll; + ot->ui = select_random::select_random_ui; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + RNA_def_int(ot->srna, + "seed", + 0, + INT32_MIN, + INT32_MAX, + "Seed", + "Source of randomness", + INT32_MIN, + INT32_MAX); + RNA_def_boolean( + ot->srna, "partial", false, "Partial", "Allow points or curves to be selected partially"); + RNA_def_float(ot->srna, + "probability", + 0.5f, + 0.0f, + 1.0f, + "Probability", + "Chance of every point or curve being included in the selection", + 0.0f, + 1.0f); + RNA_def_float(ot->srna, + "min", + 0.0f, + 0.0f, + 1.0f, + "Min", + "Minimum value for the random selection", + 0.0f, + 1.0f); + RNA_def_boolean(ot->srna, + "constant_per_curve", + true, + "Constant per Curve", + "The generated random number is the same for every control point of a curve"); +} + +namespace select_end { +static bool select_end_poll(bContext *C) +{ + if (!curves::selection_operator_poll(C)) { + return false; + } + const Curves *curves_id = static_cast(CTX_data_active_object(C)->data); + if (curves_id->selection_domain != ATTR_DOMAIN_POINT) { + CTX_wm_operator_poll_msg_set(C, "Only available in point selection mode"); + return false; + } + return true; +} + +static int select_end_exec(bContext *C, wmOperator *op) +{ + VectorSet unique_curves = curves::get_unique_editable_curves(*C); + const bool end_points = RNA_boolean_get(op->ptr, "end_points"); + const int amount = RNA_int_get(op->ptr, "amount"); + + for (Curves *curves_id : unique_curves) { + CurvesGeometry &curves = CurvesGeometry::wrap(curves_id->geometry); + const bool was_anything_selected = curves::has_anything_selected(*curves_id); + MutableSpan selection = curves.selection_point_float_for_write(); + if (!was_anything_selected) { + selection.fill(1.0f); + } + threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange range) { + for (const int curve_i : range) { + const IndexRange points = curves.points_for_curve(curve_i); + if (end_points) { + selection.slice(points.drop_back(amount)).fill(0.0f); + } + else { + selection.slice(points.drop_front(amount)).fill(0.0f); + } + } + }); + + /* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because it is handled as a generic + * attribute for now. */ + DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GEOM | ND_DATA, curves_id); + } + + return OPERATOR_FINISHED; +} +} // namespace select_end + +static void SCULPT_CURVES_OT_select_end(wmOperatorType *ot) +{ + ot->name = "Select End"; + ot->idname = __func__; + ot->description = "Select end points of curves"; + + ot->exec = select_end::select_end_exec; + ot->poll = select_end::select_end_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + RNA_def_boolean(ot->srna, + "end_points", + true, + "End Points", + "Select points at the end of the curve as opposed to the beginning"); + RNA_def_int( + ot->srna, "amount", 1, 0, INT32_MAX, "Amount", "Number of points to select", 0, INT32_MAX); +} + +namespace select_grow { + +struct GrowOperatorDataPerCurve : NonCopyable, NonMovable { + Curves *curves_id; + Vector selected_point_indices; + Vector unselected_point_indices; + Array distances_to_selected; + Array distances_to_unselected; + + Array original_selection; + float pixel_to_distance_factor; +}; + +struct GrowOperatorData { + int initial_mouse_x; + Vector> per_curve; +}; + +static void update_points_selection(const GrowOperatorDataPerCurve &data, + const float distance, + MutableSpan points_selection) +{ + if (distance > 0.0f) { + threading::parallel_for( + data.unselected_point_indices.index_range(), 256, [&](const IndexRange range) { + for (const int i : range) { + const int point_i = data.unselected_point_indices[i]; + const float distance_to_selected = data.distances_to_selected[i]; + const float selection = distance_to_selected <= distance ? 1.0f : 0.0f; + points_selection[point_i] = selection; + } + }); + threading::parallel_for( + data.selected_point_indices.index_range(), 512, [&](const IndexRange range) { + for (const int point_i : data.selected_point_indices.as_span().slice(range)) { + points_selection[point_i] = 1.0f; + } + }); + } + else { + threading::parallel_for( + data.selected_point_indices.index_range(), 256, [&](const IndexRange range) { + for (const int i : range) { + const int point_i = data.selected_point_indices[i]; + const float distance_to_unselected = data.distances_to_unselected[i]; + const float selection = distance_to_unselected <= -distance ? 0.0f : 1.0f; + points_selection[point_i] = selection; + } + }); + threading::parallel_for( + data.unselected_point_indices.index_range(), 512, [&](const IndexRange range) { + for (const int point_i : data.unselected_point_indices.as_span().slice(range)) { + points_selection[point_i] = 0.0f; + } + }); + } +} + +static int select_grow_update(bContext *C, wmOperator *op, const float mouse_diff_x) +{ + GrowOperatorData &op_data = *static_cast(op->customdata); + + for (std::unique_ptr &curve_op_data : op_data.per_curve) { + Curves &curves_id = *curve_op_data->curves_id; + CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry); + const float distance = curve_op_data->pixel_to_distance_factor * mouse_diff_x; + + /* Grow or shrink selection based on precomputed distances. */ + switch (curves_id.selection_domain) { + case ATTR_DOMAIN_POINT: { + MutableSpan points_selection = curves.selection_point_float_for_write(); + update_points_selection(*curve_op_data, distance, points_selection); + break; + } + case ATTR_DOMAIN_CURVE: { + Array new_points_selection(curves.points_num()); + update_points_selection(*curve_op_data, distance, new_points_selection); + /* Propagate grown point selection to the curve selection. */ + MutableSpan curves_selection = curves.selection_curve_float_for_write(); + for (const int curve_i : curves.curves_range()) { + const IndexRange points = curves.points_for_curve(curve_i); + const Span points_selection = new_points_selection.as_span().slice(points); + const float max_selection = *std::max_element(points_selection.begin(), + points_selection.end()); + curves_selection[curve_i] = max_selection; + } + break; + } + } + + /* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because it is handled as a generic + * attribute for now. */ + DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GEOM | ND_DATA, &curves_id); + } + + return OPERATOR_FINISHED; +} + +static void select_grow_invoke_per_curve(Curves &curves_id, + Object &curves_ob, + const ARegion ®ion, + const View3D &v3d, + const RegionView3D &rv3d, + GrowOperatorDataPerCurve &curve_op_data) +{ + curve_op_data.curves_id = &curves_id; + CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry); + const Span positions = curves.positions(); + + /* Find indices of selected and unselected points. */ + switch (curves_id.selection_domain) { + case ATTR_DOMAIN_POINT: { + const VArray points_selection = curves.selection_point_float(); + curve_op_data.original_selection.reinitialize(points_selection.size()); + points_selection.materialize(curve_op_data.original_selection); + for (const int point_i : points_selection.index_range()) { + const float point_selection = points_selection[point_i]; + if (point_selection > 0.0f) { + curve_op_data.selected_point_indices.append(point_i); + } + else { + curve_op_data.unselected_point_indices.append(point_i); + } + } + + break; + } + case ATTR_DOMAIN_CURVE: { + const VArray curves_selection = curves.selection_curve_float(); + curve_op_data.original_selection.reinitialize(curves_selection.size()); + curves_selection.materialize(curve_op_data.original_selection); + for (const int curve_i : curves_selection.index_range()) { + const float curve_selection = curves_selection[curve_i]; + const IndexRange points = curves.points_for_curve(curve_i); + if (curve_selection > 0.0f) { + for (const int point_i : points) { + curve_op_data.selected_point_indices.append(point_i); + } + } + else { + for (const int point_i : points) { + curve_op_data.unselected_point_indices.append(point_i); + } + } + } + break; + } + } + + threading::parallel_invoke( + [&]() { + /* Build kd-tree for the selected points. */ + KDTree_3d *kdtree = BLI_kdtree_3d_new(curve_op_data.selected_point_indices.size()); + BLI_SCOPED_DEFER([&]() { BLI_kdtree_3d_free(kdtree); }); + for (const int point_i : curve_op_data.selected_point_indices) { + const float3 &position = positions[point_i]; + BLI_kdtree_3d_insert(kdtree, point_i, position); + } + BLI_kdtree_3d_balance(kdtree); + + /* For each unselected point, compute the distance to the closest selected point. */ + curve_op_data.distances_to_selected.reinitialize( + curve_op_data.unselected_point_indices.size()); + threading::parallel_for(curve_op_data.unselected_point_indices.index_range(), + 256, + [&](const IndexRange range) { + for (const int i : range) { + const int point_i = curve_op_data.unselected_point_indices[i]; + const float3 &position = positions[point_i]; + KDTreeNearest_3d nearest; + BLI_kdtree_3d_find_nearest(kdtree, position, &nearest); + curve_op_data.distances_to_selected[i] = nearest.dist; + } + }); + }, + [&]() { + /* Build kd-tree for the unselected points. */ + KDTree_3d *kdtree = BLI_kdtree_3d_new(curve_op_data.unselected_point_indices.size()); + BLI_SCOPED_DEFER([&]() { BLI_kdtree_3d_free(kdtree); }); + for (const int point_i : curve_op_data.unselected_point_indices) { + const float3 &position = positions[point_i]; + BLI_kdtree_3d_insert(kdtree, point_i, position); + } + BLI_kdtree_3d_balance(kdtree); + + /* For each selected point, compute the distance to the closest unselected point. */ + curve_op_data.distances_to_unselected.reinitialize( + curve_op_data.selected_point_indices.size()); + threading::parallel_for( + curve_op_data.selected_point_indices.index_range(), 256, [&](const IndexRange range) { + for (const int i : range) { + const int point_i = curve_op_data.selected_point_indices[i]; + const float3 &position = positions[point_i]; + KDTreeNearest_3d nearest; + BLI_kdtree_3d_find_nearest(kdtree, position, &nearest); + curve_op_data.distances_to_unselected[i] = nearest.dist; + } + }); + }); + + float4x4 curves_to_world_mat = curves_ob.obmat; + float4x4 world_to_curves_mat = curves_to_world_mat.inverted(); + + float4x4 projection; + ED_view3d_ob_project_mat_get(&rv3d, &curves_ob, projection.values); + + /* Compute how mouse movements in screen space are converted into grow/shrink distances in + * object space. */ + curve_op_data.pixel_to_distance_factor = threading::parallel_reduce( + curve_op_data.selected_point_indices.index_range(), + 256, + FLT_MAX, + [&](const IndexRange range, float pixel_to_distance_factor) { + for (const int i : range) { + const int point_i = curve_op_data.selected_point_indices[i]; + const float3 &pos_cu = positions[point_i]; + + float2 pos_re; + ED_view3d_project_float_v2_m4(®ion, pos_cu, pos_re, projection.values); + if (pos_re.x < 0 || pos_re.y < 0 || pos_re.x > region.winx || pos_re.y > region.winy) { + continue; + } + /* Compute how far this point moves in curve space when it moves one unit in screen + * space. */ + const float2 pos_offset_re = pos_re + float2(1, 0); + float3 pos_offset_wo; + ED_view3d_win_to_3d( + &v3d, ®ion, curves_to_world_mat * pos_cu, pos_offset_re, pos_offset_wo); + const float3 pos_offset_cu = world_to_curves_mat * pos_offset_wo; + const float dist_cu = math::distance(pos_cu, pos_offset_cu); + const float dist_re = math::distance(pos_re, pos_offset_re); + const float factor = dist_cu / dist_re; + math::min_inplace(pixel_to_distance_factor, factor); + } + return pixel_to_distance_factor; + }, + [](const float a, const float b) { return std::min(a, b); }); +} + +static int select_grow_invoke(bContext *C, wmOperator *op, const wmEvent *event) +{ + Object *active_ob = CTX_data_active_object(C); + ARegion *region = CTX_wm_region(C); + View3D *v3d = CTX_wm_view3d(C); + RegionView3D *rv3d = CTX_wm_region_view3d(C); + + GrowOperatorData *op_data = MEM_new(__func__); + op->customdata = op_data; + + op_data->initial_mouse_x = event->xy[0]; + + Curves &curves_id = *static_cast(active_ob->data); + auto curve_op_data = std::make_unique(); + select_grow_invoke_per_curve(curves_id, *active_ob, *region, *v3d, *rv3d, *curve_op_data); + op_data->per_curve.append(std::move(curve_op_data)); + + WM_event_add_modal_handler(C, op); + return OPERATOR_RUNNING_MODAL; +} + +static int select_grow_modal(bContext *C, wmOperator *op, const wmEvent *event) +{ + GrowOperatorData &op_data = *static_cast(op->customdata); + const int mouse_x = event->xy[0]; + const int mouse_diff_x = mouse_x - op_data.initial_mouse_x; + switch (event->type) { + case MOUSEMOVE: { + select_grow_update(C, op, mouse_diff_x); + break; + } + case LEFTMOUSE: { + MEM_delete(&op_data); + return OPERATOR_FINISHED; + } + case EVT_ESCKEY: + case RIGHTMOUSE: { + /* Undo operator by resetting the selection to the original value. */ + for (std::unique_ptr &curve_op_data : op_data.per_curve) { + Curves &curves_id = *curve_op_data->curves_id; + CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry); + switch (curves_id.selection_domain) { + case ATTR_DOMAIN_POINT: { + MutableSpan points_selection = curves.selection_point_float_for_write(); + points_selection.copy_from(curve_op_data->original_selection); + break; + } + case ATTR_DOMAIN_CURVE: { + MutableSpan curves_seletion = curves.selection_curve_float_for_write(); + curves_seletion.copy_from(curve_op_data->original_selection); + break; + } + } + + /* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because it is handled as a generic + * attribute for now. */ + DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GEOM | ND_DATA, &curves_id); + } + MEM_delete(&op_data); + return OPERATOR_CANCELLED; + } + } + return OPERATOR_RUNNING_MODAL; +} + +} // namespace select_grow + +static void SCULPT_CURVES_OT_select_grow(wmOperatorType *ot) +{ + ot->name = "Select Grow"; + ot->idname = __func__; + ot->description = "Select curves which are close to curves that are selected already"; + + ot->invoke = select_grow::select_grow_invoke; + ot->modal = select_grow::select_grow_modal; + ot->poll = curves::selection_operator_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + PropertyRNA *prop; + prop = RNA_def_float(ot->srna, + "distance", + 0.1f, + -FLT_MAX, + FLT_MAX, + "Distance", + "By how much to grow the selection", + -10.0f, + 10.f); + RNA_def_property_subtype(prop, PROP_DISTANCE); +} + +namespace min_distance_edit { + +static bool min_distance_edit_poll(bContext *C) +{ + Object *ob = CTX_data_active_object(C); + if (ob == nullptr) { + return false; + } + if (ob->type != OB_CURVES) { + return false; + } + Curves *curves_id = static_cast(ob->data); + if (curves_id->surface == nullptr || curves_id->surface->type != OB_MESH) { + CTX_wm_operator_poll_msg_set(C, "Curves must have a mesh surface object set"); + return false; + } + Scene *scene = CTX_data_scene(C); + const Brush *brush = BKE_paint_brush_for_read(&scene->toolsettings->curves_sculpt->paint); + if (brush == nullptr) { + return false; + } + if (brush->curves_sculpt_tool != CURVES_SCULPT_TOOL_DENSITY) { + return false; + } + return true; +} + +struct MinDistanceEditData { + /** Brush whose minimum distance is modified. */ + Brush *brush; + float4x4 curves_to_world_mat; + + /** Where the preview is drawn. */ + float3 pos_cu; + float3 normal_cu; + + int2 initial_mouse; + float initial_minimum_distance; + + /** The operator uses a new cursor, but the existing cursors should be restored afterwards. */ + ListBase orig_paintcursors; + void *cursor; + + /** Store the viewport region in case the operator was called from the header. */ + ARegion *region; + RegionView3D *rv3d; +}; + +static int calculate_points_per_side(bContext *C, MinDistanceEditData &op_data) +{ + Scene *scene = CTX_data_scene(C); + ARegion *region = op_data.region; + + const float min_distance = op_data.brush->curves_sculpt_settings->minimum_distance; + const float brush_radius = BKE_brush_size_get(scene, op_data.brush); + + float3 tangent_x_cu = math::cross(op_data.normal_cu, float3{0, 0, 1}); + if (math::is_zero(tangent_x_cu)) { + tangent_x_cu = math::cross(op_data.normal_cu, float3{0, 1, 0}); + } + tangent_x_cu = math::normalize(tangent_x_cu); + const float3 tangent_y_cu = math::normalize(math::cross(op_data.normal_cu, tangent_x_cu)); + + /* Sample a few points to get a good estimate of how large the grid has to be. */ + Vector points_wo; + points_wo.append(op_data.pos_cu + min_distance * tangent_x_cu); + points_wo.append(op_data.pos_cu + min_distance * tangent_y_cu); + points_wo.append(op_data.pos_cu - min_distance * tangent_x_cu); + points_wo.append(op_data.pos_cu - min_distance * tangent_y_cu); + + Vector points_re; + for (const float3 &pos_wo : points_wo) { + float2 pos_re; + ED_view3d_project_v2(region, pos_wo, pos_re); + points_re.append(pos_re); + } + + float2 origin_re; + ED_view3d_project_v2(region, op_data.pos_cu, origin_re); + + int needed_points = 0; + for (const float2 &pos_re : points_re) { + const float distance = math::length(pos_re - origin_re); + const int needed_points_iter = (brush_radius * 2.0f) / distance; + + if (needed_points_iter > needed_points) { + needed_points = needed_points_iter; + } + } + + /* Limit to a harcoded number since it only adds noise at some point. */ + return std::min(300, needed_points); +} + +static void min_distance_edit_draw(bContext *C, int UNUSED(x), int UNUSED(y), void *customdata) +{ + Scene *scene = CTX_data_scene(C); + MinDistanceEditData &op_data = *static_cast(customdata); + + const float min_distance = op_data.brush->curves_sculpt_settings->minimum_distance; + + float3 tangent_x_cu = math::cross(op_data.normal_cu, float3{0, 0, 1}); + if (math::is_zero(tangent_x_cu)) { + tangent_x_cu = math::cross(op_data.normal_cu, float3{0, 1, 0}); + } + tangent_x_cu = math::normalize(tangent_x_cu); + const float3 tangent_y_cu = math::normalize(math::cross(op_data.normal_cu, tangent_x_cu)); + + const int points_per_side = calculate_points_per_side(C, op_data); + const int points_per_axis_num = 2 * points_per_side + 1; + + Vector points_wo; + for (const int x_i : IndexRange(points_per_axis_num)) { + for (const int y_i : IndexRange(points_per_axis_num)) { + const float x_iter = min_distance * (x_i - (points_per_axis_num - 1) / 2.0f); + const float y_iter = min_distance * (y_i - (points_per_axis_num - 1) / 2.0f); + + const float3 point_pos_cu = op_data.pos_cu + op_data.normal_cu * 0.0001f + + x_iter * tangent_x_cu + y_iter * tangent_y_cu; + const float3 point_pos_wo = op_data.curves_to_world_mat * point_pos_cu; + points_wo.append(point_pos_wo); + } + } + + float4 circle_col = float4(op_data.brush->add_col); + float circle_alpha = op_data.brush->cursor_overlay_alpha; + float brush_radius_re = BKE_brush_size_get(scene, op_data.brush); + + /* Draw the grid. */ + GPU_matrix_push(); + GPU_matrix_push_projection(); + GPU_blend(GPU_BLEND_ALPHA); + + ARegion *region = op_data.region; + RegionView3D *rv3d = op_data.rv3d; + wmWindow *win = CTX_wm_window(C); + + /* It does the same as: `view3d_operator_needs_opengl(C);`. */ + wmViewport(®ion->winrct); + GPU_matrix_projection_set(rv3d->winmat); + GPU_matrix_set(rv3d->viewmat); + + GPUVertFormat *format3d = immVertexFormat(); + + const uint pos3d = GPU_vertformat_attr_add(format3d, "pos", GPU_COMP_F32, 3, GPU_FETCH_FLOAT); + const uint col3d = GPU_vertformat_attr_add(format3d, "color", GPU_COMP_F32, 4, GPU_FETCH_FLOAT); + + immBindBuiltinProgram(GPU_SHADER_3D_POINT_FIXED_SIZE_VARYING_COLOR); + + GPU_point_size(3.0f); + immBegin(GPU_PRIM_POINTS, points_wo.size()); + + float3 brush_origin_wo = op_data.curves_to_world_mat * op_data.pos_cu; + float2 brush_origin_re; + ED_view3d_project_v2(region, brush_origin_wo, brush_origin_re); + + /* Smooth alpha transition until the brush edge. */ + const int alpha_border_re = 20; + const float dist_to_inner_border_re = brush_radius_re - alpha_border_re; + + for (const float3 &pos_wo : points_wo) { + float2 pos_re; + ED_view3d_project_v2(region, pos_wo, pos_re); + + const float dist_to_point_re = math::distance(pos_re, brush_origin_re); + const float alpha = 1.0f - ((dist_to_point_re - dist_to_inner_border_re) / alpha_border_re); + + immAttr4f(col3d, 0.9f, 0.9f, 0.9f, alpha); + immVertex3fv(pos3d, pos_wo); + } + immEnd(); + immUnbindProgram(); + + /* Reset the drawing settings. */ + GPU_point_size(1.0f); + GPU_matrix_pop_projection(); + GPU_matrix_pop(); + + int4 scissor; + GPU_scissor_get(scissor); + wmWindowViewport(win); + GPU_scissor(scissor[0], scissor[1], scissor[2], scissor[3]); + + /* Draw the brush circle. */ + GPU_matrix_translate_2f((float)op_data.initial_mouse.x, (float)op_data.initial_mouse.y); + + GPUVertFormat *format = immVertexFormat(); + uint pos2d = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); + + immBindBuiltinProgram(GPU_SHADER_2D_UNIFORM_COLOR); + + immUniformColor3fvAlpha(circle_col, circle_alpha); + imm_draw_circle_wire_2d(pos2d, 0.0f, 0.0f, brush_radius_re, 80); + + immUnbindProgram(); + GPU_blend(GPU_BLEND_NONE); +} + +static int min_distance_edit_invoke(bContext *C, wmOperator *op, const wmEvent *event) +{ + Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C); + ARegion *region = CTX_wm_region(C); + View3D *v3d = CTX_wm_view3d(C); + Scene *scene = CTX_data_scene(C); + + Object &curves_ob = *CTX_data_active_object(C); + Curves &curves_id = *static_cast(curves_ob.data); + Object &surface_ob = *curves_id.surface; + Mesh &surface_me = *static_cast(surface_ob.data); + + BVHTreeFromMesh surface_bvh; + BKE_bvhtree_from_mesh_get(&surface_bvh, &surface_me, BVHTREE_FROM_LOOPTRI, 2); + BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh); }); + + const int2 mouse_pos_int_re{event->mval}; + const float2 mouse_pos_re{mouse_pos_int_re}; + + float3 ray_start_wo, ray_end_wo; + ED_view3d_win_to_segment_clipped( + depsgraph, region, v3d, mouse_pos_re, ray_start_wo, ray_end_wo, true); + + const float4x4 surface_to_world_mat = surface_ob.obmat; + const float4x4 world_to_surface_mat = surface_to_world_mat.inverted(); + + const float3 ray_start_su = world_to_surface_mat * ray_start_wo; + const float3 ray_end_su = world_to_surface_mat * ray_end_wo; + const float3 ray_direction_su = math::normalize(ray_end_su - ray_start_su); + + BVHTreeRayHit ray_hit; + ray_hit.dist = FLT_MAX; + ray_hit.index = -1; + BLI_bvhtree_ray_cast(surface_bvh.tree, + ray_start_su, + ray_direction_su, + 0.0f, + &ray_hit, + surface_bvh.raycast_callback, + &surface_bvh); + if (ray_hit.index == -1) { + WM_report(RPT_ERROR, "Cursor must be over the surface mesh"); + return OPERATOR_CANCELLED; + } + + const float3 hit_pos_su = ray_hit.co; + const float3 hit_normal_su = ray_hit.no; + const float4x4 curves_to_world_mat = curves_ob.obmat; + const float4x4 world_to_curves_mat = curves_to_world_mat.inverted(); + const float4x4 surface_to_curves_mat = world_to_curves_mat * surface_to_world_mat; + const float4x4 surface_to_curves_normal_mat = surface_to_curves_mat.inverted().transposed(); + + const float3 hit_pos_cu = surface_to_curves_mat * hit_pos_su; + const float3 hit_normal_cu = math::normalize(surface_to_curves_normal_mat * hit_normal_su); + + MinDistanceEditData *op_data = MEM_new(__func__); + op_data->curves_to_world_mat = curves_to_world_mat; + op_data->normal_cu = hit_normal_cu; + op_data->pos_cu = hit_pos_cu; + op_data->initial_mouse = event->xy; + op_data->brush = BKE_paint_brush(&scene->toolsettings->curves_sculpt->paint); + op_data->initial_minimum_distance = op_data->brush->curves_sculpt_settings->minimum_distance; + + if (op_data->initial_minimum_distance <= 0.0f) { + op_data->initial_minimum_distance = 0.01f; + } + + op->customdata = op_data; + + /* Temporarily disable other paint cursors. */ + wmWindowManager *wm = CTX_wm_manager(C); + op_data->orig_paintcursors = wm->paintcursors; + BLI_listbase_clear(&wm->paintcursors); + + /* Add minimum distance paint cursor. */ + op_data->cursor = WM_paint_cursor_activate( + SPACE_TYPE_ANY, RGN_TYPE_ANY, op->type->poll, min_distance_edit_draw, op_data); + + op_data->region = CTX_wm_region(C); + op_data->rv3d = CTX_wm_region_view3d(C); + + WM_event_add_modal_handler(C, op); + ED_region_tag_redraw(region); + return OPERATOR_RUNNING_MODAL; +} + +static int min_distance_edit_modal(bContext *C, wmOperator *op, const wmEvent *event) +{ + ARegion *region = CTX_wm_region(C); + MinDistanceEditData &op_data = *static_cast(op->customdata); + + auto finish = [&]() { + wmWindowManager *wm = CTX_wm_manager(C); + + /* Remove own cursor. */ + WM_paint_cursor_end(static_cast(op_data.cursor)); + /* Restore original paint cursors. */ + wm->paintcursors = op_data.orig_paintcursors; + + ED_region_tag_redraw(region); + MEM_freeN(&op_data); + }; + + switch (event->type) { + case MOUSEMOVE: { + const int2 mouse_pos_int_re{event->xy}; + const float2 mouse_pos_re{mouse_pos_int_re}; + + const float mouse_diff_x = mouse_pos_int_re.x - op_data.initial_mouse.x; + const float factor = powf(2, mouse_diff_x / UI_UNIT_X / 10.0f); + op_data.brush->curves_sculpt_settings->minimum_distance = op_data.initial_minimum_distance * + factor; + + ED_region_tag_redraw(region); + WM_main_add_notifier(NC_SCENE | ND_TOOLSETTINGS, nullptr); + break; + } + case LEFTMOUSE: { + if (event->val == KM_PRESS) { + finish(); + return OPERATOR_FINISHED; + } + break; + } + case RIGHTMOUSE: + case EVT_ESCKEY: { + op_data.brush->curves_sculpt_settings->minimum_distance = op_data.initial_minimum_distance; + finish(); + WM_main_add_notifier(NC_SCENE | ND_TOOLSETTINGS, nullptr); + return OPERATOR_CANCELLED; + } + } + + return OPERATOR_RUNNING_MODAL; +} + +} // namespace min_distance_edit + +static void SCULPT_CURVES_OT_min_distance_edit(wmOperatorType *ot) +{ + ot->name = "Edit Minimum Distance"; + ot->idname = __func__; + ot->description = "Change the minimum distance used by the density brush"; + + ot->poll = min_distance_edit::min_distance_edit_poll; + ot->invoke = min_distance_edit::min_distance_edit_invoke; + ot->modal = min_distance_edit::min_distance_edit_modal; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_DEPENDS_ON_CURSOR; +} + } // namespace blender::ed::sculpt_paint /* -------------------------------------------------------------------- */ @@ -327,6 +1281,10 @@ void ED_operatortypes_sculpt_curves() using namespace blender::ed::sculpt_paint; WM_operatortype_append(SCULPT_CURVES_OT_brush_stroke); WM_operatortype_append(CURVES_OT_sculptmode_toggle); + WM_operatortype_append(SCULPT_CURVES_OT_select_random); + WM_operatortype_append(SCULPT_CURVES_OT_select_end); + WM_operatortype_append(SCULPT_CURVES_OT_select_grow); + WM_operatortype_append(SCULPT_CURVES_OT_min_distance_edit); } /** \} */ diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_pinch.cc b/source/blender/editors/sculpt_paint/curves_sculpt_pinch.cc new file mode 100644 index 00000000000..db890d6a054 --- /dev/null +++ b/source/blender/editors/sculpt_paint/curves_sculpt_pinch.cc @@ -0,0 +1,307 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + +#include "curves_sculpt_intern.hh" + +#include "BLI_float4x4.hh" +#include "BLI_vector.hh" + +#include "PIL_time.h" + +#include "DEG_depsgraph.h" + +#include "BKE_brush.h" +#include "BKE_context.h" +#include "BKE_curves.hh" +#include "BKE_paint.h" + +#include "DNA_brush_enums.h" +#include "DNA_brush_types.h" +#include "DNA_curves_types.h" +#include "DNA_object_types.h" +#include "DNA_screen_types.h" +#include "DNA_space_types.h" + +#include "ED_screen.h" +#include "ED_view3d.h" + +#include "WM_api.h" + +/** + * The code below uses a prefix naming convention to indicate the coordinate space: + * cu: Local space of the curves object that is being edited. + * su: Local space of the surface object. + * wo: World space. + * re: 2D coordinates within the region. + */ + +namespace blender::ed::sculpt_paint { + +class PinchOperation : public CurvesSculptStrokeOperation { + private: + bool invert_pinch_; + Array segment_lengths_cu_; + + /** Only used when a 3D brush is used. */ + CurvesBrush3D brush_3d_; + + friend struct PinchOperationExecutor; + + public: + PinchOperation(const bool invert_pinch) : invert_pinch_(invert_pinch) + { + } + + void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override; +}; + +struct PinchOperationExecutor { + PinchOperation *self_ = nullptr; + CurvesSculptCommonContext ctx_; + + Object *object_ = nullptr; + Curves *curves_id_ = nullptr; + CurvesGeometry *curves_ = nullptr; + + VArray point_factors_; + Vector selected_curve_indices_; + IndexMask curve_selection_; + + CurvesSculptTransforms transforms_; + + const CurvesSculpt *curves_sculpt_ = nullptr; + const Brush *brush_ = nullptr; + float brush_radius_base_re_; + float brush_radius_factor_; + float brush_strength_; + + float invert_factor_; + + float2 brush_pos_re_; + + PinchOperationExecutor(const bContext &C) : ctx_(C) + { + } + + void execute(PinchOperation &self, const bContext &C, const StrokeExtension &stroke_extension) + { + self_ = &self; + + object_ = CTX_data_active_object(&C); + curves_id_ = static_cast(object_->data); + curves_ = &CurvesGeometry::wrap(curves_id_->geometry); + if (curves_->curves_num() == 0) { + return; + } + + curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt; + brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint); + brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_); + brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension); + brush_strength_ = BKE_brush_alpha_get(ctx_.scene, brush_); + + invert_factor_ = self_->invert_pinch_ ? -1.0f : 1.0f; + + transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface); + + point_factors_ = get_point_selection(*curves_id_); + curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_); + + brush_pos_re_ = stroke_extension.mouse_position; + const eBrushFalloffShape falloff_shape = static_cast( + brush_->falloff_shape); + + if (stroke_extension.is_first) { + this->initialize_segment_lengths(); + + if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) { + self_->brush_3d_ = *sample_curves_3d_brush(*ctx_.depsgraph, + *ctx_.region, + *ctx_.v3d, + *ctx_.rv3d, + *object_, + brush_pos_re_, + brush_radius_base_re_); + } + } + + Array changed_curves(curves_->curves_num(), false); + if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) { + this->pinch_projected_with_symmetry(changed_curves); + } + else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) { + this->pinch_spherical_with_symmetry(changed_curves); + } + else { + BLI_assert_unreachable(); + } + + this->restore_segment_lengths(changed_curves); + curves_->tag_positions_changed(); + DEG_id_tag_update(&curves_id_->id, ID_RECALC_GEOMETRY); + WM_main_add_notifier(NC_GEOM | ND_DATA, &curves_id_->id); + ED_region_tag_redraw(ctx_.region); + } + + void pinch_projected_with_symmetry(MutableSpan r_changed_curves) + { + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + this->pinch_projected(brush_transform, r_changed_curves); + } + } + + void pinch_projected(const float4x4 &brush_transform, MutableSpan r_changed_curves) + { + const float4x4 brush_transform_inv = brush_transform.inverted(); + + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + MutableSpan positions_cu = curves_->positions_for_write(); + const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_; + const float brush_radius_sq_re = pow2f(brush_radius_re); + + threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) { + for (const int curve_i : curve_selection_.slice(range)) { + const IndexRange points = curves_->points_for_curve(curve_i); + for (const int point_i : points.drop_front(1)) { + const float3 old_pos_cu = brush_transform_inv * positions_cu[point_i]; + float2 old_pos_re; + ED_view3d_project_float_v2_m4(ctx_.region, old_pos_cu, old_pos_re, projection.values); + + const float dist_to_brush_sq_re = math::distance_squared(old_pos_re, brush_pos_re_); + if (dist_to_brush_sq_re > brush_radius_sq_re) { + continue; + } + + const float dist_to_brush_re = std::sqrt(dist_to_brush_sq_re); + const float t = safe_divide(dist_to_brush_re, brush_radius_base_re_); + const float radius_falloff = t * BKE_brush_curve_strength(brush_, t, 1.0f); + const float weight = invert_factor_ * 0.1f * brush_strength_ * radius_falloff * + point_factors_[point_i]; + + const float2 new_pos_re = math::interpolate(old_pos_re, brush_pos_re_, weight); + + const float3 old_pos_wo = transforms_.curves_to_world * old_pos_cu; + float3 new_pos_wo; + ED_view3d_win_to_3d(ctx_.v3d, ctx_.region, old_pos_wo, new_pos_re, new_pos_wo); + + const float3 new_pos_cu = transforms_.world_to_curves * new_pos_wo; + positions_cu[point_i] = brush_transform * new_pos_cu; + r_changed_curves[curve_i] = true; + } + } + }); + } + + void pinch_spherical_with_symmetry(MutableSpan r_changed_curves) + { + float3 brush_pos_wo; + ED_view3d_win_to_3d(ctx_.v3d, + ctx_.region, + transforms_.curves_to_world * self_->brush_3d_.position_cu, + brush_pos_re_, + brush_pos_wo); + const float3 brush_pos_cu = transforms_.world_to_curves * brush_pos_wo; + const float brush_radius_cu = self_->brush_3d_.radius_cu * brush_radius_factor_; + + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + this->pinch_spherical(brush_transform * brush_pos_cu, brush_radius_cu, r_changed_curves); + } + } + + void pinch_spherical(const float3 &brush_pos_cu, + const float brush_radius_cu, + MutableSpan r_changed_curves) + { + MutableSpan positions_cu = curves_->positions_for_write(); + const float brush_radius_sq_cu = pow2f(brush_radius_cu); + + threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) { + for (const int curve_i : curve_selection_.slice(range)) { + const IndexRange points = curves_->points_for_curve(curve_i); + for (const int point_i : points.drop_front(1)) { + const float3 old_pos_cu = positions_cu[point_i]; + + const float dist_to_brush_sq_cu = math::distance_squared(old_pos_cu, brush_pos_cu); + if (dist_to_brush_sq_cu > brush_radius_sq_cu) { + continue; + } + + const float dist_to_brush_cu = std::sqrt(dist_to_brush_sq_cu); + const float t = safe_divide(dist_to_brush_cu, brush_radius_cu); + const float radius_falloff = t * BKE_brush_curve_strength(brush_, t, 1.0f); + const float weight = invert_factor_ * 0.1f * brush_strength_ * radius_falloff * + point_factors_[point_i]; + + const float3 new_pos_cu = math::interpolate(old_pos_cu, brush_pos_cu, weight); + positions_cu[point_i] = new_pos_cu; + + r_changed_curves[curve_i] = true; + } + } + }); + } + + void initialize_segment_lengths() + { + const Span positions_cu = curves_->positions(); + self_->segment_lengths_cu_.reinitialize(curves_->points_num()); + threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) { + for (const int curve_i : curve_selection_.slice(range)) { + const IndexRange points = curves_->points_for_curve(curve_i); + for (const int point_i : points.drop_back(1)) { + const float3 &p1_cu = positions_cu[point_i]; + const float3 &p2_cu = positions_cu[point_i + 1]; + const float length_cu = math::distance(p1_cu, p2_cu); + self_->segment_lengths_cu_[point_i] = length_cu; + } + } + }); + } + + void restore_segment_lengths(const Span changed_curves) + { + const Span expected_lengths_cu = self_->segment_lengths_cu_; + MutableSpan positions_cu = curves_->positions_for_write(); + + threading::parallel_for(changed_curves.index_range(), 256, [&](const IndexRange range) { + for (const int curve_i : range) { + if (!changed_curves[curve_i]) { + continue; + } + const IndexRange points = curves_->points_for_curve(curve_i); + for (const int segment_i : IndexRange(points.size() - 1)) { + const float3 &p1_cu = positions_cu[points[segment_i]]; + float3 &p2_cu = positions_cu[points[segment_i] + 1]; + const float3 direction = math::normalize(p2_cu - p1_cu); + const float expected_length_cu = expected_lengths_cu[points[segment_i]]; + p2_cu = p1_cu + direction * expected_length_cu; + } + } + }); + } +}; + +void PinchOperation::on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) +{ + PinchOperationExecutor executor{C}; + executor.execute(*this, C, stroke_extension); +} + +std::unique_ptr new_pinch_operation(const BrushStrokeMode brush_mode, + const bContext &C) +{ + const Scene &scene = *CTX_data_scene(&C); + const Brush &brush = *BKE_paint_brush_for_read(&scene.toolsettings->curves_sculpt->paint); + + const bool invert_pinch = (brush_mode == BRUSH_STROKE_INVERT) != + ((brush.flag & BRUSH_DIR_IN) != 0); + return std::make_unique(invert_pinch); +} + +} // namespace blender::ed::sculpt_paint diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_puff.cc b/source/blender/editors/sculpt_paint/curves_sculpt_puff.cc new file mode 100644 index 00000000000..dc747fd0bce --- /dev/null +++ b/source/blender/editors/sculpt_paint/curves_sculpt_puff.cc @@ -0,0 +1,393 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_attribute_math.hh" +#include "BKE_brush.h" +#include "BKE_bvhutils.h" +#include "BKE_context.h" +#include "BKE_mesh.h" +#include "BKE_mesh_runtime.h" + +#include "ED_screen.h" +#include "ED_view3d.h" + +#include "DEG_depsgraph.h" + +#include "DNA_brush_types.h" +#include "DNA_mesh_types.h" +#include "DNA_meshdata_types.h" + +#include "WM_api.h" + +#include "BLI_length_parameterize.hh" + +#include "curves_sculpt_intern.hh" + +namespace blender::ed::sculpt_paint { + +class PuffOperation : public CurvesSculptStrokeOperation { + private: + /** Only used when a 3D brush is used. */ + CurvesBrush3D brush_3d_; + + /** Length of each segment indexed by the index of the first point in the segment. */ + Array segment_lengths_cu_; + + friend struct PuffOperationExecutor; + + public: + void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override; +}; + +static float3 compute_surface_point_normal(const MLoopTri &looptri, + const float3 &bary_coord, + const Span corner_normals) +{ + const int l0 = looptri.tri[0]; + const int l1 = looptri.tri[1]; + const int l2 = looptri.tri[2]; + + const float3 &l0_normal = corner_normals[l0]; + const float3 &l1_normal = corner_normals[l1]; + const float3 &l2_normal = corner_normals[l2]; + + const float3 normal = math::normalize( + attribute_math::mix3(bary_coord, l0_normal, l1_normal, l2_normal)); + return normal; +} + +/** + * 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 PuffOperationExecutor { + PuffOperation *self_ = nullptr; + CurvesSculptCommonContext ctx_; + + Object *object_ = nullptr; + Curves *curves_id_ = nullptr; + CurvesGeometry *curves_ = nullptr; + + VArray point_factors_; + Vector selected_curve_indices_; + IndexMask curve_selection_; + + const CurvesSculpt *curves_sculpt_ = nullptr; + const Brush *brush_ = nullptr; + float brush_radius_base_re_; + float brush_radius_factor_; + float brush_strength_; + float2 brush_pos_re_; + + eBrushFalloffShape falloff_shape_; + + CurvesSculptTransforms transforms_; + + Object *surface_ob_ = nullptr; + Mesh *surface_ = nullptr; + Span surface_looptris_; + Span corner_normals_su_; + BVHTreeFromMesh surface_bvh_; + + PuffOperationExecutor(const bContext &C) : ctx_(C) + { + } + + void execute(PuffOperation &self, const bContext &C, const StrokeExtension &stroke_extension) + { + UNUSED_VARS(C, stroke_extension); + self_ = &self; + + object_ = CTX_data_active_object(&C); + curves_id_ = static_cast(object_->data); + curves_ = &CurvesGeometry::wrap(curves_id_->geometry); + if (curves_->curves_num() == 0) { + return; + } + if (curves_id_->surface == nullptr || curves_id_->surface->type != OB_MESH) { + return; + } + + curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt; + brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint); + brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_); + brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension); + brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension); + brush_pos_re_ = stroke_extension.mouse_position; + + point_factors_ = get_point_selection(*curves_id_); + curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_); + + falloff_shape_ = static_cast(brush_->falloff_shape); + + surface_ob_ = curves_id_->surface; + surface_ = static_cast(surface_ob_->data); + + transforms_ = CurvesSculptTransforms(*object_, surface_ob_); + + if (!CustomData_has_layer(&surface_->ldata, CD_NORMAL)) { + BKE_mesh_calc_normals_split(surface_); + } + corner_normals_su_ = { + reinterpret_cast(CustomData_get_layer(&surface_->ldata, CD_NORMAL)), + surface_->totloop}; + + BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2); + BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh_); }); + + surface_looptris_ = {BKE_mesh_runtime_looptri_ensure(surface_), + BKE_mesh_runtime_looptri_len(surface_)}; + + if (stroke_extension.is_first) { + this->initialize_segment_lengths(); + if (falloff_shape_ == PAINT_FALLOFF_SHAPE_SPHERE) { + self.brush_3d_ = *sample_curves_3d_brush(*ctx_.depsgraph, + *ctx_.region, + *ctx_.v3d, + *ctx_.rv3d, + *object_, + brush_pos_re_, + brush_radius_base_re_); + } + } + + Array curve_weights(curve_selection_.size(), 0.0f); + + if (falloff_shape_ == PAINT_FALLOFF_SHAPE_TUBE) { + this->find_curve_weights_projected_with_symmetry(curve_weights); + } + else if (falloff_shape_ == PAINT_FALLOFF_SHAPE_SPHERE) { + this->find_curves_weights_spherical_with_symmetry(curve_weights); + } + else { + BLI_assert_unreachable(); + } + + this->puff(curve_weights); + this->restore_segment_lengths(); + + curves_->tag_positions_changed(); + DEG_id_tag_update(&curves_id_->id, ID_RECALC_GEOMETRY); + WM_main_add_notifier(NC_GEOM | ND_DATA, &curves_id_->id); + ED_region_tag_redraw(ctx_.region); + } + + void find_curve_weights_projected_with_symmetry(MutableSpan r_curve_weights) + { + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + this->find_curve_weights_projected(brush_transform, r_curve_weights); + } + } + + void find_curve_weights_projected(const float4x4 &brush_transform, + MutableSpan r_curve_weights) + { + Span positions_cu = curves_->positions(); + + const float4x4 brush_transform_inv = brush_transform.inverted(); + + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + + const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_; + const float brush_radius_sq_re = pow2f(brush_radius_re); + + threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) { + for (const int curve_selection_i : range) { + const int curve_i = curve_selection_[curve_selection_i]; + const IndexRange points = curves_->points_for_curve(curve_i); + const float3 first_pos_cu = brush_transform_inv * positions_cu[points[0]]; + float2 prev_pos_re; + ED_view3d_project_float_v2_m4(ctx_.region, first_pos_cu, prev_pos_re, projection.values); + for (const int point_i : points.drop_front(1)) { + const float3 pos_cu = brush_transform_inv * positions_cu[point_i]; + float2 pos_re; + ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values); + BLI_SCOPED_DEFER([&]() { prev_pos_re = pos_re; }); + + const float dist_to_brush_sq_re = dist_squared_to_line_segment_v2( + brush_pos_re_, prev_pos_re, pos_re); + if (dist_to_brush_sq_re > brush_radius_sq_re) { + continue; + } + + const float dist_to_brush_re = std::sqrt(dist_to_brush_sq_re); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_re, brush_radius_re); + const float weight = radius_falloff; + math::max_inplace(r_curve_weights[curve_selection_i], weight); + } + } + }); + } + + void find_curves_weights_spherical_with_symmetry(MutableSpan r_curve_weights) + { + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + + float3 brush_pos_wo; + ED_view3d_win_to_3d(ctx_.v3d, + ctx_.region, + transforms_.curves_to_world * self_->brush_3d_.position_cu, + brush_pos_re_, + brush_pos_wo); + const float3 brush_pos_cu = transforms_.world_to_curves * brush_pos_wo; + const float brush_radius_cu = self_->brush_3d_.radius_cu * brush_radius_factor_; + + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + this->find_curves_weights_spherical( + brush_transform * brush_pos_cu, brush_radius_cu, r_curve_weights); + } + } + + void find_curves_weights_spherical(const float3 &brush_pos_cu, + const float brush_radius_cu, + MutableSpan r_curve_weights) + { + const Span positions_cu = curves_->positions(); + const float brush_radius_sq_cu = pow2f(brush_radius_cu); + + threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) { + for (const int curve_selection_i : range) { + const int curve_i = curve_selection_[curve_selection_i]; + const IndexRange points = curves_->points_for_curve(curve_i); + for (const int point_i : points.drop_front(1)) { + const float3 &prev_pos_cu = positions_cu[point_i - 1]; + const float3 &pos_cu = positions_cu[point_i]; + const float dist_to_brush_sq_cu = dist_squared_to_line_segment_v3( + brush_pos_cu, prev_pos_cu, pos_cu); + if (dist_to_brush_sq_cu > brush_radius_sq_cu) { + continue; + } + + const float dist_to_brush_cu = std::sqrt(dist_to_brush_sq_cu); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_cu, brush_radius_cu); + const float weight = radius_falloff; + math::max_inplace(r_curve_weights[curve_selection_i], weight); + } + } + }); + } + + void puff(const Span curve_weights) + { + BLI_assert(curve_weights.size() == curve_selection_.size()); + MutableSpan positions_cu = curves_->positions_for_write(); + + threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) { + Vector accumulated_lengths_cu; + for (const int curve_selection_i : range) { + const int curve_i = curve_selection_[curve_selection_i]; + const IndexRange points = curves_->points_for_curve(curve_i); + const int first_point_i = points[0]; + const float3 first_pos_cu = positions_cu[first_point_i]; + const float3 first_pos_su = transforms_.curves_to_surface * first_pos_cu; + + /* Find the nearest position on the surface. The curve will be aligned to the normal of + * that point. */ + BVHTreeNearest nearest; + nearest.dist_sq = FLT_MAX; + BLI_bvhtree_find_nearest(surface_bvh_.tree, + first_pos_su, + &nearest, + surface_bvh_.nearest_callback, + &surface_bvh_); + + const MLoopTri &looptri = surface_looptris_[nearest.index]; + const float3 closest_pos_su = nearest.co; + const float3 &v0_su = surface_->mvert[surface_->mloop[looptri.tri[0]].v].co; + const float3 &v1_su = surface_->mvert[surface_->mloop[looptri.tri[1]].v].co; + const float3 &v2_su = surface_->mvert[surface_->mloop[looptri.tri[2]].v].co; + float3 bary_coords; + interp_weights_tri_v3(bary_coords, v0_su, v1_su, v2_su, closest_pos_su); + const float3 normal_su = compute_surface_point_normal( + looptri, bary_coords, corner_normals_su_); + const float3 normal_cu = math::normalize(transforms_.surface_to_curves_normal * normal_su); + + accumulated_lengths_cu.reinitialize(points.size() - 1); + length_parameterize::accumulate_lengths( + positions_cu.slice(points), false, accumulated_lengths_cu); + + /* Align curve to the surface normal while making sure that the curve does not fold up much + * in the process (e.g. when the curve was pointing in the opposite direction before). */ + for (const int i : IndexRange(points.size()).drop_front(1)) { + const int point_i = points[i]; + const float3 old_pos_cu = positions_cu[point_i]; + + /* Compute final position of the point. */ + const float length_param_cu = accumulated_lengths_cu[i - 1]; + const float3 goal_pos_cu = first_pos_cu + length_param_cu * normal_cu; + + const float weight = 0.01f * brush_strength_ * point_factors_[point_i] * + curve_weights[curve_selection_i]; + float3 new_pos_cu = math::interpolate(old_pos_cu, goal_pos_cu, weight); + + /* Make sure the point does not move closer to the root point than it was initially. This + * makes the curve kind of "rotate up". */ + const float old_dist_to_root_cu = math::distance(old_pos_cu, first_pos_cu); + const float new_dist_to_root_cu = math::distance(new_pos_cu, first_pos_cu); + if (new_dist_to_root_cu < old_dist_to_root_cu) { + const float3 offset = math::normalize(new_pos_cu - first_pos_cu); + new_pos_cu += (old_dist_to_root_cu - new_dist_to_root_cu) * offset; + } + + positions_cu[point_i] = new_pos_cu; + } + } + }); + } + + void initialize_segment_lengths() + { + const Span positions_cu = curves_->positions(); + self_->segment_lengths_cu_.reinitialize(curves_->points_num()); + threading::parallel_for(curves_->curves_range(), 128, [&](const IndexRange range) { + for (const int curve_i : range) { + const IndexRange points = curves_->points_for_curve(curve_i); + for (const int point_i : points.drop_back(1)) { + const float3 &p1_cu = positions_cu[point_i]; + const float3 &p2_cu = positions_cu[point_i + 1]; + const float length_cu = math::distance(p1_cu, p2_cu); + self_->segment_lengths_cu_[point_i] = length_cu; + } + } + }); + } + + void restore_segment_lengths() + { + const Span expected_lengths_cu = self_->segment_lengths_cu_; + MutableSpan positions_cu = curves_->positions_for_write(); + + threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange range) { + for (const int curve_i : range) { + const IndexRange points = curves_->points_for_curve(curve_i); + for (const int segment_i : points.drop_back(1)) { + const float3 &p1_cu = positions_cu[segment_i]; + float3 &p2_cu = positions_cu[segment_i + 1]; + const float3 direction = math::normalize(p2_cu - p1_cu); + const float expected_length_cu = expected_lengths_cu[segment_i]; + p2_cu = p1_cu + direction * expected_length_cu; + } + } + }); + } +}; + +void PuffOperation::on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) +{ + PuffOperationExecutor executor{C}; + executor.execute(*this, C, stroke_extension); +} + +std::unique_ptr new_puff_operation() +{ + return std::make_unique(); +} + +} // namespace blender::ed::sculpt_paint diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_slide.cc b/source/blender/editors/sculpt_paint/curves_sculpt_slide.cc new file mode 100644 index 00000000000..3e1949cbf34 --- /dev/null +++ b/source/blender/editors/sculpt_paint/curves_sculpt_slide.cc @@ -0,0 +1,310 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + +#include "curves_sculpt_intern.hh" + +#include "BLI_float4x4.hh" +#include "BLI_vector.hh" + +#include "PIL_time.h" + +#include "DEG_depsgraph.h" + +#include "BKE_attribute_math.hh" +#include "BKE_brush.h" +#include "BKE_bvhutils.h" +#include "BKE_context.h" +#include "BKE_curves.hh" +#include "BKE_geometry_set.hh" +#include "BKE_mesh.h" +#include "BKE_mesh_runtime.h" +#include "BKE_mesh_sample.hh" +#include "BKE_paint.h" + +#include "DNA_brush_enums.h" +#include "DNA_brush_types.h" +#include "DNA_curves_types.h" +#include "DNA_mesh_types.h" +#include "DNA_meshdata_types.h" +#include "DNA_object_types.h" +#include "DNA_screen_types.h" +#include "DNA_space_types.h" + +#include "ED_screen.h" +#include "ED_view3d.h" + +#include "UI_interface.h" + +#include "WM_api.h" + +namespace blender::ed::sculpt_paint { + +struct SlideCurveInfo { + /** Index of the curve to slide. */ + int curve_i; + /** A weight based on the initial distance to the brush. */ + float radius_falloff; +}; + +struct SlideInfo { + /** The transform used for the curves below (e.g. for symmetry). */ + float4x4 brush_transform; + Vector curves_to_slide; +}; + +class SlideOperation : public CurvesSculptStrokeOperation { + private: + /** Last mouse position. */ + float2 brush_pos_last_re_; + /** Information about which curves to slide. This is initialized when the brush starts. */ + Vector slide_info_; + + friend struct SlideOperationExecutor; + + public: + void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override; +}; + +/** + * 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 SlideOperationExecutor { + SlideOperation *self_ = nullptr; + CurvesSculptCommonContext ctx_; + + const CurvesSculpt *curves_sculpt_ = nullptr; + const Brush *brush_ = nullptr; + float brush_radius_base_re_; + float brush_radius_factor_; + float brush_strength_; + + Object *object_ = nullptr; + Curves *curves_id_ = nullptr; + CurvesGeometry *curves_ = nullptr; + + Object *surface_ob_ = nullptr; + Mesh *surface_ = nullptr; + Span surface_looptris_; + VArray_Span surface_uv_map_; + + VArray curve_factors_; + VArray point_factors_; + Vector selected_curve_indices_; + IndexMask curve_selection_; + + float2 brush_pos_prev_re_; + float2 brush_pos_re_; + float2 brush_pos_diff_re_; + + CurvesSculptTransforms transforms_; + + BVHTreeFromMesh surface_bvh_; + + SlideOperationExecutor(const bContext &C) : ctx_(C) + { + } + + void execute(SlideOperation &self, const bContext &C, const StrokeExtension &stroke_extension) + { + UNUSED_VARS(C, stroke_extension); + self_ = &self; + + object_ = CTX_data_active_object(&C); + curves_id_ = static_cast(object_->data); + curves_ = &CurvesGeometry::wrap(curves_id_->geometry); + if (curves_id_->surface == nullptr || curves_id_->surface->type != OB_MESH) { + return; + } + if (curves_->curves_num() == 0) { + return; + } + + curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt; + brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint); + brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_); + brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension); + brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension); + + curve_factors_ = get_curves_selection(*curves_id_); + point_factors_ = get_point_selection(*curves_id_); + curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_); + + brush_pos_prev_re_ = self_->brush_pos_last_re_; + brush_pos_re_ = stroke_extension.mouse_position; + brush_pos_diff_re_ = brush_pos_re_ - brush_pos_prev_re_; + BLI_SCOPED_DEFER([&]() { self_->brush_pos_last_re_ = brush_pos_re_; }); + + transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface); + + surface_ob_ = curves_id_->surface; + surface_ = static_cast(surface_ob_->data); + + BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2); + BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh_); }); + + surface_looptris_ = {BKE_mesh_runtime_looptri_ensure(surface_), + BKE_mesh_runtime_looptri_len(surface_)}; + + if (curves_id_->surface_uv_map != nullptr) { + MeshComponent surface_component; + surface_component.replace(surface_, GeometryOwnershipType::ReadOnly); + surface_uv_map_ = surface_component + .attribute_try_get_for_read(curves_id_->surface_uv_map, + ATTR_DOMAIN_CORNER) + .typed(); + } + + if (stroke_extension.is_first) { + const Vector brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : brush_transforms) { + this->detect_curves_to_slide(brush_transform); + } + return; + } + this->slide_projected(); + + curves_->tag_positions_changed(); + DEG_id_tag_update(&curves_id_->id, ID_RECALC_GEOMETRY); + WM_main_add_notifier(NC_GEOM | ND_DATA, &curves_id_->id); + ED_region_tag_redraw(ctx_.region); + } + + void detect_curves_to_slide(const float4x4 &brush_transform) + { + const float4x4 brush_transform_inv = brush_transform.inverted(); + + const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_; + const float brush_radius_sq_re = pow2f(brush_radius_re); + + const Span positions_cu = curves_->positions(); + + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + + self_->slide_info_.append({brush_transform}); + Vector &curves_to_slide = self_->slide_info_.last().curves_to_slide; + + /* Find curves in brush radius that should be moved. */ + for (const int curve_i : curve_selection_) { + const int first_point_i = curves_->offsets()[curve_i]; + const float3 &first_pos_cu = brush_transform_inv * positions_cu[first_point_i]; + + float2 first_pos_re; + ED_view3d_project_float_v2_m4(ctx_.region, first_pos_cu, first_pos_re, projection.values); + + const float dist_to_brush_sq_re = math::distance_squared(first_pos_re, brush_pos_re_); + if (dist_to_brush_sq_re > brush_radius_sq_re) { + continue; + } + const float dist_to_brush_re = std::sqrt(dist_to_brush_sq_re); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_re, brush_radius_re); + curves_to_slide.append({curve_i, radius_falloff}); + } + } + + void slide_projected() + { + MutableSpan positions_cu = curves_->positions_for_write(); + + MutableSpan surface_uv_coords; + if (!surface_uv_map_.is_empty()) { + surface_uv_coords = curves_->surface_uv_coords_for_write(); + } + + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + + for (const SlideInfo &slide_info : self_->slide_info_) { + const float4x4 &brush_transform = slide_info.brush_transform; + const float4x4 brush_transform_inv = brush_transform.inverted(); + const Span curves_to_slide = slide_info.curves_to_slide; + + threading::parallel_for(curves_to_slide.index_range(), 256, [&](const IndexRange range) { + for (const SlideCurveInfo &curve_slide_info : curves_to_slide.slice(range)) { + const int curve_i = curve_slide_info.curve_i; + const IndexRange points = curves_->points_for_curve(curve_i); + const int first_point_i = points.first(); + const float3 old_first_pos_cu = brush_transform_inv * positions_cu[first_point_i]; + + float2 old_first_pos_re; + ED_view3d_project_float_v2_m4( + ctx_.region, old_first_pos_cu, old_first_pos_re, projection.values); + const float first_point_weight = brush_strength_ * curve_slide_info.radius_falloff; + + /* Slide root position in region space and then project it back onto the surface. */ + const float2 new_first_pos_re = old_first_pos_re + + first_point_weight * brush_pos_diff_re_; + + float3 ray_start_wo, ray_end_wo; + ED_view3d_win_to_segment_clipped(ctx_.depsgraph, + ctx_.region, + ctx_.v3d, + new_first_pos_re, + ray_start_wo, + ray_end_wo, + true); + const float3 ray_start_su = transforms_.world_to_surface * ray_start_wo; + const float3 ray_end_su = transforms_.world_to_surface * ray_end_wo; + + const float3 ray_direction_su = math::normalize(ray_end_su - ray_start_su); + BVHTreeRayHit hit; + hit.dist = FLT_MAX; + hit.index = -1; + BLI_bvhtree_ray_cast(surface_bvh_.tree, + ray_start_su, + ray_direction_su, + 0.0f, + &hit, + surface_bvh_.raycast_callback, + &surface_bvh_); + if (hit.index == -1) { + continue; + } + + const int looptri_index = hit.index; + const float3 attached_pos_su = hit.co; + + const float3 attached_pos_cu = transforms_.surface_to_curves * attached_pos_su; + const float3 pos_offset_cu = brush_transform * (attached_pos_cu - old_first_pos_cu); + + /* Update positions. The first point doesn't have an additional weight here, because then + * it wouldn't be attached to the surface anymore. */ + positions_cu[first_point_i] += pos_offset_cu; + for (const int point_i : points.drop_front(1)) { + const float weight = point_factors_[point_i]; + positions_cu[point_i] += weight * pos_offset_cu; + } + + /* Update surface attachment information if necessary. */ + if (!surface_uv_map_.is_empty()) { + const MLoopTri &looptri = surface_looptris_[looptri_index]; + const float3 bary_coord = bke::mesh_surface_sample::compute_bary_coord_in_triangle( + *surface_, looptri, attached_pos_su); + const float2 &uv0 = surface_uv_map_[looptri.tri[0]]; + const float2 &uv1 = surface_uv_map_[looptri.tri[1]]; + const float2 &uv2 = surface_uv_map_[looptri.tri[2]]; + const float2 uv = attribute_math::mix3(bary_coord, uv0, uv1, uv2); + surface_uv_coords[curve_i] = uv; + } + } + }); + } + } +}; + +void SlideOperation::on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) +{ + SlideOperationExecutor executor{C}; + executor.execute(*this, C, stroke_extension); +} + +std::unique_ptr new_slide_operation() +{ + return std::make_unique(); +} + +} // namespace blender::ed::sculpt_paint diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_smooth.cc b/source/blender/editors/sculpt_paint/curves_sculpt_smooth.cc new file mode 100644 index 00000000000..e72b17d448b --- /dev/null +++ b/source/blender/editors/sculpt_paint/curves_sculpt_smooth.cc @@ -0,0 +1,259 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_brush.h" +#include "BKE_context.h" + +#include "ED_screen.h" +#include "ED_view3d.h" + +#include "DEG_depsgraph.h" + +#include "DNA_brush_types.h" + +#include "WM_api.h" + +#include "BLI_enumerable_thread_specific.hh" + +#include "curves_sculpt_intern.hh" + +namespace blender::ed::sculpt_paint { + +class SmoothOperation : public CurvesSculptStrokeOperation { + private: + /** Only used when a 3D brush is used. */ + CurvesBrush3D brush_3d_; + + friend struct SmoothOperationExecutor; + + public: + void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override; +}; + +/** + * 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 SmoothOperationExecutor { + SmoothOperation *self_ = nullptr; + CurvesSculptCommonContext ctx_; + + Object *object_ = nullptr; + Curves *curves_id_ = nullptr; + CurvesGeometry *curves_ = nullptr; + + VArray point_factors_; + Vector selected_curve_indices_; + IndexMask curve_selection_; + + const CurvesSculpt *curves_sculpt_ = nullptr; + const Brush *brush_ = nullptr; + float brush_radius_base_re_; + float brush_radius_factor_; + float brush_strength_; + float2 brush_pos_re_; + + CurvesSculptTransforms transforms_; + + SmoothOperationExecutor(const bContext &C) : ctx_(C) + { + } + + void execute(SmoothOperation &self, const bContext &C, const StrokeExtension &stroke_extension) + { + UNUSED_VARS(C, stroke_extension); + self_ = &self; + + object_ = CTX_data_active_object(&C); + curves_id_ = static_cast(object_->data); + curves_ = &CurvesGeometry::wrap(curves_id_->geometry); + if (curves_->curves_num() == 0) { + return; + } + + curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt; + brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint); + brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_); + brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension); + brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension); + brush_pos_re_ = stroke_extension.mouse_position; + + point_factors_ = get_point_selection(*curves_id_); + curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_); + transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface); + + const eBrushFalloffShape falloff_shape = static_cast( + brush_->falloff_shape); + if (stroke_extension.is_first) { + if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) { + self.brush_3d_ = *sample_curves_3d_brush(*ctx_.depsgraph, + *ctx_.region, + *ctx_.v3d, + *ctx_.rv3d, + *object_, + brush_pos_re_, + brush_radius_base_re_); + } + } + + if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) { + this->smooth_projected_with_symmetry(); + } + else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) { + this->smooth_spherical_with_symmetry(); + } + else { + BLI_assert_unreachable(); + } + + curves_->tag_positions_changed(); + DEG_id_tag_update(&curves_id_->id, ID_RECALC_GEOMETRY); + WM_main_add_notifier(NC_GEOM | ND_DATA, &curves_id_->id); + ED_region_tag_redraw(ctx_.region); + } + + void smooth_projected_with_symmetry() + { + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + this->smooth_projected(brush_transform); + } + } + + void smooth_projected(const float4x4 &brush_transform) + { + const float4x4 brush_transform_inv = brush_transform.inverted(); + + MutableSpan positions_cu = curves_->positions_for_write(); + const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_; + const float brush_radius_sq_re = pow2f(brush_radius_re); + + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + + threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) { + Vector old_curve_positions_re; + for (const int curve_i : curve_selection_.slice(range)) { + const IndexRange points = curves_->points_for_curve(curve_i); + + /* Find position of control points in screen space. */ + old_curve_positions_re.clear(); + old_curve_positions_re.reserve(points.size()); + for (const int point_i : points) { + const float3 &pos_cu = brush_transform_inv * positions_cu[point_i]; + float2 pos_re; + ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values); + old_curve_positions_re.append_unchecked(pos_re); + } + for (const int i : IndexRange(points.size()).drop_front(1).drop_back(1)) { + const int point_i = points[i]; + const float2 &old_pos_re = old_curve_positions_re[i]; + const float dist_to_brush_sq_re = math::distance_squared(old_pos_re, brush_pos_re_); + if (dist_to_brush_sq_re > brush_radius_sq_re) { + continue; + } + + const float dist_to_brush_re = std::sqrt(dist_to_brush_sq_re); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_re, brush_radius_re); + /* Used to make the brush easier to use. Otherwise a strength of 1 would be way too + * large. */ + const float weight_factor = 0.1f; + const float weight = weight_factor * brush_strength_ * radius_falloff * + point_factors_[point_i]; + + /* Move points towards the middle of their neighbors. */ + const float2 &old_pos_prev_re = old_curve_positions_re[i - 1]; + const float2 &old_pos_next_re = old_curve_positions_re[i + 1]; + const float2 goal_pos_re = math::midpoint(old_pos_prev_re, old_pos_next_re); + const float2 new_pos_re = math::interpolate(old_pos_re, goal_pos_re, weight); + const float3 old_pos_cu = brush_transform_inv * positions_cu[point_i]; + float3 new_pos_wo; + ED_view3d_win_to_3d(ctx_.v3d, + ctx_.region, + transforms_.curves_to_world * old_pos_cu, + new_pos_re, + new_pos_wo); + const float3 new_pos_cu = brush_transform * (transforms_.world_to_curves * new_pos_wo); + positions_cu[point_i] = new_pos_cu; + } + } + }); + } + + void smooth_spherical_with_symmetry() + { + float4x4 projection; + ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values); + + float3 brush_pos_wo; + ED_view3d_win_to_3d(ctx_.v3d, + ctx_.region, + transforms_.curves_to_world * self_->brush_3d_.position_cu, + brush_pos_re_, + brush_pos_wo); + const float3 brush_pos_cu = transforms_.world_to_curves * brush_pos_wo; + const float brush_radius_cu = self_->brush_3d_.radius_cu * brush_radius_factor_; + + const Vector symmetry_brush_transforms = get_symmetry_brush_transforms( + eCurvesSymmetryType(curves_id_->symmetry)); + for (const float4x4 &brush_transform : symmetry_brush_transforms) { + this->smooth_spherical(brush_transform * brush_pos_cu, brush_radius_cu); + } + } + + void smooth_spherical(const float3 &brush_pos_cu, const float brush_radius_cu) + { + MutableSpan positions_cu = curves_->positions_for_write(); + const float brush_radius_sq_cu = pow2f(brush_radius_cu); + + threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) { + Vector old_curve_positions_cu; + for (const int curve_i : curve_selection_.slice(range)) { + const IndexRange points = curves_->points_for_curve(curve_i); + /* Remember original positions so that we don't smooth based on already smoothed points + * below. */ + old_curve_positions_cu.clear(); + old_curve_positions_cu.extend(positions_cu.slice(points)); + for (const int i : IndexRange(points.size()).drop_front(1).drop_back(1)) { + const int point_i = points[i]; + const float3 &old_pos_cu = old_curve_positions_cu[i]; + const float dist_to_brush_sq_cu = math::distance_squared(old_pos_cu, brush_pos_cu); + if (dist_to_brush_sq_cu > brush_radius_sq_cu) { + continue; + } + + const float dist_to_brush_cu = std::sqrt(dist_to_brush_sq_cu); + const float radius_falloff = BKE_brush_curve_strength( + brush_, dist_to_brush_cu, brush_radius_cu); + /* Used to make the brush easier to use. Otherwise a strength of 1 would be way too + * large. */ + const float weight_factor = 0.1f; + const float weight = weight_factor * brush_strength_ * radius_falloff * + point_factors_[point_i]; + + /* Move points towards the middle of their neighbors. */ + const float3 &old_pos_prev_cu = old_curve_positions_cu[i - 1]; + const float3 &old_pos_next_cu = old_curve_positions_cu[i + 1]; + const float3 goal_pos_cu = math::midpoint(old_pos_prev_cu, old_pos_next_cu); + const float3 new_pos_cu = math::interpolate(old_pos_cu, goal_pos_cu, weight); + positions_cu[point_i] = new_pos_cu; + } + } + }); + } +}; + +void SmoothOperation::on_stroke_extended(const bContext &C, + const StrokeExtension &stroke_extension) +{ + SmoothOperationExecutor executor{C}; + executor.execute(*this, C, stroke_extension); +} + +std::unique_ptr new_smooth_operation() +{ + return std::make_unique(); +} + +} // namespace blender::ed::sculpt_paint diff --git a/source/blender/editors/sculpt_paint/paint_stroke.c b/source/blender/editors/sculpt_paint/paint_stroke.c index 63e6dc7e965..1ee26935dc9 100644 --- a/source/blender/editors/sculpt_paint/paint_stroke.c +++ b/source/blender/editors/sculpt_paint/paint_stroke.c @@ -995,7 +995,7 @@ static void stroke_done(bContext *C, wmOperator *op, PaintStroke *stroke) static bool curves_sculpt_brush_uses_spacing(const eBrushCurvesSculptTool tool) { - return ELEM(tool, CURVES_SCULPT_TOOL_ADD); + return ELEM(tool, CURVES_SCULPT_TOOL_ADD, CURVES_SCULPT_TOOL_DENSITY); } bool paint_space_stroke_enabled(Brush *br, ePaintMode mode) diff --git a/source/blender/makesdna/DNA_brush_enums.h b/source/blender/makesdna/DNA_brush_enums.h index f2cd49b6dea..adda23c26f2 100644 --- a/source/blender/makesdna/DNA_brush_enums.h +++ b/source/blender/makesdna/DNA_brush_enums.h @@ -469,6 +469,11 @@ typedef enum eBrushCurvesSculptTool { CURVES_SCULPT_TOOL_ADD = 3, CURVES_SCULPT_TOOL_GROW_SHRINK = 4, CURVES_SCULPT_TOOL_SELECTION_PAINT = 5, + CURVES_SCULPT_TOOL_PINCH = 6, + CURVES_SCULPT_TOOL_SMOOTH = 7, + CURVES_SCULPT_TOOL_PUFF = 8, + CURVES_SCULPT_TOOL_DENSITY = 9, + CURVES_SCULPT_TOOL_SLIDE = 10, } eBrushCurvesSculptTool; /** When #BRUSH_ACCUMULATE is used */ @@ -622,6 +627,12 @@ typedef enum eBrushCurvesSculptFlag { BRUSH_CURVES_SCULPT_FLAG_INTERPOLATE_POINT_COUNT = (1 << 4), } eBrushCurvesSculptFlag; +typedef enum eBrushCurvesSculptDensityMode { + BRUSH_CURVES_SCULPT_DENSITY_MODE_AUTO = 0, + BRUSH_CURVES_SCULPT_DENSITY_MODE_ADD = 1, + BRUSH_CURVES_SCULPT_DENSITY_MODE_REMOVE = 2, +} eBrushCurvesSculptDensityMode; + #define MAX_BRUSH_PIXEL_RADIUS 500 #define GP_MAX_BRUSH_PIXEL_RADIUS 1000 diff --git a/source/blender/makesdna/DNA_brush_types.h b/source/blender/makesdna/DNA_brush_types.h index d13496b21f7..b24bb786593 100644 --- a/source/blender/makesdna/DNA_brush_types.h +++ b/source/blender/makesdna/DNA_brush_types.h @@ -148,6 +148,13 @@ typedef struct BrushCurvesSculptSettings { float minimum_length; /** Length of newly added curves when it is not interpolated from other curves. */ float curve_length; + /** Minimum distance between curve root points used by the Density brush. */ + float minimum_distance; + /** How often the Density brush tries to add a new curve. */ + int density_add_attempts; + /** #eBrushCurvesSculptDensityMode. */ + uint8_t density_mode; + char _pad[7]; } BrushCurvesSculptSettings; typedef struct Brush { diff --git a/source/blender/makesrna/intern/rna_brush.c b/source/blender/makesrna/intern/rna_brush.c index 80a61543b5a..e0d55050c63 100644 --- a/source/blender/makesrna/intern/rna_brush.c +++ b/source/blender/makesrna/intern/rna_brush.c @@ -251,6 +251,11 @@ const EnumPropertyItem rna_enum_brush_curves_sculpt_tool_items[] = { {CURVES_SCULPT_TOOL_ADD, "ADD", ICON_BRUSH_CURVES_ADD, "Add Curves", ""}, {CURVES_SCULPT_TOOL_GROW_SHRINK, "GROW_SHRINK", ICON_BRUSH_CURVES_GROW_SHRINK, "Grow / Shrink Curves", ""}, {CURVES_SCULPT_TOOL_SELECTION_PAINT, "SELECTION_PAINT", ICON_BRUSH_PAINT_SELECT, "Paint Selection", ""}, + {CURVES_SCULPT_TOOL_PINCH, "PINCH", ICON_BRUSH_CURVES_PINCH, "Pinch Curves", ""}, + {CURVES_SCULPT_TOOL_SMOOTH, "SMOOTH", ICON_BRUSH_CURVES_SMOOTH, "Smooth Curves", ""}, + {CURVES_SCULPT_TOOL_PUFF, "PUFF", ICON_BRUSH_CURVES_PUFF, "Puff Curves", ""}, + {CURVES_SCULPT_TOOL_DENSITY, "DENSITY", ICON_BRUSH_CURVES_DENSITY, "Density Curves", ""}, + {CURVES_SCULPT_TOOL_SLIDE, "SLIDE", ICON_BRUSH_CURVES_SLIDE, "Slide Curves", ""}, {0, NULL, 0, NULL, NULL}, }; /* clang-format on */ @@ -889,6 +894,7 @@ static const EnumPropertyItem *rna_Brush_direction_itemf(bContext *C, switch (me->curves_sculpt_tool) { case CURVES_SCULPT_TOOL_GROW_SHRINK: case CURVES_SCULPT_TOOL_SELECTION_PAINT: + case CURVES_SCULPT_TOOL_PINCH: return prop_direction_items; default: return DummyRNA_DEFAULT_items; @@ -1949,6 +1955,26 @@ static void rna_def_curves_sculpt_options(BlenderRNA *brna) StructRNA *srna; PropertyRNA *prop; + static const EnumPropertyItem density_mode_items[] = { + {BRUSH_CURVES_SCULPT_DENSITY_MODE_AUTO, + "AUTO", + ICON_AUTO, + "Auto", + "Either add or remove curves depending on the minimum distance of the curves under the " + "cursor"}, + {BRUSH_CURVES_SCULPT_DENSITY_MODE_ADD, + "ADD", + ICON_ADD, + "Add", + "Add new curves between existing curves, taking the minimum distance into account"}, + {BRUSH_CURVES_SCULPT_DENSITY_MODE_REMOVE, + "REMOVE", + ICON_REMOVE, + "Remove", + "Remove curves whose root points are too close"}, + {0, NULL, 0, NULL, NULL}, + }; + srna = RNA_def_struct(brna, "BrushCurvesSculptSettings", NULL); RNA_def_struct_sdna(srna, "BrushCurvesSculptSettings"); RNA_def_struct_ui_text(srna, "Curves Sculpt Brush Settings", ""); @@ -1997,6 +2023,22 @@ static void rna_def_curves_sculpt_options(BlenderRNA *brna) prop, "Curve Length", "Length of newly added curves when it is not interpolated from other curves"); + + prop = RNA_def_property(srna, "minimum_distance", PROP_FLOAT, PROP_DISTANCE); + RNA_def_property_range(prop, 0.0f, FLT_MAX); + RNA_def_property_ui_range(prop, 0.0, 1000.0f, 0.001, 2); + RNA_def_property_ui_text( + prop, "Minimum Distance", "Goal distance between curve roots for the Density brush"); + + prop = RNA_def_property(srna, "density_add_attempts", PROP_INT, PROP_NONE); + RNA_def_property_range(prop, 0, INT32_MAX); + RNA_def_property_ui_text( + prop, "Density Add Attempts", "How many times the Density brush tries to add a new curve"); + + prop = RNA_def_property(srna, "density_mode", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_items(prop, density_mode_items); + RNA_def_property_ui_text( + prop, "Density Mode", "Determines whether the brush adds or removes curves"); } static void rna_def_brush(BlenderRNA *brna) -- cgit v1.2.3