Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacques Lucke <jacques@blender.org>2022-05-04 10:51:32 +0300
committerJacques Lucke <jacques@blender.org>2022-05-04 10:51:32 +0300
commit7dc94155f62025248e8c111efa8bf0561b3bb492 (patch)
treea940d5e0588ef93afc336cbda8a6c44bfe019e6c
parentaa21087d5655deccb6e5ff05c13b30ab9ecec69c (diff)
Curves: support symmetry in curves sculpting brushes
This adds support for X/Y/Z symmetry for all brushes in curves sculpt mode. In theory this can be extended to support radial symmetry, but that's not part of this patch. It works by essentially applying a brush stroke multiple with different transforms. This is similiar to how symmetry works in mesh sculpt mode, but is quite different from how it worked in the old hair system (there it tried to find matching hair strands on both sides of the surface; if none was found, symmetry did not work). Differential Revision: https://developer.blender.org/D14795
-rw-r--r--release/scripts/startup/bl_ui/space_view3d.py5
-rw-r--r--release/scripts/startup/bl_ui/space_view3d_toolbar.py32
-rw-r--r--source/blender/editors/sculpt_paint/CMakeLists.txt2
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_add.cc73
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_brush.cc (renamed from source/blender/editors/sculpt_paint/curves_sculpt_3d_brush.cc)28
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_comb.cc47
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_delete.cc46
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_grow_shrink.cc166
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_intern.hh2
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_snake_hook.cc41
-rw-r--r--source/blender/makesdna/DNA_curves_types.h18
-rw-r--r--source/blender/makesrna/intern/rna_curves.c16
12 files changed, 356 insertions, 120 deletions
diff --git a/release/scripts/startup/bl_ui/space_view3d.py b/release/scripts/startup/bl_ui/space_view3d.py
index 2f2c146b8cf..9756d9ab3a1 100644
--- a/release/scripts/startup/bl_ui/space_view3d.py
+++ b/release/scripts/startup/bl_ui/space_view3d.py
@@ -151,6 +151,11 @@ class VIEW3D_HT_tool_header(Header):
row.popover(panel="VIEW3D_PT_sculpt_symmetry_for_topbar", text="")
elif mode_string == 'PAINT_VERTEX':
row.popover(panel="VIEW3D_PT_tools_vertexpaint_symmetry_for_topbar", text="")
+ elif mode_string == 'SCULPT_CURVES':
+ _row, sub = row_for_mirror()
+ sub.prop(context.object.data, "use_mirror_x", text="X", toggle=True)
+ sub.prop(context.object.data, "use_mirror_y", text="Y", toggle=True)
+ sub.prop(context.object.data, "use_mirror_z", text="Z", toggle=True)
# Expand panels from the side-bar as popovers.
popover_kw = {"space_type": 'VIEW_3D', "region_type": 'UI', "category": "Tool"}
diff --git a/release/scripts/startup/bl_ui/space_view3d_toolbar.py b/release/scripts/startup/bl_ui/space_view3d_toolbar.py
index 8c3215abc36..ba02bfb4082 100644
--- a/release/scripts/startup/bl_ui/space_view3d_toolbar.py
+++ b/release/scripts/startup/bl_ui/space_view3d_toolbar.py
@@ -1048,6 +1048,35 @@ class VIEW3D_PT_sculpt_symmetry_for_topbar(Panel):
draw = VIEW3D_PT_sculpt_symmetry.draw
+class VIEW3D_PT_curves_sculpt_symmetry(Panel, View3DPaintPanel):
+ bl_context = ".curves_sculpt" # dot on purpose (access from topbar)
+ bl_label = "Symmetry"
+ bl_options = {'DEFAULT_CLOSED'}
+
+ @classmethod
+ def poll(cls, context):
+ return context.object and context.object.type == 'CURVES'
+
+ def draw(self, context):
+ layout = self.layout
+ layout.use_property_split = True
+ layout.use_property_decorate = False
+
+ curves = context.object.data
+
+ row = layout.row(align=True, heading="Mirror")
+ row.prop(curves, "use_mirror_x", text="X", toggle=True)
+ row.prop(curves, "use_mirror_y", text="Y", toggle=True)
+ row.prop(curves, "use_mirror_z", text="Z", toggle=True)
+
+class VIEW3D_PT_curves_sculpt_symmetry_for_topbar(Panel):
+ bl_space_type = 'TOPBAR'
+ bl_region_type = 'HEADER'
+ bl_label = "Symmetry"
+
+ draw = VIEW3D_PT_curves_sculpt_symmetry.draw
+
+
# ********** default tools for weight-paint ****************
@@ -2351,6 +2380,9 @@ classes = (
VIEW3D_PT_sculpt_options,
VIEW3D_PT_sculpt_options_gravity,
+ VIEW3D_PT_curves_sculpt_symmetry,
+ VIEW3D_PT_curves_sculpt_symmetry_for_topbar,
+
VIEW3D_PT_tools_weightpaint_symmetry,
VIEW3D_PT_tools_weightpaint_symmetry_for_topbar,
VIEW3D_PT_tools_weightpaint_options,
diff --git a/source/blender/editors/sculpt_paint/CMakeLists.txt b/source/blender/editors/sculpt_paint/CMakeLists.txt
index 08eed52f440..d3bf28798c4 100644
--- a/source/blender/editors/sculpt_paint/CMakeLists.txt
+++ b/source/blender/editors/sculpt_paint/CMakeLists.txt
@@ -27,8 +27,8 @@ set(INC
)
set(SRC
- curves_sculpt_3d_brush.cc
curves_sculpt_add.cc
+ curves_sculpt_brush.cc
curves_sculpt_comb.cc
curves_sculpt_delete.cc
curves_sculpt_grow_shrink.cc
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_add.cc b/source/blender/editors/sculpt_paint/curves_sculpt_add.cc
index 0d399419ad8..99d725fb4cb 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_add.cc
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_add.cc
@@ -194,13 +194,13 @@ struct AddOperationExecutor {
/* Sample points on the surface using one of multiple strategies. */
AddedPoints added_points;
if (add_amount_ == 1) {
- this->sample_in_center(added_points);
+ this->sample_in_center_with_symmetry(added_points);
}
else if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) {
- this->sample_projected(rng, added_points);
+ this->sample_projected_with_symmetry(rng, added_points);
}
else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
- this->sample_spherical(rng, added_points);
+ this->sample_spherical_with_symmetry(rng, added_points);
}
else {
BLI_assert_unreachable();
@@ -241,13 +241,27 @@ struct AddOperationExecutor {
/**
* Sample a single point exactly at the mouse position.
*/
- void sample_in_center(AddedPoints &r_added_points)
+ void sample_in_center_with_symmetry(AddedPoints &r_added_points)
{
float3 ray_start_wo, ray_end_wo;
ED_view3d_win_to_segment_clipped(
depsgraph_, region_, v3d_, brush_pos_re_, ray_start_wo, ray_end_wo, true);
const float3 ray_start_su = world_to_surface_mat_ * ray_start_wo;
const float3 ray_end_su = world_to_surface_mat_ * ray_end_wo;
+
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->sample_in_center(
+ r_added_points, brush_transform * ray_start_su, brush_transform * ray_end_su);
+ }
+ }
+
+ void sample_in_center(AddedPoints &r_added_points,
+ const float3 &ray_start_su,
+ const float3 &ray_end_su)
+ {
const float3 ray_direction_su = math::normalize(ray_end_su - ray_start_su);
BVHTreeRayHit ray_hit;
@@ -280,11 +294,23 @@ struct AddOperationExecutor {
/**
* Sample points by shooting rays within the brush radius in the 3D view.
*/
- void sample_projected(RandomNumberGenerator &rng, AddedPoints &r_added_points)
+ void sample_projected_with_symmetry(RandomNumberGenerator &rng, AddedPoints &r_added_points)
{
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->sample_projected(rng, r_added_points, brush_transform);
+ }
+ }
+
+ void sample_projected(RandomNumberGenerator &rng,
+ AddedPoints &r_added_points,
+ const float4x4 &brush_transform)
+ {
+ const int old_amount = r_added_points.bary_coords.size();
const int max_iterations = std::max(100'000, add_amount_ * 10);
int current_iteration = 0;
- while (r_added_points.bary_coords.size() < add_amount_) {
+ while (r_added_points.bary_coords.size() < old_amount + add_amount_) {
if (current_iteration++ >= max_iterations) {
break;
}
@@ -296,8 +322,8 @@ struct AddOperationExecutor {
float3 ray_start_wo, ray_end_wo;
ED_view3d_win_to_segment_clipped(
depsgraph_, region_, v3d_, pos_re, ray_start_wo, ray_end_wo, true);
- const float3 ray_start_su = world_to_surface_mat_ * ray_start_wo;
- const float3 ray_end_su = world_to_surface_mat_ * ray_end_wo;
+ const float3 ray_start_su = brush_transform * (world_to_surface_mat_ * ray_start_wo);
+ const float3 ray_end_su = brush_transform * (world_to_surface_mat_ * ray_end_wo);
const float3 ray_direction_su = math::normalize(ray_end_su - ray_start_su);
BVHTreeRayHit ray_hit;
@@ -339,7 +365,7 @@ struct AddOperationExecutor {
/**
* Sample points in a 3D sphere around the surface position that the mouse hovers over.
*/
- void sample_spherical(RandomNumberGenerator &rng, AddedPoints &r_added_points)
+ void sample_spherical_with_symmetry(RandomNumberGenerator &rng, AddedPoints &r_added_points)
{
/* Find ray that starts in the center of the brush. */
float3 brush_ray_start_wo, brush_ray_end_wo;
@@ -347,7 +373,6 @@ struct AddOperationExecutor {
depsgraph_, region_, v3d_, brush_pos_re_, brush_ray_start_wo, brush_ray_end_wo, true);
const float3 brush_ray_start_su = world_to_surface_mat_ * brush_ray_start_wo;
const float3 brush_ray_end_su = world_to_surface_mat_ * brush_ray_end_wo;
- const float3 brush_ray_direction_su = math::normalize(brush_ray_end_su - brush_ray_start_su);
/* Find ray that starts on the boundary of the brush. That is used to compute the brush radius
* in 3D. */
@@ -362,6 +387,27 @@ struct AddOperationExecutor {
const float3 brush_radius_ray_start_su = world_to_surface_mat_ * brush_radius_ray_start_wo;
const float3 brush_radius_ray_end_su = world_to_surface_mat_ * brush_radius_ray_end_wo;
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->sample_spherical(rng,
+ r_added_points,
+ brush_transform * brush_ray_start_su,
+ brush_transform * brush_ray_end_su,
+ brush_transform * brush_radius_ray_start_su,
+ brush_transform * brush_radius_ray_end_su);
+ }
+ }
+
+ void sample_spherical(RandomNumberGenerator &rng,
+ AddedPoints &r_added_points,
+ const float3 &brush_ray_start_su,
+ const float3 &brush_ray_end_su,
+ const float3 &brush_radius_ray_start_su,
+ const float3 &brush_radius_ray_end_su)
+ {
+ const float3 brush_ray_direction_su = math::normalize(brush_ray_end_su - brush_ray_start_su);
+
BVHTreeRayHit ray_hit;
ray_hit.dist = FLT_MAX;
ray_hit.index = -1;
@@ -426,7 +472,8 @@ struct AddOperationExecutor {
const int max_iterations = 5;
int current_iteration = 0;
- while (r_added_points.bary_coords.size() < add_amount_) {
+ const int old_amount = r_added_points.bary_coords.size();
+ while (r_added_points.bary_coords.size() < old_amount + add_amount_) {
if (current_iteration++ >= max_iterations) {
break;
}
@@ -506,8 +553,8 @@ struct AddOperationExecutor {
}
/* Remove samples when there are too many. */
- while (r_added_points.bary_coords.size() > add_amount_) {
- const int index_to_remove = rng.get_int32(r_added_points.bary_coords.size());
+ while (r_added_points.bary_coords.size() > old_amount + add_amount_) {
+ const int index_to_remove = rng.get_int32(add_amount_) + old_amount;
r_added_points.bary_coords.remove_and_reorder(index_to_remove);
r_added_points.looptri_indices.remove_and_reorder(index_to_remove);
r_added_points.positions_cu.remove_and_reorder(index_to_remove);
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_3d_brush.cc b/source/blender/editors/sculpt_paint/curves_sculpt_brush.cc
index 3d2d4f8527e..89470772e1c 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_3d_brush.cc
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_brush.cc
@@ -232,4 +232,32 @@ std::optional<CurvesBrush3D> sample_curves_3d_brush(bContext &C,
return brush_3d;
}
+Vector<float4x4> get_symmetry_brush_transforms(const eCurvesSymmetryType symmetry)
+{
+ Vector<float4x4> matrices;
+
+ auto symmetry_to_factors = [&](const eCurvesSymmetryType type) -> Span<float> {
+ if (symmetry & type) {
+ static std::array<float, 2> values = {1.0f, -1.0f};
+ return values;
+ }
+ static std::array<float, 1> values = {1.0f};
+ return values;
+ };
+
+ for (const float x : symmetry_to_factors(CURVES_SYMMETRY_X)) {
+ for (const float y : symmetry_to_factors(CURVES_SYMMETRY_Y)) {
+ for (const float z : symmetry_to_factors(CURVES_SYMMETRY_Z)) {
+ float4x4 matrix = float4x4::identity();
+ matrix.values[0][0] = x;
+ matrix.values[1][1] = y;
+ matrix.values[2][2] = z;
+ matrices.append(matrix);
+ }
+ }
+ }
+
+ return matrices;
+}
+
} // namespace blender::ed::sculpt_paint
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_comb.cc b/source/blender/editors/sculpt_paint/curves_sculpt_comb.cc
index 232d632aa3f..28258a26f74 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_comb.cc
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_comb.cc
@@ -173,10 +173,10 @@ struct CombOperationExecutor {
EnumerableThreadSpecific<Vector<int>> changed_curves;
if (falloff_shape_ == PAINT_FALLOFF_SHAPE_TUBE) {
- this->comb_projected(changed_curves);
+ this->comb_projected_with_symmetry(changed_curves);
}
else if (falloff_shape_ == PAINT_FALLOFF_SHAPE_SPHERE) {
- this->comb_spherical(changed_curves);
+ this->comb_spherical_with_symmetry(changed_curves);
}
else {
BLI_assert_unreachable();
@@ -192,8 +192,20 @@ struct CombOperationExecutor {
/**
* Do combing in screen space.
*/
- void comb_projected(EnumerableThreadSpecific<Vector<int>> &r_changed_curves)
+ void comb_projected_with_symmetry(EnumerableThreadSpecific<Vector<int>> &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->comb_projected(r_changed_curves, brush_transform);
+ }
+ }
+
+ void comb_projected(EnumerableThreadSpecific<Vector<int>> &r_changed_curves,
+ const float4x4 &brush_transform)
+ {
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+
MutableSpan<float3> positions_cu = curves_->positions_for_write();
float4x4 projection;
@@ -207,7 +219,7 @@ struct CombOperationExecutor {
bool curve_changed = false;
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 float3 old_pos_cu = brush_transform_inv * positions_cu[point_i];
/* Find the position of the point in screen space. */
float2 old_pos_re;
@@ -232,7 +244,8 @@ struct CombOperationExecutor {
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;
+ const float3 new_position_cu = brush_transform *
+ (world_to_curves_mat_ * new_position_wo);
positions_cu[point_i] = new_position_cu;
curve_changed = true;
@@ -247,10 +260,8 @@ struct CombOperationExecutor {
/**
* Do combing in 3D space.
*/
- void comb_spherical(EnumerableThreadSpecific<Vector<int>> &r_changed_curves)
+ void comb_spherical_with_symmetry(EnumerableThreadSpecific<Vector<int>> &r_changed_curves)
{
- MutableSpan<float3> positions_cu = curves_->positions_for_write();
-
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
@@ -268,10 +279,26 @@ struct CombOperationExecutor {
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 Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->comb_spherical(r_changed_curves,
+ brush_transform * brush_start_cu,
+ brush_transform * brush_end_cu,
+ brush_radius_cu);
+ }
+ }
+
+ void comb_spherical(EnumerableThreadSpecific<Vector<int>> &r_changed_curves,
+ const float3 &brush_start_cu,
+ const float3 &brush_end_cu,
+ const float brush_radius_cu)
+ {
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
+ const float3 brush_diff_cu = brush_end_cu - brush_start_cu;
threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange curves_range) {
Vector<int> &local_changed_curves = r_changed_curves.local();
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_delete.cc b/source/blender/editors/sculpt_paint/curves_sculpt_delete.cc
index 9ccb777c44e..3f3c48ecbb6 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_delete.cc
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_delete.cc
@@ -117,12 +117,12 @@ struct DeleteOperationExecutor {
}
}
- Array<bool> curves_to_delete(curves_->curves_num());
+ Array<bool> curves_to_delete(curves_->curves_num(), false);
if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) {
- this->delete_projected(curves_to_delete);
+ this->delete_projected_with_symmetry(curves_to_delete);
}
else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
- this->delete_spherical(curves_to_delete);
+ this->delete_spherical_with_symmetry(curves_to_delete);
}
else {
BLI_assert_unreachable();
@@ -140,8 +140,19 @@ struct DeleteOperationExecutor {
ED_region_tag_redraw(region_);
}
- void delete_projected(MutableSpan<bool> curves_to_delete)
+ void delete_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->delete_projected(curves_to_delete, brush_transform);
+ }
+ }
+
+ void delete_projected(MutableSpan<bool> curves_to_delete, const float4x4 &brush_transform)
+ {
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
@@ -149,12 +160,10 @@ struct DeleteOperationExecutor {
threading::parallel_for(curves_->curves_range(), 512, [&](IndexRange curve_range) {
for (const int curve_i : curve_range) {
- curves_to_delete[curve_i] = false;
-
const IndexRange point_range = curves_->points_for_curve(curve_i);
for (const int segment_i : IndexRange(point_range.size() - 1)) {
- const float3 pos1_cu = positions_cu[point_range[segment_i]];
- const float3 pos2_cu = positions_cu[point_range[segment_i + 1]];
+ const float3 pos1_cu = brush_transform_inv * positions_cu[point_range[segment_i]];
+ const float3 pos2_cu = brush_transform_inv * positions_cu[point_range[segment_i + 1]];
float2 pos1_re, pos2_re;
ED_view3d_project_float_v2_m4(region_, pos1_cu, pos1_re, projection.values);
@@ -170,10 +179,8 @@ struct DeleteOperationExecutor {
});
}
- void delete_spherical(MutableSpan<bool> curves_to_delete)
+ void delete_spherical_with_symmetry(MutableSpan<bool> curves_to_delete)
{
- Span<float3> positions_cu = curves_->positions();
-
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
@@ -191,13 +198,26 @@ struct DeleteOperationExecutor {
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 Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->delete_spherical(
+ curves_to_delete, brush_transform * brush_start_cu, brush_transform * brush_end_cu);
+ }
+ }
+
+ void delete_spherical(MutableSpan<bool> curves_to_delete,
+ const float3 &brush_start_cu,
+ const float3 &brush_end_cu)
+ {
+ Span<float3> positions_cu = curves_->positions();
+
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(), 512, [&](IndexRange curve_range) {
for (const int curve_i : curve_range) {
- curves_to_delete[curve_i] = false;
-
const IndexRange points = curves_->points_for_curve(curve_i);
for (const int segment_i : IndexRange(points.size() - 1)) {
const float3 pos1_cu = positions_cu[points[segment_i]];
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_grow_shrink.cc b/source/blender/editors/sculpt_paint/curves_sculpt_grow_shrink.cc
index 6228a643a76..4ac4559cbbc 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_grow_shrink.cc
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_grow_shrink.cc
@@ -367,6 +367,13 @@ struct CurvesEffectOperationExecutor {
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ Vector<float4x4> symmetry_brush_transforms_inv;
+ for (const float4x4 brush_transform : symmetry_brush_transforms) {
+ symmetry_brush_transforms_inv.append(brush_transform.inverted());
+ }
+
threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange curves_range) {
Influences &local_influences = influences_for_thread.local();
@@ -374,55 +381,59 @@ struct CurvesEffectOperationExecutor {
const IndexRange points = curves_->points_for_curve(curve_i);
const int tot_segments = points.size() - 1;
float max_move_distance_cu = 0.0f;
- for (const int segment_i : IndexRange(tot_segments)) {
- const float3 &p1_cu = positions_cu[points[segment_i]];
- const float3 &p2_cu = positions_cu[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_on_brush_re;
- float2 closest_on_segment_re;
- float lambda_on_brush;
- float lambda_on_segment;
- const float dist_to_brush_sq_re = closest_seg_seg_v2(closest_on_brush_re,
- closest_on_segment_re,
- &lambda_on_brush,
- &lambda_on_segment,
- brush_pos_start_re_,
- brush_pos_end_re_,
- p1_re,
- p2_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;
-
- const float3 closest_on_segment_cu = math::interpolate(p1_cu, p2_cu, lambda_on_segment);
-
- float3 brush_start_pos_wo, brush_end_pos_wo;
- ED_view3d_win_to_3d(v3d_,
- region_,
- curves_to_world_mat_ * closest_on_segment_cu,
- brush_pos_start_re_,
- brush_start_pos_wo);
- ED_view3d_win_to_3d(v3d_,
- region_,
- curves_to_world_mat_ * closest_on_segment_cu,
- brush_pos_end_re_,
- brush_end_pos_wo);
- const float3 brush_start_pos_cu = world_to_curves_mat_ * brush_start_pos_wo;
- const float3 brush_end_pos_cu = world_to_curves_mat_ * brush_end_pos_wo;
-
- const float move_distance_cu = weight *
- math::distance(brush_start_pos_cu, brush_end_pos_cu);
- max_move_distance_cu = std::max(max_move_distance_cu, move_distance_cu);
+ for (const float4x4 &brush_transform_inv : symmetry_brush_transforms_inv) {
+ for (const int segment_i : IndexRange(tot_segments)) {
+ const float3 &p1_cu = brush_transform_inv * positions_cu[points[segment_i]];
+ const float3 &p2_cu = brush_transform_inv * positions_cu[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_on_brush_re;
+ float2 closest_on_segment_re;
+ float lambda_on_brush;
+ float lambda_on_segment;
+ const float dist_to_brush_sq_re = closest_seg_seg_v2(closest_on_brush_re,
+ closest_on_segment_re,
+ &lambda_on_brush,
+ &lambda_on_segment,
+ brush_pos_start_re_,
+ brush_pos_end_re_,
+ p1_re,
+ p2_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;
+
+ const float3 closest_on_segment_cu = math::interpolate(
+ p1_cu, p2_cu, lambda_on_segment);
+
+ float3 brush_start_pos_wo, brush_end_pos_wo;
+ ED_view3d_win_to_3d(v3d_,
+ region_,
+ curves_to_world_mat_ * closest_on_segment_cu,
+ brush_pos_start_re_,
+ brush_start_pos_wo);
+ ED_view3d_win_to_3d(v3d_,
+ region_,
+ curves_to_world_mat_ * closest_on_segment_cu,
+ brush_pos_end_re_,
+ brush_end_pos_wo);
+ const float3 brush_start_pos_cu = world_to_curves_mat_ * brush_start_pos_wo;
+ const float3 brush_end_pos_cu = world_to_curves_mat_ * brush_end_pos_wo;
+
+ const float move_distance_cu = weight *
+ math::distance(brush_start_pos_cu, brush_end_pos_cu);
+ max_move_distance_cu = std::max(max_move_distance_cu, move_distance_cu);
+ }
}
if (max_move_distance_cu > 0.0f) {
local_influences.curve_indices.append(curve_i);
@@ -454,6 +465,10 @@ struct CurvesEffectOperationExecutor {
const float brush_pos_diff_length_cu = math::length(brush_pos_diff_cu);
const float brush_radius_cu = self_->brush_3d_.radius_cu;
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
+
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+
threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange curves_range) {
Influences &local_influences = influences_for_thread.local();
@@ -461,32 +476,37 @@ struct CurvesEffectOperationExecutor {
const IndexRange points = curves_->points_for_curve(curve_i);
const int tot_segments = points.size() - 1;
float max_move_distance_cu = 0.0f;
- for (const int segment_i : IndexRange(tot_segments)) {
- const float3 &p1_cu = positions_cu[points[segment_i]];
- const float3 &p2_cu = positions_cu[points[segment_i] + 1];
-
- float3 closest_on_segment_cu;
- float3 closest_on_brush_cu;
- isect_seg_seg_v3(p1_cu,
- p2_cu,
- brush_pos_start_cu,
- brush_pos_end_cu,
- closest_on_segment_cu,
- closest_on_brush_cu);
-
- const float dist_to_brush_sq_cu = math::distance_squared(closest_on_segment_cu,
- closest_on_brush_cu);
- if (dist_to_brush_sq_cu > brush_radius_sq_cu) {
- continue;
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ const float3 brush_pos_start_transformed_cu = brush_transform * brush_pos_start_cu;
+ const float3 brush_pos_end_transformed_cu = brush_transform * brush_pos_end_cu;
+
+ for (const int segment_i : IndexRange(tot_segments)) {
+ const float3 &p1_cu = positions_cu[points[segment_i]];
+ const float3 &p2_cu = positions_cu[points[segment_i] + 1];
+
+ float3 closest_on_segment_cu;
+ float3 closest_on_brush_cu;
+ isect_seg_seg_v3(p1_cu,
+ p2_cu,
+ brush_pos_start_transformed_cu,
+ brush_pos_end_transformed_cu,
+ closest_on_segment_cu,
+ closest_on_brush_cu);
+
+ const float dist_to_brush_sq_cu = math::distance_squared(closest_on_segment_cu,
+ closest_on_brush_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;
+
+ const float move_distance_cu = weight * brush_pos_diff_length_cu;
+ max_move_distance_cu = std::max(max_move_distance_cu, move_distance_cu);
}
-
- 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;
-
- const float move_distance_cu = weight * brush_pos_diff_length_cu;
- max_move_distance_cu = std::max(max_move_distance_cu, move_distance_cu);
}
if (max_move_distance_cu > 0.0f) {
local_influences.curve_indices.append(curve_i);
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh b/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh
index 03413221907..b5835abecbd 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh
@@ -53,4 +53,6 @@ std::optional<CurvesBrush3D> sample_curves_3d_brush(bContext &C,
const float2 &brush_pos_re,
float brush_radius_re);
+Vector<float4x4> get_symmetry_brush_transforms(eCurvesSymmetryType symmetry);
+
} // namespace blender::ed::sculpt_paint
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_snake_hook.cc b/source/blender/editors/sculpt_paint/curves_sculpt_snake_hook.cc
index 6d930d35f04..d2814c17682 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_snake_hook.cc
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_snake_hook.cc
@@ -136,10 +136,10 @@ struct SnakeHookOperatorExecutor {
}
if (falloff_shape_ == PAINT_FALLOFF_SHAPE_SPHERE) {
- this->spherical_snake_hook();
+ this->spherical_snake_hook_with_symmetry();
}
else if (falloff_shape_ == PAINT_FALLOFF_SHAPE_TUBE) {
- this->projected_snake_hook();
+ this->projected_snake_hook_with_symmetry();
}
else {
BLI_assert_unreachable();
@@ -150,8 +150,19 @@ struct SnakeHookOperatorExecutor {
ED_region_tag_redraw(region_);
}
- void projected_snake_hook()
+ void projected_snake_hook_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->projected_snake_hook(brush_transform);
+ }
+ }
+
+ void projected_snake_hook(const float4x4 &brush_transform)
+ {
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+
MutableSpan<float3> positions_cu = curves_->positions_for_write();
float4x4 projection;
@@ -161,7 +172,7 @@ struct SnakeHookOperatorExecutor {
for (const int curve_i : curves_range) {
const IndexRange points = curves_->points_for_curve(curve_i);
const int last_point_i = points.last();
- const float3 old_pos_cu = positions_cu[last_point_i];
+ const float3 old_pos_cu = brush_transform_inv * positions_cu[last_point_i];
float2 old_pos_re;
ED_view3d_project_float_v2_m4(region_, old_pos_cu, old_pos_re, projection.values);
@@ -179,17 +190,15 @@ struct SnakeHookOperatorExecutor {
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;
+ const float3 new_position_cu = brush_transform * (world_to_curves_mat_ * new_position_wo);
this->move_last_point_and_resample(positions_cu.slice(points), new_position_cu);
}
});
}
- void spherical_snake_hook()
+ void spherical_snake_hook_with_symmetry()
{
- MutableSpan<float3> positions_cu = curves_->positions_for_write();
-
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
@@ -206,9 +215,23 @@ struct SnakeHookOperatorExecutor {
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 Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->spherical_snake_hook(
+ brush_transform * brush_start_cu, brush_transform * brush_end_cu, brush_radius_cu);
+ }
+ }
+
+ void spherical_snake_hook(const float3 &brush_start_cu,
+ const float3 &brush_end_cu,
+ const float brush_radius_cu)
+ {
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+ const float3 brush_diff_cu = brush_end_cu - brush_start_cu;
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange curves_range) {
diff --git a/source/blender/makesdna/DNA_curves_types.h b/source/blender/makesdna/DNA_curves_types.h
index bb53dbafdc8..e909abc70cb 100644
--- a/source/blender/makesdna/DNA_curves_types.h
+++ b/source/blender/makesdna/DNA_curves_types.h
@@ -9,6 +9,8 @@
#include "DNA_ID.h"
#include "DNA_customdata_types.h"
+#include "BLI_utildefines.h"
+
#ifdef __cplusplus
extern "C" {
#endif
@@ -130,7 +132,13 @@ typedef struct Curves {
/* Materials. */
struct Material **mat;
short totcol;
- short _pad2[3];
+
+ /**
+ * User-defined symmetry flag (#eCurvesSymmetryType) that causes editing operations to maintain
+ * symmetrical geometry.
+ */
+ char symmetry;
+ char _pad2[5];
/**
* Used as base mesh when curves represent e.g. hair or fur. This surface is used in edit modes.
@@ -150,6 +158,14 @@ enum {
HA_DS_EXPAND = (1 << 0),
};
+/** #Curves.symmetry */
+typedef enum eCurvesSymmetryType {
+ CURVES_SYMMETRY_X = 1 << 0,
+ CURVES_SYMMETRY_Y = 1 << 1,
+ CURVES_SYMMETRY_Z = 1 << 2,
+} eCurvesSymmetryType;
+ENUM_OPERATORS(eCurvesSymmetryType, CURVES_SYMMETRY_Z)
+
/* Only one material supported currently. */
#define CURVES_MATERIAL_NR 1
diff --git a/source/blender/makesrna/intern/rna_curves.c b/source/blender/makesrna/intern/rna_curves.c
index 7a1a368551f..2dc568d0b8a 100644
--- a/source/blender/makesrna/intern/rna_curves.c
+++ b/source/blender/makesrna/intern/rna_curves.c
@@ -274,6 +274,22 @@ static void rna_def_curves(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Surface", "Mesh object that the curves can be attached to");
RNA_def_property_update(prop, NC_OBJECT | ND_DRAW, NULL);
+ /* Symmetry. */
+ prop = RNA_def_property(srna, "use_mirror_x", PROP_BOOLEAN, PROP_NONE);
+ RNA_def_property_boolean_sdna(prop, NULL, "symmetry", CURVES_SYMMETRY_X);
+ RNA_def_property_ui_text(prop, "X", "Enable symmetry in the X axis");
+ RNA_def_property_update(prop, 0, "rna_Mesh_update_draw");
+
+ prop = RNA_def_property(srna, "use_mirror_y", PROP_BOOLEAN, PROP_NONE);
+ RNA_def_property_boolean_sdna(prop, NULL, "symmetry", CURVES_SYMMETRY_Y);
+ RNA_def_property_ui_text(prop, "Y", "Enable symmetry in the Y axis");
+ RNA_def_property_update(prop, 0, "rna_Mesh_update_draw");
+
+ prop = RNA_def_property(srna, "use_mirror_z", PROP_BOOLEAN, PROP_NONE);
+ RNA_def_property_boolean_sdna(prop, NULL, "symmetry", CURVES_SYMMETRY_Z);
+ RNA_def_property_ui_text(prop, "Z", "Enable symmetry in the Z axis");
+ RNA_def_property_update(prop, 0, "rna_Mesh_update_draw");
+
/* attributes */
rna_def_attributes_common(srna);