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
This commit is contained in:
Jacques Lucke 2022-05-04 09:51:32 +02:00
parent aa21087d56
commit 7dc94155f6
Notes: blender-bot 2023-02-13 15:40:16 +01:00
Referenced by issue #97441, Curves sculpting mirroring tools.
12 changed files with 351 additions and 115 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
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 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);
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);
if (dist_to_brush_sq_re > brush_radius_sq_re_) {
continue;
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);
}
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];
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;
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);
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];
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;
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);

View File

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

View File

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

View File

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

View File

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