From ea875f6f3278998506e41418353a86c8c845bed4 Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Sat, 23 Nov 2024 00:25:52 +0100 Subject: [PATCH] Geometry Nodes: Port triangulate node from BMesh to Mesh Add a Mesh implementation of triangulation, which is currently just implemented for BMesh. The main benefit of this is performance and decreased memory usage. The benefit is especially large because the node currently converts to BMesh, triangulates, and then converts back. But the BMesh implementation is entirely single threaded, so it will always be much slower. The new implementation uses the principle of "never just process a single element at a time" but it also tries to avoid processing _too_ many elements at once, to decrease the size of temporary buffers. Practically that means the work is organized into chunks of selected faces, but within each chunk, each task is done in a separate loop. Arguably I went a bit far with some optimizations, and some of the complexity isn't necessary, but I hope everything is clear anyway. Unlike some other Mesh ports like the extrude node or the split edges code, this generates a new mesh. I still go back and forth on that aspect, but reusing the same mesh would have required reallocating face attributes from scratch anyway. Implicit sharing is used to avoid copying vertex attributes though. The result mesh is reorganized a bit. Unselected face data comes first, then selected triangles, then triangulated NGons, then triangulated quads. This makes attribute interpolation and internal calculations more efficient. The "Minimum Vertices" socket is replaced with versioning. In the new implementation it would have an impact on code complexity, and for a builtin "atomic" node it makes more sense as part of the selection. The performance difference depends on the number of CPU threads, the number of attributes, and the selection size. As all of those numbers go up, the benefit will grow. The "modes" also affect the performance, obviously. With a Ryzen 7950x, I tested performance in a few situations (in ms): | | Selection | Before | After | Change | | -------------------------- | --------- | ------ | ----- | ------ | | 1.4 m quads (fixed) | 50% | 1533 | 15.9 | 96x | | 10 m quads (shortest) | 100% | 9700 | 165.0 | 59x | | 1 m 10-side Ngons (beauty) | 90% | 3785 | 115.9 | 33x | | 1 m quads many attributes | 100% | 54600 | 332.3 | 164x | In follow-up commits, I'll move other areas to use mesh triangulation instead of BMesh. This is the last geometry node using BMesh besides the Ico Sphere primitive. Pull Request: https://projects.blender.org/blender/blender/pulls/112264 --- .../blender/blenkernel/BKE_blender_version.h | 2 +- .../blenloader/intern/versioning_400.cc | 128 +++ source/blender/geometry/CMakeLists.txt | 3 + .../blender/geometry/GEO_mesh_triangulate.hh | 48 + .../geometry/intern/mesh_triangulate.cc | 910 ++++++++++++++++++ source/blender/makesdna/DNA_node_types.h | 13 - .../geometry/nodes/node_geo_triangulate.cc | 101 +- tests/data | 2 +- 8 files changed, 1131 insertions(+), 76 deletions(-) create mode 100644 source/blender/geometry/GEO_mesh_triangulate.hh create mode 100644 source/blender/geometry/intern/mesh_triangulate.cc diff --git a/source/blender/blenkernel/BKE_blender_version.h b/source/blender/blenkernel/BKE_blender_version.h index 78c44d00c52..73680b05d21 100644 --- a/source/blender/blenkernel/BKE_blender_version.h +++ b/source/blender/blenkernel/BKE_blender_version.h @@ -31,7 +31,7 @@ extern "C" { /* Blender file format version. */ #define BLENDER_FILE_VERSION BLENDER_VERSION -#define BLENDER_FILE_SUBVERSION 7 +#define BLENDER_FILE_SUBVERSION 8 /* Minimum Blender version that supports reading file written with the current * version. Older Blender versions will test this and cancel loading the file, showing a warning to diff --git a/source/blender/blenloader/intern/versioning_400.cc b/source/blender/blenloader/intern/versioning_400.cc index 2fa22285793..86d69444d1c 100644 --- a/source/blender/blenloader/intern/versioning_400.cc +++ b/source/blender/blenloader/intern/versioning_400.cc @@ -2404,6 +2404,126 @@ static void version_principled_bsdf_coat(bNodeTree *ntree) ntree, SH_NODE_BSDF_PRINCIPLED, "Clearcoat Normal", "Coat Normal"); } +static void remove_triangulate_node_min_size_input(bNodeTree *tree) +{ + using namespace blender; + Set triangulate_nodes; + LISTBASE_FOREACH (bNode *, node, &tree->nodes) { + if (node->type == GEO_NODE_TRIANGULATE) { + triangulate_nodes.add(node); + } + } + + Map input_links; + LISTBASE_FOREACH (bNodeLink *, link, &tree->links) { + if (triangulate_nodes.contains(link->tonode)) { + input_links.add_new(link->tosock, link); + } + } + + for (bNode *triangulate : triangulate_nodes) { + bNodeSocket *selection = bke::node_find_socket(triangulate, SOCK_IN, "Selection"); + bNodeSocket *min_verts = bke::node_find_socket(triangulate, SOCK_IN, "Minimum Vertices"); + if (!min_verts) { + /* Make versioning idempotent. */ + continue; + } + const int old_min_verts = static_cast(min_verts->default_value)->value; + if (!input_links.contains(min_verts) && old_min_verts <= 4) { + continue; + } + bNode &corners_of_face = version_node_add_empty(*tree, "GeometryNodeCornersOfFace"); + version_node_add_socket_if_not_exist( + tree, &corners_of_face, SOCK_IN, SOCK_INT, PROP_NONE, "Face Index", "Face Index"); + version_node_add_socket_if_not_exist( + tree, &corners_of_face, SOCK_IN, SOCK_FLOAT, PROP_NONE, "Weights", "Weights"); + version_node_add_socket_if_not_exist( + tree, &corners_of_face, SOCK_IN, SOCK_INT, PROP_NONE, "Sort Index", "Sort Index"); + version_node_add_socket_if_not_exist( + tree, &corners_of_face, SOCK_OUT, SOCK_INT, PROP_NONE, "Corner Index", "Corner Index"); + version_node_add_socket_if_not_exist( + tree, &corners_of_face, SOCK_OUT, SOCK_INT, PROP_NONE, "Total", "Total"); + corners_of_face.locx = triangulate->locx - 200; + corners_of_face.locy = triangulate->locy - 50; + corners_of_face.parent = triangulate->parent; + LISTBASE_FOREACH (bNodeSocket *, socket, &corners_of_face.inputs) { + socket->flag |= SOCK_HIDDEN; + } + LISTBASE_FOREACH (bNodeSocket *, socket, &corners_of_face.outputs) { + if (!STREQ(socket->identifier, "Total")) { + socket->flag |= SOCK_HIDDEN; + } + } + + bNode &greater_or_equal = version_node_add_empty(*tree, "FunctionNodeCompare"); + auto *compare_storage = MEM_cnew(__func__); + compare_storage->operation = NODE_COMPARE_GREATER_EQUAL; + compare_storage->data_type = SOCK_INT; + greater_or_equal.storage = compare_storage; + version_node_add_socket_if_not_exist( + tree, &greater_or_equal, SOCK_IN, SOCK_INT, PROP_NONE, "A_INT", "A"); + version_node_add_socket_if_not_exist( + tree, &greater_or_equal, SOCK_IN, SOCK_INT, PROP_NONE, "B_INT", "B"); + version_node_add_socket_if_not_exist( + tree, &greater_or_equal, SOCK_OUT, SOCK_BOOLEAN, PROP_NONE, "Result", "Result"); + greater_or_equal.locx = triangulate->locx - 100; + greater_or_equal.locy = triangulate->locy - 50; + greater_or_equal.parent = triangulate->parent; + greater_or_equal.flag &= ~NODE_OPTIONS; + version_node_add_link(*tree, + corners_of_face, + *bke::node_find_socket(&corners_of_face, SOCK_OUT, "Total"), + greater_or_equal, + *bke::node_find_socket(&greater_or_equal, SOCK_IN, "A_INT")); + if (bNodeLink **min_verts_link = input_links.lookup_ptr(min_verts)) { + (*min_verts_link)->tonode = &greater_or_equal; + (*min_verts_link)->tosock = bke::node_find_socket(&greater_or_equal, SOCK_IN, "B_INT"); + } + else { + bNodeSocket *new_min_verts = bke::node_find_socket(&greater_or_equal, SOCK_IN, "B_INT"); + static_cast(new_min_verts->default_value)->value = old_min_verts; + } + + if (bNodeLink **selection_link = input_links.lookup_ptr(selection)) { + bNode &boolean_and = version_node_add_empty(*tree, "FunctionNodeBooleanMath"); + version_node_add_socket_if_not_exist( + tree, &boolean_and, SOCK_IN, SOCK_BOOLEAN, PROP_NONE, "Boolean", "Boolean"); + version_node_add_socket_if_not_exist( + tree, &boolean_and, SOCK_IN, SOCK_BOOLEAN, PROP_NONE, "Boolean_001", "Boolean"); + version_node_add_socket_if_not_exist( + tree, &boolean_and, SOCK_OUT, SOCK_BOOLEAN, PROP_NONE, "Boolean", "Boolean"); + boolean_and.locx = triangulate->locx - 75; + boolean_and.locy = triangulate->locy - 50; + boolean_and.parent = triangulate->parent; + boolean_and.flag &= ~NODE_OPTIONS; + boolean_and.custom1 = NODE_BOOLEAN_MATH_AND; + + (*selection_link)->tonode = &boolean_and; + (*selection_link)->tosock = bke::node_find_socket(&boolean_and, SOCK_IN, "Boolean"); + version_node_add_link(*tree, + greater_or_equal, + *bke::node_find_socket(&greater_or_equal, SOCK_OUT, "Result"), + boolean_and, + *bke::node_find_socket(&boolean_and, SOCK_IN, "Boolean_001")); + + version_node_add_link(*tree, + boolean_and, + *bke::node_find_socket(&boolean_and, SOCK_OUT, "Boolean"), + *triangulate, + *selection); + } + else { + version_node_add_link(*tree, + greater_or_equal, + *bke::node_find_socket(&greater_or_equal, SOCK_OUT, "Result"), + *triangulate, + *selection); + } + + /* Make versioning idempotent. */ + bke::node_remove_socket(tree, triangulate, min_verts); + } +} /* Convert specular tint in Principled BSDF. */ static void version_principled_bsdf_specular_tint(bNodeTree *ntree) { @@ -5043,6 +5163,14 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain) add_subsurf_node_limit_surface_option(*bmain); } + if (!MAIN_VERSION_FILE_ATLEAST(bmain, 404, 8)) { + LISTBASE_FOREACH (bNodeTree *, ntree, &bmain->nodetrees) { + if (ntree->type == NTREE_GEOMETRY) { + remove_triangulate_node_min_size_input(ntree); + } + } + } + /* Always run this versioning; meshes are written with the legacy format which always needs to * be converted to the new format on file load. Can be moved to a subversion check in a larger * breaking release. */ diff --git a/source/blender/geometry/CMakeLists.txt b/source/blender/geometry/CMakeLists.txt index 9f812f5bf1d..6d83f7b5978 100644 --- a/source/blender/geometry/CMakeLists.txt +++ b/source/blender/geometry/CMakeLists.txt @@ -37,6 +37,7 @@ set(SRC intern/mesh_split_edges.cc intern/mesh_to_curve_convert.cc intern/mesh_to_volume.cc + intern/mesh_triangulate.cc intern/mix_geometries.cc intern/point_merge_by_distance.cc intern/points_to_volume.cc @@ -77,6 +78,7 @@ set(SRC GEO_mesh_split_edges.hh GEO_mesh_to_curve.hh GEO_mesh_to_volume.hh + GEO_mesh_triangulate.hh GEO_mix_geometries.hh GEO_point_merge_by_distance.hh GEO_points_to_volume.hh @@ -101,6 +103,7 @@ set(LIB bf_blenkernel PRIVATE bf::blenlib PRIVATE bf::dna + PRIVATE bf::intern::atomic PRIVATE bf::intern::guardedalloc PRIVATE bf::extern::fmtlib ) diff --git a/source/blender/geometry/GEO_mesh_triangulate.hh b/source/blender/geometry/GEO_mesh_triangulate.hh new file mode 100644 index 00000000000..b2375bc3ee4 --- /dev/null +++ b/source/blender/geometry/GEO_mesh_triangulate.hh @@ -0,0 +1,48 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include + +#include "BLI_index_mask.hh" + +#include "BKE_attribute.hh" + +struct Mesh; + +namespace blender::geometry { + +/** \warning Values are saved in files. */ +enum class TriangulateNGonMode { + /** Add a "beauty" pass on top of the standard ear-clipping algorithm. */ + Beauty = 0, + EarClip = 1, +}; + +/** \warning Values are saved in files. */ +enum class TriangulateQuadMode { + /** Complex method to determine the best looking edge. */ + Beauty = 0, + /** Create a new edge from the first corner to the last. */ + Fixed = 1, + /** Create a new edge from the second corner to the third. */ + Alternate = 2, + /** Create a new edge along the shortest diagonal. */ + ShortEdge = 3, + /** Create a new edge along the longest diagonal. */ + LongEdge = 4, +}; + +/** + * \return #std::nullopt if the mesh is not changed (when every selected face is already a + * triangle). + */ +std::optional mesh_triangulate(const Mesh &src_mesh, + const IndexMask &selection, + TriangulateNGonMode ngon_mode, + TriangulateQuadMode quad_mode, + const bke::AttributeFilter &attribute_filter); + +} // namespace blender::geometry diff --git a/source/blender/geometry/intern/mesh_triangulate.cc b/source/blender/geometry/intern/mesh_triangulate.cc new file mode 100644 index 00000000000..cd5d38992fb --- /dev/null +++ b/source/blender/geometry/intern/mesh_triangulate.cc @@ -0,0 +1,910 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + +#include "atomic_ops.h" + +#include "BLI_array_utils.hh" +#include "BLI_enumerable_thread_specific.hh" +#include "BLI_index_mask.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" + +namespace blender::geometry { + +static void gather(const Span src, const Span indices, MutableSpan dst) +{ + for (const int i : indices.index_range()) { + dst[i] = src[indices[i]]; + } +} + +static Span gather_or_reference(const Span src, + const Span indices, + Vector &dst) +{ + if (unique_sorted_indices::non_empty_is_range(indices)) { + return src.slice(indices[0], indices.size()); + } + dst.reinitialize(indices.size()); + gather(src, indices, dst); + return dst.as_span(); +} + +static Span gather_or_reference(const Span src, + const IndexMaskSegment mask, + Vector &dst) +{ + return gather_or_reference(src.drop_front(mask.offset()), mask.base_span(), dst); +} + +/** + * If a significant number of Ngons are selected (> 25% of the faces), then use the + * face normals cache, in case the cache is persistent (or already calculated). + */ +static Span face_normals_if_worthwhile(const Mesh &src_mesh, const int selection_size) +{ + if (src_mesh.runtime->face_normals_cache.is_cached()) { + return src_mesh.face_normals(); + } + if (selection_size > src_mesh.faces_num / 4) { + return src_mesh.face_normals(); + } + return {}; +} + +static void copy_loose_vert_hint(const Mesh &src, Mesh &dst) +{ + const auto &src_cache = src.runtime->loose_verts_cache; + if (src_cache.is_cached() && src_cache.data().count == 0) { + dst.tag_loose_verts_none(); + } +} + +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 { + +/** + * #Edge_0_2 #Edge_1_3 + * 3 ------- 2 3 ------- 2 + * | 1 / | | \ 1 | + * | / | | \ | + * | / | | \ | + * | / 0 | | 0 \ | + * 0 ------- 1 0 ------- 1 + */ +enum class QuadDirection : int8_t { + Edge_0_2 = 0, + Edge_1_3 = 1, +}; + +/** + * \note This behavior is meant to be the same as #BM_verts_calc_rotate_beauty. + * The order of vertices requires special attention. + */ +static QuadDirection calc_quad_direction_beauty(const float3 &v0, + const float3 &v1, + const float3 &v2, + const float3 &v3) +{ + const int flip_flag = is_quad_flip_v3(v1, v2, v3, v0); + if (UNLIKELY(flip_flag & (1 << 0))) { + return QuadDirection::Edge_0_2; + } + if (UNLIKELY(flip_flag & (1 << 1))) { + return QuadDirection::Edge_1_3; + } + return BLI_polyfill_edge_calc_rotate_beauty__area(v1, v2, v3, v0, false) > 0.0f ? + QuadDirection::Edge_0_2 : + QuadDirection::Edge_1_3; +} + +static void calc_quad_directions(const Span positions, + const Span face_offsets, + const Span corner_verts, + const TriangulateQuadMode quad_mode, + MutableSpan directions) +{ + switch (quad_mode) { + case TriangulateQuadMode::Fixed: { + directions.fill(QuadDirection::Edge_0_2); + break; + } + case TriangulateQuadMode::Alternate: { + directions.fill(QuadDirection::Edge_1_3); + break; + } + case TriangulateQuadMode::ShortEdge: { + for (const int i : face_offsets.index_range()) { + const Span verts = corner_verts.slice(face_offsets[i], 4); + const float dist_0_2 = math::distance_squared(positions[verts[0]], positions[verts[2]]); + const float dist_1_3 = math::distance_squared(positions[verts[1]], positions[verts[3]]); + directions[i] = dist_0_2 < dist_1_3 ? QuadDirection::Edge_0_2 : QuadDirection::Edge_1_3; + } + break; + } + case TriangulateQuadMode::LongEdge: { + for (const int i : face_offsets.index_range()) { + const Span verts = corner_verts.slice(face_offsets[i], 4); + const float dist_0_2 = math::distance_squared(positions[verts[0]], positions[verts[2]]); + const float dist_1_3 = math::distance_squared(positions[verts[1]], positions[verts[3]]); + directions[i] = dist_0_2 > dist_1_3 ? QuadDirection::Edge_0_2 : QuadDirection::Edge_1_3; + } + break; + } + case TriangulateQuadMode::Beauty: { + for (const int i : face_offsets.index_range()) { + const Span verts = corner_verts.slice(face_offsets[i], 4); + directions[i] = calc_quad_direction_beauty( + positions[verts[0]], positions[verts[1]], positions[verts[2]], positions[verts[3]]); + } + + break; + } + } +} + +static void calc_corner_tris(const Span face_offsets, + const Span directions, + MutableSpan corner_tris) +{ + for (const int i : face_offsets.index_range()) { + MutableSpan quad_map = corner_tris.slice(2 * i, 2).cast(); + /* These corner orders give new edges based on the first vertex of each triangle. */ + switch (directions[i]) { + case QuadDirection::Edge_0_2: + quad_map.copy_from({2, 0, 1, 0, 2, 3}); + break; + case QuadDirection::Edge_1_3: + quad_map.copy_from({1, 3, 0, 3, 1, 2}); + break; + } + const int src_face_start = face_offsets[i]; + for (int &i : quad_map) { + i += src_face_start; + } + } +} + +static void calc_corner_tris(const Span positions, + const OffsetIndices src_faces, + const Span src_corner_verts, + const IndexMask &quads, + const TriangulateQuadMode quad_mode, + MutableSpan corner_tris) +{ + struct TLS { + Vector offsets; + Vector directions; + }; + threading::EnumerableThreadSpecific tls; + + quads.foreach_segment(GrainSize(1024), [&](const IndexMaskSegment quads, const int64_t pos) { + TLS &data = tls.local(); + data.directions.reinitialize(quads.size()); + + /* Find the offsets of each face in the local selection. We can gather them together even if + * they aren't contiguous because we only need to know the start of each face; the size is + * just 4. */ + const Span offsets = gather_or_reference(src_faces.data(), quads, data.offsets); + calc_quad_directions(positions, offsets, src_corner_verts, quad_mode, data.directions); + const IndexRange tris_range(pos * 2, offsets.size() * 2); + quad::calc_corner_tris(offsets, data.directions, corner_tris.slice(tris_range)); + }); +} + +/** + * 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, + const IndexMaskSegment selection, + MutableSpan dst_offsets) +{ + int offset = 0; + for (const int64_t i : selection.index_range()) { + dst_offsets[i] = offset; + offset += src_offsets[selection[i]].size(); + } + dst_offsets.last() = offset; + return OffsetIndices(dst_offsets); +} + +namespace ngon { + +static OffsetIndices calc_tris_by_ngon(const OffsetIndices src_faces, + const IndexMask &ngons, + MutableSpan face_offset_data) +{ + ngons.foreach_index(GrainSize(2048), [&](const int face, const int mask) { + face_offset_data[mask] = bke::mesh::face_triangles_num(src_faces[face].size()); + }); + 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, + const Span face_normals, + const IndexMask &ngons, + const OffsetIndices tris_by_ngon, + const TriangulateNGonMode ngon_mode, + MutableSpan corner_tris) +{ + struct LocalData { + Vector projections; + Array offset_data; + Vector projected_positions; + + /* Only used for the "Beauty" method. */ + MemArena *arena = nullptr; + Heap *heap = nullptr; + + ~LocalData() + { + if (arena) { + BLI_memarena_free(arena); + } + if (heap) { + BLI_heap_free(heap, nullptr); + } + } + }; + threading::EnumerableThreadSpecific tls; + + ngons.foreach_segment(GrainSize(128), [&](const IndexMaskSegment ngons, const int pos) { + LocalData &data = tls.local(); + + /* In order to simplify and "parallelize" the next loops, gather offsets used to group an array + * large enough for all the local face corners. */ + data.offset_data.reinitialize(ngons.size() + 1); + const OffsetIndices local_corner_offsets = gather_selected_offsets( + src_faces, ngons, data.offset_data); + + /* Use face normals to build projection matrices to make the face positions 2D. */ + data.projections.reinitialize(ngons.size()); + MutableSpan projections = data.projections; + if (face_normals.is_empty()) { + for (const int i : ngons.index_range()) { + const IndexRange src_face = src_faces[ngons[i]]; + const Span face_verts = src_corner_verts.slice(src_face); + const float3 normal = bke::mesh::face_normal_calc(positions, face_verts); + axis_dominant_v3_to_m3_negate(projections[i].ptr(), normal); + } + } + else { + for (const int i : ngons.index_range()) { + axis_dominant_v3_to_m3_negate(projections[i].ptr(), face_normals[ngons[i]]); + } + } + + /* Project the face positions into 2D using the matrices calculated above. */ + data.projected_positions.reinitialize(local_corner_offsets.total_size()); + MutableSpan projected_positions = data.projected_positions; + for (const int i : ngons.index_range()) { + const IndexRange src_face = src_faces[ngons[i]]; + const Span face_verts = src_corner_verts.slice(src_face); + const float3x3 &matrix = projections[i]; + + MutableSpan positions_2d = projected_positions.slice(local_corner_offsets[i]); + for (const int i : face_verts.index_range()) { + mul_v2_m3v3(positions_2d[i], matrix.ptr(), positions[face_verts[i]]); + } + } + + if (ngon_mode == TriangulateNGonMode::Beauty) { + if (!data.arena) { + data.arena = BLI_memarena_new(BLI_POLYFILL_ARENA_SIZE, __func__); + } + if (!data.heap) { + data.heap = BLI_heap_new_ex(BLI_POLYFILL_ALLOC_NGON_RESERVE); + } + } + + /* Calculate the triangulation of corners indices local to each face. */ + for (const int i : ngons.index_range()) { + const Span positions_2d = projected_positions.slice(local_corner_offsets[i]); + const IndexRange tris_range = tris_by_ngon[pos + i]; + MutableSpan map = corner_tris.slice(tris_range).cast(); + BLI_polyfill_calc(reinterpret_cast(positions_2d.data()), + positions_2d.size(), + 1, + reinterpret_cast(map.data())); + if (ngon_mode == TriangulateNGonMode::Beauty) { + BLI_polyfill_beautify(reinterpret_cast(positions_2d.data()), + positions_2d.size(), + reinterpret_cast(map.data()), + data.arena, + data.heap); + BLI_memarena_clear(data.arena); + } + } + + /* "Globalize" the triangulation created above so the map source indices reference _all_ of the + * source vertices, not just within the source face. */ + for (const int i : ngons.index_range()) { + const IndexRange tris_range = tris_by_ngon[pos + i]; + const int src_face_start = src_faces[ngons[i]].start(); + MutableSpan map = corner_tris.slice(tris_range).cast(); + for (int &vert : map) { + vert += src_face_start; + } + } + }); +} + +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 { + +static GroupedSpan build_vert_to_tri_map(const int verts_num, + const Span vert_tris, + Array &r_offsets, + Array &r_indices) +{ + 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_cnew_array(size_t(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()}; +} + +/** + * 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) +{ + 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, + GrainSize(4096), + memory, + [&](const IndexMaskSegment universe_segment, IndexRangesBuilder &builder) { + if (unique_sorted_indices::non_empty_is_range(universe_segment.base_span())) { + const IndexRange segment_range(universe_segment[0], universe_segment.size()); + const OffsetIndices segment_faces = src_faces.slice(segment_range); + if (segment_faces.total_size() == segment_faces.size() * 3) { + /* All faces in segment are triangles. */ + builder.add_range(universe_segment.base_span().first(), + universe_segment.base_span().last()); + return universe_segment.offset(); + } + } + + for (const int16_t i : universe_segment.base_span()) { + const int face = int(universe_segment.offset() + i); + if (src_faces[face].size() == 3) { + builder.add(i); + } + } + 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) +{ + 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; +} + +/** + * 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) +{ + 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); + } + }); + 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 + +std::optional mesh_triangulate(const Mesh &src_mesh, + const IndexMask &selection_with_tris, + const TriangulateNGonMode ngon_mode, + const TriangulateQuadMode quad_mode, + 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(); + + /* 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; + }); + const IndexMask ngons = IndexMask::from_predicate( + selection_with_tris, 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); + const OffsetIndices tris_by_ngon = ngon::calc_tris_by_ngon(src_faces, ngons, tris_by_ngon_data); + const int ngon_tris_num = tris_by_ngon.total_size(); + const int quad_tris_num = quads.size() * 2; + const IndexRange tris_range(ngon_tris_num + quad_tris_num); + 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()); + + if (!ngons.is_empty()) { + ngon::calc_corner_tris(positions, + src_faces, + src_corner_verts, + face_normals_if_worthwhile(src_mesh, ngons.size()), + ngons, + tris_by_ngon, + ngon_mode, + corner_tris.as_mutable_span().slice(ngon_tris_range)); + } + if (!quads.is_empty()) { + quad::calc_corner_tris(positions, + src_faces, + src_corner_verts, + quads, + quad_mode, + 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()); + + /* 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 facilite 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); + 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()); + 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 unnaffected 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); + + 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)); + } + + 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)); + attribute.dst.finish(); + } + if (CustomData_has_layer(&src_mesh.face_data, CD_ORIGINDEX)) { + const Span src( + static_cast(CustomData_get_layer(&src_mesh.face_data, CD_ORIGINDEX)), + src_mesh.faces_num); + 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_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); + for (auto &attribute : bke::retrieve_attributes_for_transfer( + src_attributes, + attributes, + ATTR_DOMAIN_MASK_CORNER, + bke::attribute_filter_with_skip_ref(attribute_filter, + {".corner_vert", ".corner_edge"}))) + { + bke::attribute_math::gather_group_to_group( + src_faces, faces_unselected, unselected, attribute.src, attribute.dst.span); + bke::attribute_math::gather(attribute.src, + corner_tris.as_span().cast(), + attribute.dst.span.slice(tri_corners_range)); + attribute.dst.finish(); + } + + 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(); + } + BLI_assert(BKE_mesh_is_valid(mesh)); + return mesh; +} + +} // namespace blender::geometry diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index 7435dc64c94..78440bd0ff9 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -2893,19 +2893,6 @@ typedef enum GeometryNodeCurveHandleMode { GEO_NODE_CURVE_HANDLE_RIGHT = (1 << 1) } GeometryNodeCurveHandleMode; -typedef enum GeometryNodeTriangulateNGons { - GEO_NODE_TRIANGULATE_NGON_BEAUTY = 0, - GEO_NODE_TRIANGULATE_NGON_EARCLIP = 1, -} GeometryNodeTriangulateNGons; - -typedef enum GeometryNodeTriangulateQuads { - GEO_NODE_TRIANGULATE_QUAD_BEAUTY = 0, - GEO_NODE_TRIANGULATE_QUAD_FIXED = 1, - GEO_NODE_TRIANGULATE_QUAD_ALTERNATE = 2, - GEO_NODE_TRIANGULATE_QUAD_SHORTEDGE = 3, - GEO_NODE_TRIANGULATE_QUAD_LONGEDGE = 4, -} GeometryNodeTriangulateQuads; - typedef enum GeometryNodeDistributePointsInVolumeMode { GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM = 0, GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID = 1, diff --git a/source/blender/nodes/geometry/nodes/node_geo_triangulate.cc b/source/blender/nodes/geometry/nodes/node_geo_triangulate.cc index 0c8370d683a..c060e99e0f3 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_triangulate.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_triangulate.cc @@ -2,16 +2,12 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ -#include "BKE_customdata.hh" #include "BKE_mesh.hh" -#include "bmesh.hh" -#include "bmesh_tools.hh" - -#include "DNA_mesh_types.h" - #include "NOD_rna_define.hh" +#include "GEO_mesh_triangulate.hh" + #include "UI_interface.hh" #include "UI_resources.hh" @@ -25,7 +21,6 @@ static void node_declare(NodeDeclarationBuilder &b) { b.add_input("Mesh").supported_type(GeometryComponent::Type::Mesh); b.add_input("Selection").default_value(true).field_on_all().hide_value(); - b.add_input("Minimum Vertices").default_value(4).min(4).max(10000); b.add_output("Mesh").propagate_all(); } @@ -37,69 +32,53 @@ static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) static void geo_triangulate_init(bNodeTree * /*tree*/, bNode *node) { - node->custom1 = GEO_NODE_TRIANGULATE_QUAD_SHORTEDGE; - node->custom2 = GEO_NODE_TRIANGULATE_NGON_BEAUTY; -} - -static Mesh *triangulate_mesh_selection(const Mesh &mesh, - const int quad_method, - const int ngon_method, - const IndexMask &selection, - const int min_vertices) -{ - CustomData_MeshMasks cd_mask_extra = { - CD_MASK_ORIGINDEX, CD_MASK_ORIGINDEX, 0, CD_MASK_ORIGINDEX}; - BMeshCreateParams create_params{false}; - BMeshFromMeshParams from_mesh_params{}; - from_mesh_params.calc_face_normal = true; - from_mesh_params.calc_vert_normal = true; - from_mesh_params.cd_mask_extra = cd_mask_extra; - BMesh *bm = BKE_mesh_to_bmesh_ex(&mesh, &create_params, &from_mesh_params); - - /* Tag faces to be triangulated from the selection mask. */ - BM_mesh_elem_table_ensure(bm, BM_FACE); - selection.foreach_index([&](const int i_face) { - BM_elem_flag_set(BM_face_at_index(bm, i_face), BM_ELEM_TAG, true); - }); - - BM_mesh_triangulate(bm, quad_method, ngon_method, min_vertices, true, nullptr, nullptr, nullptr); - Mesh *result = BKE_mesh_from_bmesh_for_eval_nomain(bm, &cd_mask_extra, &mesh); - BM_mesh_free(bm); - - /* Positions are not changed by the triangulation operation, so the bounds are the same. */ - result->runtime->bounds_cache = mesh.runtime->bounds_cache; - - /* Vertex order is not affected. */ - geometry::debug_randomize_edge_order(result); - geometry::debug_randomize_face_order(result); - - return result; + node->custom1 = int(geometry::TriangulateQuadMode::ShortEdge); + node->custom2 = int(geometry::TriangulateNGonMode::Beauty); } static void node_geo_exec(GeoNodeExecParams params) { GeometrySet geometry_set = params.extract_input("Mesh"); Field selection_field = params.extract_input>("Selection"); - const int min_vertices = std::max(params.extract_input("Minimum Vertices"), 4); + const AttributeFilter &attribute_filter = params.get_attribute_filter("Mesh"); - GeometryNodeTriangulateQuads quad_method = GeometryNodeTriangulateQuads(params.node().custom1); - GeometryNodeTriangulateNGons ngon_method = GeometryNodeTriangulateNGons(params.node().custom2); + geometry::TriangulateNGonMode ngon_method = geometry::TriangulateNGonMode(params.node().custom2); + geometry::TriangulateQuadMode quad_method = geometry::TriangulateQuadMode(params.node().custom1); geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) { - if (!geometry_set.has_mesh()) { + const Mesh *src_mesh = geometry_set.get_mesh(); + if (!src_mesh) { + return; + } + if (src_mesh->corners_num == src_mesh->faces_num * 3) { + /* The mesh is already completely triangulated. */ return; } - const Mesh &mesh_in = *geometry_set.get_mesh(); - const bke::MeshFieldContext context{mesh_in, AttrDomain::Face}; - FieldEvaluator evaluator{context, mesh_in.faces_num}; + const bke::MeshFieldContext context(*src_mesh, AttrDomain::Face); + FieldEvaluator evaluator{context, src_mesh->faces_num}; evaluator.add(selection_field); evaluator.evaluate(); const IndexMask selection = evaluator.get_evaluated_as_mask(0); + if (selection.is_empty()) { + return; + } - Mesh *mesh_out = triangulate_mesh_selection( - mesh_in, quad_method, ngon_method, selection, min_vertices); - geometry_set.replace_mesh(mesh_out); + std::optional mesh = geometry::mesh_triangulate( + *src_mesh, + selection, + geometry::TriangulateNGonMode(ngon_method), + geometry::TriangulateQuadMode(quad_method), + attribute_filter); + if (!mesh) { + return; + } + + /* Vertex order is not affected. */ + geometry::debug_randomize_edge_order(*mesh); + geometry::debug_randomize_face_order(*mesh); + + geometry_set.replace_mesh(*mesh); }); params.set_output("Mesh", std::move(geometry_set)); @@ -108,27 +87,27 @@ static void node_geo_exec(GeoNodeExecParams params) static void node_rna(StructRNA *srna) { static const EnumPropertyItem rna_node_geometry_triangulate_quad_method_items[] = { - {GEO_NODE_TRIANGULATE_QUAD_BEAUTY, + {int(geometry::TriangulateQuadMode::Beauty), "BEAUTY", 0, "Beauty", "Split the quads in nice triangles, slower method"}, - {GEO_NODE_TRIANGULATE_QUAD_FIXED, + {int(geometry::TriangulateQuadMode::Fixed), "FIXED", 0, "Fixed", "Split the quads on the first and third vertices"}, - {GEO_NODE_TRIANGULATE_QUAD_ALTERNATE, + {int(geometry::TriangulateQuadMode::Alternate), "FIXED_ALTERNATE", 0, "Fixed Alternate", "Split the quads on the 2nd and 4th vertices"}, - {GEO_NODE_TRIANGULATE_QUAD_SHORTEDGE, + {int(geometry::TriangulateQuadMode::ShortEdge), "SHORTEST_DIAGONAL", 0, "Shortest Diagonal", "Split the quads along their shortest diagonal"}, - {GEO_NODE_TRIANGULATE_QUAD_LONGEDGE, + {int(geometry::TriangulateQuadMode::LongEdge), "LONGEST_DIAGONAL", 0, "Longest Diagonal", @@ -137,12 +116,12 @@ static void node_rna(StructRNA *srna) }; static const EnumPropertyItem rna_node_geometry_triangulate_ngon_method_items[] = { - {GEO_NODE_TRIANGULATE_NGON_BEAUTY, + {int(geometry::TriangulateNGonMode::Beauty), "BEAUTY", 0, "Beauty", "Arrange the new triangles evenly (slow)"}, - {GEO_NODE_TRIANGULATE_NGON_EARCLIP, + {int(geometry::TriangulateNGonMode::EarClip), "CLIP", 0, "Clip", diff --git a/tests/data b/tests/data index c1bf96df576..3a49714aeb4 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit c1bf96df57682065a7bef28f1ffbf461b0062df6 +Subproject commit 3a49714aeb4aeb57782878e7246544e5def21a78