Improving non-circular superellipse bevel profiles

Hello all—I’ve never contributed to Blender before, but I’ve recently taken an interest in making some improvements to the bevel modifier to better support non-circular superellipse profiles. I’ve outlined the motivation at some length in a Blender Stack Exchange question, but the executive summary is that currently, beveling using a rectangular (i.e. “Profile Shape” >0.50) superellipse profile produces a lot of unhelpful geometry:

In the above image, I’ve used 0.85 for the profile shape, which produces a nice curve, but requires 35 bevel segments! That’s totally unnecessary—the bevel modifier currently tries to make each bevel segment equal width, but that means a lot of the faces end up nearly coplanar. What I really want is for the edges to be denser at the corners, where there’s more curvature.

To fix this, I’ve taken a stab at implementing a different algorithm, which produces results like this, instead:

As you can see, the result looks quite nice despite the small amount of geometry. My current approach is also relatively computationally inexpensive compared to the existing bevel spacing strategy, since it uses a closed-form solution for each vertex’s location, avoiding the iterative approximation process needed by the equal-width strategy. Of course, all that said, I do not propose replacing the existing spacing algorithm, only adding an alternative option; you can see the option listed as “Profile Spacing” in the screenshot above.


My patch is currently quite small, but I unfortunately can’t claim it’s nearly complete yet. Although it seems to work well on simple examples like the one above, I have not yet implemented n-ary intersections (called M_ADJ in the code) or miters, so beveling all edges of a cube using this strategy produces some highly unsatisfactory results at the corners:

I am currently looking into fixing this, but this is quite a bit trickier, so it would be nice to know if this is something other people would be interested in before I go to the effort of figuring out how to do it properly. If anyone has any thoughts, questions, suggestions, or words of encouragement, it would be appreciated. :slight_smile:

Also, if anyone is interested in trying this out for themselves, I have included my very work-in-progress patch below, since it really is very small:

bevel-angle-spacing.patch
diff --git a/release/scripts/presets/keyconfig/keymap_data/blender_default.py b/release/scripts/presets/keyconfig/keymap_data/blender_default.py
index 9404bfe..b5bfd8a 100644
--- a/release/scripts/presets/keyconfig/keymap_data/blender_default.py
+++ b/release/scripts/presets/keyconfig/keymap_data/blender_default.py
@@ -5396,6 +5396,7 @@ def km_bevel_modal_map(_params):
         ("OUTER_MITER_CHANGE", {"type": 'O', "value": 'PRESS', "any": True}, None),
         ("INNER_MITER_CHANGE", {"type": 'I', "value": 'PRESS', "any": True}, None),
         ("PROFILE_TYPE_CHANGE", {"type": 'Z', "value": 'PRESS', "any": True}, None),
+        ("PROFILE_SPACING_CHANGE", {"type": 'G', "value": 'PRESS', "any": True}, None),
         ("VERTEX_MESH_CHANGE", {"type": 'N', "value": 'PRESS', "any": True}, None),
     ])
 
diff --git a/source/blender/bmesh/intern/bmesh_opdefines.c b/source/blender/bmesh/intern/bmesh_opdefines.c
index bccac00..6e7c4a4 100644
--- a/source/blender/bmesh/intern/bmesh_opdefines.c
+++ b/source/blender/bmesh/intern/bmesh_opdefines.c
@@ -1732,6 +1732,12 @@ static BMO_FlagSet bmo_enum_bevel_profile_type[] = {
   {0, NULL},
 };
 
+static BMO_FlagSet bmo_enum_bevel_spacing_type[] = {
+  {BEVEL_SPACING_WIDTH, "WIDTH"},
+  {BEVEL_SPACING_ANGLE, "ANGLE"},
+  {0, NULL},
+};
+
 static BMO_FlagSet bmo_enum_bevel_face_strength_type[] = {
   {BEVEL_FACE_STRENGTH_NONE, "NONE"},
   {BEVEL_FACE_STRENGTH_NEW, "NEW"},
@@ -1775,6 +1781,8 @@ static BMOpDefine bmo_bevel_def = {
     bmo_enum_bevel_profile_type},         /* The profile type to use for bevel. */
    {"segments", BMO_OP_SLOT_INT},         /* number of segments in bevel */
    {"profile", BMO_OP_SLOT_FLT},          /* profile shape, 0->1 (.5=>round) */
+   {"profile_spacing", BMO_OP_SLOT_INT, {(int)BMO_OP_SLOT_SUBTYPE_INT_ENUM},
+    bmo_enum_bevel_spacing_type},         /* Whether to space segments by width or angle. */
    {"affect", BMO_OP_SLOT_INT, {(int)BMO_OP_SLOT_SUBTYPE_INT_ENUM},
     bmo_enum_bevel_affect_type},          /* Whether to bevel vertices or edges. */
    {"clamp_overlap", BMO_OP_SLOT_BOOL},   /* do not allow beveled edges/vertices to overlap each other */
diff --git a/source/blender/bmesh/intern/bmesh_operator_api.h b/source/blender/bmesh/intern/bmesh_operator_api.h
index 706979a..cd52ea7 100644
--- a/source/blender/bmesh/intern/bmesh_operator_api.h
+++ b/source/blender/bmesh/intern/bmesh_operator_api.h
@@ -295,7 +295,7 @@ typedef struct BMOpSlot {
              ((slot >= (op)->slots_out) && (slot < &(op)->slots_out[BMO_OP_MAX_SLOTS])))
 
 /* Limit hit, so expanded for bevel operator. Compiler complains if limit is hit. */
-#define BMO_OP_MAX_SLOTS 21
+#define BMO_OP_MAX_SLOTS 22
 
 /* BMOpDefine->type_flag */
 typedef enum {
diff --git a/source/blender/bmesh/intern/bmesh_operators.h b/source/blender/bmesh/intern/bmesh_operators.h
index 2d9e244..217b1b1 100644
--- a/source/blender/bmesh/intern/bmesh_operators.h
+++ b/source/blender/bmesh/intern/bmesh_operators.h
@@ -117,6 +117,12 @@ enum {
   BEVEL_PROFILE_CUSTOM,
 };
 
+/* Bevel superellipse sample spacing mode */
+enum {
+  BEVEL_SPACING_WIDTH,
+  BEVEL_SPACING_ANGLE,
+};
+
 /* Bevel face_strength_mode values: should match face_str mode enum in DNA_modifier_types.h */
 enum {
   BEVEL_FACE_STRENGTH_NONE,
diff --git a/source/blender/bmesh/operators/bmo_bevel.c b/source/blender/bmesh/operators/bmo_bevel.c
index 4e708b5..8a7c291 100644
--- a/source/blender/bmesh/operators/bmo_bevel.c
+++ b/source/blender/bmesh/operators/bmo_bevel.c
@@ -37,6 +37,7 @@ void bmo_bevel_exec(BMesh *bm, BMOperator *op)
   const int seg = BMO_slot_int_get(op->slots_in, "segments");
   const int affect_type = BMO_slot_int_get(op->slots_in, "affect");
   const float profile = BMO_slot_float_get(op->slots_in, "profile");
+  const int profile_spacing = BMO_slot_int_get(op->slots_in, "profile_spacing");
   const bool clamp_overlap = BMO_slot_bool_get(op->slots_in, "clamp_overlap");
   const int material = BMO_slot_int_get(op->slots_in, "material");
   const bool loop_slide = BMO_slot_bool_get(op->slots_in, "loop_slide");
@@ -79,6 +80,7 @@ void bmo_bevel_exec(BMesh *bm, BMOperator *op)
                   profile_type,
                   seg,
                   profile,
+                  profile_spacing,
                   affect_type,
                   false,
                   clamp_overlap,
diff --git a/source/blender/bmesh/tools/bmesh_bevel.c b/source/blender/bmesh/tools/bmesh_bevel.c
index cef97f2..78253c3 100644
--- a/source/blender/bmesh/tools/bmesh_bevel.c
+++ b/source/blender/bmesh/tools/bmesh_bevel.c
@@ -342,6 +342,8 @@ typedef struct BevelParams {
   float profile;
   /** Superellipse parameter for edge profile. */
   float pro_super_r;
+  /** Superellipse sample spacing mode: even widths or even angles. */
+  int profile_spacing;
   /** Bevel amount affected by weights on edges or verts. */
   bool use_weights;
   /** Should bevel prefer to slide along edges rather than keep widths spec? */
@@ -4443,7 +4445,9 @@ static int tri_corner_test(BevelParams *bp, BevVert *bv)
   int in_plane_e = 0;
 
   /* The superellipse snapping of this case isn't helpful with custom profiles enabled. */
-  if (bp->affect_type == BEVEL_AFFECT_VERTICES || bp->profile_type == BEVEL_PROFILE_CUSTOM) {
+  if (bp->affect_type == BEVEL_AFFECT_VERTICES
+      || bp->profile_type == BEVEL_PROFILE_CUSTOM
+      || bp->profile_spacing == BEVEL_SPACING_ANGLE) {
     return -1;
   }
   if (bv->vmesh->count != 3) {
@@ -6919,7 +6923,7 @@ static double find_superellipse_chord_endpoint(double x0, double dtarget, float
  * for r<1 use only x in [mx,1]. Points are initially spaced and iteratively
  * repositioned to have the same distance.
  */
-static void find_even_superellipse_chords_general(int seg, float r, double *xvals, double *yvals)
+static void find_even_width_superellipse_chords_general(int seg, float r, double *xvals, double *yvals)
 {
   const int smoothitermax = 10;
   const double error_tol = 1e-7;
@@ -7019,7 +7023,7 @@ static void find_even_superellipse_chords_general(int seg, float r, double *xval
  * form for equidistant parametrization.
  * xvals and yvals should be size n+1.
  */
-static void find_even_superellipse_chords(int n, float r, double *xvals, double *yvals)
+static void find_even_width_superellipse_chords(int n, float r, double *xvals, double *yvals)
 {
   bool seg_odd = n % 2;
   int n2 = n / 2;
@@ -7087,7 +7091,73 @@ static void find_even_superellipse_chords(int n, float r, double *xvals, double
     return;
   }
   /* For general case use the more expensive search algorithm. */
-  find_even_superellipse_chords_general(n, r, xvals, yvals);
+  find_even_width_superellipse_chords_general(n, r, xvals, yvals);
+}
+
+static void find_even_angle_superellipse_chords(int n, float r, double *xvals, double *yvals)
+{
+  int n2 = n / 2;
+
+  if (r == PRO_SQUARE_IN_R) {
+    xvals[0] = 0;
+    yvals[0] = 1;
+    xvals[n] = 1;
+    yvals[n] = 0;
+    for (int i = 0; i < n; i++) {
+      xvals[i] = 0;
+      yvals[i] = 0;
+    }
+  }
+  else if (r == PRO_SQUARE_R) {
+    xvals[0] = 0;
+    yvals[0] = 1;
+    xvals[n] = 1;
+    yvals[n] = 0;
+    for (int i = 0; i < n; i++) {
+      xvals[i] = 1;
+      yvals[i] = 1;
+    }
+  }
+  else if (r <= 1.0) {
+    for (int i = 0; i <= n2; i++) {
+      double yval = pow(1.0 - (i / (double)n), 1.0 / r);
+      xvals[i] = 1.0 - superellipse_co(1.0 - yval, r, false);
+      yvals[i] = yval;
+      xvals[n - i] = yvals[i];
+      yvals[n - i] = xvals[i];
+    }
+  }
+  else if (r >= 2.0) {
+    double step_angle = (M_PI / 2) / n;
+    for (int i = 0; i <= n2; i++) {
+      xvals[i] = pow(sin(i * step_angle), 2.0 / r);
+      yvals[i] = superellipse_co(xvals[i], r, true);
+      xvals[n - i] = yvals[i];
+      yvals[n - i] = xvals[i];
+    }
+  }
+  else {
+    double interp_fac = r - 1.0;
+    double step_angle = (M_PI / 2) / n;
+    for (int i = 0; i <= n2; i++) {
+      double x_concave = pow(i / (double)n, 1.0 / r);
+      double x_convex = pow(sin(i * step_angle), 2.0 / r);
+      xvals[i] = interpd(x_convex, x_concave, interp_fac);
+      yvals[i] = superellipse_co(xvals[i], r, true);
+      xvals[n - i] = yvals[i];
+      yvals[n - i] = xvals[i];
+    }
+  }
+}
+
+static void find_even_superellipse_chords(int n, float r, double *xvals, double *yvals, int spacing)
+{
+  switch (spacing) {
+    case BEVEL_SPACING_WIDTH:
+      return find_even_width_superellipse_chords(n, r, xvals, yvals);
+    case BEVEL_SPACING_ANGLE:
+      return find_even_angle_superellipse_chords(n, r, xvals, yvals);
+  }
 }
 
 /**
@@ -7195,7 +7265,7 @@ static void set_profile_spacing(BevelParams *bp, ProfileSpacing *pro_spacing, bo
     }
     else {
       find_even_superellipse_chords(
-          seg_2, bp->pro_super_r, pro_spacing->xvals_2, pro_spacing->yvals_2);
+          seg_2, bp->pro_super_r, pro_spacing->xvals_2, pro_spacing->yvals_2, bp->profile_spacing);
     }
   }
 
@@ -7215,7 +7285,7 @@ static void set_profile_spacing(BevelParams *bp, ProfileSpacing *pro_spacing, bo
     }
   }
   else {
-    find_even_superellipse_chords(seg, bp->pro_super_r, pro_spacing->xvals, pro_spacing->yvals);
+    find_even_superellipse_chords(seg, bp->pro_super_r, pro_spacing->xvals, pro_spacing->yvals, bp->profile_spacing);
   }
 }
 
@@ -7457,6 +7527,7 @@ void BM_mesh_bevel(BMesh *bm,
                    const int profile_type,
                    const int segments,
                    const float profile,
+                   const int profile_spacing,
                    const bool affect_type,
                    const bool use_weights,
                    const bool limit_offset,
@@ -7488,6 +7559,7 @@ void BM_mesh_bevel(BMesh *bm,
       .seg = max_ii(segments, 1),
       .profile = profile,
       .pro_super_r = -logf(2.0) / logf(sqrtf(profile)), /* Convert to superellipse exponent. */
+      .profile_spacing = profile_spacing,
       .affect_type = affect_type,
       .use_weights = use_weights,
       .loop_slide = loop_slide,
diff --git a/source/blender/bmesh/tools/bmesh_bevel.h b/source/blender/bmesh/tools/bmesh_bevel.h
index de57e1c..5a46135 100644
--- a/source/blender/bmesh/tools/bmesh_bevel.h
+++ b/source/blender/bmesh/tools/bmesh_bevel.h
@@ -29,6 +29,7 @@ void BM_mesh_bevel(BMesh *bm,
                    const int profile_type,
                    const int segments,
                    const float profile,
+                   const int profile_spacing,
                    const bool affect_type,
                    const bool use_weights,
                    const bool limit_offset,
diff --git a/source/blender/editors/mesh/editmesh_bevel.c b/source/blender/editors/mesh/editmesh_bevel.c
index 43492cd..13bf95b 100644
--- a/source/blender/editors/mesh/editmesh_bevel.c
+++ b/source/blender/editors/mesh/editmesh_bevel.c
@@ -121,6 +121,7 @@ enum {
   BEV_MODAL_OUTER_MITER_CHANGE,
   BEV_MODAL_INNER_MITER_CHANGE,
   BEV_MODAL_PROFILE_TYPE_CHANGE,
+  BEV_MODAL_PROFILE_SPACING_CHANGE,
   BEV_MODAL_VERTEX_MESH_CHANGE,
 };
 
@@ -160,13 +161,17 @@ static void edbm_bevel_update_status_text(bContext *C, wmOperator *op)
   }
 
   PropertyRNA *prop;
-  const char *mode_str, *omiter_str, *imiter_str, *vmesh_str, *profile_type_str, *affect_str;
+  const char *mode_str, *omiter_str, *imiter_str, *vmesh_str, *profile_type_str,
+    *profile_spacing_str, *affect_str;
   prop = RNA_struct_find_property(op->ptr, "offset_type");
   RNA_property_enum_name_gettexted(
       C, op->ptr, prop, RNA_property_enum_get(op->ptr, prop), &mode_str);
   prop = RNA_struct_find_property(op->ptr, "profile_type");
   RNA_property_enum_name_gettexted(
       C, op->ptr, prop, RNA_property_enum_get(op->ptr, prop), &profile_type_str);
+  prop = RNA_struct_find_property(op->ptr, "profile_spacing");
+  RNA_property_enum_name_gettexted(
+      C, op->ptr, prop, RNA_property_enum_get(op->ptr, prop), &profile_spacing_str);
   prop = RNA_struct_find_property(op->ptr, "miter_outer");
   RNA_property_enum_name_gettexted(
       C, op->ptr, prop, RNA_property_enum_get(op->ptr, prop), &omiter_str);
@@ -196,6 +201,7 @@ static void edbm_bevel_update_status_text(bContext *C, wmOperator *op)
                     "%s: Mark Seam (%s), "
                     "%s: Mark Sharp (%s), "
                     "%s: Profile Type (%s), "
+                    "%s: Spacing (%s), "
                     "%s: Intersection (%s)"),
                WM_MODALKEY(BEV_MODAL_CONFIRM),
                WM_MODALKEY(BEV_MODAL_CANCEL),
@@ -223,6 +229,8 @@ static void edbm_bevel_update_status_text(bContext *C, wmOperator *op)
                WM_bool_as_string(RNA_boolean_get(op->ptr, "mark_sharp")),
                WM_MODALKEY(BEV_MODAL_PROFILE_TYPE_CHANGE),
                profile_type_str,
+               WM_MODALKEY(BEV_MODAL_PROFILE_SPACING_CHANGE),
+               profile_spacing_str,
                WM_MODALKEY(BEV_MODAL_VERTEX_MESH_CHANGE),
                vmesh_str);
 
@@ -328,6 +336,7 @@ static bool edbm_bevel_calc(wmOperator *op)
   const int profile_type = RNA_enum_get(op->ptr, "profile_type");
   const int segments = RNA_int_get(op->ptr, "segments");
   const float profile = RNA_float_get(op->ptr, "profile");
+  const int profile_spacing = RNA_enum_get(op->ptr, "profile_spacing");
   const bool affect = RNA_enum_get(op->ptr, "affect");
   const bool clamp_overlap = RNA_boolean_get(op->ptr, "clamp_overlap");
   const int material_init = RNA_int_get(op->ptr, "material");
@@ -363,10 +372,10 @@ static bool edbm_bevel_calc(wmOperator *op)
                  &bmop,
                  op,
                  "bevel geom=%hev offset=%f segments=%i affect=%i offset_type=%i "
-                 "profile_type=%i profile=%f clamp_overlap=%b material=%i loop_slide=%b "
-                 "mark_seam=%b mark_sharp=%b harden_normals=%b face_strength_mode=%i "
-                 "miter_outer=%i miter_inner=%i spread=%f smoothresh=%f custom_profile=%p "
-                 "vmesh_method=%i",
+                 "profile_type=%i profile=%f profile_spacing=%i clamp_overlap=%b "
+                 "material=%i loop_slide=%b mark_seam=%b mark_sharp=%b harden_normals=%b "
+                 "face_strength_mode=%i miter_outer=%i miter_inner=%i spread=%f "
+                 "smoothresh=%f custom_profile=%p vmesh_method=%i",
                  BM_ELEM_SELECT,
                  offset,
                  segments,
@@ -374,6 +383,7 @@ static bool edbm_bevel_calc(wmOperator *op)
                  offset_type,
                  profile_type,
                  profile,
+                 profile_spacing,
                  clamp_overlap,
                  material,
                  loop_slide,
@@ -654,6 +664,7 @@ wmKeyMap *bevel_modal_keymap(wmKeyConfig *keyconf)
        "Change Inner Miter",
        "Cycle through inner miter kinds"},
       {BEV_MODAL_PROFILE_TYPE_CHANGE, "PROFILE_TYPE_CHANGE", 0, "Cycle through profile types", ""},
+      {BEV_MODAL_PROFILE_SPACING_CHANGE, "PROFILE_SPACING_CHANGE", 0, "Cycle through profile spacing modes", ""},
       {BEV_MODAL_VERTEX_MESH_CHANGE,
        "VERTEX_MESH_CHANGE",
        0,
@@ -881,6 +892,19 @@ static int edbm_bevel_modal(bContext *C, wmOperator *op, const wmEvent *event)
         break;
       }
 
+      case BEV_MODAL_PROFILE_SPACING_CHANGE: {
+        int profile_type = RNA_enum_get(op->ptr, "profile_spacing");
+        profile_type++;
+        if (profile_type > BEVEL_SPACING_ANGLE) {
+          profile_type = BEVEL_SPACING_WIDTH;
+        }
+        RNA_enum_set(op->ptr, "profile_spacing", profile_type);
+        edbm_bevel_calc(op);
+        edbm_bevel_update_status_text(C, op);
+        handled = true;
+        break;
+      }
+
       case BEV_MODAL_VERTEX_MESH_CHANGE: {
         int vmesh_method = RNA_enum_get(op->ptr, "vmesh_method");
         vmesh_method++;
@@ -978,7 +1002,11 @@ static void edbm_bevel_ui(bContext *C, wmOperator *op)
 
   row = uiLayoutRow(layout, false);
   uiItemR(row, op->ptr, "profile_type", UI_ITEM_R_EXPAND, NULL, ICON_NONE);
-  if (profile_type == BEVEL_PROFILE_CUSTOM) {
+  if (profile_type == BEVEL_PROFILE_SUPERELLIPSE) {
+    row = uiLayoutRow(layout, false);
+    uiItemR(row, op->ptr, "profile_spacing", UI_ITEM_R_EXPAND, NULL, ICON_NONE);
+  }
+  else if (profile_type == BEVEL_PROFILE_CUSTOM) {
     /* Get an RNA pointer to ToolSettings to give to the curve profile template code. */
     Scene *scene = CTX_data_scene(C);
     RNA_pointer_create(&scene->id, &RNA_ToolSettings, scene->toolsettings, &toolsettings_ptr);
@@ -1021,6 +1049,20 @@ void MESH_OT_bevel(wmOperatorType *ot)
       {0, NULL, 0, NULL, NULL},
   };
 
+  static const EnumPropertyItem prop_profile_spacing_items[] = {
+      {BEVEL_SPACING_WIDTH,
+       "WIDTH",
+       0,
+       "Width",
+       "Produce evenly-sized bevel segments"},
+      {BEVEL_SPACING_ANGLE,
+       "ANGLE",
+       0,
+       "Angle",
+       "Adaptively size bevel segments to minimize variation in angle"},
+      {0, NULL, 0, NULL, NULL},
+  };
+
   static const EnumPropertyItem face_strength_mode_items[] = {
       {BEVEL_FACE_STRENGTH_NONE, "NONE", 0, "None", "Do not set face strength"},
       {BEVEL_FACE_STRENGTH_NEW, "NEW", 0, "New", "Set face strength on new faces only"},
@@ -1121,6 +1163,13 @@ void MESH_OT_bevel(wmOperatorType *ot)
                 PROFILE_HARD_MIN,
                 1.0f);
 
+  RNA_def_enum(ot->srna,
+               "profile_spacing",
+               prop_profile_spacing_items,
+               0,
+               "Profile Spacing",
+               "Method for distributing bevel segments along the profile");
+
   RNA_def_enum(ot->srna,
                "affect",
                prop_affect_items,
diff --git a/source/blender/makesdna/DNA_modifier_defaults.h b/source/blender/makesdna/DNA_modifier_defaults.h
index d8e48c5..bc53449 100644
--- a/source/blender/makesdna/DNA_modifier_defaults.h
+++ b/source/blender/makesdna/DNA_modifier_defaults.h
@@ -55,7 +55,7 @@
     .res = 1, \
     .flags = 0, \
     .val_flags = MOD_BEVEL_AMT_OFFSET, \
-    .profile_type = MOD_BEVEL_PROFILE_SUPERELLIPSE, \
+    .profile_type = MOD_BEVEL_SPACING_WIDTH, \
     .lim_flags = MOD_BEVEL_ANGLE, \
     .e_flags = 0, \
     .mat = -1, \
@@ -65,6 +65,7 @@
     .miter_outer = MOD_BEVEL_MITER_SHARP, \
     .affect_type = MOD_BEVEL_AFFECT_EDGES, \
     .profile = 0.5f, \
+    .profile_spacing = MOD_BEVEL_SPACING_WIDTH, \
     .bevel_angle = DEG2RADF(30.0f), \
     .spread = 0.1f, \
     .defgrp_name = "", \
diff --git a/source/blender/makesdna/DNA_modifier_types.h b/source/blender/makesdna/DNA_modifier_types.h
index ca6f146..d4f9129 100644
--- a/source/blender/makesdna/DNA_modifier_types.h
+++ b/source/blender/makesdna/DNA_modifier_types.h
@@ -434,7 +434,8 @@ typedef struct BevelModifierData {
   short vmesh_method;
   /** Whether to affect vertices or edges. */
   char affect_type;
-  char _pad;
+  /** TODO */
+  char profile_spacing;
   /** Controls profile shape (0->1, .5 is round). */
   float profile;
   /** if the MOD_BEVEL_ANGLE is set,
@@ -489,6 +490,12 @@ enum {
   MOD_BEVEL_PROFILE_CUSTOM = 1,
 };
 
+/* BevelModifierData->profile_spacing */
+enum {
+  MOD_BEVEL_SPACING_WIDTH = 0,
+  MOD_BEVEL_SPACING_ANGLE = 1,
+};
+
 /* BevelModifierData->edge_flags */
 enum {
   MOD_BEVEL_MARK_SEAM = (1 << 0),
diff --git a/source/blender/makesrna/intern/rna_modifier.c b/source/blender/makesrna/intern/rna_modifier.c
index 67335b8..d501583 100644
--- a/source/blender/makesrna/intern/rna_modifier.c
+++ b/source/blender/makesrna/intern/rna_modifier.c
@@ -4027,6 +4027,20 @@ static void rna_def_modifier_bevel(BlenderRNA *brna)
       {0, NULL, 0, NULL, NULL},
   };
 
+  static const EnumPropertyItem prop_profile_spacing_items[] = {
+      {MOD_BEVEL_SPACING_WIDTH,
+       "WIDTH",
+       0,
+       "Width",
+       "Produce evenly-sized bevel segments"},
+      {MOD_BEVEL_SPACING_ANGLE,
+       "ANGLE",
+       0,
+       "Angle",
+       "Adaptively size bevel segments to minimize variation in angle"},
+      {0, NULL, 0, NULL, NULL},
+  };
+
   static EnumPropertyItem prop_harden_normals_items[] = {
       {MOD_BEVEL_FACE_STRENGTH_NONE, "FSTR_NONE", 0, "None", "Do not set face strength"},
       {MOD_BEVEL_FACE_STRENGTH_NEW, "FSTR_NEW", 0, "New", "Set face strength on new faces only"},
@@ -4150,6 +4164,13 @@ static void rna_def_modifier_bevel(BlenderRNA *brna)
   RNA_def_property_ui_text(prop, "Profile", "The profile shape (0.5 = round)");
   RNA_def_property_update(prop, 0, "rna_Modifier_update");
 
+  prop = RNA_def_property(srna, "profile_spacing", PROP_ENUM, PROP_NONE);
+  RNA_def_property_enum_sdna(prop, NULL, "profile_spacing");
+  RNA_def_property_enum_items(prop, prop_profile_spacing_items);
+  RNA_def_property_ui_text(
+      prop, "Spacing", "Method for distributing bevel segments along the profile");
+  RNA_def_property_update(prop, 0, "rna_Modifier_update");
+
   prop = RNA_def_property(srna, "material", PROP_INT, PROP_NONE);
   RNA_def_property_int_sdna(prop, NULL, "mat");
   RNA_def_property_range(prop, -1, SHRT_MAX);
diff --git a/source/blender/modifiers/intern/MOD_bevel.c b/source/blender/modifiers/intern/MOD_bevel.c
index a94411d..4a28e03 100644
--- a/source/blender/modifiers/intern/MOD_bevel.c
+++ b/source/blender/modifiers/intern/MOD_bevel.c
@@ -218,6 +218,7 @@ static Mesh *modifyMesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh *
                 profile_type,
                 bmd->res,
                 bmd->profile,
+                bmd->profile_spacing,
                 bmd->affect_type,
                 bmd->lim_flags & MOD_BEVEL_WEIGHT,
                 do_clamp,
@@ -338,7 +339,10 @@ static void profile_panel_draw(const bContext *UNUSED(C), Panel *panel)
                                                                IFACE_("Miter Shape"),
             ICON_NONE);
 
-    if (profile_type == MOD_BEVEL_PROFILE_CUSTOM) {
+    if (profile_type == MOD_BEVEL_PROFILE_SUPERELLIPSE) {
+      uiItemR(layout, ptr, "profile_spacing", UI_ITEM_R_EXPAND, NULL, ICON_NONE);
+    }
+    else if (profile_type == MOD_BEVEL_PROFILE_CUSTOM) {
       uiLayout *sub = uiLayoutColumn(layout, false);
       uiLayoutSetPropDecorate(sub, false);
       uiTemplateCurveProfile(sub, ptr, "custom_profile");

I’m not sure who currently “owns” the bevel code, so to speak, especially since I imagine much of it has existed for a very long time, but it would be super helpful if anyone could give me a basic overview of how all the BevVert/VMesh stuff fits together. I’ve mostly been able to figure things out on my own so far, and I imagine I can continue to do so given enough time, but I suspect a little extra perspective would go a long way.

17 Likes

You can create a patch in developer.blender.org and keep working on it there, this way you may mention some experienced Blender developer that can help you out and guide you over some things like code style that may be problematic later in the process if you have not had them into account :slight_smile:

I think @Howard_Trickey and @mano-wii have a lot of experience working with these things.

3 Likes

@lexi-lambda
I wrote most of the Bevel code, and now it is currently jointly owned by Hans Goudy (who wrote the custom profile stuff) and me. It is a bit ironic to me that you desire non-equal-width segments, because it was quite a bit of trouble to achieve that (as you’ve seen: since there is no closed-form formula to get that spacing, I had to write an iterative routine to do it).

As to whether such a patch would be accepted: it comes down to a tension between added value for users vs added complexity of the user interface (users need to understand the option, it needs to be documented, it needs to be maintained into the future, etc.). Bevel is already the operator with by far the most options. Ton has always had the philosophy of minimizing the number of options – the code should just “do the right thing” without the need for options. So every option I’ve added to Bevel has come with that agonizing debate.

What do other users here think: is this a valuable addition to Blender / Bevel, worth an extra option?

I wrote some requirements and implementation notes about bevel here, though it is now somewhat out of date, and isn’t quite as detailed as what you are looking for, I think. Feel free to chat me on blender.chat if you have more questions.

5 Likes

This seems really nice, I’d love it to be considered for inclusion. This seems closer to the way we tend to try and distribute curvature in traditional subd modeling.

2 Likes

You can create a patch in developer.blender.org and keep working on it there

Yes, I’ve been meaning to figure out how Arcanist works, since I have never used it before, but I will submit a patch eventually. I still have some code cleanup to do and comments to write, though, and I also probably need to write some test cases.

Yes, I can appreciate the irony. It’s perhaps worth noting that, in the common case of a circular bevel (i.e. profile = 0.5), the two strategies produce the same results. In any case, I imagine the equal-widths strategy is sometimes very useful, especially when the bevel is used destructively to create geometry that’s manually altered later.

Yes, I totally understand this, and I agree that it feels like Bevel in particular is possibly getting out of hand. My hope was originally that this would be a very small, simple change, but while it did turn out that the basic patch is quite small, I think updating the join/miter code to accommodate the different spacing strategy will probably be meaningfully more involved.

If it were practical, I’d have considered implementing my Bevel variant as an add-on first, rather than trying to modify the built-in operator itself. Unfortunately, my understanding is that is currently not possible, or if it is, it would at least be very hard:

  1. I really want to be able to use this feature non-destructively, as a modifier, and my understanding is that it is currently not possible to add custom modifiers via add-ons. And while the Geometry Nodes project looks super cool as far as making the modifier stack more flexible goes, implementing something like Bevel in it seems totally out of scope for the foreseeable future.

  2. Even if I were to accept using the operator exclusively destructively, there is a lot of logic in the existing Bevel operator that I would have to duplicate in my custom add-on, so I’d really like to be able to share all that code if at all possible.

I do think there are some reasons to hope this feature might be able to pay its weight despite being even more Bevel feature creep. For one, it’s pretty immediately obvious what it does, so documenting it to users should be pretty easy, and it only requires adding a single extra (boolean) option. For another, it seems pretty generally useful to me, especially for hard surface modeling workflows. But obviously this is just my perspective.

A little more generally, I do think the destructive Bevel operator options UI (i.e. the F9 panel) has gotten rather cluttered—I strongly suspect things like the Material Index option don’t need to be exposed there, since that only really makes sense when using Bevel as a modifier. So maybe some of the UX complexity could be ameliorated by rearranging that UI somewhat.

Thank you, this is all super helpful! I still need to take the time to read it more thoroughly, but I will certainly take you up on your offer if I have any questions. :slight_smile:

3 Likes

I don’t really have anything to add, except that I’d really like this option.

With the new GeometryNodes the importance of creating as-light-as-possible geometry for instancing has become a lot more important in my own projects.

Just last week I had to apply my bevel modifier just to be able to do a limited dissolve afterwards. I’d love to keep it non destructive.

2 Likes

Just figured I would post a small update. With the help of Howard’s very nice notes, I have taken a stab at implementing multi-way corners. I think the result is pretty decent:

The above render has relatively little geometry:

After quite some time fumbling around with different approaches, I finally managed to get something that mostly works (using a closed-form solution rather than subdivision), but there are still some problems. One is some visible distortion of some of the edge loops at the corners, which you can see in the above wireframe screenshot, which is probably impossible to avoid completely, but could be made a lot less obvious. Also, though I’m hopeful this approach will work on more complicated geometry, I haven’t tested it yet on anything other than a simple cube. I think there’s still probably a fair amount of work left to be done, but I’m a lot more confident now that it’s within my grasp to accomplish.

11 Likes

The video looks good!
As you have seen in the code, it already special cases some “cube corner” cases, so it is not out of the question to continue that path with your approach. Does it handle the case where you have cube corners but not with 90 degrees between all of the faces? Does it handle the case where there are extra in-plane edges attached to the corners (e.g., diagonals?).

It’s interesting to have this as an option-- however, I wanted to offer a different perspective. I think here we’re adding functionality to bevel that is a bit tangential to the basic purpose of the tool. That can be helpful, but sometimes when a single tool becomes so specific it’s a sign that it’s trying to do too much. Recently I have been thinking about how bevel would work as a node:


Here the “Profile” input is just curve data. The curve profile creates a curve primitive based on the preset, and then the bevel node takes care of sampling the curve data (with the existing curve sampling API that’s being added to geometry nodes soon).

I’m not sure it totally fits, but this functionality could be part of such a “curve profile” primitive node: i.e. it has a “Superellipe” preset. Then even length sampling is just another curve node that we’re planning on adding relatively soon anyway. This can be made faster with builtin node groups, or operators buttons to attach a profile node to the bevel node, etc.

I know this is a bit of a larger change, but I really feel like it’s the solution to the problem that keeps coming up-- the bevel modifier / operator / modifier is doing so much! That makes it harder to use, harder to code, less flexible, and more intimidating for newcomers.

3 Likes

Well, you do have a point!
On the other hand its not always possible nor practical or user-friendly to always follow the low-level approach.
I see this more as a problem with Blenders node paradigm to always cram all options directly onto small nodes instead of displaying those options in a designated area - messy looking nodegraphs, individual nodes having a horrible and confusing UI, endless UI discussions to reduce the complexity/features, and therefore slower progress.

I agree with your general sentiment, but I think what you’re describing is unfortunately difficult to use to achieve this particular goal. To explain why, I want to highlight that this change does not change profile contours themselves in any way. Given my current approach, this change requires you use the existing, baked-in superellipse contour.

What this change actually does is alter the way the contour is sampled. On paper, that doesn’t sound totally incompatible with the thrust of what you’re describing: intuitively, it sounds like maybe a Bevel node could accept a second curve (alongside the profile curve) that drives the sampler. Sadly, this is not enough:

  • Arbitrarily parameterizing the sampler works fine as long as each vertex is only connected to at most two beveled edges. But as soon as you have an intersection between three or more beveled edges, you have to perform a sort of grid fill, and it’s totally unclear (to me, anyway) how to parameterize that over a sampling strategy.

  • What’s more, generating intersections is so tricky that currently intersections with custom profiles produce very unsatisfying results, even given the hardwired sampling strategies. To quote the current manual:

    Without Custom Profile enabled, the curve of the profile continues through the intersection, but with a custom profile it just creates a smooth grid within the boundary of the intersection.

    This means that to remove the hardwired support for superellipse profiles, we’d need to somehow solve this problem in a far more general fashion than currently exists, something I am skeptical is even possible at all.

  • The current implementation has lots of special cases to handle all sorts of tricky scenarios, and those special cases rely upon details of the sampling strategy being known ahead of time. As part of this change, I have to update many of those special cases to handle the angle-based spacing, and somehow scripting those numerous special situations via nodes is obviously a non-starter.

So as much as I love the idea of moving more logic into nodes and simplifying various modifiers, I think that’s rather tricky to do here. A more tractable approach might be to provide some way to let add-ons define new modifiers and allow them to call into parts of the existing Bevel code, but that would obviously be an entirely different can of worms!

In the meantime, I believe this change can be implemented in a way that pays its weight, even if it sadly compounds the current complexity problem. Maybe beveling a mesh is just fundamentally complicated.

1 Like

I think a bevel node should contain all this already, but the curve profile could be useful for so many other things.

Yes, I have noticed that, but I am trying to see if I can avoid the special case for this code path for now. Only time will tell if I am successful or not.

It did not when I posted that video, but I have continued working on it, and it does now. Here is a beveled triangular pyramid as an example:

However, some problems begin to appear when the faces connected to the beveled edges are joined at irregular angles:

This issue occurs because of the way I sample the superellipsoid to construct the corner cap:

  • In the case of a three-way intersection, I can mostly get away with using a standard superellipsoid parameterization and transforming one quadrant appropriately to fit it to the corner. (In truth it’s a little more complicated than that, but only a little, and not important here.)

  • The question is therefore: how do we do the transformation to fit it into the corner? There are two obvious approaches:

    1. Form a basis by combining the BevVert’s location with the locations of the three boundary vertices.

    2. Form a basis by combining the BevVert’s location with appropriately chosen points along the beveled edges.

    Both approaches are equivalent if all faces are joined at regular angles, but are different in the irregular case.

  • Option 1 seems easiest, since you don’t have to figure out how to “appropriately choose” any points, so I tried that first. This unfortunately does not work because it turns out “appropriately choosing” the points is important. :slight_smile:

    More specifically, it’s crucially important that the corners of the chosen basis at (1,1,0), (1,0,1), and (0,1,1) fall upon the beveled edges so that the outermost ring of the grid fill (i.e. the vertices with j = 0 using VMesh coordinates) conforms to the profiles of the beveled edges themselves.

    (I apologize for not having a visualization of this, as I imagine my textual explanation is rather useless without thinking pretty hard, but I sadly did not think to commit before changing to Option 2, and I do not want to replicate the old behavior right now.)

  • Option 2 is what I have just implemented, which does indeed avoid the slope discontinuities caused by Option 1. However, it introduces a new problem, namely that the points (1,0,0), (0,1,0), and (0,0,1) may fall outside the polyhedron formed by the boundary vertices and the BevVert, which can lead to the sort of overlapping geometry shown above.

While this issue looks quite bad in the example I’ve shown above, it is actually rather easy to avoid in practice:

  • The issue only really appears with near-circular superellipse exponents, which largely do not benefit from the Angle spacing strategy. With larger superellipse exponents, the overlapping geometry is avoided, and the discontinuities are much less noticeable:

  • This issue does not occur at all with the Percent width type, since in that case, Option 1 and Option 2 are necessarily equivalent again:

    Using the Percent width type with the Angle-based spacing also confers other advantages, such as symmetry of spacing across beveled edges, so in practice, I find myself picking Percent most often when using large superellipse exponents, anyway.

It’s also worth noting that the existing Width spacing strategy handles shapes like this poorly, too, so it isn’t as if perfection in these situations is absolutely required. On this shape in particular, it produces a cap that does not follow the bevel slope particularly well:

In fact, the discontinuities produced by the existing strategy are even more noticeable on a regular pyramid:

Switching to my Angle strategy for this mesh clearly produces a better result, even when using a circular profile:

All this is to say that the current strategy seems passable on the three-way intersections I’ve tested, albeit not perfect. However, I think it may well be possible to come up with a solution that resolves the current issues, so I will keep experimenting.

Sort of. It can handle some simple cases, but not anything more than that. For starters, here is a successful example of beveling a cube after applying Poke Faces:

Unfortunately, anything more complicated than a cube, and significant problems start to appear. Here’s the result of my current algorithm beveling three of the six edges on a six-sided pyramid, chosen so an unbeveled edge bisects each pair of beveled edges:

This is fundamentally kind of tricky, because the natural placement of edge loops will create some pretty horribly non-planar quads:

It is genuinely unclear to me what to do in these situations. More obviously broken is what happens if I try to bevel an irregular set of edges along the same six-sided pyramid:

This is clearly just a broken mess, so there’s probably something more readily fixable happening here. I haven’t taken the time yet to figure out what it is, though.


Interestingly, under my current algorithm, n-way intersections almost work, despite the fact that I have made no concerted effort yet to handle them properly. Here is the result of beveling all six edges along a six-sided pyramid:

The curvature is very obviously wrong, but that seems probably fixable. I will have to look into it more, but that’s all I’ve got for now.

5 Likes

I will follow what you are trying with interest. One thing I’ve always wished for is to be able to prove, say, G2 continuity at the join seams between the edge polygons and the existing sides, and between the cap surfaces and the rest.

1 Like

Liked this comment because I don’t understand a thing and yet it’s fascinating. What’s G2 continuity @Howard_Trickey ? if you have a link or a short explanation

You can read about Geometric Continuity here.

1 Like