From bfb0d2ad20a2cf700d85e8f121009cd4cdb795ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D1=8F=20=5F?= Date: Thu, 9 Oct 2025 19:29:18 +0200 Subject: [PATCH] Fix #144846: Mesh triangulation can generate duplicate faces and edges Mesh invariants imply that edges and faces must be unique, so things like reversed edges or duplicate faces with equal vertices are invalid. For this reason, every time we generate new elements we have to ensure that all new elements are unique between each other and already existing elements. The recent refactor (ea875f6f3278998506e4) introduced a new algorithm to generate new mesh elements, and deduplication of new elements was also a part of it. The problem is that the deduplication only guaranteed that the original elements and new elements don't overlap; deduplication between each new elements is not complete. To solve the problem both new triangles and new edges have to be deduplicated, even if there is no duplicates. Just to know this we have to build a hash sets. Triangle deduplication is a special part of the triangulation code, but edges already handled elsewhere in the code base. This refactor fixes this by replacing the original approach with one which guarantees distinct faces and edges in the result. Unfortunately, this fix increases runtime of the node 10x for a simple cube with 500-vertex sides. It should be possible to make the performance better again, but that requires more work. Other work had to be done to enable this, so this depends on: - [x] 157e7e0351262671260b766c77cdcba711decf47 - [x] fa8574b80b21de7cc93b4ebe2a72a89884facb4c Co-authored-by: Hans Goudey Pull Request: https://projects.blender.org/blender/blender/pulls/147634 --- source/blender/blenlib/BLI_vector_set.hh | 6 + .../geometry/intern/mesh_triangulate.cc | 648 ++++++------------ .../mesh/triangulate_deduplication.blend | 3 + 3 files changed, 235 insertions(+), 422 deletions(-) create mode 100644 tests/files/modeling/geometry_nodes/mesh/triangulate_deduplication.blend diff --git a/source/blender/blenlib/BLI_vector_set.hh b/source/blender/blenlib/BLI_vector_set.hh index bd87c1e7ba8..22f3df53611 100644 --- a/source/blender/blenlib/BLI_vector_set.hh +++ b/source/blender/blenlib/BLI_vector_set.hh @@ -183,6 +183,12 @@ class VectorSet { keys_ = inline_buffer_; } + VectorSet(Hash hash, IsEqual is_equal) : VectorSet() + { + hash_ = std::move(hash); + is_equal_ = std::move(is_equal); + } + VectorSet(NoExceptConstructor, Allocator allocator = {}) : VectorSet(allocator) {} VectorSet(Span keys, Allocator allocator = {}) : VectorSet(NoExceptConstructor(), allocator) diff --git a/source/blender/geometry/intern/mesh_triangulate.cc b/source/blender/geometry/intern/mesh_triangulate.cc index 7d6563070e0..bcd394cfdbe 100644 --- a/source/blender/geometry/intern/mesh_triangulate.cc +++ b/source/blender/geometry/intern/mesh_triangulate.cc @@ -2,27 +2,24 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ -#include "atomic_ops.h" - #include "BLI_array_utils.hh" #include "BLI_enumerable_thread_specific.hh" #include "BLI_index_mask.hh" +#include "BLI_index_mask_expression.hh" +#include "BLI_index_ranges_builder.hh" #include "BLI_math_geom.h" #include "BLI_math_matrix.h" -#include "BLI_ordered_edge.hh" #include "BLI_polyfill_2d.h" #include "BLI_polyfill_2d_beautify.h" #include "BLI_vector_set.hh" #include "BLI_heap.h" -#include "BLI_index_ranges_builder.hh" #include "BLI_memarena.h" #include "BKE_attribute.hh" #include "BKE_attribute_math.hh" #include "BKE_customdata.hh" #include "BKE_mesh.hh" -#include "BKE_mesh_mapping.hh" #include "GEO_mesh_triangulate.hh" @@ -77,25 +74,6 @@ static void copy_loose_vert_hint(const Mesh &src, Mesh &dst) } } -static void copy_loose_edge_hint(const Mesh &src, Mesh &dst) -{ - const auto &src_cache = src.runtime->loose_edges_cache; - if (src_cache.is_cached() && src_cache.data().count == 0) { - dst.tag_loose_edges_none(); - } -} - -static OffsetIndices calc_face_offsets(const OffsetIndices src_faces, - const IndexMask &unselected, - MutableSpan offsets) -{ - MutableSpan new_tri_offsets = offsets.drop_back(unselected.size()); - offset_indices::fill_constant_group_size(3, new_tri_offsets.first(), new_tri_offsets); - offset_indices::gather_selected_offsets( - src_faces, unselected, new_tri_offsets.last(), offsets.take_back(unselected.size() + 1)); - return OffsetIndices(offsets); -} - namespace quad { /** @@ -227,71 +205,6 @@ static void calc_corner_tris(const Span positions, }); } -/** - * Each triangulated quad creates one additional edge in the result mesh, between the two - * triangles. The corner_verts are just the corners of the quads, and the edges are just the new - * edges for these quads. - */ -static void calc_edges(const Span quad_corner_verts, MutableSpan new_quad_edges) -{ - const int quads_num = quad_corner_verts.size() / 6; - for (const int i : IndexRange(quads_num)) { - const Span verts = quad_corner_verts.slice(6 * i, 6); - /* Use the first vertex of each triangle. */ - new_quad_edges[i] = int2(verts[0], verts[1]); - } -} - -static void calc_quad_corner_edges(const Span src_corner_edges, - const Span corner_tris, - const int edges_start, - MutableSpan corner_edges) -{ - /* Each triangle starts at the new edge and winds in the same order as corner vertices - * described by the corner map. */ - for (const int tri : corner_tris.index_range()) { - corner_edges[3 * tri + 0] = edges_start + tri / 2; - corner_edges[3 * tri + 1] = src_corner_edges[corner_tris[tri][1]]; - corner_edges[3 * tri + 2] = src_corner_edges[corner_tris[tri][2]]; - } -} - -static void calc_edges(const Span src_corner_edges, - const Span corner_tris, - const Span corner_verts, - const int edges_start, - MutableSpan edges, - MutableSpan quad_corner_edges) -{ - const int quads_num = corner_tris.size() / 2; - threading::parallel_for(IndexRange(quads_num), 1024, [&](const IndexRange quads) { - const IndexRange tris_range(quads.start() * 2, quads.size() * 2); - const IndexRange corners(quads.start() * 6, quads.size() * 6); - calc_edges(corner_verts.slice(corners), edges.slice(quads)); - calc_quad_corner_edges(src_corner_edges, - corner_tris.slice(tris_range), - edges_start + quads.start(), - quad_corner_edges.slice(corners)); - }); -} - -template -static void copy_quad_data_to_tris(const Span src, const IndexMask &quads, MutableSpan dst) -{ - quads.foreach_index_optimized([&](const int src_i, const int dst_i) { - dst[2 * dst_i + 0] = src[src_i]; - dst[2 * dst_i + 1] = src[src_i]; - }); -} - -static void copy_quad_data_to_tris(const GSpan src, const IndexMask &quads, GMutableSpan dst) -{ - bke::attribute_math::convert_to_static_type(src.type(), [&](auto dummy) { - using T = decltype(dummy); - copy_quad_data_to_tris(src.typed(), quads, dst.typed()); - }); -} - } // namespace quad static OffsetIndices gather_selected_offsets(const OffsetIndices src_offsets, @@ -319,17 +232,6 @@ static OffsetIndices calc_tris_by_ngon(const OffsetIndices src_faces, return offset_indices::accumulate_counts_to_offsets(face_offset_data); } -static OffsetIndices calc_edges_by_ngon(const OffsetIndices src_faces, - const IndexMask &selection, - MutableSpan edge_offset_data) -{ - selection.foreach_index(GrainSize(2048), [&](const int face, const int mask) { - /* The number of new inner edges for each face is the number of corners - 3. */ - edge_offset_data[mask] = src_faces[face].size() - 3; - }); - return offset_indices::accumulate_counts_to_offsets(edge_offset_data); -} - static void calc_corner_tris(const Span positions, const OffsetIndices src_faces, const Span src_corner_verts, @@ -441,113 +343,74 @@ static void calc_corner_tris(const Span positions, }); } -static void calc_inner_tri_edges(const IndexRange src_face, - const Span src_corner_verts, - const Span src_corner_edges, - const Span corner_tris, - const int edges_start, - MutableSpan corner_edges, - VectorSet &deduplication) -{ - const OrderedEdge last_edge(int(src_face.first()), int(src_face.last())); - auto add_edge = [&](const OrderedEdge corner_edge) -> int { - if (corner_edge == last_edge) { - return src_corner_edges[src_face.last()]; - } - if (corner_edge.v_high == corner_edge.v_low + 1) { - return src_corner_edges[corner_edge.v_low]; - } - const OrderedEdge vert_edge(src_corner_verts[corner_edge.v_low], - src_corner_verts[corner_edge.v_high]); - return edges_start + deduplication.index_of_or_add(vert_edge); - }; - - for (const int i : corner_tris.index_range()) { - const int3 tri = corner_tris[i]; - corner_edges[3 * i + 0] = add_edge({tri[0], tri[1]}); - corner_edges[3 * i + 1] = add_edge({tri[1], tri[2]}); - corner_edges[3 * i + 2] = add_edge({tri[2], tri[0]}); - } -} - -static void calc_edges(const OffsetIndices src_faces, - const Span src_corner_verts, - const Span src_corner_edges, - const IndexMask &ngons, - const OffsetIndices tris_by_ngon, - const OffsetIndices edges_by_ngon, - const IndexRange ngon_edges_range, - const Span corner_tris, - MutableSpan edges, - MutableSpan corner_edges) -{ - MutableSpan inner_edges = edges.slice(ngon_edges_range); - threading::EnumerableThreadSpecific> tls; - ngons.foreach_segment(GrainSize(128), [&](const IndexMaskSegment ngons, const int pos) { - VectorSet &deduplication = tls.local(); - for (const int16_t i : ngons.index_range()) { - const IndexRange edges = edges_by_ngon[pos + i]; - const IndexRange tris_range = tris_by_ngon[pos + i]; - const IndexRange corners(tris_range.start() * 3, tris_range.size() * 3); - deduplication.clear(); - calc_inner_tri_edges(src_faces[ngons[i]], - src_corner_verts, - src_corner_edges, - corner_tris.slice(tris_range), - ngon_edges_range[edges.start()], - corner_edges.slice(corners), - deduplication); - inner_edges.slice(edges).copy_from(deduplication.as_span().cast()); - } - }); -} - } // namespace ngon -namespace deduplication { +struct TriKey { + int tri_index; + /* The lowest vertex index in the face is used as a hash value and a way to compare face keys to + * avoid memory lookup in all false cases. */ + int tri_lower_vert; -static GroupedSpan build_vert_to_tri_map(const int verts_num, - const Span vert_tris, - Array &r_offsets, - Array &r_indices) + TriKey(const int tri_index, Span tris) + : tri_index(tri_index), tri_lower_vert(tris[tri_index][0]) + { + [[maybe_unused]] const int3 &tri_verts = tris[tri_index]; + BLI_assert(std::is_sorted(&tri_verts[0], &tri_verts[0] + 3)); + } +}; + +struct FaceHash { + uint64_t operator()(const TriKey value) const + { + return uint64_t(value.tri_lower_vert); + } + + uint64_t operator()(const int3 value) const + { + BLI_assert(std::is_sorted(&value[0], &value[0] + 3)); + return uint64_t(value[0]); + } +}; + +struct FacesEquality { + Span tris; + bool operator()(const TriKey a, const TriKey b) const + { + return a.tri_lower_vert == b.tri_lower_vert && tris[a.tri_index] == tris[b.tri_index]; + } + + bool operator()(const int3 a, const TriKey b) const + { + BLI_assert(std::is_sorted(&a[0], &a[0] + 3)); + return b.tri_lower_vert == a[0] && tris[b.tri_index] == a; + } +}; + +static int3 tri_to_ordered(const int3 tri) { - r_offsets = Array(verts_num + 1, 0); - offset_indices::build_reverse_offsets(vert_tris.cast(), r_offsets); - const OffsetIndices offsets(r_offsets.as_span()); - - r_indices.reinitialize(offsets.total_size()); - int *counts = MEM_calloc_arrayN(offsets.size(), __func__); - BLI_SCOPED_DEFER([&]() { MEM_freeN(counts); }) - threading::parallel_for(vert_tris.index_range(), 1024, [&](const IndexRange range) { - for (const int tri : range) { - for (const int vert : {vert_tris[tri][0], vert_tris[tri][1], vert_tris[tri][2]}) { - const int index_in_group = atomic_fetch_and_add_int32(&counts[vert], 1); - r_indices[offsets[vert][index_in_group]] = tri; - } - } - }); - - return {r_offsets.as_span(), r_indices.as_span()}; + int3 res; + res[0] = std::min({tri[0], tri[1], tri[2]}); + res[2] = std::max({tri[0], tri[1], tri[2]}); + res[1] = (tri[0] - res[0]) + (tri[2] - res[2]) + tri[1]; + return res; } -/** - * To avoid adding duplicate faces to the mesh without complicating the triangulation code to - * support that unlikely case, check if triangles (which are all unselected) have an equivalent - * newly created triangle, and don't copy them to the result mesh if so. - */ -static IndexMask calc_unselected_faces(const Mesh &mesh, - const OffsetIndices src_faces, - const Span src_corner_verts, - const IndexMask &selection, - const Span corner_tris, - IndexMaskMemory &memory) +static Span tri_to_ordered_tri(MutableSpan tris) { - const IndexMask unselected = selection.complement(src_faces.index_range(), memory); - if (mesh.no_overlapping_topology()) { - return unselected; - } - const IndexMask unselected_tris = IndexMask::from_batch_predicate( - unselected, + threading::parallel_for(tris.index_range(), 4096, [&](const IndexRange range) { + for (int3 &tri : tris.slice(range)) { + tri = tri_to_ordered(tri); + } + }); + return tris; +} + +static IndexMask face_tris_mask(const OffsetIndices src_faces, + const IndexMask &mask, + IndexMaskMemory &memory) +{ + return IndexMask::from_batch_predicate( + mask, GrainSize(4096), memory, [&](const IndexMaskSegment universe_segment, IndexRangesBuilder &builder) { @@ -571,114 +434,55 @@ static IndexMask calc_unselected_faces(const Mesh &mesh, } return universe_segment.offset(); }); - - if (unselected_tris.is_empty()) { - return unselected; - } - - Array vert_tris(corner_tris.size()); - bke::attribute_math::gather( - src_corner_verts, corner_tris.cast(), vert_tris.as_mutable_span().cast()); - - Array vert_to_tri_offsets; - Array vert_to_tri_indices; - const GroupedSpan vert_to_tri = build_vert_to_tri_map( - mesh.verts_num, vert_tris, vert_to_tri_offsets, vert_to_tri_indices); - - auto tri_exists = [&](const std::array &tri_verts) { - /* TODO: Sorting the three values with a few comparisons would be faster than a #Set. */ - const Set vert_set(tri_verts); - return std::any_of(tri_verts.begin(), tri_verts.end(), [&](const int vert) { - return std::any_of(vert_to_tri[vert].begin(), vert_to_tri[vert].end(), [&](const int tri) { - const Set other_tri_verts(Span(&vert_tris[tri].x, 3)); - return other_tri_verts == vert_set; - }); - }); - }; - - const IndexMask duplicate_triangles = IndexMask::from_predicate( - unselected_tris, GrainSize(1024), memory, [&](const int i) { - const Span face_verts = src_corner_verts.slice(src_faces[i]); - return tri_exists({face_verts[0], face_verts[1], face_verts[2]}); - }); - - return IndexMask::from_difference(unselected, duplicate_triangles, memory); } -static std::optional find_edge_duplicate(const GroupedSpan vert_to_edge_map, - const Span edges, - const OrderedEdge edge) +static IndexMask tris_in_set(const IndexMask &tri_mask, + const OffsetIndices faces, + const Span corner_verts, + const VectorSet> &unique_tris, + IndexMaskMemory &memory) { - for (const int vert : {edge.v_low, edge.v_high}) { - for (const int src_edge : vert_to_edge_map[vert]) { - if (OrderedEdge(edges[src_edge]) == edge) { - return src_edge; - } - } - } - return std::nullopt; + return IndexMask::from_predicate(tri_mask, GrainSize(4096), memory, [&](const int face_i) { + BLI_assert(faces[face_i].size() == 3); + const int3 corner_tri(&corner_verts[faces[face_i].start()]); + return unique_tris.contains_as(tri_to_ordered(corner_tri)); + }); } -/** - * Given all the edges on the new mesh, find new edges that are duplicates of existing edges. - * If there are any, remove them and references to them in the corner edge array. - * - * \return The final number of edges in the mesh. - */ -static int calc_new_edges(const Mesh &src_mesh, - const Span src_edges, - const IndexRange new_edges_range, - MutableSpan edges, - MutableSpan corner_edges) +static void face_keys_to_face_indices(const Span faces, MutableSpan indices) { - if (src_mesh.no_overlapping_topology()) { - return edges.size(); - } - - Array vert_to_edge_offsets; - Array vert_to_edge_indices; - const GroupedSpan vert_to_edge = bke::mesh::build_vert_to_edge_map( - src_edges, src_mesh.verts_num, vert_to_edge_offsets, vert_to_edge_indices); - - const Span new_edges = edges.slice(new_edges_range); - Array duplicate_remap(new_edges.size()); - threading::parallel_for(new_edges.index_range(), 1024, [&](const IndexRange range) { - for (const int i : range) { - duplicate_remap[i] = find_edge_duplicate(vert_to_edge, src_edges, new_edges[i]).value_or(-1); + BLI_assert(faces.size() == indices.size()); + threading::parallel_for(faces.index_range(), 4096, [&](const IndexRange range) { + for (const int face_i : range) { + indices[face_i] = faces[face_i].tri_index; } }); - IndexMaskMemory memory; - const IndexMask non_duplicate_new_edges = IndexMask::from_predicate( - new_edges.index_range(), GrainSize(4096), memory, [&](const int i) { - return duplicate_remap[i] == -1; - }); - if (non_duplicate_new_edges.size() == new_edges.size()) { - return edges.size(); - } - - non_duplicate_new_edges.foreach_index_optimized( - GrainSize(4096), [&](const int index, const int pos) { - duplicate_remap[index] = pos + new_edges_range.start(); - }); - threading::parallel_for(corner_edges.index_range(), 4096, [&](const IndexRange range) { - for (const int corner : range) { - const int edge = corner_edges[corner]; - if (edge < new_edges_range.start()) { - continue; - } - const int remap_index = edge - new_edges_range.start(); - corner_edges[corner] = duplicate_remap[remap_index]; - } - }); - - Array edges_with_duplicates = new_edges; - array_utils::gather(edges_with_duplicates.as_span(), - non_duplicate_new_edges, - edges.slice(new_edges_range.start(), non_duplicate_new_edges.size())); - return src_edges.size() + non_duplicate_new_edges.size(); } -} // namespace deduplication +static void quad_indices_of_tris(const IndexMask &quads, MutableSpan indices) +{ + BLI_assert(quads.size() * 2 == indices.size()); + quads.foreach_index_optimized(GrainSize(4096), [&](const int index, const int pos) { + indices[2 * pos + 0] = index; + indices[2 * pos + 1] = index; + }); +} + +static void ngon_indices_of_tris(const IndexMask &ngons, + const OffsetIndices tris_by_ngon, + MutableSpan indices) +{ + BLI_assert(tris_by_ngon.size() == ngons.size()); + BLI_assert(tris_by_ngon.total_size() == indices.size()); + ngons.foreach_index_optimized(GrainSize(4096), [&](const int index, const int pos) { + indices.slice(tris_by_ngon[pos]).fill(index); + }); +} std::optional mesh_triangulate(const Mesh &src_mesh, const IndexMask &selection_with_tris, @@ -687,31 +491,28 @@ std::optional mesh_triangulate(const Mesh &src_mesh, const bke::AttributeFilter &attribute_filter) { const Span positions = src_mesh.vert_positions(); - const Span src_edges = src_mesh.edges(); const OffsetIndices src_faces = src_mesh.faces(); const Span src_corner_verts = src_mesh.corner_verts(); - const Span src_corner_edges = src_mesh.corner_edges(); const bke::AttributeAccessor src_attributes = src_mesh.attributes(); + IndexMaskMemory memory; + + /* If there are a lot of triangles, they can be skipped quickly for filtering. */ + const IndexMask src_tris = face_tris_mask(src_faces, src_faces.index_range(), memory); + const IndexMask selection = IndexMask::from_difference(selection_with_tris, src_tris, memory); + /* Divide the input selection into separate selections for each face type. This isn't necessary * for correctness, but considering groups of each face type separately simplifies optimizing * for each type. For example, quad triangulation is much simpler than Ngon triangulation. */ - IndexMaskMemory memory; const IndexMask quads = IndexMask::from_predicate( - selection_with_tris, GrainSize(4096), memory, [&](const int i) { - return src_faces[i].size() == 4; - }); + selection, GrainSize(4096), memory, [&](const int i) { return src_faces[i].size() == 4; }); const IndexMask ngons = IndexMask::from_predicate( - selection_with_tris, GrainSize(4096), memory, [&](const int i) { - return src_faces[i].size() > 4; - }); + selection, GrainSize(4096), memory, [&](const int i) { return src_faces[i].size() > 4; }); if (quads.is_empty() && ngons.is_empty()) { /* All selected faces are already triangles. */ return std::nullopt; } - const IndexMask selection = IndexMask::from_union(quads, ngons, memory); - /* Calculate group of triangle indices for each selected Ngon to facilitate calculating them in * parallel later. */ Array tris_by_ngon_data(ngons.size() + 1); @@ -722,29 +523,7 @@ std::optional mesh_triangulate(const Mesh &src_mesh, const IndexRange ngon_tris_range = tris_range.take_front(ngon_tris_num); const IndexRange quad_tris_range = tris_range.take_back(quad_tris_num); - const int ngon_corners_num = tris_by_ngon.total_size() * 3; - const int quad_corners_num = quads.size() * 6; - const IndexRange tri_corners_range(quad_corners_num + ngon_corners_num); - const IndexRange ngon_corners_range = tri_corners_range.take_front(ngon_corners_num); - const IndexRange quad_corners_range = tri_corners_range.take_back(quad_corners_num); - - /* Calculate groups of new inner edges for each selected Ngon so they can be filled in parallel - * later. */ - Array edge_offset_data(ngons.size() + 1); - const OffsetIndices edges_by_ngon = ngon::calc_edges_by_ngon(src_faces, ngons, edge_offset_data); - const int ngon_edges_num = edges_by_ngon.total_size(); - const int quad_edges_num = quads.size(); - const IndexRange src_edges_range(0, src_edges.size()); - const IndexRange tri_edges_range(src_edges_range.one_after_last(), - ngon_edges_num + quad_edges_num); - const IndexRange ngon_edges_range = tri_edges_range.take_front(ngon_edges_num); - const IndexRange quad_edges_range = tri_edges_range.take_back(quad_edges_num); - - /* An index map that maps from newly created corners in `tri_corners_range` to original corner - * indices. This is used to interpolate `corner_vert` indices and face corner attributes. If - * there are no face corner attributes, theoretically the map could be skipped and corner - * vertex indices could be interpolated immediately, but that isn't done for simplicity. */ - Array corner_tris(tris_range.size()); + Array corner_tris(ngon_tris_num + quad_tris_num); if (!ngons.is_empty()) { ngon::calc_corner_tris(positions, @@ -765,104 +544,115 @@ std::optional mesh_triangulate(const Mesh &src_mesh, corner_tris.as_mutable_span().slice(quad_tris_range)); } - const IndexMask unselected = deduplication::calc_unselected_faces( - src_mesh, src_faces, src_corner_verts, selection, corner_tris, memory); - const IndexRange unselected_range(tris_range.one_after_last(), unselected.size()); + /* There are 3 separate sets of triangles: original mesh triangles, new triangles from quads, + * and triangles from n-gons. Deduplication can result in a mix of parts of multiple quads, + * multiple quads, original triangle, and even concatenation of parts of multiple n-gons. + * So we have to deduplicate all triangles together. */ + Array vert_tris(ngon_tris_num + quad_tris_num); + array_utils::gather(src_corner_verts, + corner_tris.as_span().cast(), + vert_tris.as_mutable_span().cast()); + const Span ordered_vert_tris = tri_to_ordered_tri(vert_tris.as_mutable_span()); + + /* Use ordered vertex triplets (a < b < c) to represent all new triangles. + * #TriKey knows indices of the face and points into #ordered_vert_tris, but probe can be done + * without #TriKey but dirrectly with a triplet so probe not necessary to be a part of + * #ordered_vert_tris. */ + VectorSet> + unique_tris(FaceHash{}, FacesEquality{ordered_vert_tris}); + + /* Could be done parallel using grouping of faces by their lowest vertex and the next linear + * deduplication, but right now this is just a sequential hash-set. */ + for (const int face_i : ordered_vert_tris.index_range()) { + const TriKey face_key(face_i, ordered_vert_tris); + unique_tris.add(face_key); + } + const int unique_tri_num = unique_tris.size(); + + /* Since currently deduplication is greedy, there is no mix of data of deduplicated triangles, + * instead some of them are removed. Priority: Original triangles removed if any of new triangles + * are the same. For all new triangles here is direct order dependency. */ + const IndexMask src_tris_duplicated = tris_in_set( + src_tris, src_faces, src_corner_verts, unique_tris, memory); + + index_mask::ExprBuilder mask_builder; + const IndexMask unique_src_faces = index_mask::evaluate_expression( + mask_builder.subtract(src_faces.index_range(), {&quads, &ngons, &src_tris_duplicated}), + memory); + + const IndexRange unique_faces_range(unique_tri_num + unique_src_faces.size()); + const IndexRange unique_tri_range = unique_faces_range.take_front(unique_tri_num); + const IndexRange unique_src_faces_range = unique_faces_range.take_back(unique_src_faces.size()); /* Create a mesh with no face corners. * - We haven't yet counted the number of corners from unselected faces. Creating the final face * offsets will give us that number anyway, so wait to create the edges. - * - The number of edges is a guess that doesn't include deduplication of new edges with - * existing edges. If those are found, the mesh will be resized later. * - Don't create attributes to facilitate implicit sharing of the positions array. */ - Mesh *mesh = bke::mesh_new_no_attributes(src_mesh.verts_num, - src_edges.size() + tri_edges_range.size(), - tris_range.size() + unselected.size(), - 0); + Mesh *mesh = bke::mesh_new_no_attributes( + src_mesh.verts_num, src_mesh.edges_num, unique_faces_range.size(), 0); BKE_mesh_copy_parameters_for_eval(mesh, &src_mesh); - /* Find the face corner ranges using the offsets array from the new mesh. That gives us the - * final number of face corners. */ - const OffsetIndices faces = calc_face_offsets( - src_faces, unselected, mesh->face_offsets_for_write()); + MutableSpan dst_offsets = mesh->face_offsets_for_write(); + offset_indices::fill_constant_group_size( + 3, 0, dst_offsets.take_front(unique_tri_range.size() + 1)); + const int total_new_tri_corners = unique_tri_range.size() * 3; + offset_indices::gather_selected_offsets( + src_faces, + unique_src_faces, + total_new_tri_corners, + dst_offsets.take_back(unique_src_faces_range.size() + 1)); + + const OffsetIndices faces(dst_offsets); mesh->corners_num = faces.total_size(); - const OffsetIndices faces_unselected = faces.slice(unselected_range); - - bke::MutableAttributeAccessor attributes = mesh->attributes_for_write(); - attributes.add(".edge_verts", bke::AttrDomain::Edge, bke::AttributeInitConstruct()); - attributes.add(".corner_vert", bke::AttrDomain::Corner, bke::AttributeInitConstruct()); - attributes.add(".corner_edge", bke::AttrDomain::Corner, bke::AttributeInitConstruct()); - - MutableSpan edges_with_duplicates = mesh->edges_for_write(); - MutableSpan corner_verts = mesh->corner_verts_for_write(); - MutableSpan corner_edges = mesh->corner_edges_for_write(); - - array_utils::gather( - src_corner_verts, corner_tris.as_span().cast(), corner_verts.slice(tri_corners_range)); - - if (!ngons.is_empty()) { - ngon::calc_edges(src_faces, - src_corner_verts, - src_corner_edges, - ngons, - tris_by_ngon, - edges_by_ngon, - ngon_edges_range, - corner_tris.as_mutable_span().slice(ngon_tris_range), - edges_with_duplicates, - corner_edges.slice(ngon_corners_range)); - } - - if (!quads.is_empty()) { - quad::calc_edges(src_corner_edges, - corner_tris.as_mutable_span().slice(quad_tris_range), - corner_verts.slice(quad_corners_range), - quad_edges_range.start(), - edges_with_duplicates.slice(quad_edges_range), - corner_edges.slice(quad_corners_range)); - } - - mesh->edges_num = deduplication::calc_new_edges( - src_mesh, src_edges, tri_edges_range, edges_with_duplicates, corner_edges); - - edges_with_duplicates.take_front(src_edges.size()).copy_from(src_edges); /* Vertex attributes are totally unaffected and can be shared with implicit sharing. * Use the #CustomData API for simpler support for vertex groups. */ CustomData_merge(&src_mesh.vert_data, &mesh->vert_data, CD_MASK_MESH.vmask, mesh->verts_num); + /* Edge attributes are the same for original edges. New edges will be generated by + * #bke::mesh_calc_edges later. */ + CustomData_merge(&src_mesh.edge_data, &mesh->edge_data, CD_MASK_MESH.emask, mesh->edges_num); + + bke::MutableAttributeAccessor attributes = mesh->attributes_for_write(); + + const bool has_duplicate_faces = unique_tri_num != (ngon_tris_num + quad_tris_num); + + Array dst_tri_to_src_face(unique_tri_num); + face_keys_to_face_indices(unique_tris.as_span(), dst_tri_to_src_face.as_mutable_span()); + + Array unique_corner_tris_data; + if (has_duplicate_faces) { + unique_corner_tris_data.reinitialize(unique_tri_num); + array_utils::gather(corner_tris.as_span(), + dst_tri_to_src_face.as_span(), + unique_corner_tris_data.as_mutable_span()); + } - for (auto &attribute : bke::retrieve_attributes_for_transfer( - src_attributes, - attributes, - ATTR_DOMAIN_MASK_EDGE, - bke::attribute_filter_with_skip_ref(attribute_filter, {".edge_verts"}))) { - attribute.dst.span.slice(src_edges_range).copy_from(attribute.src); - GMutableSpan new_data = attribute.dst.span.drop_front(src_edges.size()); - /* It would be reasonable interpolate data from connected edges within each face. - * Currently the data from new edges is just set to the type's default value. */ - const void *default_value = new_data.type().default_value(); - new_data.type().fill_construct_n(default_value, new_data.data(), new_data.size()); - attribute.dst.finish(); - } - if (CustomData_has_layer(&src_mesh.edge_data, CD_ORIGINDEX)) { - const Span src( - static_cast(CustomData_get_layer(&src_mesh.edge_data, CD_ORIGINDEX)), - src_mesh.edges_num); - MutableSpan dst(static_cast(CustomData_add_layer( - &mesh->edge_data, CD_ORIGINDEX, CD_CONSTRUCT, mesh->edges_num)), - mesh->edges_num); - dst.drop_front(src_edges.size()).fill(ORIGINDEX_NONE); - array_utils::copy(src, dst.slice(src_edges_range)); + Array src_to_unique_map(ngon_tris_num + quad_tris_num); + quad_indices_of_tris(quads, src_to_unique_map.as_mutable_span().slice(quad_tris_range)); + ngon_indices_of_tris( + ngons, tris_by_ngon, src_to_unique_map.as_mutable_span().slice(ngon_tris_range)); + + array_utils::gather(src_to_unique_map.as_span(), + dst_tri_to_src_face.as_span(), + dst_tri_to_src_face.as_mutable_span()); } + const Span unique_corner_tris = has_duplicate_faces ? unique_corner_tris_data.as_span() : + corner_tris.as_span(); + for (auto &attribute : bke::retrieve_attributes_for_transfer( src_attributes, attributes, ATTR_DOMAIN_MASK_FACE, attribute_filter)) { - bke::attribute_math::gather_to_groups( - tris_by_ngon, ngons, attribute.src, attribute.dst.span.slice(ngon_tris_range)); - quad::copy_quad_data_to_tris(attribute.src, quads, attribute.dst.span.slice(quad_tris_range)); - array_utils::gather(attribute.src, unselected, attribute.dst.span.slice(unselected_range)); + bke::attribute_math::gather( + attribute.src, dst_tri_to_src_face.as_span(), attribute.dst.span.slice(unique_tri_range)); + array_utils::gather( + attribute.src, unique_src_faces, attribute.dst.span.slice(unique_src_faces_range)); attribute.dst.finish(); } if (CustomData_has_layer(&src_mesh.face_data, CD_ORIGINDEX)) { @@ -872,15 +662,23 @@ std::optional mesh_triangulate(const Mesh &src_mesh, MutableSpan dst(static_cast(CustomData_add_layer( &mesh->face_data, CD_ORIGINDEX, CD_CONSTRUCT, mesh->faces_num)), mesh->faces_num); - bke::attribute_math::gather_to_groups(tris_by_ngon, ngons, src, dst.slice(ngon_tris_range)); - quad::copy_quad_data_to_tris(src, quads, dst.slice(quad_tris_range)); - array_utils::gather(src, unselected, dst.slice(unselected_range)); + + array_utils::gather(src, dst_tri_to_src_face.as_span(), dst.slice(unique_tri_range)); + array_utils::gather(src, unique_src_faces, dst.slice(unique_src_faces_range)); } - array_utils::gather_group_to_group( - src_faces, faces_unselected, unselected, src_corner_verts, corner_verts); - array_utils::gather_group_to_group( - src_faces, faces_unselected, unselected, src_corner_edges, corner_edges); + attributes.add(".corner_vert", bke::AttrDomain::Corner, bke::AttributeInitConstruct()); + + MutableSpan corner_verts = mesh->corner_verts_for_write(); + array_utils::gather_group_to_group(src_faces, + faces.slice(unique_src_faces_range), + unique_src_faces, + src_corner_verts, + corner_verts); + array_utils::gather(src_corner_verts, + unique_corner_tris.cast(), + corner_verts.take_front(total_new_tri_corners)); + for (auto &attribute : bke::retrieve_attributes_for_transfer( src_attributes, attributes, @@ -889,16 +687,22 @@ std::optional mesh_triangulate(const Mesh &src_mesh, {".corner_vert", ".corner_edge"}))) { bke::attribute_math::gather_group_to_group( - src_faces, faces_unselected, unselected, attribute.src, attribute.dst.span); + src_faces, + faces.slice(IndexRange(unique_tri_num, unique_src_faces.size())), + unique_src_faces, + attribute.src, + attribute.dst.span); bke::attribute_math::gather(attribute.src, - corner_tris.as_span().cast(), - attribute.dst.span.slice(tri_corners_range)); + unique_corner_tris.cast(), + attribute.dst.span.slice(0, unique_tri_num * 3)); attribute.dst.finish(); } + /* Automatically generate new edges between new triangles, with necessary deduplication. */ + bke::mesh_calc_edges(*mesh, true, false, attribute_filter); + mesh->runtime->bounds_cache = src_mesh.runtime->bounds_cache; copy_loose_vert_hint(src_mesh, *mesh); - copy_loose_edge_hint(src_mesh, *mesh); if (src_mesh.no_overlapping_topology()) { mesh->tag_overlapping_none(); } diff --git a/tests/files/modeling/geometry_nodes/mesh/triangulate_deduplication.blend b/tests/files/modeling/geometry_nodes/mesh/triangulate_deduplication.blend new file mode 100644 index 00000000000..950301aba9e --- /dev/null +++ b/tests/files/modeling/geometry_nodes/mesh/triangulate_deduplication.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efda0da9f4e5ba65eb85dce4c7cbaa5f2cff2f004fcbe9e10226255d0671d0df +size 490307