Modeling: add an angle threshold that affects vertex dissolve
When dissolving an edge merges faces, use an angle threshold before dissolving vertices from the face which have become chains as reult of the merge (connected to 2 edges). Also fix edge-flag handling when dissolving multiple edges from a chain into a single edge, previously flags from the resulting edge was effectively random. Now flags from all edges are merged. Resolves #100184. Ref !134017
This commit is contained in:
committed by
Campbell Barton
parent
6fbef14f4b
commit
e418f7b1f1
@@ -1344,6 +1344,8 @@ static BMOpDefine bmo_dissolve_edges_def = {
|
||||
{"use_verts", BMO_OP_SLOT_BOOL},
|
||||
/* Split off face corners to maintain surrounding geometry. */
|
||||
{"use_face_split", BMO_OP_SLOT_BOOL},
|
||||
/* Do not dissolve verts between 2 edges when their angle exceeds this threshold. */
|
||||
{"angle_threshold", BMO_OP_SLOT_FLT},
|
||||
{{'\0'}},
|
||||
},
|
||||
/*slot_types_out*/
|
||||
|
||||
@@ -595,7 +595,7 @@ bool BM_vert_is_edge_pair_manifold(const BMVert *v)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BM_vert_edge_pair(BMVert *v, BMEdge **r_e_a, BMEdge **r_e_b)
|
||||
bool BM_vert_edge_pair(const BMVert *v, BMEdge **r_e_a, BMEdge **r_e_b)
|
||||
{
|
||||
BMEdge *e_a = v->e;
|
||||
if (e_a) {
|
||||
|
||||
@@ -289,7 +289,7 @@ bool BM_vert_is_edge_pair_manifold(const BMVert *v) ATTR_WARN_UNUSED_RESULT ATTR
|
||||
*
|
||||
* \return true when only 2 verts are found.
|
||||
*/
|
||||
bool BM_vert_edge_pair(BMVert *v, BMEdge **r_e_a, BMEdge **r_e_b);
|
||||
bool BM_vert_edge_pair(const BMVert *v, BMEdge **r_e_a, BMEdge **r_e_b);
|
||||
/**
|
||||
* Return true if the vertex is connected to _any_ faces.
|
||||
*
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
* Removes isolated geometry regions without creating holes in the mesh.
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
#include "BLI_math_vector.h"
|
||||
@@ -31,6 +33,11 @@ using blender::Vector;
|
||||
#define EDGE_MARK 1
|
||||
#define EDGE_TAG 2
|
||||
#define EDGE_ISGC 8
|
||||
/**
|
||||
* Set when the edge is part of a chain,
|
||||
* where at least of it's vertices has exactly one other connected edge.
|
||||
*/
|
||||
#define EDGE_CHAIN 16
|
||||
|
||||
#define VERT_MARK 1
|
||||
#define VERT_MARK_PAIR 4
|
||||
@@ -38,6 +45,10 @@ using blender::Vector;
|
||||
#define VERT_ISGC 8
|
||||
#define VERT_MARK_TEAR 16
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Internal Utility API
|
||||
* \{ */
|
||||
|
||||
static bool UNUSED_FUNCTION(check_hole_in_region)(BMesh *bm, BMFace *f)
|
||||
{
|
||||
BMWalker regwalker;
|
||||
@@ -73,6 +84,58 @@ static bool UNUSED_FUNCTION(check_hole_in_region)(BMesh *bm, BMFace *f)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the angle of an edge pair, from a combination of raw angle and normal angle.
|
||||
*/
|
||||
static float bmo_vert_calc_edge_angle_blended(const BMVert *v)
|
||||
{
|
||||
BMEdge *e_pair[2];
|
||||
const bool is_edge_pair = BM_vert_edge_pair(v, &e_pair[0], &e_pair[1]);
|
||||
|
||||
BLI_assert(is_edge_pair);
|
||||
UNUSED_VARS_NDEBUG(is_edge_pair);
|
||||
|
||||
/* Compute the angle between the edges. Start with the raw angle. */
|
||||
BMVert *v_a = BM_edge_other_vert(e_pair[0], v);
|
||||
BMVert *v_b = BM_edge_other_vert(e_pair[1], v);
|
||||
float angle = M_PI - angle_v3v3v3(v_a->co, v->co, v_b->co);
|
||||
|
||||
/* There are two ways to measure the angle around a vert with two edges. The first is to
|
||||
* measure the raw angle between the two neighboring edges, the second is to measure the
|
||||
* angle of the edges around the vertex normal vector. When the vert is an edge pair
|
||||
* between two faces, The normal measurement is better in general. In the specific case of
|
||||
* a vert between two faces, but the faces have a *very* sharp angle between them, then the
|
||||
* raw angle is better, because the normal is perpendicular to average of the two faces,
|
||||
* and if the faces are folded almost 180 degrees, the vertex normal becomes more an more
|
||||
* edge-on to the faces, meaning the angle *around the normal* becomes more and more flat,
|
||||
* even if it makes a sharp angle when viewed from the side.
|
||||
*
|
||||
* When the faces become very folded, the `raw_factor` adds some of the "as seen from the side"
|
||||
* angle back into the computation, making the algorithm behave more intuitively.
|
||||
*
|
||||
* The `raw_factor` is computed as follows:
|
||||
* - When not a face pair, part this is skipped, and the raw angle is used.
|
||||
* - When a face pair is co-planar, or has an angle up to 90 degrees, `raw_factor` is 0.0.
|
||||
* - As angle increases from 90 to 180 degrees, `raw_factor` increases from 0.0 to 1.0.
|
||||
*/
|
||||
BMFace *f_pair[2];
|
||||
if (BM_edge_face_pair(v->e, &f_pair[0], &f_pair[1])) {
|
||||
/* Due to merges, the normals are not currently trustworthy. Compute them. */
|
||||
float no_a[3], no_b[3];
|
||||
BM_face_calc_normal(f_pair[0], no_a);
|
||||
BM_face_calc_normal(f_pair[1], no_b);
|
||||
|
||||
/* Now determine the raw factor based on how folded the faces are.*/
|
||||
const float raw_factor = std::clamp(-dot_v3v3(no_a, no_b), 0.0f, 1.0f);
|
||||
|
||||
/* Blend the two ways of computing the angle. */
|
||||
float normal_angle = M_PI - angle_on_axis_v3v3v3_v3(v_a->co, v->co, v_b->co, v->no);
|
||||
angle = interpf(angle, normal_angle, raw_factor);
|
||||
}
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
static void bm_face_split(BMesh *bm, const short oflag, bool use_edge_delete)
|
||||
{
|
||||
BLI_Stack *edge_delete_verts;
|
||||
@@ -117,6 +180,12 @@ static void bm_face_split(BMesh *bm, const short oflag, bool use_edge_delete)
|
||||
}
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Public Execute Functions
|
||||
* \{ */
|
||||
|
||||
void bmo_dissolve_faces_exec(BMesh *bm, BMOperator *op)
|
||||
{
|
||||
BMOIter oiter;
|
||||
@@ -258,7 +327,20 @@ void bmo_dissolve_edges_exec(BMesh *bm, BMOperator *op)
|
||||
BMEdge *e, *e_next;
|
||||
BMVert *v, *v_next;
|
||||
|
||||
const bool use_verts = BMO_slot_bool_get(op->slots_in, "use_verts");
|
||||
/* Even when geometry has exact angles like 0 or 90 or 180 deg, `angle_on_axis_v3v3v3_v3`
|
||||
* can return slightly incorrect values due to cos/sin functions, floating point error, etc.
|
||||
* This lets the test ignore that tiny bit of math error so users won't notice. */
|
||||
const float angle_epsilon = RAD2DEGF(0.0001f);
|
||||
|
||||
const float angle_threshold = BMO_slot_float_get(op->slots_in, "angle_threshold");
|
||||
|
||||
/* Use verts when told to... except, do *not* use verts when angle_threshold is 0.0. */
|
||||
const bool use_verts = BMO_slot_bool_get(op->slots_in, "use_verts") &&
|
||||
(angle_threshold > angle_epsilon);
|
||||
|
||||
/* If angle threshold is 180, don't bother with angle math, just dissolve everything. */
|
||||
const bool dissolve_all = (angle_threshold > M_PI - angle_epsilon);
|
||||
|
||||
const bool use_face_split = BMO_slot_bool_get(op->slots_in, "use_face_split");
|
||||
|
||||
if (use_face_split) {
|
||||
@@ -291,12 +373,22 @@ void bmo_dissolve_edges_exec(BMesh *bm, BMOperator *op)
|
||||
}
|
||||
}
|
||||
|
||||
/* tag all verts/edges connected to faces */
|
||||
/* Any element tagged with xxx_ISGC is an edge or vert of a face that borders an edge to be
|
||||
* dissolved, and it could end up being cleaned up after a face merge has made it irrelevant. */
|
||||
/* Tag certain geometry around the selected edges, for later processing. */
|
||||
BMO_ITER (e, &eiter, op->slots_in, "edges", BM_EDGE) {
|
||||
|
||||
/* Connected edge chains have endpoints with edge pairs. The existing behavior was to dissolve
|
||||
* the verts, both in the middle, and at the ends, of any selected edges in chains. Mark these
|
||||
* kind of edges, so we know to skip the angle threshold test later. */
|
||||
if (BM_vert_is_edge_pair(e->v1) || BM_vert_is_edge_pair(e->v2)) {
|
||||
BMO_edge_flag_enable(bm, e, EDGE_CHAIN);
|
||||
}
|
||||
|
||||
BMFace *f_pair[2];
|
||||
if (BM_edge_face_pair(e, &f_pair[0], &f_pair[1])) {
|
||||
/* Tag all the edges and verts of the two faces on either side of this edge.
|
||||
* This edge is going to be dissolved, and after that happens, some of those elements of the
|
||||
* surrounding faces might end up as loose geometry, depending on how the dissolve affected
|
||||
* geometry near them. Tag them `*_ISGC`, to be checked later, and cleaned up if loose. */
|
||||
uint j;
|
||||
for (j = 0; j < 2; j++) {
|
||||
BMLoop *l_first, *l_iter;
|
||||
@@ -335,12 +427,63 @@ void bmo_dissolve_edges_exec(BMesh *bm, BMOperator *op)
|
||||
|
||||
/* If dissolving verts, then evaluate each VERT_MARK vert. */
|
||||
if (use_verts) {
|
||||
BM_ITER_MESH_MUTABLE (v, v_next, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
if (BMO_vert_flag_test(bm, v, VERT_MARK)) {
|
||||
if (BM_vert_is_edge_pair(v)) {
|
||||
BM_vert_collapse_edge(bm, v->e, v, true, true, true);
|
||||
}
|
||||
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
if (!BMO_vert_flag_test(bm, v, VERT_MARK)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If it is not an edge pair, it cannot be merged. */
|
||||
BMEdge *e_pair[2];
|
||||
if (BM_vert_edge_pair(v, &e_pair[0], &e_pair[1]) == false) {
|
||||
BMO_vert_flag_disable(bm, v, VERT_MARK);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* At an angle threshold of 180, dissolve everything, skip the math of the angle test. */
|
||||
if (dissolve_all) {
|
||||
/* VERT_MARK remains enabled. */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Verts in edge chains ignore the angle test. This maintains the previous behavior,
|
||||
* where such verts were not subject to the angle threshold.
|
||||
*
|
||||
* When edge chains are selected for dissolve, all edge-pair verts at *both* ends of each
|
||||
* selected edge will be dissolved, combining the selected edges into their neighbors.
|
||||
*
|
||||
* Note that when only *part* of a chain is selected, this *will* alter unselected edges,
|
||||
* because selected edges will merge *into their unselected neighbors*. This too, has been
|
||||
* maintained, for consistency with the previous (but possibly unintentional) behavior. */
|
||||
if (BMO_edge_flag_test(bm, e_pair[0], EDGE_CHAIN) ||
|
||||
BMO_edge_flag_test(bm, e_pair[1], EDGE_CHAIN))
|
||||
{
|
||||
/* VERT_MARK remains enabled. */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If the angle at the vert is larger than the threshold, it cannot be merged. */
|
||||
if (bmo_vert_calc_edge_angle_blended(v) > angle_threshold - angle_epsilon) {
|
||||
BMO_vert_flag_disable(bm, v, VERT_MARK);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dissolve all verts that remain tagged. This is done in a separate iteration pass. Otherwise
|
||||
* the early dissolves would alter the angles measured at neighboring verts tested later. */
|
||||
BM_ITER_MESH_MUTABLE (v, v_next, &iter, bm, BM_VERTS_OF_MESH) {
|
||||
if (!BMO_vert_flag_test(bm, v, VERT_MARK)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Ensured in the previous loop. */
|
||||
BLI_assert(BM_vert_is_edge_pair(v));
|
||||
|
||||
/* Merge the header flags on the two edges that will be merged. */
|
||||
BMEdge *e_pair[2];
|
||||
BM_vert_edge_pair(v, &e_pair[0], &e_pair[1]);
|
||||
BM_elem_flag_merge_ex(e_pair[0], e_pair[1], BM_ELEM_HIDDEN);
|
||||
|
||||
BM_vert_collapse_edge(bm, v->e, v, true, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -616,3 +759,5 @@ void bmo_dissolve_degenerate_exec(BMesh *bm, BMOperator *op)
|
||||
bm_mesh_edge_collapse_flagged(bm, op->flag, EDGE_COLLAPSE);
|
||||
}
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
@@ -5798,8 +5798,11 @@ static void edbm_dissolve_prop__use_verts(wmOperatorType *ot, bool value, int fl
|
||||
{
|
||||
PropertyRNA *prop;
|
||||
|
||||
prop = RNA_def_boolean(
|
||||
ot->srna, "use_verts", value, "Dissolve Vertices", "Dissolve remaining vertices");
|
||||
prop = RNA_def_boolean(ot->srna,
|
||||
"use_verts",
|
||||
value,
|
||||
"Dissolve Vertices",
|
||||
"Dissolve remaining vertices which connect to only two edges");
|
||||
|
||||
if (flag) {
|
||||
RNA_def_property_flag(prop, PropertyFlag(flag));
|
||||
@@ -5821,6 +5824,22 @@ static void edbm_dissolve_prop__use_boundary_tear(wmOperatorType *ot)
|
||||
"Tear Boundary",
|
||||
"Split off face corners instead of merging faces");
|
||||
}
|
||||
static void edbm_dissolve_prop__use_angle_threshold(wmOperatorType *ot)
|
||||
{
|
||||
PropertyRNA *prop = RNA_def_float_rotation(
|
||||
ot->srna,
|
||||
"angle_threshold",
|
||||
0,
|
||||
nullptr,
|
||||
0.0f,
|
||||
DEG2RADF(180.0f),
|
||||
"Angle Threshold",
|
||||
"Remaining vertices which separate edge pairs are preserved if their edge angle exceeds "
|
||||
"this threshold.",
|
||||
0.0f,
|
||||
DEG2RADF(180.0f));
|
||||
RNA_def_property_float_default(prop, DEG2RADF(20.0f));
|
||||
}
|
||||
|
||||
static wmOperatorStatus edbm_dissolve_verts_exec(bContext *C, wmOperator *op)
|
||||
{
|
||||
@@ -5891,6 +5910,7 @@ static wmOperatorStatus edbm_dissolve_edges_exec(bContext *C, wmOperator *op)
|
||||
{
|
||||
const bool use_verts = RNA_boolean_get(op->ptr, "use_verts");
|
||||
const bool use_face_split = RNA_boolean_get(op->ptr, "use_face_split");
|
||||
const float angle_threshold = RNA_float_get(op->ptr, "angle_threshold");
|
||||
|
||||
const Scene *scene = CTX_data_scene(C);
|
||||
ViewLayer *view_layer = CTX_data_view_layer(C);
|
||||
@@ -5905,12 +5925,14 @@ static wmOperatorStatus edbm_dissolve_edges_exec(bContext *C, wmOperator *op)
|
||||
|
||||
BM_custom_loop_normals_to_vector_layer(em->bm);
|
||||
|
||||
if (!EDBM_op_callf(em,
|
||||
op,
|
||||
"dissolve_edges edges=%he use_verts=%b use_face_split=%b",
|
||||
BM_ELEM_SELECT,
|
||||
use_verts,
|
||||
use_face_split))
|
||||
if (!EDBM_op_callf(
|
||||
em,
|
||||
op,
|
||||
"dissolve_edges edges=%he use_verts=%b use_face_split=%b angle_threshold=%f",
|
||||
BM_ELEM_SELECT,
|
||||
use_verts,
|
||||
use_face_split,
|
||||
angle_threshold))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -5942,6 +5964,7 @@ void MESH_OT_dissolve_edges(wmOperatorType *ot)
|
||||
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
||||
|
||||
edbm_dissolve_prop__use_verts(ot, true, 0);
|
||||
edbm_dissolve_prop__use_angle_threshold(ot);
|
||||
edbm_dissolve_prop__use_face_split(ot);
|
||||
}
|
||||
|
||||
@@ -6036,6 +6059,32 @@ static wmOperatorStatus edbm_dissolve_mode_exec(bContext *C, wmOperator *op)
|
||||
return edbm_dissolve_faces_exec(C, op);
|
||||
}
|
||||
|
||||
static bool dissolve_mode_poll_property(const bContext *C, wmOperator *op, const PropertyRNA *prop)
|
||||
{
|
||||
UNUSED_VARS(op);
|
||||
|
||||
const char *prop_id = RNA_property_identifier(prop);
|
||||
|
||||
Object *obedit = CTX_data_edit_object(C);
|
||||
const BMEditMesh *em = BKE_editmesh_from_object(obedit);
|
||||
bool is_edge_select_mode = false;
|
||||
|
||||
if (em->selectmode & SCE_SELECT_VERTEX) {
|
||||
/* Pass. */
|
||||
}
|
||||
if (em->selectmode & SCE_SELECT_EDGE) {
|
||||
is_edge_select_mode = true;
|
||||
}
|
||||
|
||||
if (!is_edge_select_mode) {
|
||||
/* Angle Threshold is only used in edge select mode. */
|
||||
if (STREQ(prop_id, "angle_threshold")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MESH_OT_dissolve_mode(wmOperatorType *ot)
|
||||
{
|
||||
/* identifiers */
|
||||
@@ -6046,6 +6095,7 @@ void MESH_OT_dissolve_mode(wmOperatorType *ot)
|
||||
/* API callbacks. */
|
||||
ot->exec = edbm_dissolve_mode_exec;
|
||||
ot->poll = ED_operator_editmesh;
|
||||
ot->poll_property = dissolve_mode_poll_property;
|
||||
|
||||
/* flags */
|
||||
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
||||
@@ -6053,6 +6103,7 @@ void MESH_OT_dissolve_mode(wmOperatorType *ot)
|
||||
edbm_dissolve_prop__use_verts(ot, false, PROP_SKIP_SAVE);
|
||||
edbm_dissolve_prop__use_face_split(ot);
|
||||
edbm_dissolve_prop__use_boundary_tear(ot);
|
||||
edbm_dissolve_prop__use_angle_threshold(ot);
|
||||
}
|
||||
|
||||
/** \} */
|
||||
@@ -6295,12 +6346,14 @@ static wmOperatorStatus edbm_delete_edgeloop_exec(bContext *C, wmOperator *op)
|
||||
}
|
||||
}
|
||||
|
||||
if (!EDBM_op_callf(em,
|
||||
op,
|
||||
"dissolve_edges edges=%he use_verts=%b use_face_split=%b",
|
||||
BM_ELEM_SELECT,
|
||||
true,
|
||||
use_face_split))
|
||||
if (!EDBM_op_callf(
|
||||
em,
|
||||
op,
|
||||
"dissolve_edges edges=%he use_verts=%b use_face_split=%b angle_threshold=%f",
|
||||
BM_ELEM_SELECT,
|
||||
true,
|
||||
use_face_split,
|
||||
M_PI))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -112,8 +112,21 @@ def main():
|
||||
),
|
||||
|
||||
SpecMeshTest(
|
||||
"CylinderDissolveEdges.UseVertsTrue", "testCylinderDissolveEdges", "expectedCylinderDissolveEdges.DissolveAllVerts",
|
||||
[OperatorSpecEditMode("dissolve_edges", {"use_verts": True}, "EDGE", {0, 5, 6, 9})],
|
||||
"CylinderDissolveEdges.AngleThrehsoldNoDissolve", "testCylinderDissolveEdges", "expectedCylinderDissolveEdges.DissolveNoVerts",
|
||||
[OperatorSpecEditMode("dissolve_edges", {"use_verts": True,
|
||||
"angle_threshold": 0}, "EDGE", {0, 5, 6, 9})],
|
||||
),
|
||||
|
||||
SpecMeshTest(
|
||||
"CylinderDissolveEdges.AngleThresholdSomeDissolve", "testCylinderDissolveEdges", "expectedCylinderDissolveEdges.DissolveThresh.218166",
|
||||
[OperatorSpecEditMode("dissolve_edges", {"use_verts": True,
|
||||
"angle_threshold": 0.218166}, "EDGE", {0, 5, 6, 9})],
|
||||
),
|
||||
|
||||
SpecMeshTest(
|
||||
"CylinderDissolveEdges.AngleThresholdAllDissolve", "testCylinderDissolveEdges", "expectedCylinderDissolveEdges.DissolveAllVerts",
|
||||
[OperatorSpecEditMode("dissolve_edges", {"use_verts": True,
|
||||
"angle_threshold": 3.14159}, "EDGE", {0, 5, 6, 9})],
|
||||
),
|
||||
|
||||
# dissolve faces
|
||||
|
||||
Reference in New Issue
Block a user