Curves: improve Comb brush

New supported features:
* 3D/spherical brush that samples a good position on the curves.
* Falloff.

The custom falloff curve mapping is not yet available in the ui because that
requires some more ui reorganization. This is better done when we have
a better understanding of what settings we need exactly.

Currently, the depth of the 3d brush is only sampled once per stroke, when
first pressing LMB. Sometimes it is expected that the depth of the brush can
change within a single brush. However, implementing that in a good way
is not straight forward and might need additional options. Therefore that
will be handled separately. Some experimentation results are in D14376.

Ref T96445.

Differential Revision: https://developer.blender.org/D14376
This commit is contained in:
Jacques Lucke 2022-03-23 18:19:38 +01:00
parent 5d38b13e61
commit d67f9820b8
Notes: blender-bot 2024-01-31 11:35:08 +01:00
Referenced by issue #96445, Comb brush - curve sculpting
7 changed files with 606 additions and 129 deletions

View File

@ -502,6 +502,10 @@ class _draw_tool_settings_context_mode:
header=True
)
if brush.curves_sculpt_tool == 'COMB':
layout.prop(brush, "falloff_shape", expand=True)
layout.prop(brush, "curve_preset")
if brush.curves_sculpt_tool == 'ADD':
layout.prop(brush, "use_frontface")
layout.prop(brush, "falloff_shape", expand=True)

View File

@ -322,18 +322,22 @@ double closest_to_line_v2_db(double r_close[2],
float closest_to_line_v3(float r_close[3], const float p[3], const float l1[3], const float l2[3]);
/**
* Point closest to v1 on line v2-v3 in 2D.
*
* \return A value in [0, 1] that corresponds to the position of #r_close on the line segment.
*/
void closest_to_line_segment_v2(float r_close[2],
const float p[2],
const float l1[2],
const float l2[2]);
float closest_to_line_segment_v2(float r_close[2],
const float p[2],
const float l1[2],
const float l2[2]);
/**
* Point closest to v1 on line v2-v3 in 3D.
*
* \return A value in [0, 1] that corresponds to the position of #r_close on the line segment.
*/
void closest_to_line_segment_v3(float r_close[3],
const float p[3],
const float l1[3],
const float l2[3]);
float closest_to_line_segment_v3(float r_close[3],
const float p[3],
const float l1[3],
const float l2[3]);
void closest_to_plane_normalized_v3(float r_close[3], const float plane[4], const float pt[3]);
/**
* Find the closest point on a plane.

View File

@ -294,46 +294,48 @@ float dist_to_line_segment_v2(const float p[2], const float l1[2], const float l
return sqrtf(dist_squared_to_line_segment_v2(p, l1, l2));
}
void closest_to_line_segment_v2(float r_close[2],
const float p[2],
const float l1[2],
const float l2[2])
float closest_to_line_segment_v2(float r_close[2],
const float p[2],
const float l1[2],
const float l2[2])
{
float lambda, cp[2];
lambda = closest_to_line_v2(cp, p, l1, l2);
/* flip checks for !finite case (when segment is a point) */
if (!(lambda > 0.0f)) {
if (lambda <= 0.0f) {
copy_v2_v2(r_close, l1);
return 0.0f;
}
else if (!(lambda < 1.0f)) {
if (lambda >= 1.0f) {
copy_v2_v2(r_close, l2);
return 1.0f;
}
else {
copy_v2_v2(r_close, cp);
}
copy_v2_v2(r_close, cp);
return lambda;
}
void closest_to_line_segment_v3(float r_close[3],
const float p[3],
const float l1[3],
const float l2[3])
float closest_to_line_segment_v3(float r_close[3],
const float p[3],
const float l1[3],
const float l2[3])
{
float lambda, cp[3];
lambda = closest_to_line_v3(cp, p, l1, l2);
/* flip checks for !finite case (when segment is a point) */
if (!(lambda > 0.0f)) {
if (lambda <= 0.0f) {
copy_v3_v3(r_close, l1);
return 0.0f;
}
else if (!(lambda < 1.0f)) {
if (lambda >= 1.0f) {
copy_v3_v3(r_close, l2);
return 1.0f;
}
else {
copy_v3_v3(r_close, cp);
}
copy_v3_v3(r_close, cp);
return lambda;
}
void closest_to_plane_v3(float r_close[3], const float plane[4], const float pt[3])

View File

@ -27,6 +27,7 @@ set(INC
)
set(SRC
curves_sculpt_3d_brush.cc
curves_sculpt_add.cc
curves_sculpt_comb.cc
curves_sculpt_delete.cc

View File

@ -0,0 +1,232 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#include "curves_sculpt_intern.hh"
#include "BKE_bvhutils.h"
#include "BKE_context.h"
#include "BKE_curves.hh"
#include "ED_view3d.h"
#include "UI_interface.h"
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_task.hh"
/**
* 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 {
struct BrushPositionCandidate {
/** 3D position of the brush. */
float3 position_cu;
/** Squared distance from the mouse position in screen space. */
float distance_sq_re = FLT_MAX;
/** Measure for how far away the candidate is from the camera. */
float depth_sq_cu = FLT_MAX;
};
/**
* Determine the 3D position of a brush based on curve segments under a screen position.
*/
static std::optional<float3> find_curves_brush_position(const CurvesGeometry &curves,
const float3 &ray_start_cu,
const float3 &ray_end_cu,
const float brush_radius_re,
ARegion &region,
RegionView3D &rv3d,
Object &object)
{
/* This value might have to be adjusted based on user feedback. */
const float brush_inner_radius_re = std::min<float>(brush_radius_re, (float)UI_UNIT_X / 3.0f);
const float brush_inner_radius_sq_re = pow2f(brush_inner_radius_re);
float4x4 projection;
ED_view3d_ob_project_mat_get(&rv3d, &object, projection.values);
float2 brush_pos_re;
ED_view3d_project_float_v2_m4(&region, ray_start_cu, brush_pos_re, projection.values);
const float max_depth_sq_cu = math::distance_squared(ray_start_cu, ray_end_cu);
/* Contains the logic that checks if `b` is a better candidate than `a`. */
auto is_better_candidate = [&](const BrushPositionCandidate &a,
const BrushPositionCandidate &b) {
if (b.distance_sq_re <= brush_inner_radius_sq_re) {
if (a.distance_sq_re > brush_inner_radius_sq_re) {
/* New candidate is in inner radius while old one is not. */
return true;
}
else if (b.depth_sq_cu < a.depth_sq_cu) {
/* Both candidates are in inner radius, but new one is closer to the camera. */
return true;
}
}
else if (b.distance_sq_re < a.distance_sq_re) {
/* Both candidates are outside of inner radius, but new on is closer to the brush center. */
return true;
}
return false;
};
auto update_if_better = [&](BrushPositionCandidate &a, const BrushPositionCandidate &b) {
if (is_better_candidate(a, b)) {
a = b;
}
};
const Span<float3> positions = curves.positions();
BrushPositionCandidate best_candidate = threading::parallel_reduce(
curves.curves_range(),
128,
BrushPositionCandidate(),
[&](IndexRange curves_range, const BrushPositionCandidate &init) {
BrushPositionCandidate best_candidate = init;
for (const int curve_i : curves_range) {
const IndexRange points = curves.range_for_curve(curve_i);
const int tot_segments = points.size() - 1;
for (const int segment_i : IndexRange(tot_segments)) {
const float3 &p1_cu = positions[points[segment_i]];
const float3 &p2_cu = positions[points[segment_i] + 1];
float2 p1_re, p2_re;
ED_view3d_project_float_v2_m4(&region, p1_cu, p1_re, projection.values);
ED_view3d_project_float_v2_m4(&region, p2_cu, p2_re, projection.values);
float2 closest_re;
const float lambda = closest_to_line_segment_v2(
closest_re, brush_pos_re, p1_re, p2_re);
const float3 closest_cu = math::interpolate(p1_cu, p2_cu, lambda);
const float depth_sq_cu = math::distance_squared(ray_start_cu, closest_cu);
if (depth_sq_cu > max_depth_sq_cu) {
continue;
}
const float distance_sq_re = math::distance_squared(brush_pos_re, closest_re);
BrushPositionCandidate candidate;
candidate.position_cu = closest_cu;
candidate.depth_sq_cu = depth_sq_cu;
candidate.distance_sq_re = distance_sq_re;
update_if_better(best_candidate, candidate);
}
}
return best_candidate;
},
[&](const BrushPositionCandidate &a, const BrushPositionCandidate &b) {
return is_better_candidate(a, b) ? b : a;
});
if (best_candidate.distance_sq_re == FLT_MAX) {
/* Nothing found. */
return std::nullopt;
}
return best_candidate.position_cu;
}
std::optional<CurvesBrush3D> sample_curves_3d_brush(bContext &C,
Object &curves_object,
const float2 &brush_pos_re,
const float brush_radius_re)
{
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(&C);
ARegion *region = CTX_wm_region(&C);
View3D *v3d = CTX_wm_view3d(&C);
RegionView3D *rv3d = CTX_wm_region_view3d(&C);
Curves &curves_id = *static_cast<Curves *>(curves_object.data);
CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
Object *surface_object = curves_id.surface;
float3 center_ray_start_wo, center_ray_end_wo;
ED_view3d_win_to_segment_clipped(
depsgraph, region, v3d, brush_pos_re, center_ray_start_wo, center_ray_end_wo, true);
/* Shorten ray when the surface object is hit. */
if (surface_object != nullptr) {
const float4x4 surface_to_world_mat = surface_object->obmat;
const float4x4 world_to_surface_mat = surface_to_world_mat.inverted();
Mesh &surface = *static_cast<Mesh *>(surface_object->data);
BVHTreeFromMesh surface_bvh;
BKE_bvhtree_from_mesh_get(&surface_bvh, &surface, BVHTREE_FROM_LOOPTRI, 2);
BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh); });
const float3 center_ray_start_su = world_to_surface_mat * center_ray_start_wo;
float3 center_ray_end_su = world_to_surface_mat * center_ray_end_wo;
const float3 center_ray_direction_su = math::normalize(center_ray_end_su -
center_ray_start_su);
BVHTreeRayHit center_ray_hit;
center_ray_hit.dist = FLT_MAX;
center_ray_hit.index = -1;
BLI_bvhtree_ray_cast(surface_bvh.tree,
center_ray_start_su,
center_ray_direction_su,
0.0f,
&center_ray_hit,
surface_bvh.raycast_callback,
&surface_bvh);
if (center_ray_hit.index >= 0) {
const float3 hit_position_su = center_ray_hit.co;
if (math::distance(center_ray_start_su, center_ray_end_su) >
math::distance(center_ray_start_su, hit_position_su)) {
center_ray_end_su = hit_position_su;
center_ray_end_wo = surface_to_world_mat * center_ray_end_su;
}
}
}
const float4x4 curves_to_world_mat = curves_object.obmat;
const float4x4 world_to_curves_mat = curves_to_world_mat.inverted();
const float3 center_ray_start_cu = world_to_curves_mat * center_ray_start_wo;
const float3 center_ray_end_cu = world_to_curves_mat * center_ray_end_wo;
const std::optional<float3> brush_position_optional_cu = find_curves_brush_position(
curves,
center_ray_start_cu,
center_ray_end_cu,
brush_radius_re,
*region,
*rv3d,
curves_object);
if (!brush_position_optional_cu.has_value()) {
/* Nothing found. */
return std::nullopt;
}
const float3 brush_position_cu = *brush_position_optional_cu;
/* Determine the 3D brush radius. */
float3 radius_ray_start_wo, radius_ray_end_wo;
ED_view3d_win_to_segment_clipped(depsgraph,
region,
v3d,
brush_pos_re + float2(brush_radius_re, 0.0f),
radius_ray_start_wo,
radius_ray_end_wo,
true);
const float3 radius_ray_start_cu = world_to_curves_mat * radius_ray_start_wo;
const float3 radius_ray_end_cu = world_to_curves_mat * radius_ray_end_wo;
CurvesBrush3D brush_3d;
brush_3d.position_cu = brush_position_cu;
brush_3d.radius_cu = dist_to_line_v3(brush_position_cu, radius_ray_start_cu, radius_ray_end_cu);
return brush_3d;
}
} // namespace blender::ed::sculpt_paint

View File

@ -35,129 +35,340 @@
#include "ED_screen.h"
#include "ED_view3d.h"
#include "UI_interface.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 {
using blender::bke::CurvesGeometry;
using threading::EnumerableThreadSpecific;
/**
* Moves individual points under the brush and does a length preservation step afterwards.
*/
class CombOperation : public CurvesSculptStrokeOperation {
private:
float2 last_mouse_position_;
/** Last mouse position. */
float2 brush_pos_last_re_;
/** 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 CombOperationExecutor;
public:
void on_stroke_extended(bContext *C, const StrokeExtension &stroke_extension) override
{
BLI_SCOPED_DEFER([&]() { last_mouse_position_ = stroke_extension.mouse_position; });
void on_stroke_extended(bContext *C, const StrokeExtension &stroke_extension) override;
};
if (stroke_extension.is_first) {
return;
/**
* 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 CombOperationExecutor {
CombOperation *self_ = nullptr;
bContext *C_ = nullptr;
Depsgraph *depsgraph_ = nullptr;
Scene *scene_ = nullptr;
Object *object_ = nullptr;
ARegion *region_ = nullptr;
View3D *v3d_ = nullptr;
RegionView3D *rv3d_ = nullptr;
CurvesSculpt *curves_sculpt_ = nullptr;
Brush *brush_ = nullptr;
float brush_radius_re_;
float brush_strength_;
eBrushFalloffShape falloff_shape_;
Curves *curves_id_ = nullptr;
CurvesGeometry *curves_ = nullptr;
const Object *surface_ob_ = nullptr;
const Mesh *surface_ = nullptr;
Span<MLoopTri> surface_looptris_;
float2 brush_pos_prev_re_;
float2 brush_pos_re_;
float2 brush_pos_diff_re_;
float brush_pos_diff_length_re_;
float4x4 curves_to_world_mat_;
float4x4 world_to_curves_mat_;
float4x4 surface_to_world_mat_;
float4x4 world_to_surface_mat_;
BVHTreeFromMesh surface_bvh_;
void execute(CombOperation &self, bContext *C, const StrokeExtension &stroke_extension)
{
self_ = &self;
BLI_SCOPED_DEFER([&]() { self_->brush_pos_last_re_ = stroke_extension.mouse_position; });
C_ = C;
depsgraph_ = CTX_data_depsgraph_pointer(C);
scene_ = CTX_data_scene(C);
object_ = CTX_data_active_object(C);
region_ = CTX_wm_region(C);
v3d_ = CTX_wm_view3d(C);
rv3d_ = CTX_wm_region_view3d(C);
curves_sculpt_ = scene_->toolsettings->curves_sculpt;
brush_ = BKE_paint_brush(&curves_sculpt_->paint);
brush_radius_re_ = BKE_brush_size_get(scene_, brush_);
brush_strength_ = BKE_brush_alpha_get(scene_, brush_);
curves_to_world_mat_ = object_->obmat;
world_to_curves_mat_ = curves_to_world_mat_.inverted();
falloff_shape_ = static_cast<eBrushFalloffShape>(brush_->falloff_shape);
curves_id_ = static_cast<Curves *>(object_->data);
curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
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_;
brush_pos_diff_length_re_ = math::length(brush_pos_diff_re_);
surface_ob_ = curves_id_->surface;
if (surface_ob_ != nullptr) {
surface_ = static_cast<const Mesh *>(surface_ob_->data);
surface_looptris_ = {BKE_mesh_runtime_looptri_ensure(surface_),
BKE_mesh_runtime_looptri_len(surface_)};
surface_to_world_mat_ = surface_ob_->obmat;
world_to_surface_mat_ = surface_to_world_mat_.inverted();
BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2);
}
Scene &scene = *CTX_data_scene(C);
Object &object = *CTX_data_active_object(C);
ARegion *region = CTX_wm_region(C);
View3D *v3d = CTX_wm_view3d(C);
RegionView3D *rv3d = CTX_wm_region_view3d(C);
CurvesSculpt &curves_sculpt = *scene.toolsettings->curves_sculpt;
Brush &brush = *BKE_paint_brush(&curves_sculpt.paint);
const float brush_radius = BKE_brush_size_get(&scene, &brush);
const float brush_strength = BKE_brush_alpha_get(&scene, &brush);
const float4x4 ob_mat = object.obmat;
const float4x4 ob_imat = ob_mat.inverted();
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d, &object, projection.values);
Curves &curves_id = *static_cast<Curves *>(object.data);
CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
MutableSpan<float3> positions = curves.positions();
const float2 mouse_prev = last_mouse_position_;
const float2 mouse_cur = stroke_extension.mouse_position;
const float2 mouse_diff = mouse_cur - mouse_prev;
const float mouse_diff_len = math::length(mouse_diff);
threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange curves_range) {
for (const int curve_i : curves_range) {
const IndexRange curve_points = curves.range_for_curve(curve_i);
/* Compute lengths of the segments. Those are used to make sure that the lengths don't
* change. */
Vector<float, 16> segment_lengths(curve_points.size() - 1);
for (const int segment_i : IndexRange(curve_points.size() - 1)) {
const float3 &p1 = positions[curve_points[segment_i]];
const float3 &p2 = positions[curve_points[segment_i] + 1];
const float length = math::distance(p1, p2);
segment_lengths[segment_i] = length;
}
bool curve_changed = false;
for (const int point_i : curve_points.drop_front(1)) {
const float3 old_position = positions[point_i];
/* Find the position of the point in screen space. */
float2 old_position_screen;
ED_view3d_project_float_v2_m4(
region, old_position, old_position_screen, projection.values);
/* Project the point onto the line drawn by the mouse. Note, it's projected on the
* infinite line, not only on the line segment. */
float2 old_position_screen_proj;
/* t is 0 when the point is closest to the previous mouse position and 1 when it's
* closest to the current mouse position. */
const float t = closest_to_line_v2(
old_position_screen_proj, old_position_screen, mouse_prev, mouse_cur);
/* Compute the distance to the mouse line segment. */
const float2 old_position_screen_proj_segment = mouse_prev +
std::clamp(t, 0.0f, 1.0f) * mouse_diff;
const float distance_screen = math::distance(old_position_screen,
old_position_screen_proj_segment);
if (distance_screen > brush_radius) {
/* Ignore the point because it's too far away. */
continue;
}
/* Compute a falloff that is based on how far along the point along the last stroke
* segment is. */
const float t_overshoot = brush_radius / mouse_diff_len;
const float t_falloff = 1.0f - std::max(t, 0.0f) / (1.0f + t_overshoot);
/* A falloff that is based on how far away the point is from the stroke. */
const float radius_falloff = pow2f(1.0f - distance_screen / brush_radius);
/* Combine the different falloffs and brush strength. */
const float weight = brush_strength * t_falloff * radius_falloff;
/* Offset the old point position in screen space and transform it back into 3D space. */
const float2 new_position_screen = old_position_screen + mouse_diff * weight;
float3 new_position;
ED_view3d_win_to_3d(
v3d, region, ob_mat * old_position, new_position_screen, new_position);
new_position = ob_imat * new_position;
positions[point_i] = new_position;
curve_changed = true;
}
if (!curve_changed) {
continue;
}
/* Ensure that the length of each segment stays the same. */
for (const int segment_i : IndexRange(curve_points.size() - 1)) {
const float3 &p1 = positions[curve_points[segment_i]];
float3 &p2 = positions[curve_points[segment_i] + 1];
const float3 direction = math::normalize(p2 - p1);
const float desired_length = segment_lengths[segment_i];
p2 = p1 + direction * desired_length;
}
BLI_SCOPED_DEFER([&]() {
if (surface_ob_ != nullptr) {
free_bvhtree_from_mesh(&surface_bvh_);
}
});
curves.tag_positions_changed();
DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY);
ED_region_tag_redraw(region);
if (stroke_extension.is_first) {
if (falloff_shape_ == PAINT_FALLOFF_SHAPE_SPHERE) {
this->initialize_spherical_brush_reference_point();
}
this->initialize_segment_lengths();
/* Combing does nothing when there is no mouse movement, so return directly. */
return;
}
EnumerableThreadSpecific<Vector<int>> changed_curves;
if (falloff_shape_ == PAINT_FALLOFF_SHAPE_TUBE) {
this->comb_projected(changed_curves);
}
else if (falloff_shape_ == PAINT_FALLOFF_SHAPE_SPHERE) {
this->comb_spherical(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);
ED_region_tag_redraw(region_);
}
/**
* Do combing in screen space.
*/
void comb_projected(EnumerableThreadSpecific<Vector<int>> &r_changed_curves)
{
MutableSpan<float3> positions_cu = curves_->positions();
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
const float brush_radius_sq_re = pow2f(brush_radius_re_);
threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange curves_range) {
Vector<int> &local_changed_curves = r_changed_curves.local();
for (const int curve_i : curves_range) {
bool curve_changed = false;
const IndexRange points = curves_->range_for_curve(curve_i);
for (const int point_i : points.drop_front(1)) {
const float3 old_pos_cu = positions_cu[point_i];
/* Find the position of the point in screen space. */
float2 old_pos_re;
ED_view3d_project_float_v2_m4(region_, old_pos_cu, old_pos_re, projection.values);
const float distance_to_brush_sq_re = dist_squared_to_line_segment_v2(
old_pos_re, brush_pos_prev_re_, brush_pos_re_);
if (distance_to_brush_sq_re > brush_radius_sq_re) {
/* Ignore the point because it's too far away. */
continue;
}
const float distance_to_brush_re = std::sqrt(distance_to_brush_sq_re);
/* A falloff that is based on how far away the point is from the stroke. */
const float radius_falloff = BKE_brush_curve_strength(
brush_, distance_to_brush_re, brush_radius_re_);
/* Combine the falloff and brush strength. */
const float weight = brush_strength_ * radius_falloff;
/* Offset the old point position in screen space and transform it back into 3D space. */
const float2 new_position_re = old_pos_re + brush_pos_diff_re_ * weight;
float3 new_position_wo;
ED_view3d_win_to_3d(
v3d_, region_, curves_to_world_mat_ * old_pos_cu, new_position_re, new_position_wo);
const float3 new_position_cu = world_to_curves_mat_ * new_position_wo;
positions_cu[point_i] = new_position_cu;
curve_changed = true;
}
if (curve_changed) {
local_changed_curves.append(curve_i);
}
}
});
}
/**
* Do combing in 3D space.
*/
void comb_spherical(EnumerableThreadSpecific<Vector<int>> &r_changed_curves)
{
MutableSpan<float3> positions_cu = curves_->positions();
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
float3 brush_start_wo, brush_end_wo;
ED_view3d_win_to_3d(v3d_,
region_,
curves_to_world_mat_ * self_->brush_3d_.position_cu,
brush_pos_prev_re_,
brush_start_wo);
ED_view3d_win_to_3d(v3d_,
region_,
curves_to_world_mat_ * self_->brush_3d_.position_cu,
brush_pos_re_,
brush_end_wo);
const float3 brush_start_cu = world_to_curves_mat_ * brush_start_wo;
const float3 brush_end_cu = world_to_curves_mat_ * brush_end_wo;
const float3 brush_diff_cu = brush_end_cu - brush_start_cu;
const float brush_radius_cu = self_->brush_3d_.radius_cu;
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange curves_range) {
Vector<int> &local_changed_curves = r_changed_curves.local();
for (const int curve_i : curves_range) {
bool curve_changed = false;
const IndexRange points = curves_->range_for_curve(curve_i);
for (const int point_i : points.drop_front(1)) {
const float3 pos_old_cu = positions_cu[point_i];
/* Compute distance to the brush. */
const float distance_to_brush_sq_cu = dist_squared_to_line_segment_v3(
pos_old_cu, brush_start_cu, brush_end_cu);
if (distance_to_brush_sq_cu > brush_radius_sq_cu) {
/* Ignore the point because it's too far away. */
continue;
}
const float distance_to_brush_cu = std::sqrt(distance_to_brush_sq_cu);
/* A falloff that is based on how far away the point is from the stroke. */
const float radius_falloff = BKE_brush_curve_strength(
brush_, distance_to_brush_cu, brush_radius_cu);
/* Combine the falloff and brush strength. */
const float weight = brush_strength_ * radius_falloff;
/* Update the point position. */
positions_cu[point_i] = pos_old_cu + weight * brush_diff_cu;
curve_changed = true;
}
if (curve_changed) {
local_changed_curves.append(curve_i);
}
}
});
}
/**
* Sample depth under mouse by looking at curves and the surface.
*/
void initialize_spherical_brush_reference_point()
{
std::optional<CurvesBrush3D> brush_3d = sample_curves_3d_brush(
*C_, *object_, brush_pos_re_, brush_radius_re_);
if (brush_3d.has_value()) {
self_->brush_3d_ = *brush_3d;
}
}
/**
* Remember the initial length of all curve segments. This allows restoring the length after
* combing.
*/
void initialize_segment_lengths()
{
const Span<float3> positions_cu = curves_->positions();
self_->segment_lengths_cu_.reinitialize(curves_->points_size());
threading::parallel_for(curves_->curves_range(), 128, [&](const IndexRange range) {
for (const int curve_i : range) {
const IndexRange points = curves_->range_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;
}
}
});
}
/**
* Restore previously stored length for each segment in the changed curves.
*/
void restore_segment_lengths(EnumerableThreadSpecific<Vector<int>> &changed_curves)
{
const Span<float> expected_lengths_cu = self_->segment_lengths_cu_;
MutableSpan<float3> positions_cu = curves_->positions();
threading::parallel_for_each(changed_curves, [&](const Vector<int> &changed_curves) {
threading::parallel_for(changed_curves.index_range(), 256, [&](const IndexRange range) {
for (const int curve_i : changed_curves.as_span().slice(range)) {
const IndexRange points = curves_->range_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 CombOperation::on_stroke_extended(bContext *C, const StrokeExtension &stroke_extension)
{
CombOperationExecutor executor;
executor.execute(*this, C, stroke_extension);
}
std::unique_ptr<CurvesSculptStrokeOperation> new_comb_operation()
{
return std::make_unique<CombOperation>();

View File

@ -2,12 +2,22 @@
#pragma once
#include <optional>
#include "curves_sculpt_intern.h"
#include "BLI_math_vector.hh"
#include "BKE_curves.hh"
struct ARegion;
struct RegionView3D;
struct Object;
namespace blender::ed::sculpt_paint {
using bke::CurvesGeometry;
struct StrokeExtension {
bool is_first;
float2 mouse_position;
@ -27,4 +37,17 @@ std::unique_ptr<CurvesSculptStrokeOperation> new_comb_operation();
std::unique_ptr<CurvesSculptStrokeOperation> new_delete_operation();
std::unique_ptr<CurvesSculptStrokeOperation> new_snake_hook_operation();
struct CurvesBrush3D {
float3 position_cu;
float radius_cu;
};
/**
* Find 3d brush position based on cursor position for curves sculpting.
*/
std::optional<CurvesBrush3D> sample_curves_3d_brush(bContext &C,
Object &curves_object,
const float2 &brush_pos_re,
float brush_radius_re);
} // namespace blender::ed::sculpt_paint