From a48d155c871931cc96ce6491b7c810e7dfc5e5da Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Tue, 4 Mar 2025 19:16:36 +0100 Subject: [PATCH] Geometry Nodes: Add "Face" option to Mesh to Curve node Adds a mode option to the node to choose between the existing behavior and new behavior that converts each face to a cyclic curve. Generally this is much faster than the existing mode because it's easy to parallelize and because curve offsets and face and corner attributes can be implicitly shared to avoid copies. Resolves #134671. Pull Request: https://projects.blender.org/blender/blender/pulls/134773 --- .../blenkernel/intern/attribute_access.cc | 8 + source/blender/geometry/GEO_mesh_to_curve.hh | 11 +- .../geometry/intern/mesh_to_curve_convert.cc | 138 ++++++++++++++++-- .../geometry/nodes/node_geo_mesh_to_curve.cc | 82 +++++++++-- 4 files changed, 212 insertions(+), 27 deletions(-) diff --git a/source/blender/blenkernel/intern/attribute_access.cc b/source/blender/blenkernel/intern/attribute_access.cc index 1a129661cfc..10cb756da60 100644 --- a/source/blender/blenkernel/intern/attribute_access.cc +++ b/source/blender/blenkernel/intern/attribute_access.cc @@ -955,6 +955,14 @@ void gather_attributes_group_to_group(const AttributeAccessor src_attributes, const IndexMask &selection, MutableAttributeAccessor dst_attributes) { + if (selection.size() == src_offsets.size()) { + if (src_attributes.domain_size(src_domain) == dst_attributes.domain_size(src_domain)) { + /* When all groups are selected and the domains are the same size, all values are copied, + * because corresponding groups are required to be the same size. */ + copy_attributes(src_attributes, src_domain, dst_domain, attribute_filter, dst_attributes); + return; + } + } src_attributes.foreach_attribute([&](const AttributeIter &iter) { if (iter.domain != src_domain) { return; diff --git a/source/blender/geometry/GEO_mesh_to_curve.hh b/source/blender/geometry/GEO_mesh_to_curve.hh index d32b5c97307..feeaaabcda0 100644 --- a/source/blender/geometry/GEO_mesh_to_curve.hh +++ b/source/blender/geometry/GEO_mesh_to_curve.hh @@ -21,9 +21,9 @@ namespace blender::geometry { * intersections of more than three edges will become breaks in curves. Attributes that * are not built-in on meshes and not curves are transferred to the result curve. */ -bke::CurvesGeometry mesh_to_curve_convert(const Mesh &mesh, - const IndexMask &selection, - const bke::AttributeFilter &attribute_filter); +bke::CurvesGeometry mesh_edges_to_curves_convert(const Mesh &mesh, + const IndexMask &selection, + const bke::AttributeFilter &attribute_filter); bke::CurvesGeometry create_curve_from_vert_indices(const bke::AttributeAccessor &mesh_attributes, Span vert_indices, @@ -31,4 +31,9 @@ bke::CurvesGeometry create_curve_from_vert_indices(const bke::AttributeAccessor IndexRange cyclic_curves, const bke::AttributeFilter &attribute_filter); +/** Convert each mesh face into a cyclic curve. */ +bke::CurvesGeometry mesh_faces_to_curves_convert(const Mesh &mesh, + const IndexMask &selection, + const bke::AttributeFilter &attribute_filter); + } // namespace blender::geometry diff --git a/source/blender/geometry/intern/mesh_to_curve_convert.cc b/source/blender/geometry/intern/mesh_to_curve_convert.cc index 6b26b94ccaf..c651f7aabc4 100644 --- a/source/blender/geometry/intern/mesh_to_curve_convert.cc +++ b/source/blender/geometry/intern/mesh_to_curve_convert.cc @@ -2,6 +2,7 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ +#include "BKE_deform.hh" #include "BLI_array.hh" #include "BLI_array_utils.hh" #include "BLI_set.hh" @@ -16,6 +17,20 @@ namespace blender::geometry { +/* Don't copy attributes that are built-in on meshes but not on curves. */ +static auto filter_builtin_attributes(const bke::AttributeAccessor &mesh_attributes, + const bke::AttributeAccessor &curves_attributes, + Set &storage, + const bke::AttributeFilter &attribute_filter) +{ + for (const StringRef id : mesh_attributes.all_ids()) { + if (mesh_attributes.is_builtin(id) && !curves_attributes.is_builtin(id)) { + storage.add(id); + } + } + return bke::attribute_filter_with_skip_ref(attribute_filter, storage); +} + BLI_NOINLINE bke::CurvesGeometry create_curve_from_vert_indices( const bke::AttributeAccessor &mesh_attributes, const Span vert_indices, @@ -34,15 +49,9 @@ BLI_NOINLINE bke::CurvesGeometry create_curve_from_vert_indices( curves.cyclic_for_write().slice(cyclic_curves).fill(true); } - /* Don't copy attributes that are built-in on meshes but not on curves. */ - Set skip; - for (const StringRef id : mesh_attributes.all_ids()) { - if (mesh_attributes.is_builtin(id) && !curves_attributes.is_builtin(id)) { - skip.add(id); - } - } - const auto attribute_filter_with_skip = bke::attribute_filter_with_skip_ref(attribute_filter, - skip); + Set skip_storage; + const auto attribute_filter_with_skip = filter_builtin_attributes( + mesh_attributes, curves_attributes, skip_storage, attribute_filter); bke::gather_attributes(mesh_attributes, bke::AttrDomain::Point, @@ -211,9 +220,9 @@ BLI_NOINLINE static bke::CurvesGeometry edges_to_curves_convert( attribute_filter); } -bke::CurvesGeometry mesh_to_curve_convert(const Mesh &mesh, - const IndexMask &selection, - const bke::AttributeFilter &attribute_filter) +bke::CurvesGeometry mesh_edges_to_curves_convert(const Mesh &mesh, + const IndexMask &selection, + const bke::AttributeFilter &attribute_filter) { const Span edges = mesh.edges(); if (selection.size() == edges.size()) { @@ -224,4 +233,109 @@ bke::CurvesGeometry mesh_to_curve_convert(const Mesh &mesh, return edges_to_curves_convert(mesh, selected_edges, attribute_filter); } +static bke::CurvesGeometry create_curves_for_faces(const Mesh &mesh, + const OffsetIndices faces, + const IndexMask &selection) +{ + bke::CurvesGeometry curves; + if (selection.size() == faces.size()) { + implicit_sharing::copy_shared_pointer(mesh.face_offset_indices, + mesh.runtime->face_offsets_sharing_info, + &curves.curve_offsets, + &curves.runtime->curve_offsets_sharing_info); + curves.curve_num = faces.size(); + curves.resize(mesh.corners_num, faces.size()); + } + else { + curves.resize(0, selection.size()); + offset_indices::gather_selected_offsets(faces, selection, curves.offsets_for_write()); + curves.resize(curves.offsets().last(), curves.curves_num()); + } + + BKE_defgroup_copy_list(&curves.vertex_group_names, &mesh.vertex_group_names); + curves.cyclic_for_write().fill(true); + curves.fill_curve_types(CURVE_TYPE_POLY); + return curves; +} + +static Span create_point_to_vert_map(const Mesh &mesh, + const OffsetIndices faces, + const OffsetIndices points_by_curve, + const IndexMask &selection, + Array &map_data) +{ + if (selection.size() == faces.size()) { + return mesh.corner_verts(); + } + map_data.reinitialize(points_by_curve.total_size()); + array_utils::gather_group_to_group( + faces, points_by_curve, selection, mesh.corner_verts(), map_data.as_mutable_span()); + return map_data; +} + +bke::CurvesGeometry mesh_faces_to_curves_convert(const Mesh &mesh, + const IndexMask &selection, + const bke::AttributeFilter &attribute_filter) +{ + const OffsetIndices faces = mesh.faces(); + const bke::AttributeAccessor src_attributes = mesh.attributes(); + + bke::CurvesGeometry curves = create_curves_for_faces(mesh, faces, selection); + const OffsetIndices points_by_curve = curves.points_by_curve(); + bke::MutableAttributeAccessor dst_attributes = curves.attributes_for_write(); + + Array point_to_vert_data; + const Span point_to_vert_map = create_point_to_vert_map( + mesh, faces, points_by_curve, selection, point_to_vert_data); + + Set skip_storage; + const auto attribute_filter_with_skip = filter_builtin_attributes( + src_attributes, dst_attributes, skip_storage, attribute_filter); + + bke::gather_attributes(src_attributes, + bke::AttrDomain::Point, + bke::AttrDomain::Point, + attribute_filter_with_skip, + point_to_vert_map, + dst_attributes); + + src_attributes.foreach_attribute([&](const bke::AttributeIter &iter) { + if (iter.domain != bke::AttrDomain::Edge) { + return; + } + if (iter.data_type == CD_PROP_STRING) { + return; + } + if (attribute_filter_with_skip.allow_skip(iter.name)) { + return; + } + const GVArray src = *iter.get(bke::AttrDomain::Point); + bke::GSpanAttributeWriter dst = dst_attributes.lookup_or_add_for_write_only_span( + iter.name, bke::AttrDomain::Point, iter.data_type); + if (!dst) { + return; + } + bke::attribute_math::gather(src, point_to_vert_map, dst.span); + dst.finish(); + }); + + bke::gather_attributes(src_attributes, + bke::AttrDomain::Face, + bke::AttrDomain::Curve, + attribute_filter_with_skip, + selection, + dst_attributes); + + bke::gather_attributes_group_to_group(src_attributes, + bke::AttrDomain::Corner, + bke::AttrDomain::Point, + attribute_filter_with_skip, + faces, + points_by_curve, + selection, + dst_attributes); + + return curves; +} + } // namespace blender::geometry diff --git a/source/blender/nodes/geometry/nodes/node_geo_mesh_to_curve.cc b/source/blender/nodes/geometry/nodes/node_geo_mesh_to_curve.cc index 82be221a3b8..6b178d7f711 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_mesh_to_curve.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_mesh_to_curve.cc @@ -4,12 +4,21 @@ #include "DNA_mesh_types.h" +#include "NOD_rna_define.hh" + #include "GEO_mesh_to_curve.hh" +#include "UI_interface_c.hh" + #include "node_geometry_util.hh" namespace blender::nodes::node_geo_mesh_to_curve_cc { +enum class Mode : int8_t { + Edges = 0, + Faces = 1, +}; + static void node_declare(NodeDeclarationBuilder &b) { b.add_input("Mesh").supported_type(GeometryComponent::Type::Mesh); @@ -17,8 +26,14 @@ static void node_declare(NodeDeclarationBuilder &b) b.add_output("Curve").propagate_all(); } +static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) +{ + uiItemR(layout, ptr, "mode", UI_ITEM_R_EXPAND, std::nullopt, 0); +} + static void node_geo_exec(GeoNodeExecParams params) { + const Mode mode = Mode(params.node().custom1); GeometrySet geometry_set = params.extract_input("Mesh"); geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) { @@ -28,25 +43,66 @@ static void node_geo_exec(GeoNodeExecParams params) return; } - const bke::MeshFieldContext context{*mesh, AttrDomain::Edge}; - fn::FieldEvaluator evaluator{context, mesh->edges_num}; - evaluator.add(params.get_input>("Selection")); - evaluator.evaluate(); - const IndexMask selection = evaluator.get_evaluated_as_mask(0); - if (selection.is_empty()) { - geometry_set.remove_geometry_during_modify(); - return; - } + switch (mode) { + case Mode::Edges: { + const bke::MeshFieldContext context{*mesh, AttrDomain::Edge}; + fn::FieldEvaluator evaluator{context, mesh->edges_num}; + evaluator.add(params.get_input>("Selection")); + evaluator.evaluate(); + const IndexMask selection = evaluator.get_evaluated_as_mask(0); + if (selection.is_empty()) { + geometry_set.remove_geometry_during_modify(); + return; + } - bke::CurvesGeometry curves = geometry::mesh_to_curve_convert( - *mesh, selection, params.get_attribute_filter("Curve")); - geometry_set.replace_curves(bke::curves_new_nomain(std::move(curves))); + bke::CurvesGeometry curves = geometry::mesh_edges_to_curves_convert( + *mesh, selection, params.get_attribute_filter("Curve")); + geometry_set.replace_curves(bke::curves_new_nomain(std::move(curves))); + break; + } + case Mode::Faces: { + const bke::MeshFieldContext context{*mesh, AttrDomain::Face}; + fn::FieldEvaluator evaluator{context, mesh->faces_num}; + evaluator.add(params.get_input>("Selection")); + evaluator.evaluate(); + const IndexMask selection = evaluator.get_evaluated_as_mask(0); + if (selection.is_empty()) { + geometry_set.remove_geometry_during_modify(); + return; + } + + bke::CurvesGeometry curves = geometry::mesh_faces_to_curves_convert( + *mesh, selection, params.get_attribute_filter("Curve")); + geometry_set.replace_curves(bke::curves_new_nomain(std::move(curves))); + break; + } + } geometry_set.keep_only_during_modify({GeometryComponent::Type::Curve}); }); params.set_output("Curve", std::move(geometry_set)); } +static void node_rna(StructRNA *srna) +{ + static const EnumPropertyItem mode_items[] = { + {int(Mode::Edges), + "EDGES", + 0, + "Edges", + "Convert mesh edges to curve segments. Attributes are propagated to curve points."}, + {int(Mode::Faces), + "FACES", + 0, + "Faces", + "Convert each mesh face to a cyclic curve. Face attributes are propagated to curves."}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + RNA_def_node_enum( + srna, "mode", "Mode", "", mode_items, NOD_inline_enum_accessors(custom1), int(Mode::Edges)); +} + static void node_register() { static blender::bke::bNodeType ntype; @@ -57,8 +113,10 @@ static void node_register() ntype.enum_name_legacy = "MESH_TO_CURVE"; ntype.nclass = NODE_CLASS_GEOMETRY; ntype.declare = node_declare; + ntype.draw_buttons = node_layout; ntype.geometry_node_execute = node_geo_exec; blender::bke::node_register_type(ntype); + node_rna(ntype.rna_ext.srna); } NOD_REGISTER_NODE(node_register)