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
This commit is contained in:
@@ -31,7 +31,7 @@ extern "C" {
|
|||||||
|
|
||||||
/* Blender file format version. */
|
/* Blender file format version. */
|
||||||
#define BLENDER_FILE_VERSION BLENDER_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
|
/* 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
|
* version. Older Blender versions will test this and cancel loading the file, showing a warning to
|
||||||
|
|||||||
@@ -2404,6 +2404,126 @@ static void version_principled_bsdf_coat(bNodeTree *ntree)
|
|||||||
ntree, SH_NODE_BSDF_PRINCIPLED, "Clearcoat Normal", "Coat Normal");
|
ntree, SH_NODE_BSDF_PRINCIPLED, "Clearcoat Normal", "Coat Normal");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void remove_triangulate_node_min_size_input(bNodeTree *tree)
|
||||||
|
{
|
||||||
|
using namespace blender;
|
||||||
|
Set<bNode *> triangulate_nodes;
|
||||||
|
LISTBASE_FOREACH (bNode *, node, &tree->nodes) {
|
||||||
|
if (node->type == GEO_NODE_TRIANGULATE) {
|
||||||
|
triangulate_nodes.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<bNodeSocket *, bNodeLink *> 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<bNodeSocketValueInt *>(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<NodeFunctionCompare>(__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<bNodeSocketValueInt *>(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. */
|
/* Convert specular tint in Principled BSDF. */
|
||||||
static void version_principled_bsdf_specular_tint(bNodeTree *ntree)
|
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);
|
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
|
/* 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
|
* be converted to the new format on file load. Can be moved to a subversion check in a larger
|
||||||
* breaking release. */
|
* breaking release. */
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ set(SRC
|
|||||||
intern/mesh_split_edges.cc
|
intern/mesh_split_edges.cc
|
||||||
intern/mesh_to_curve_convert.cc
|
intern/mesh_to_curve_convert.cc
|
||||||
intern/mesh_to_volume.cc
|
intern/mesh_to_volume.cc
|
||||||
|
intern/mesh_triangulate.cc
|
||||||
intern/mix_geometries.cc
|
intern/mix_geometries.cc
|
||||||
intern/point_merge_by_distance.cc
|
intern/point_merge_by_distance.cc
|
||||||
intern/points_to_volume.cc
|
intern/points_to_volume.cc
|
||||||
@@ -77,6 +78,7 @@ set(SRC
|
|||||||
GEO_mesh_split_edges.hh
|
GEO_mesh_split_edges.hh
|
||||||
GEO_mesh_to_curve.hh
|
GEO_mesh_to_curve.hh
|
||||||
GEO_mesh_to_volume.hh
|
GEO_mesh_to_volume.hh
|
||||||
|
GEO_mesh_triangulate.hh
|
||||||
GEO_mix_geometries.hh
|
GEO_mix_geometries.hh
|
||||||
GEO_point_merge_by_distance.hh
|
GEO_point_merge_by_distance.hh
|
||||||
GEO_points_to_volume.hh
|
GEO_points_to_volume.hh
|
||||||
@@ -101,6 +103,7 @@ set(LIB
|
|||||||
bf_blenkernel
|
bf_blenkernel
|
||||||
PRIVATE bf::blenlib
|
PRIVATE bf::blenlib
|
||||||
PRIVATE bf::dna
|
PRIVATE bf::dna
|
||||||
|
PRIVATE bf::intern::atomic
|
||||||
PRIVATE bf::intern::guardedalloc
|
PRIVATE bf::intern::guardedalloc
|
||||||
PRIVATE bf::extern::fmtlib
|
PRIVATE bf::extern::fmtlib
|
||||||
)
|
)
|
||||||
|
|||||||
48
source/blender/geometry/GEO_mesh_triangulate.hh
Normal file
48
source/blender/geometry/GEO_mesh_triangulate.hh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#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 *> mesh_triangulate(const Mesh &src_mesh,
|
||||||
|
const IndexMask &selection,
|
||||||
|
TriangulateNGonMode ngon_mode,
|
||||||
|
TriangulateQuadMode quad_mode,
|
||||||
|
const bke::AttributeFilter &attribute_filter);
|
||||||
|
|
||||||
|
} // namespace blender::geometry
|
||||||
910
source/blender/geometry/intern/mesh_triangulate.cc
Normal file
910
source/blender/geometry/intern/mesh_triangulate.cc
Normal file
@@ -0,0 +1,910 @@
|
|||||||
|
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#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<int> src, const Span<int16_t> indices, MutableSpan<int> dst)
|
||||||
|
{
|
||||||
|
for (const int i : indices.index_range()) {
|
||||||
|
dst[i] = src[indices[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Span<int> gather_or_reference(const Span<int> src,
|
||||||
|
const Span<int16_t> indices,
|
||||||
|
Vector<int> &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<int> gather_or_reference(const Span<int> src,
|
||||||
|
const IndexMaskSegment mask,
|
||||||
|
Vector<int> &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<float3> 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<int> calc_face_offsets(const OffsetIndices<int> src_faces,
|
||||||
|
const IndexMask &unselected,
|
||||||
|
MutableSpan<int> offsets)
|
||||||
|
{
|
||||||
|
MutableSpan<int> 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<int>(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<float3> positions,
|
||||||
|
const Span<int> face_offsets,
|
||||||
|
const Span<int> corner_verts,
|
||||||
|
const TriangulateQuadMode quad_mode,
|
||||||
|
MutableSpan<QuadDirection> 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<int> 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<int> 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<int> 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<int> face_offsets,
|
||||||
|
const Span<QuadDirection> directions,
|
||||||
|
MutableSpan<int3> corner_tris)
|
||||||
|
{
|
||||||
|
for (const int i : face_offsets.index_range()) {
|
||||||
|
MutableSpan<int> quad_map = corner_tris.slice(2 * i, 2).cast<int>();
|
||||||
|
/* 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<float3> positions,
|
||||||
|
const OffsetIndices<int> src_faces,
|
||||||
|
const Span<int> src_corner_verts,
|
||||||
|
const IndexMask &quads,
|
||||||
|
const TriangulateQuadMode quad_mode,
|
||||||
|
MutableSpan<int3> corner_tris)
|
||||||
|
{
|
||||||
|
struct TLS {
|
||||||
|
Vector<int> offsets;
|
||||||
|
Vector<QuadDirection> directions;
|
||||||
|
};
|
||||||
|
threading::EnumerableThreadSpecific<TLS> 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<int> 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<int> quad_corner_verts, MutableSpan<int2> new_quad_edges)
|
||||||
|
{
|
||||||
|
const int quads_num = quad_corner_verts.size() / 6;
|
||||||
|
for (const int i : IndexRange(quads_num)) {
|
||||||
|
const Span<int> 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<int> src_corner_edges,
|
||||||
|
const Span<int3> corner_tris,
|
||||||
|
const int edges_start,
|
||||||
|
MutableSpan<int> 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<int> src_corner_edges,
|
||||||
|
const Span<int3> corner_tris,
|
||||||
|
const Span<int> corner_verts,
|
||||||
|
const int edges_start,
|
||||||
|
MutableSpan<int2> edges,
|
||||||
|
MutableSpan<int> 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<typename T>
|
||||||
|
static void copy_quad_data_to_tris(const Span<T> src, const IndexMask &quads, MutableSpan<T> dst)
|
||||||
|
{
|
||||||
|
quads.foreach_index_optimized<int>([&](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<T>(), quads, dst.typed<T>());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace quad
|
||||||
|
|
||||||
|
static OffsetIndices<int> gather_selected_offsets(const OffsetIndices<int> src_offsets,
|
||||||
|
const IndexMaskSegment selection,
|
||||||
|
MutableSpan<int> 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<int>(dst_offsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ngon {
|
||||||
|
|
||||||
|
static OffsetIndices<int> calc_tris_by_ngon(const OffsetIndices<int> src_faces,
|
||||||
|
const IndexMask &ngons,
|
||||||
|
MutableSpan<int> 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<int> calc_edges_by_ngon(const OffsetIndices<int> src_faces,
|
||||||
|
const IndexMask &selection,
|
||||||
|
MutableSpan<int> 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<float3> positions,
|
||||||
|
const OffsetIndices<int> src_faces,
|
||||||
|
const Span<int> src_corner_verts,
|
||||||
|
const Span<float3> face_normals,
|
||||||
|
const IndexMask &ngons,
|
||||||
|
const OffsetIndices<int> tris_by_ngon,
|
||||||
|
const TriangulateNGonMode ngon_mode,
|
||||||
|
MutableSpan<int3> corner_tris)
|
||||||
|
{
|
||||||
|
struct LocalData {
|
||||||
|
Vector<float3x3> projections;
|
||||||
|
Array<int> offset_data;
|
||||||
|
Vector<float2> 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<LocalData> 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<float3x3> 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<int> 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<float2> projected_positions = data.projected_positions;
|
||||||
|
for (const int i : ngons.index_range()) {
|
||||||
|
const IndexRange src_face = src_faces[ngons[i]];
|
||||||
|
const Span<int> face_verts = src_corner_verts.slice(src_face);
|
||||||
|
const float3x3 &matrix = projections[i];
|
||||||
|
|
||||||
|
MutableSpan<float2> 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<float2> positions_2d = projected_positions.slice(local_corner_offsets[i]);
|
||||||
|
const IndexRange tris_range = tris_by_ngon[pos + i];
|
||||||
|
MutableSpan<int> map = corner_tris.slice(tris_range).cast<int>();
|
||||||
|
BLI_polyfill_calc(reinterpret_cast<const float(*)[2]>(positions_2d.data()),
|
||||||
|
positions_2d.size(),
|
||||||
|
1,
|
||||||
|
reinterpret_cast<uint(*)[3]>(map.data()));
|
||||||
|
if (ngon_mode == TriangulateNGonMode::Beauty) {
|
||||||
|
BLI_polyfill_beautify(reinterpret_cast<const float(*)[2]>(positions_2d.data()),
|
||||||
|
positions_2d.size(),
|
||||||
|
reinterpret_cast<uint(*)[3]>(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<int> map = corner_tris.slice(tris_range).cast<int>();
|
||||||
|
for (int &vert : map) {
|
||||||
|
vert += src_face_start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void calc_inner_tri_edges(const IndexRange src_face,
|
||||||
|
const Span<int> src_corner_verts,
|
||||||
|
const Span<int> src_corner_edges,
|
||||||
|
const Span<int3> corner_tris,
|
||||||
|
const int edges_start,
|
||||||
|
MutableSpan<int> corner_edges,
|
||||||
|
VectorSet<OrderedEdge> &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<int> src_faces,
|
||||||
|
const Span<int> src_corner_verts,
|
||||||
|
const Span<int> src_corner_edges,
|
||||||
|
const IndexMask &ngons,
|
||||||
|
const OffsetIndices<int> tris_by_ngon,
|
||||||
|
const OffsetIndices<int> edges_by_ngon,
|
||||||
|
const IndexRange ngon_edges_range,
|
||||||
|
const Span<int3> corner_tris,
|
||||||
|
MutableSpan<int2> edges,
|
||||||
|
MutableSpan<int> corner_edges)
|
||||||
|
{
|
||||||
|
MutableSpan<int2> inner_edges = edges.slice(ngon_edges_range);
|
||||||
|
threading::EnumerableThreadSpecific<VectorSet<OrderedEdge>> tls;
|
||||||
|
ngons.foreach_segment(GrainSize(128), [&](const IndexMaskSegment ngons, const int pos) {
|
||||||
|
VectorSet<OrderedEdge> &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<int2>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ngon
|
||||||
|
|
||||||
|
namespace deduplication {
|
||||||
|
|
||||||
|
static GroupedSpan<int> build_vert_to_tri_map(const int verts_num,
|
||||||
|
const Span<int3> vert_tris,
|
||||||
|
Array<int> &r_offsets,
|
||||||
|
Array<int> &r_indices)
|
||||||
|
{
|
||||||
|
r_offsets = Array<int>(verts_num + 1, 0);
|
||||||
|
offset_indices::build_reverse_offsets(vert_tris.cast<int>(), r_offsets);
|
||||||
|
const OffsetIndices offsets(r_offsets.as_span());
|
||||||
|
|
||||||
|
r_indices.reinitialize(offsets.total_size());
|
||||||
|
int *counts = MEM_cnew_array<int>(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<int> src_faces,
|
||||||
|
const Span<int> src_corner_verts,
|
||||||
|
const IndexMask &selection,
|
||||||
|
const Span<int3> 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<int16_t> &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<int3> vert_tris(corner_tris.size());
|
||||||
|
bke::attribute_math::gather(
|
||||||
|
src_corner_verts, corner_tris.cast<int>(), vert_tris.as_mutable_span().cast<int>());
|
||||||
|
|
||||||
|
Array<int> vert_to_tri_offsets;
|
||||||
|
Array<int> vert_to_tri_indices;
|
||||||
|
const GroupedSpan<int> 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<int, 3> &tri_verts) {
|
||||||
|
/* TODO: Sorting the three values with a few comparisons would be faster than a #Set. */
|
||||||
|
const Set<int, 3> 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<int, 3> 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<int> 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<int> find_edge_duplicate(const GroupedSpan<int> vert_to_edge_map,
|
||||||
|
const Span<int2> 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<int2> src_edges,
|
||||||
|
const IndexRange new_edges_range,
|
||||||
|
MutableSpan<int2> edges,
|
||||||
|
MutableSpan<int> corner_edges)
|
||||||
|
{
|
||||||
|
if (src_mesh.no_overlapping_topology()) {
|
||||||
|
return edges.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
Array<int> vert_to_edge_offsets;
|
||||||
|
Array<int> vert_to_edge_indices;
|
||||||
|
const GroupedSpan<int> 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<int2> new_edges = edges.slice(new_edges_range);
|
||||||
|
Array<int> 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<int>(
|
||||||
|
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<int2> 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 *> 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<float3> positions = src_mesh.vert_positions();
|
||||||
|
const Span<int2> src_edges = src_mesh.edges();
|
||||||
|
const OffsetIndices src_faces = src_mesh.faces();
|
||||||
|
const Span<int> src_corner_verts = src_mesh.corner_verts();
|
||||||
|
const Span<int> 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<int> 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<int> 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<int3> 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<int2>(".edge_verts", bke::AttrDomain::Edge, bke::AttributeInitConstruct());
|
||||||
|
attributes.add<int>(".corner_vert", bke::AttrDomain::Corner, bke::AttributeInitConstruct());
|
||||||
|
attributes.add<int>(".corner_edge", bke::AttrDomain::Corner, bke::AttributeInitConstruct());
|
||||||
|
|
||||||
|
MutableSpan<int2> edges_with_duplicates = mesh->edges_for_write();
|
||||||
|
MutableSpan<int> corner_verts = mesh->corner_verts_for_write();
|
||||||
|
MutableSpan<int> corner_edges = mesh->corner_edges_for_write();
|
||||||
|
|
||||||
|
array_utils::gather(
|
||||||
|
src_corner_verts, corner_tris.as_span().cast<int>(), 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<const int *>(CustomData_get_layer(&src_mesh.edge_data, CD_ORIGINDEX)),
|
||||||
|
src_mesh.edges_num);
|
||||||
|
MutableSpan dst(static_cast<int *>(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<const int *>(CustomData_get_layer(&src_mesh.face_data, CD_ORIGINDEX)),
|
||||||
|
src_mesh.faces_num);
|
||||||
|
MutableSpan dst(static_cast<int *>(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<int>(),
|
||||||
|
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
|
||||||
@@ -2893,19 +2893,6 @@ typedef enum GeometryNodeCurveHandleMode {
|
|||||||
GEO_NODE_CURVE_HANDLE_RIGHT = (1 << 1)
|
GEO_NODE_CURVE_HANDLE_RIGHT = (1 << 1)
|
||||||
} GeometryNodeCurveHandleMode;
|
} 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 {
|
typedef enum GeometryNodeDistributePointsInVolumeMode {
|
||||||
GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM = 0,
|
GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM = 0,
|
||||||
GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID = 1,
|
GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID = 1,
|
||||||
|
|||||||
@@ -2,16 +2,12 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
|
||||||
#include "BKE_customdata.hh"
|
|
||||||
#include "BKE_mesh.hh"
|
#include "BKE_mesh.hh"
|
||||||
|
|
||||||
#include "bmesh.hh"
|
|
||||||
#include "bmesh_tools.hh"
|
|
||||||
|
|
||||||
#include "DNA_mesh_types.h"
|
|
||||||
|
|
||||||
#include "NOD_rna_define.hh"
|
#include "NOD_rna_define.hh"
|
||||||
|
|
||||||
|
#include "GEO_mesh_triangulate.hh"
|
||||||
|
|
||||||
#include "UI_interface.hh"
|
#include "UI_interface.hh"
|
||||||
#include "UI_resources.hh"
|
#include "UI_resources.hh"
|
||||||
|
|
||||||
@@ -25,7 +21,6 @@ static void node_declare(NodeDeclarationBuilder &b)
|
|||||||
{
|
{
|
||||||
b.add_input<decl::Geometry>("Mesh").supported_type(GeometryComponent::Type::Mesh);
|
b.add_input<decl::Geometry>("Mesh").supported_type(GeometryComponent::Type::Mesh);
|
||||||
b.add_input<decl::Bool>("Selection").default_value(true).field_on_all().hide_value();
|
b.add_input<decl::Bool>("Selection").default_value(true).field_on_all().hide_value();
|
||||||
b.add_input<decl::Int>("Minimum Vertices").default_value(4).min(4).max(10000);
|
|
||||||
b.add_output<decl::Geometry>("Mesh").propagate_all();
|
b.add_output<decl::Geometry>("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)
|
static void geo_triangulate_init(bNodeTree * /*tree*/, bNode *node)
|
||||||
{
|
{
|
||||||
node->custom1 = GEO_NODE_TRIANGULATE_QUAD_SHORTEDGE;
|
node->custom1 = int(geometry::TriangulateQuadMode::ShortEdge);
|
||||||
node->custom2 = GEO_NODE_TRIANGULATE_NGON_BEAUTY;
|
node->custom2 = int(geometry::TriangulateNGonMode::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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void node_geo_exec(GeoNodeExecParams params)
|
static void node_geo_exec(GeoNodeExecParams params)
|
||||||
{
|
{
|
||||||
GeometrySet geometry_set = params.extract_input<GeometrySet>("Mesh");
|
GeometrySet geometry_set = params.extract_input<GeometrySet>("Mesh");
|
||||||
Field<bool> selection_field = params.extract_input<Field<bool>>("Selection");
|
Field<bool> selection_field = params.extract_input<Field<bool>>("Selection");
|
||||||
const int min_vertices = std::max(params.extract_input<int>("Minimum Vertices"), 4);
|
const AttributeFilter &attribute_filter = params.get_attribute_filter("Mesh");
|
||||||
|
|
||||||
GeometryNodeTriangulateQuads quad_method = GeometryNodeTriangulateQuads(params.node().custom1);
|
geometry::TriangulateNGonMode ngon_method = geometry::TriangulateNGonMode(params.node().custom2);
|
||||||
GeometryNodeTriangulateNGons ngon_method = GeometryNodeTriangulateNGons(params.node().custom2);
|
geometry::TriangulateQuadMode quad_method = geometry::TriangulateQuadMode(params.node().custom1);
|
||||||
|
|
||||||
geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const Mesh &mesh_in = *geometry_set.get_mesh();
|
|
||||||
|
|
||||||
const bke::MeshFieldContext context{mesh_in, AttrDomain::Face};
|
const bke::MeshFieldContext context(*src_mesh, AttrDomain::Face);
|
||||||
FieldEvaluator evaluator{context, mesh_in.faces_num};
|
FieldEvaluator evaluator{context, src_mesh->faces_num};
|
||||||
evaluator.add(selection_field);
|
evaluator.add(selection_field);
|
||||||
evaluator.evaluate();
|
evaluator.evaluate();
|
||||||
const IndexMask selection = evaluator.get_evaluated_as_mask(0);
|
const IndexMask selection = evaluator.get_evaluated_as_mask(0);
|
||||||
|
if (selection.is_empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Mesh *mesh_out = triangulate_mesh_selection(
|
std::optional<Mesh *> mesh = geometry::mesh_triangulate(
|
||||||
mesh_in, quad_method, ngon_method, selection, min_vertices);
|
*src_mesh,
|
||||||
geometry_set.replace_mesh(mesh_out);
|
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));
|
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 void node_rna(StructRNA *srna)
|
||||||
{
|
{
|
||||||
static const EnumPropertyItem rna_node_geometry_triangulate_quad_method_items[] = {
|
static const EnumPropertyItem rna_node_geometry_triangulate_quad_method_items[] = {
|
||||||
{GEO_NODE_TRIANGULATE_QUAD_BEAUTY,
|
{int(geometry::TriangulateQuadMode::Beauty),
|
||||||
"BEAUTY",
|
"BEAUTY",
|
||||||
0,
|
0,
|
||||||
"Beauty",
|
"Beauty",
|
||||||
"Split the quads in nice triangles, slower method"},
|
"Split the quads in nice triangles, slower method"},
|
||||||
{GEO_NODE_TRIANGULATE_QUAD_FIXED,
|
{int(geometry::TriangulateQuadMode::Fixed),
|
||||||
"FIXED",
|
"FIXED",
|
||||||
0,
|
0,
|
||||||
"Fixed",
|
"Fixed",
|
||||||
"Split the quads on the first and third vertices"},
|
"Split the quads on the first and third vertices"},
|
||||||
{GEO_NODE_TRIANGULATE_QUAD_ALTERNATE,
|
{int(geometry::TriangulateQuadMode::Alternate),
|
||||||
"FIXED_ALTERNATE",
|
"FIXED_ALTERNATE",
|
||||||
0,
|
0,
|
||||||
"Fixed Alternate",
|
"Fixed Alternate",
|
||||||
"Split the quads on the 2nd and 4th vertices"},
|
"Split the quads on the 2nd and 4th vertices"},
|
||||||
{GEO_NODE_TRIANGULATE_QUAD_SHORTEDGE,
|
{int(geometry::TriangulateQuadMode::ShortEdge),
|
||||||
"SHORTEST_DIAGONAL",
|
"SHORTEST_DIAGONAL",
|
||||||
0,
|
0,
|
||||||
"Shortest Diagonal",
|
"Shortest Diagonal",
|
||||||
"Split the quads along their shortest diagonal"},
|
"Split the quads along their shortest diagonal"},
|
||||||
{GEO_NODE_TRIANGULATE_QUAD_LONGEDGE,
|
{int(geometry::TriangulateQuadMode::LongEdge),
|
||||||
"LONGEST_DIAGONAL",
|
"LONGEST_DIAGONAL",
|
||||||
0,
|
0,
|
||||||
"Longest Diagonal",
|
"Longest Diagonal",
|
||||||
@@ -137,12 +116,12 @@ static void node_rna(StructRNA *srna)
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const EnumPropertyItem rna_node_geometry_triangulate_ngon_method_items[] = {
|
static const EnumPropertyItem rna_node_geometry_triangulate_ngon_method_items[] = {
|
||||||
{GEO_NODE_TRIANGULATE_NGON_BEAUTY,
|
{int(geometry::TriangulateNGonMode::Beauty),
|
||||||
"BEAUTY",
|
"BEAUTY",
|
||||||
0,
|
0,
|
||||||
"Beauty",
|
"Beauty",
|
||||||
"Arrange the new triangles evenly (slow)"},
|
"Arrange the new triangles evenly (slow)"},
|
||||||
{GEO_NODE_TRIANGULATE_NGON_EARCLIP,
|
{int(geometry::TriangulateNGonMode::EarClip),
|
||||||
"CLIP",
|
"CLIP",
|
||||||
0,
|
0,
|
||||||
"Clip",
|
"Clip",
|
||||||
|
|||||||
Submodule tests/data updated: c1bf96df57...3a49714aeb
Reference in New Issue
Block a user