Sculpt: Multiplane Scrape Brush
authorPablo Dobarro <pablodp606@gmail.com>
Wed, 6 Nov 2019 18:39:34 +0000 (19:39 +0100)
committerPablo Dobarro <pablodp606@gmail.com>
Thu, 21 Nov 2019 17:16:42 +0000 (18:16 +0100)
The Multiplane Scrape brush creates sharp edges with a given fixed angle by trimming the mesh with two planes in local space at the same time.  When working with stylized or hard surface models, this brush produces way better results and is more predictable than any other crease/flatten brush based on curves and alphas.
It is also the first brush we have than can produce hard surface concave creases.
The Multiplane Scrape Brush also has a dynamic mode where it samples the surface to fit the angle and scrape planes during a stroke. With this mode enabled you can sculpt multiple times over the same edge without creating artifacts.
It can also create creases that change between concave and convex during the same stroke.

The behavior of this brush will improve after merging patches like D5993 and its behavior in concave creases can still be improved, so I will keep tweaking its parameters and default values once we have all brush properties available.

Reviewed By: jbakker

Differential Revision: https://developer.blender.org/D6174

release/scripts/startup/bl_ui/space_view3d_toolbar.py
source/blender/blenkernel/intern/brush.c
source/blender/blenloader/intern/versioning_defaults.c
source/blender/editors/sculpt_paint/paint_cursor.c
source/blender/editors/sculpt_paint/sculpt.c
source/blender/editors/sculpt_paint/sculpt_intern.h
source/blender/makesdna/DNA_brush_types.h
source/blender/makesrna/intern/rna_brush.c

index efd26c2daf86497fad8ca14e2314f9a3ee3e2a8b..63e5aa53077372cfa99cb80a3353b9a1c86b38f0 100644 (file)
@@ -430,6 +430,13 @@ class VIEW3D_PT_tools_brush(Panel, View3DPaintPanel):
                 col.separator()
                 row = col.row()
                 row.prop(brush, "use_grab_active_vertex")
+            elif brush.sculpt_tool == 'MULTIPLANE_SCRAPE':
+                row = col.row()
+                row.prop(brush, "multiplane_scrape_angle")
+                row = col.row()
+                row.prop(brush, "use_multiplane_scrape_dynamic")
+                row = col.row()
+                row.prop(brush, "show_multiplane_scrape_planes_preview")
 
             # topology_rake_factor
             if (
index eebed72103f422e89e849b2c7bc376ad02c01663..794876ec444c3f7163cb0c8969563b14de76d900 100644 (file)
@@ -938,6 +938,14 @@ void BKE_brush_sculpt_reset(Brush *br)
       br->curve_preset = BRUSH_CURVE_SPHERE;
       br->spacing = 6;
       break;
+    case SCULPT_TOOL_MULTIPLANE_SCRAPE:
+      br->flag2 |= BRUSH_MULTIPLANE_SCRAPE_DYNAMIC | BRUSH_MULTIPLANE_SCRAPE_PLANES_PREVIEW;
+      br->alpha = 0.7f;
+      br->normal_radius_factor = 0.70f;
+      br->multiplane_scrape_angle = 60;
+      br->curve_preset = BRUSH_CURVE_SMOOTH;
+      br->spacing = 5;
+      break;
     case SCULPT_TOOL_CREASE:
       br->flag |= BRUSH_DIR_IN;
       br->alpha = 0.25;
@@ -1010,6 +1018,7 @@ void BKE_brush_sculpt_reset(Brush *br)
     case SCULPT_TOOL_FLATTEN:
     case SCULPT_TOOL_FILL:
     case SCULPT_TOOL_SCRAPE:
+    case SCULPT_TOOL_MULTIPLANE_SCRAPE:
       br->add_col[0] = 1.0f;
       br->add_col[1] = 0.39f;
       br->add_col[2] = 0.39f;
index 05758b446ad1ee6f42c21cbe830f105a6ed45978..45ec6eef8130f436d8a89def81a56573974ff4da 100644 (file)
@@ -509,6 +509,14 @@ void BLO_update_defaults_startup_blend(Main *bmain, const char *app_template)
       brush->sculpt_tool = SCULPT_TOOL_POSE;
     }
 
+    brush_name = "Multiplane Scrape";
+    brush = BLI_findstring(&bmain->brushes, brush_name, offsetof(ID, name) + 2);
+    if (!brush) {
+      brush = BKE_brush_add(bmain, brush_name, OB_MODE_SCULPT);
+      id_us_min(&brush->id);
+      brush->sculpt_tool = SCULPT_TOOL_MULTIPLANE_SCRAPE;
+    }
+
     brush_name = "Simplify";
     brush = BLI_findstring(&bmain->brushes, brush_name, offsetof(ID, name) + 2);
     if (!brush) {
index b9af33e7ad684aa5eac7d93c0e4aefc4293bf6ec..ac738f326a302699d934b2f55f98ecced0375054 100644 (file)
@@ -1214,6 +1214,70 @@ static void sculpt_geometry_preview_lines_draw(const uint gpuattr, SculptSession
   }
 }
 
+static void sculpt_multiplane_scrape_preview_draw(const uint gpuattr,
+                                                  SculptSession *ss,
+                                                  float *outline_col,
+                                                  float outline_alpha)
+{
+  float local_mat_inv[4][4];
+  invert_m4_m4(local_mat_inv, ss->cache->stroke_local_mat);
+  GPU_matrix_mul(local_mat_inv);
+  float angle = ss->cache->multiplane_scrape_sampled_angle;
+  if (ss->cache->pen_flip || ss->cache->invert) {
+    angle = -angle;
+  }
+
+  float offset = ss->cache->radius * 0.25f;
+
+  float p[3] = {0.0f, 0.0f, ss->cache->radius};
+  float y_axis[3] = {0.0f, 1.0f, 0.0f};
+  float p_l[3];
+  float p_r[3];
+  float area_center[3] = {0.0f, 0.0f, 0.0f};
+  rotate_v3_v3v3fl(p_r, p, y_axis, DEG2RADF((angle + 180) * 0.5f));
+  rotate_v3_v3v3fl(p_l, p, y_axis, DEG2RADF(-(angle + 180) * 0.5f));
+
+  immBegin(GPU_PRIM_LINES, 14);
+  immVertex3f(gpuattr, area_center[0], area_center[1] + offset, area_center[2]);
+  immVertex3f(gpuattr, p_r[0], p_r[1] + offset, p_r[2]);
+  immVertex3f(gpuattr, area_center[0], area_center[1] + offset, area_center[2]);
+  immVertex3f(gpuattr, p_l[0], p_l[1] + offset, p_l[2]);
+
+  immVertex3f(gpuattr, area_center[0], area_center[1] - offset, area_center[2]);
+  immVertex3f(gpuattr, p_r[0], p_r[1] - offset, p_r[2]);
+  immVertex3f(gpuattr, area_center[0], area_center[1] - offset, area_center[2]);
+  immVertex3f(gpuattr, p_l[0], p_l[1] - offset, p_l[2]);
+
+  immVertex3f(gpuattr, area_center[0], area_center[1] - offset, area_center[2]);
+  immVertex3f(gpuattr, area_center[0], area_center[1] + offset, area_center[2]);
+
+  immVertex3f(gpuattr, p_r[0], p_r[1] - offset, p_r[2]);
+  immVertex3f(gpuattr, p_r[0], p_r[1] + offset, p_r[2]);
+
+  immVertex3f(gpuattr, p_l[0], p_l[1] - offset, p_l[2]);
+  immVertex3f(gpuattr, p_l[0], p_l[1] + offset, p_l[2]);
+
+  immEnd();
+
+  immUniformColor3fvAlpha(outline_col, outline_alpha * 0.1f);
+  immBegin(GPU_PRIM_TRIS, 12);
+  immVertex3f(gpuattr, area_center[0], area_center[1] + offset, area_center[2]);
+  immVertex3f(gpuattr, p_r[0], p_r[1] + offset, p_r[2]);
+  immVertex3f(gpuattr, p_r[0], p_r[1] - offset, p_r[2]);
+  immVertex3f(gpuattr, area_center[0], area_center[1] + offset, area_center[2]);
+  immVertex3f(gpuattr, area_center[0], area_center[1] - offset, area_center[2]);
+  immVertex3f(gpuattr, p_r[0], p_r[1] - offset, p_r[2]);
+
+  immVertex3f(gpuattr, area_center[0], area_center[1] + offset, area_center[2]);
+  immVertex3f(gpuattr, p_l[0], p_l[1] + offset, p_l[2]);
+  immVertex3f(gpuattr, p_l[0], p_l[1] - offset, p_l[2]);
+  immVertex3f(gpuattr, area_center[0], area_center[1] + offset, area_center[2]);
+  immVertex3f(gpuattr, area_center[0], area_center[1] - offset, area_center[2]);
+  immVertex3f(gpuattr, p_l[0], p_l[1] - offset, p_l[2]);
+
+  immEnd();
+}
+
 static bool paint_use_2d_cursor(ePaintMode mode)
 {
   if (mode >= PAINT_MODE_TEXTURE_3D) {
@@ -1516,6 +1580,24 @@ static void paint_draw_cursor(bContext *C, int x, int y, void *UNUSED(unused))
             }
           }
 
+          if (brush->sculpt_tool == SCULPT_TOOL_MULTIPLANE_SCRAPE &&
+              brush->flag2 & BRUSH_MULTIPLANE_SCRAPE_PLANES_PREVIEW && !ss->cache->first_time) {
+            GPU_matrix_push_projection();
+            ED_view3d_draw_setup_view(CTX_wm_window(C),
+                                      CTX_data_depsgraph_pointer(C),
+                                      CTX_data_scene(C),
+                                      ar,
+                                      CTX_wm_view3d(C),
+                                      NULL,
+                                      NULL,
+                                      NULL);
+            GPU_matrix_push();
+            GPU_matrix_mul(vc.obact->obmat);
+            sculpt_multiplane_scrape_preview_draw(pos, ss, outline_col, outline_alpha);
+            GPU_matrix_pop();
+            GPU_matrix_pop_projection();
+          }
+
           wmWindowViewport(win);
         }
       }
index fcc2a7a08b3e704ee8d297ddca12913a12a164b9..6cda6ccac067f06a578eb7ad93568d1f44bc47e7 100644 (file)
@@ -1771,6 +1771,10 @@ static float brush_strength(const Sculpt *sd,
         return 0.125f * alpha * flip * pressure * overlap * feather;
       }
 
+    case SCULPT_TOOL_MULTIPLANE_SCRAPE:
+      overlap = (1.0f + overlap) / 2.0f;
+      return alpha * flip * pressure * overlap * feather;
+
     case SCULPT_TOOL_FILL:
     case SCULPT_TOOL_SCRAPE:
     case SCULPT_TOOL_FLATTEN:
@@ -4843,6 +4847,341 @@ static void do_clay_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode)
   BKE_pbvh_parallel_range(0, totnode, &data, do_clay_brush_task_cb_ex, &settings);
 }
 
+/* -------------------------------------------------------------------- */
+
+/** \name Sculpt Multiplane Scrape Brush
+ * \{ */
+
+typedef struct MultiplaneScrapeSampleData {
+  float area_cos[2][3];
+  float area_nos[2][3];
+  int area_count[2];
+} MultiplaneScrapeSampleData;
+
+static void calc_multiplane_scrape_surface_task_cb(void *__restrict userdata,
+                                                   const int n,
+                                                   const TaskParallelTLS *__restrict tls)
+{
+  SculptThreadedTaskData *data = userdata;
+  SculptSession *ss = data->ob->sculpt;
+  const Brush *brush = data->brush;
+  MultiplaneScrapeSampleData *mssd = tls->userdata_chunk;
+  float(*mat)[4] = data->mat;
+
+  PBVHVertexIter vd;
+
+  SculptBrushTest test;
+  SculptBrushTestFn sculpt_brush_test_sq_fn = sculpt_brush_test_init_with_falloff_shape(
+      ss, &test, brush->falloff_shape);
+
+  /* Apply the brush normal radius to the test before sampling */
+  float test_radius = sqrtf(test.radius_squared);
+  test_radius *= brush->normal_radius_factor;
+  test.radius_squared = test_radius * test_radius;
+
+  BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE)
+  {
+
+    if (sculpt_brush_test_sq_fn(&test, vd.co)) {
+      float local_co[3];
+      float normal[3];
+      if (vd.no) {
+        normal_short_to_float_v3(normal, vd.no);
+      }
+      else {
+        copy_v3_v3(normal, vd.fno);
+      }
+      mul_v3_m4v3(local_co, mat, vd.co);
+      /* Use the brush falloff to weight the sampled normals */
+      const float fade = tex_strength(ss,
+                                      brush,
+                                      vd.co,
+                                      sqrtf(test.dist),
+                                      vd.no,
+                                      vd.fno,
+                                      vd.mask ? *vd.mask : 0.0f,
+                                      vd.index,
+                                      tls->thread_id);
+
+      /* Sample the normal and area of the +X and -X axis individually */
+      if (local_co[0] > 0.0f) {
+        madd_v3_v3fl(mssd->area_nos[0], normal, fade);
+        add_v3_v3(mssd->area_cos[0], vd.co);
+        mssd->area_count[0]++;
+      }
+      else {
+        madd_v3_v3fl(mssd->area_nos[1], normal, fade);
+        add_v3_v3(mssd->area_cos[1], vd.co);
+        mssd->area_count[1]++;
+      }
+    }
+    BKE_pbvh_vertex_iter_end;
+  }
+}
+
+static void calc_multiplane_scrape_surface_reduce(const void *__restrict UNUSED(userdata),
+                                                  void *__restrict chunk_join,
+                                                  void *__restrict chunk)
+{
+  MultiplaneScrapeSampleData *join = chunk_join;
+  MultiplaneScrapeSampleData *mssd = chunk;
+
+  add_v3_v3(join->area_cos[0], mssd->area_cos[0]);
+  add_v3_v3(join->area_cos[1], mssd->area_cos[1]);
+
+  add_v3_v3(join->area_nos[0], mssd->area_nos[0]);
+  add_v3_v3(join->area_nos[1], mssd->area_nos[1]);
+
+  join->area_count[0] += mssd->area_count[0];
+  join->area_count[1] += mssd->area_count[1];
+}
+
+static void do_multiplane_scrape_brush_task_cb_ex(void *__restrict userdata,
+                                                  const int n,
+                                                  const TaskParallelTLS *__restrict tls)
+{
+  SculptThreadedTaskData *data = userdata;
+  SculptSession *ss = data->ob->sculpt;
+  const Brush *brush = data->brush;
+  float(*mat)[4] = data->mat;
+  float(*scrape_planes)[4] = data->multiplane_scrape_planes;
+
+  float angle = data->multiplane_scrape_angle;
+
+  PBVHVertexIter vd;
+  float(*proxy)[3];
+  const float bstrength = fabsf(ss->cache->bstrength);
+
+  proxy = BKE_pbvh_node_add_proxy(ss->pbvh, data->nodes[n])->co;
+
+  SculptBrushTest test;
+  SculptBrushTestFn sculpt_brush_test_sq_fn = sculpt_brush_test_init_with_falloff_shape(
+      ss, &test, data->brush->falloff_shape);
+
+  BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE)
+  {
+
+    if (sculpt_brush_test_sq_fn(&test, vd.co)) {
+      float local_co[3];
+      bool deform = false;
+
+      mul_v3_m4v3(local_co, mat, vd.co);
+
+      if (local_co[0] > 0.0f) {
+        deform = !plane_point_side(vd.co, scrape_planes[0]);
+      }
+      else {
+        deform = !plane_point_side(vd.co, scrape_planes[1]);
+      }
+
+      if (angle < 0.0f) {
+        deform = true;
+      }
+
+      if (deform) {
+        float intr[3];
+        float val[3];
+
+        if (local_co[0] > 0.0f) {
+          closest_to_plane_normalized_v3(intr, scrape_planes[0], vd.co);
+        }
+        else {
+          closest_to_plane_normalized_v3(intr, scrape_planes[1], vd.co);
+        }
+
+        sub_v3_v3v3(val, intr, vd.co);
+        if (plane_trim(ss->cache, brush, val)) {
+          /* Deform the local space along the Y axis to avoid artifacts on curved strokes */
+          /* This produces a not round brush tip */
+          local_co[1] *= 2.0f;
+          const float fade = bstrength * tex_strength(ss,
+                                                      brush,
+                                                      vd.co,
+                                                      len_v3(local_co),
+                                                      vd.no,
+                                                      vd.fno,
+                                                      vd.mask ? *vd.mask : 0.0f,
+                                                      vd.index,
+                                                      tls->thread_id);
+
+          mul_v3_v3fl(proxy[vd.i], val, fade);
+
+          if (vd.mvert) {
+            vd.mvert->flag |= ME_VERT_PBVH_UPDATE;
+          }
+        }
+      }
+    }
+  }
+  BKE_pbvh_vertex_iter_end;
+}
+
+static void do_multiplane_scrape_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode)
+{
+  SculptSession *ss = ob->sculpt;
+  Brush *brush = BKE_paint_brush(&sd->paint);
+
+  const bool flip = (ss->cache->bstrength < 0.0f);
+  const float radius = flip ? -ss->cache->radius : ss->cache->radius;
+  const float offset = get_offset(sd, ss);
+  const float displace = -radius * offset;
+
+  float area_no_sp[3]; /* the sculpt-plane normal (whatever its set to) */
+  float area_no[3];    /* geometry normal */
+  float area_co[3];
+
+  float temp[3];
+  float mat[4][4];
+
+  calc_sculpt_plane(sd, ob, nodes, totnode, area_no_sp, area_co);
+
+  if (brush->sculpt_plane != SCULPT_DISP_DIR_AREA || (brush->flag & BRUSH_ORIGINAL_NORMAL)) {
+    calc_area_normal(sd, ob, nodes, totnode, area_no);
+  }
+  else {
+    copy_v3_v3(area_no, area_no_sp);
+  }
+
+  /* delay the first daub because grab delta is not setup */
+  if (ss->cache->first_time) {
+    return;
+  }
+
+  if (is_zero_v3(ss->cache->grab_delta_symmetry)) {
+    return;
+  }
+
+  mul_v3_v3v3(temp, area_no_sp, ss->cache->scale);
+  mul_v3_fl(temp, displace);
+  add_v3_v3(area_co, temp);
+
+  /* Init brush local space matrix */
+  cross_v3_v3v3(mat[0], area_no, ss->cache->grab_delta_symmetry);
+  mat[0][3] = 0;
+  cross_v3_v3v3(mat[1], area_no, mat[0]);
+  mat[1][3] = 0;
+  copy_v3_v3(mat[2], area_no);
+  mat[2][3] = 0;
+  copy_v3_v3(mat[3], ss->cache->location);
+  mat[3][3] = 1;
+  normalize_m4(mat);
+  invert_m4(mat);
+
+  float angle = brush->multiplane_scrape_angle;
+
+  /* Update matrix for the cursor preview */
+  if (ss->cache->mirror_symmetry_pass == 0) {
+    copy_m4_m4(ss->cache->stroke_local_mat, mat);
+  }
+
+  /* Dynamic mode */
+
+  if (brush->flag2 & BRUSH_MULTIPLANE_SCRAPE_DYNAMIC) {
+    /* Sample the individual normal and area center of the two areas at both sides of the cursor */
+    SculptThreadedTaskData sample_data = {
+        .sd = NULL,
+        .ob = ob,
+        .brush = brush,
+        .nodes = nodes,
+        .totnode = totnode,
+        .mat = mat,
+    };
+
+    MultiplaneScrapeSampleData mssd = {{{0}}};
+
+    PBVHParallelSettings sample_settings;
+    BKE_pbvh_parallel_range_settings(&sample_settings, (sd->flags & SCULPT_USE_OPENMP), totnode);
+    sample_settings.func_reduce = calc_multiplane_scrape_surface_reduce;
+    sample_settings.userdata_chunk = &mssd;
+    sample_settings.userdata_chunk_size = sizeof(MultiplaneScrapeSampleData);
+
+    BKE_pbvh_parallel_range(
+        0, totnode, &sample_data, calc_multiplane_scrape_surface_task_cb, &sample_settings);
+
+    float sampled_plane_normals[2][3];
+    float sampled_plane_co[2][3];
+    float sampled_cv[2][3];
+    float mid_co[3];
+
+    /* Use the area center of both planes to detect if we are sculpting along a concave or convex
+     * edge */
+    mul_v3_v3fl(sampled_plane_co[0], mssd.area_cos[0], 1.0f / (float)mssd.area_count[0]);
+    mul_v3_v3fl(sampled_plane_co[1], mssd.area_cos[1], 1.0f / (float)mssd.area_count[1]);
+    mid_v3_v3v3(mid_co, sampled_plane_co[0], sampled_plane_co[1]);
+
+    /* Calculate the scrape planes angle based on the sampled normals */
+    mul_v3_v3fl(sampled_plane_normals[0], mssd.area_nos[0], 1.0f / (float)mssd.area_count[0]);
+    mul_v3_v3fl(sampled_plane_normals[1], mssd.area_nos[1], 1.0f / (float)mssd.area_count[1]);
+    normalize_v3(sampled_plane_normals[0]);
+    normalize_v3(sampled_plane_normals[1]);
+
+    float sampled_angle = angle_v3v3(sampled_plane_normals[0], sampled_plane_normals[1]);
+    copy_v3_v3(sampled_cv[0], area_no);
+    sub_v3_v3v3(sampled_cv[1], ss->cache->location, mid_co);
+
+    sampled_angle += DEG2RADF(brush->multiplane_scrape_angle) * ss->cache->pressure;
+
+    /* Invert the angle if we are sculpting along a concave edge */
+    if (dot_v3v3(sampled_cv[0], sampled_cv[1]) < 0.0f) {
+      sampled_angle = -sampled_angle;
+    }
+
+    /* In dynamic mode, set the angle to 0 when inverting the brush, so you can trim plane surfaces
+     * without changing the brush */
+    if (flip) {
+      sampled_angle = 0.0f;
+    }
+    else {
+      copy_v3_v3(area_co, ss->cache->location);
+    }
+
+    angle = RAD2DEGF(sampled_angle);
+  }
+  else {
+
+    /* Standard mode: Scrape with the brush property fixed angle */
+    copy_v3_v3(area_co, ss->cache->location);
+    if (flip) {
+      angle = -angle;
+    }
+  }
+
+  /* Set the angle for the cursor preview */
+  ss->cache->multiplane_scrape_sampled_angle = angle;
+
+  SculptThreadedTaskData data = {
+      .sd = sd,
+      .ob = ob,
+      .brush = brush,
+      .nodes = nodes,
+      .mat = mat,
+      .multiplane_scrape_angle = angle,
+  };
+
+  /* Calculate the final left and right scrape planes */
+  float plane_no[3];
+  float plane_no_rot[3];
+  float y_axis[3] = {0.0f, 1.0f, 0.0f};
+  float mat_inv[4][4];
+  invert_m4_m4(mat_inv, mat);
+
+  mul_v3_mat3_m4v3(plane_no, mat, area_no);
+  rotate_v3_v3v3fl(plane_no_rot, plane_no, y_axis, DEG2RADF(-angle * 0.5f));
+  mul_v3_mat3_m4v3(plane_no, mat_inv, plane_no_rot);
+  normalize_v3(plane_no);
+  plane_from_point_normal_v3(data.multiplane_scrape_planes[1], area_co, plane_no);
+
+  mul_v3_mat3_m4v3(plane_no, mat, area_no);
+  rotate_v3_v3v3fl(plane_no_rot, plane_no, y_axis, DEG2RADF(angle * 0.5f));
+  mul_v3_mat3_m4v3(plane_no, mat_inv, plane_no_rot);
+  normalize_v3(plane_no);
+  plane_from_point_normal_v3(data.multiplane_scrape_planes[0], area_co, plane_no);
+
+  PBVHParallelSettings settings;
+  BKE_pbvh_parallel_range_settings(&settings, (sd->flags & SCULPT_USE_OPENMP), totnode);
+  BKE_pbvh_parallel_range(0, totnode, &data, do_multiplane_scrape_brush_task_cb_ex, &settings);
+}
+
 /** \} */
 
 static void do_clay_strips_brush_task_cb_ex(void *__restrict userdata,
@@ -5468,6 +5807,9 @@ static void do_brush_action(Sculpt *sd, Object *ob, Brush *brush, UnifiedPaintSe
       case SCULPT_TOOL_CLAY_STRIPS:
         do_clay_strips_brush(sd, ob, nodes, totnode);
         break;
+      case SCULPT_TOOL_MULTIPLANE_SCRAPE:
+        do_multiplane_scrape_brush(sd, ob, nodes, totnode);
+        break;
       case SCULPT_TOOL_FILL:
         do_fill_brush(sd, ob, nodes, totnode);
         break;
@@ -5997,6 +6339,8 @@ static const char *sculpt_tool_name(Sculpt *sd)
       return "Elastic Deform Brush";
     case SCULPT_TOOL_POSE:
       return "Pose Brush";
+    case SCULPT_TOOL_MULTIPLANE_SCRAPE:
+      return "Multiplane Scrape Brush";
   }
 
   return "Sculpting";
@@ -6260,6 +6604,7 @@ static void sculpt_update_brush_delta(UnifiedPaintSettings *ups, Object *ob, Bru
            SCULPT_TOOL_ELASTIC_DEFORM,
            SCULPT_TOOL_NUDGE,
            SCULPT_TOOL_CLAY_STRIPS,
+           SCULPT_TOOL_MULTIPLANE_SCRAPE,
            SCULPT_TOOL_SNAKE_HOOK,
            SCULPT_TOOL_POSE,
            SCULPT_TOOL_THUMB) ||
@@ -6295,6 +6640,7 @@ static void sculpt_update_brush_delta(UnifiedPaintSettings *ups, Object *ob, Bru
           add_v3_v3(cache->grab_delta, delta);
           break;
         case SCULPT_TOOL_CLAY_STRIPS:
+        case SCULPT_TOOL_MULTIPLANE_SCRAPE:
         case SCULPT_TOOL_NUDGE:
         case SCULPT_TOOL_SNAKE_HOOK:
           if (brush->flag & BRUSH_ANCHORED) {
@@ -7759,7 +8105,7 @@ static int sculpt_symmetrize_exec(bContext *C, wmOperator *UNUSED(op))
       break;
     case PBVH_FACES:
       /* Mesh Symmetrize */
-      ED_sculpt_undo_geometry_begin(ob);
+      ED_sculpt_undo_geometry_begin(ob, "mesh symmetrize");
       Mesh *mesh = ob->data;
       Mesh *mesh_mirror;
       MirrorModifierData mmd = {0};
index 93e4a7775697fbc37051b95aac18bb5efe075fb5..0b25ab31ce0fe71ef5b130ba1190379c24b45ea3 100644 (file)
@@ -206,6 +206,9 @@ typedef struct SculptThreadedTaskData {
   float *pose_factor;
   float (*transform_rot)[4], (*transform_trans)[4], (*transform_trans_inv)[4];
 
+  float multiplane_scrape_angle;
+  float multiplane_scrape_planes[2][4];
+
   float max_distance_squared;
   float nearest_vertex_search_co[3];
 
@@ -392,6 +395,9 @@ typedef struct StrokeCache {
 
   float *automask;
 
+  float stroke_local_mat[4][4];
+  float multiplane_scrape_sampled_angle;
+
   rcti previous_r; /* previous redraw rectangle */
   rcti current_r;  /* current redraw rectangle */
 
index 67bca60e7e1e137ea953b3564297a7b53d96a34f..c55ab81a733324a1d7218813a4ae40eaa3cec215 100644 (file)
@@ -247,7 +247,9 @@ typedef struct Brush {
   int size;
   /** General purpose flags. */
   int flag;
+  int flag2;
   int sampling_flag;
+
   /** Pressure influence for mask. */
   int mask_pressure;
   /** Jitter the position of the brush. */
@@ -288,7 +290,8 @@ typedef struct Brush {
   /** Source for fill tool color gradient application. */
   char gradient_fill_mode;
 
-  char _pad[5];
+  char _pad0;
+
   /** Projection shape (sphere, circle). */
   char falloff_shape;
   float falloff_angle;
@@ -307,7 +310,7 @@ typedef struct Brush {
   char mask_tool;
   /** Active grease pencil tool. */
   char gpencil_tool;
-  char _pad0[1];
+  char _pad1[5];
 
   float autosmooth_factor;
 
@@ -332,6 +335,9 @@ typedef struct Brush {
   /* pose */
   float pose_offset;
 
+  /* multiplane scrape */
+  float multiplane_scrape_angle;
+
   /* overlay */
   int texture_overlay_alpha;
   int mask_overlay_alpha;
@@ -445,6 +451,12 @@ typedef enum eBrushSamplingFlags {
   BRUSH_PAINT_ANTIALIASING = (1 << 0),
 } eBrushSamplingFlags;
 
+/* Brush.flag2 */
+typedef enum eBrushFlags2 {
+  BRUSH_MULTIPLANE_SCRAPE_DYNAMIC = (1 << 0),
+  BRUSH_MULTIPLANE_SCRAPE_PLANES_PREVIEW = (1 << 1),
+} eBrushFlags2;
+
 typedef enum {
   BRUSH_MASK_PRESSURE_RAMP = (1 << 1),
   BRUSH_MASK_PRESSURE_CUTOFF = (1 << 2),
@@ -488,6 +500,7 @@ typedef enum eBrushSculptTool {
   SCULPT_TOOL_DRAW_SHARP = 20,
   SCULPT_TOOL_ELASTIC_DEFORM = 21,
   SCULPT_TOOL_POSE = 22,
+  SCULPT_TOOL_MULTIPLANE_SCRAPE = 23,
 } eBrushSculptTool;
 
 /* Brush.uv_sculpt_tool */
index 3f269299461c988450ae0bae56e956093dd676ad..86b1ed9234902ada94d73cb88e5ddba8f7af0cd9 100644 (file)
@@ -68,6 +68,7 @@ static const EnumPropertyItem sculpt_stroke_method_items[] = {
     {0, NULL, 0, NULL, NULL},
 };
 
+/* clang-format off */
 const EnumPropertyItem rna_enum_brush_sculpt_tool_items[] = {
     {SCULPT_TOOL_DRAW, "DRAW", ICON_BRUSH_SCULPT_DRAW, "Draw", ""},
     {SCULPT_TOOL_DRAW_SHARP, "DRAW_SHARP", ICON_BRUSH_SCULPT_DRAW, "Draw Sharp", ""},
@@ -82,6 +83,7 @@ const EnumPropertyItem rna_enum_brush_sculpt_tool_items[] = {
     {SCULPT_TOOL_FLATTEN, "FLATTEN", ICON_BRUSH_FLATTEN, "Flatten", ""},
     {SCULPT_TOOL_FILL, "FILL", ICON_BRUSH_FILL, "Fill", ""},
     {SCULPT_TOOL_SCRAPE, "SCRAPE", ICON_BRUSH_SCRAPE, "Scrape", ""},
+    {SCULPT_TOOL_MULTIPLANE_SCRAPE, "MULTIPLANE_SCRAPE", ICON_BRUSH_SCRAPE, "Multiplane Scrape", ""},
     {SCULPT_TOOL_PINCH, "PINCH", ICON_BRUSH_PINCH, "Pinch", ""},
     {0, "", 0, NULL, NULL},
     {SCULPT_TOOL_GRAB, "GRAB", ICON_BRUSH_GRAB, "Grab", ""},
@@ -96,6 +98,7 @@ const EnumPropertyItem rna_enum_brush_sculpt_tool_items[] = {
     {SCULPT_TOOL_MASK, "MASK", ICON_BRUSH_MASK, "Mask", ""},
     {0, NULL, 0, NULL, NULL},
 };
+/* clang-format on */
 
 const EnumPropertyItem rna_enum_brush_uv_sculpt_tool_items[] = {
     {UV_SCULPT_TOOL_GRAB, "GRAB", 0, "Grab", "Grab UVs"},
@@ -1878,6 +1881,12 @@ static void rna_def_brush(BlenderRNA *brna)
       prop, "Pose Origin Offset", "Offset of the pose origin in relation to the brush radius");
   RNA_def_property_update(prop, 0, "rna_Brush_update");
 
+  prop = RNA_def_property(srna, "multiplane_scrape_angle", PROP_FLOAT, PROP_FACTOR);
+  RNA_def_property_float_sdna(prop, NULL, "multiplane_scrape_angle");
+  RNA_def_property_range(prop, 0.0f, 160.0f);
+  RNA_def_property_ui_text(prop, "Plane Angle", "Angle between the planes of the crease");
+  RNA_def_property_update(prop, 0, "rna_Brush_update");
+
   prop = RNA_def_property(srna, "auto_smooth_factor", PROP_FLOAT, PROP_FACTOR);
   RNA_def_property_float_sdna(prop, NULL, "autosmooth_factor");
   RNA_def_property_float_default(prop, 0);
@@ -2026,6 +2035,19 @@ static void rna_def_brush(BlenderRNA *brna)
   prop = RNA_def_property(srna, "use_paint_antialiasing", PROP_BOOLEAN, PROP_NONE);
   RNA_def_property_boolean_sdna(prop, NULL, "sampling_flag", BRUSH_PAINT_ANTIALIASING);
   RNA_def_property_ui_text(prop, "Antialasing", "Smooths the edges of the strokes");
+
+  prop = RNA_def_property(srna, "use_multiplane_scrape_dynamic", PROP_BOOLEAN, PROP_NONE);
+  RNA_def_property_boolean_sdna(prop, NULL, "flag2", BRUSH_MULTIPLANE_SCRAPE_DYNAMIC);
+  RNA_def_property_ui_text(prop,
+                           "Dynamic Mode",
+                           "The angle between the planes changes during the stroke to fit the "
+                           "surface under the cursor");
+  RNA_def_property_update(prop, 0, "rna_Brush_update");
+
+  prop = RNA_def_property(srna, "show_multiplane_scrape_planes_preview", PROP_BOOLEAN, PROP_NONE);
+  RNA_def_property_boolean_sdna(prop, NULL, "flag2", BRUSH_MULTIPLANE_SCRAPE_PLANES_PREVIEW);
+  RNA_def_property_ui_text(
+      prop, "Show Cursor Preview", "Preview the scrape planes in the cursor during the stroke");
   RNA_def_property_update(prop, 0, "rna_Brush_update");
 
   prop = RNA_def_property(srna, "use_pressure_strength", PROP_BOOLEAN, PROP_NONE);