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
This commit is contained in:
Jacques Lucke 2022-06-30 15:09:13 +02:00
parent 0f22b5599a
commit 416aef4e13
Notes: blender-bot 2023-11-20 12:14:32 +01:00
Referenced by issue #96259, Curves Sculpt Brushes
20 changed files with 3260 additions and 14 deletions

View File

@ -5624,6 +5624,7 @@ def km_sculpt_curves(params):
("curves.disable_selection", {"type": 'TWO', "value": 'PRESS', "alt": True}, None),
*_template_paint_radial_control("curves_sculpt"),
*_template_items_select_actions(params, "sculpt_curves.select_all"),
("sculpt_curves.min_distance_edit", {"type": 'R', "value": 'PRESS', "shift": True}, {}),
])
return keymap

View File

@ -2377,6 +2377,51 @@ class _defs_curves_sculpt:
data_block='GROW_SHRINK'
)
@ToolDef.from_fn
def pinch():
return dict(
idname="builtin_brush.pinch",
label="Pinch",
icon="ops.curves.sculpt_pinch",
data_block='PINCH'
)
@ToolDef.from_fn
def smooth():
return dict(
idname="builtin_brush.smooth",
label="Smooth",
icon="ops.curves.sculpt_smooth",
data_block='SMOOTH'
)
@ToolDef.from_fn
def puff():
return dict(
idname="builtin_brush.puff",
label="Puff",
icon="ops.curves.sculpt_puff",
data_block='PUFF'
)
@ToolDef.from_fn
def density():
return dict(
idname="builtin_brush.density",
label="Density",
icon="ops.curves.sculpt_density",
data_block="DENSITY"
)
@ToolDef.from_fn
def slide():
return dict(
idname="builtin_brush.slide",
label="Slide",
icon="ops.curves.sculpt_slide",
data_block="SLIDE"
)
class _defs_gpencil_vertex:
@ -3140,6 +3185,11 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel):
_defs_curves_sculpt.delete,
_defs_curves_sculpt.snake_hook,
_defs_curves_sculpt.grow_shrink,
_defs_curves_sculpt.pinch,
_defs_curves_sculpt.smooth,
_defs_curves_sculpt.puff,
_defs_curves_sculpt.density,
_defs_curves_sculpt.slide,
None,
*_tools_annotate,
],

View File

@ -535,6 +535,31 @@ class _draw_tool_settings_context_mode:
layout.prop(brush, "direction", expand=True, text="")
layout.prop(brush, "falloff_shape", expand=True)
layout.popover("VIEW3D_PT_tools_brush_falloff")
elif curves_tool == 'PINCH':
layout.prop(brush, "direction", expand=True, text="")
layout.prop(brush, "falloff_shape", expand=True)
layout.popover("VIEW3D_PT_tools_brush_falloff")
elif curves_tool == 'SMOOTH':
layout.prop(brush, "falloff_shape", expand=True)
layout.popover("VIEW3D_PT_tools_brush_falloff")
elif curves_tool == 'PUFF':
layout.prop(brush, "falloff_shape", expand=True)
layout.popover("VIEW3D_PT_tools_brush_falloff")
elif curves_tool == 'DENSITY':
layout.prop(brush, "falloff_shape", expand=True)
row = layout.row(align=True)
row.prop(brush.curves_sculpt_settings, "density_mode", text="", expand=True)
row = layout.row(align=True)
row.prop(brush.curves_sculpt_settings, "minimum_distance")
row.operator_context = 'INVOKE_REGION_WIN'
row.operator("sculpt_curves.min_distance_edit", text="", icon='DRIVER_DISTANCE')
row = layout.row(align=True)
row.enabled = brush.curves_sculpt_settings.density_mode != 'REMOVE'
row.prop(brush.curves_sculpt_settings, "density_add_attempts", text="Max Count")
layout.popover("VIEW3D_PT_tools_brush_falloff")
layout.popover("VIEW3D_PT_curves_sculpt_add_shape", text="Curve Shape")
elif curves_tool == "SLIDE":
layout.popover("VIEW3D_PT_tools_brush_falloff")
class VIEW3D_HT_header(Header):
@ -2005,6 +2030,9 @@ class VIEW3D_MT_select_sculpt_curves(Menu):
layout.operator("sculpt_curves.select_all", text="All").action = 'SELECT'
layout.operator("sculpt_curves.select_all", text="None").action = 'DESELECT'
layout.operator("sculpt_curves.select_all", text="Invert").action = 'INVERT'
layout.operator("sculpt_curves.select_random", text="Random")
layout.operator("sculpt_curves.select_end", text="Endpoints")
layout.operator("sculpt_curves.select_grow", text="Grow")
class VIEW3D_MT_angle_control(Menu):

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -72,7 +72,7 @@ static bool object_has_editable_curves(const Main &bmain, const Object &object)
return true;
}
static VectorSet<Curves *> get_unique_editable_curves(const bContext &C)
VectorSet<Curves *> get_unique_editable_curves(const bContext &C)
{
VectorSet<Curves *> 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<float> &data)
{
bool contains_nonzero = false;
@ -841,6 +839,19 @@ static bool varray_contains_nonzero(const VArray<float> &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 *> curves_ids)
return false;
}
namespace select_all {
static void invert_selection(MutableSpan<float> 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;

View File

@ -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<Curves *> get_unique_editable_curves(const bContext &C);
}
} // namespace blender::ed::curves
#endif

View File

@ -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

View File

@ -0,0 +1,834 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include <numeric>
#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<MLoopTri> surface_looptris_;
Span<float3> corner_normals_su_;
VArray_Span<float2> 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<Curves *>(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<Mesh *>(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<const float3 *>(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<eBrushFalloffShape>(
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<float3> new_bary_coords;
Vector<int> new_looptri_indices;
Vector<float3> 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<bool> new_curve_skipped(new_positions_cu.size(), false);
threading::parallel_invoke(
/* Build kdtree from root points created by the current stroke. */
[&]() {
const Span<float3> 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<float2> 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<float2>();
}
/* Find normals. */
if (!CustomData_has_layer(&surface_->ldata, CD_NORMAL)) {
BKE_mesh_calc_normals_split(surface_);
}
const Span<float3> corner_normals_su = {
reinterpret_cast<const float3 *>(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<float3> &r_bary_coords,
Vector<int> &r_looptri_indices,
Vector<float3> &r_positions_su)
{
float4x4 projection;
ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
const Vector<float4x4> 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<float3> &r_bary_coords,
Vector<int> &r_looptri_indices,
Vector<float3> &r_positions_su)
{
const std::optional<CurvesBrush3D> 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<float4x4> 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<int> 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<int64_t> 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<Curves *>(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<Mesh *>(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<eBrushFalloffShape>(
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<float3> 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<bool> 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<int64_t> 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<bool> curves_to_delete)
{
const Vector<float4x4> 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<bool> curves_to_delete)
{
const Span<float3> 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<int> offsets = curves_->offsets();
/* Randomly select the curves that are allowed to be removed, based on the brush radius and
* strength. */
Array<bool> 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<bool> curves_to_delete)
{
const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_;
const std::optional<CurvesBrush3D> 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<float4x4> 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<bool> curves_to_delete)
{
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
const Span<float3> positions_cu = curves_->positions();
const Span<int> offsets = curves_->offsets();
/* Randomly select the curves that are allowed to be removed, based on the brush radius and
* strength. */
Array<bool> 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<eBrushCurvesSculptDensityMode>(
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 *>(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<const Mesh *>(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 &region = *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<CurvesBrush3D> 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<int> offsets = curves.offsets();
const Span<float3> positions_cu = curves.positions();
/* Compute distance from brush to curve roots. */
Array<std::pair<float, int>> distances_sq_to_brush(curves.curves_num());
threading::EnumerableThreadSpecific<int> 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<int>(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<CurvesSculptStrokeOperation> 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<DensityAddOperation>();
}
return std::make_unique<DensitySubtractOperation>();
}
} // namespace blender::ed::sculpt_paint

View File

@ -66,7 +66,7 @@ std::unique_ptr<CurvesSculptStrokeOperation> new_pinch_operation(const BrushStro
std::unique_ptr<CurvesSculptStrokeOperation> new_smooth_operation();
std::unique_ptr<CurvesSculptStrokeOperation> new_puff_operation();
std::unique_ptr<CurvesSculptStrokeOperation> new_density_operation(
const BrushStrokeMode brush_mode, const bContext &C);
const BrushStrokeMode brush_mode, const bContext &C, const StrokeExtension &stroke_start);
std::unique_ptr<CurvesSculptStrokeOperation> new_slide_operation();
struct CurvesBrush3D {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,307 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#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<float> 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<float> point_factors_;
Vector<int64_t> 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<Curves *>(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<eBrushFalloffShape>(
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<bool> 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<bool> r_changed_curves)
{
const Vector<float4x4> 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<bool> 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<float3> 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<bool> 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<float4x4> 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<bool> r_changed_curves)
{
MutableSpan<float3> 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<float3> 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<bool> changed_curves)
{
const Span<float> expected_lengths_cu = self_->segment_lengths_cu_;
MutableSpan<float3> 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<CurvesSculptStrokeOperation> 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<PinchOperation>(invert_pinch);
}
} // namespace blender::ed::sculpt_paint

View File

@ -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<float> 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<float3> 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<float> point_factors_;
Vector<int64_t> 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<MLoopTri> surface_looptris_;
Span<float3> 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<Curves *>(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<eBrushFalloffShape>(brush_->falloff_shape);
surface_ob_ = curves_id_->surface;
surface_ = static_cast<Mesh *>(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<const float3 *>(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<float> 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<float> r_curve_weights)
{
const Vector<float4x4> 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<float> r_curve_weights)
{
Span<float3> 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<float> 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<float4x4> 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<float> r_curve_weights)
{
const Span<float3> 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<float> curve_weights)
{
BLI_assert(curve_weights.size() == curve_selection_.size());
MutableSpan<float3> positions_cu = curves_->positions_for_write();
threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
Vector<float> 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<float3>(
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<float3> 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<float> expected_lengths_cu = self_->segment_lengths_cu_;
MutableSpan<float3> 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<CurvesSculptStrokeOperation> new_puff_operation()
{
return std::make_unique<PuffOperation>();
}
} // namespace blender::ed::sculpt_paint

View File

@ -0,0 +1,310 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#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<SlideCurveInfo> 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<SlideInfo> 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<MLoopTri> surface_looptris_;
VArray_Span<float2> surface_uv_map_;
VArray<float> curve_factors_;
VArray<float> point_factors_;
Vector<int64_t> 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<Curves *>(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<Mesh *>(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<float2>();
}
if (stroke_extension.is_first) {
const Vector<float4x4> 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<float3> positions_cu = curves_->positions();
float4x4 projection;
ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
self_->slide_info_.append({brush_transform});
Vector<SlideCurveInfo> &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<float3> positions_cu = curves_->positions_for_write();
MutableSpan<float2> 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<SlideCurveInfo> 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<CurvesSculptStrokeOperation> new_slide_operation()
{
return std::make_unique<SlideOperation>();
}
} // namespace blender::ed::sculpt_paint

View File

@ -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<float> point_factors_;
Vector<int64_t> 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<Curves *>(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<eBrushFalloffShape>(
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<float4x4> 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<float3> 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<float2> 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<float4x4> 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<float3> 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<float3> 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<CurvesSculptStrokeOperation> new_smooth_operation()
{
return std::make_unique<SmoothOperation>();
}
} // namespace blender::ed::sculpt_paint

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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)