diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index 736e20882b9..33836382d8c 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -469,6 +469,7 @@ class NODE_MT_category_GEO_POINT(Menu): node_add_menu.add_node_type(layout, "GeometryNodeDistributePointsOnFaces") layout.separator() node_add_menu.add_node_type(layout, "GeometryNodePoints") + node_add_menu.add_node_type(layout, "GeometryNodePointsToCurves") node_add_menu.add_node_type(layout, "GeometryNodePointsToVertices") if context.preferences.experimental.use_new_volume_nodes: node_add_menu.add_node_type(layout, "GeometryNodePointsToSDFVolume") diff --git a/source/blender/blenkernel/BKE_attribute.hh b/source/blender/blenkernel/BKE_attribute.hh index 7f602ca5e10..d79cc855ca7 100644 --- a/source/blender/blenkernel/BKE_attribute.hh +++ b/source/blender/blenkernel/BKE_attribute.hh @@ -927,6 +927,16 @@ void gather_attributes(AttributeAccessor src_attributes, const IndexMask &selection, MutableAttributeAccessor dst_attributes); +/** + * Fill the destination attribute by gathering indexed values from src attributes. + */ +void gather_attributes(AttributeAccessor src_attributes, + eAttrDomain domain, + const AnonymousAttributePropagationInfo &propagation_info, + const Set &skip, + const Span indices, + MutableAttributeAccessor dst_attributes); + /** * Copy attribute values from groups defined by \a src_offsets to groups defined by \a * dst_offsets. The group indices are gathered to the result by \a selection. The size of each diff --git a/source/blender/blenkernel/BKE_node.h b/source/blender/blenkernel/BKE_node.h index da7f9727ebd..003f9ff7037 100644 --- a/source/blender/blenkernel/BKE_node.h +++ b/source/blender/blenkernel/BKE_node.h @@ -1322,6 +1322,7 @@ void BKE_nodetree_remove_layer_n(struct bNodeTree *ntree, struct Scene *scene, i #define GEO_NODE_TOOL_3D_CURSOR 2111 #define GEO_NODE_TOOL_FACE_SET 2112 #define GEO_NODE_TOOL_SET_FACE_SET 2113 +#define GEO_NODE_POINTS_TO_CURVES 2114 /** \} */ diff --git a/source/blender/blenkernel/intern/attribute_access.cc b/source/blender/blenkernel/intern/attribute_access.cc index 43d0ace0a99..3c50a7d6c5f 100644 --- a/source/blender/blenkernel/intern/attribute_access.cc +++ b/source/blender/blenkernel/intern/attribute_access.cc @@ -1033,6 +1033,35 @@ void gather_attributes(const AttributeAccessor src_attributes, }); } +void gather_attributes(const AttributeAccessor src_attributes, + const eAttrDomain domain, + const AnonymousAttributePropagationInfo &propagation_info, + const Set &skip, + const Span indices, + MutableAttributeAccessor dst_attributes) +{ + src_attributes.for_all([&](const AttributeIDRef &id, const AttributeMetaData meta_data) { + if (meta_data.domain != domain) { + return true; + } + if (id.is_anonymous() && !propagation_info.propagate(id.anonymous_id())) { + return true; + } + if (skip.contains(id.name())) { + return true; + } + const bke::GAttributeReader src = src_attributes.lookup(id, domain); + bke::GSpanAttributeWriter dst = dst_attributes.lookup_or_add_for_write_only_span( + id, domain, meta_data.data_type); + if (!dst) { + return true; + } + attribute_math::gather(src.varray, indices, dst.span); + dst.finish(); + return true; + }); +} + template static void gather_group_to_group(const OffsetIndices src_offsets, const OffsetIndices dst_offsets, diff --git a/source/blender/nodes/NOD_static_types.h b/source/blender/nodes/NOD_static_types.h index bdd4a7057a1..de1dfe7aafc 100644 --- a/source/blender/nodes/NOD_static_types.h +++ b/source/blender/nodes/NOD_static_types.h @@ -460,6 +460,7 @@ DefNode(GeometryNode, GEO_NODE_VOLUME_CUBE, 0, "VOLUME_CUBE", VolumeCube, "Volum DefNode(GeometryNode, GEO_NODE_VOLUME_TO_MESH, 0, "VOLUME_TO_MESH", VolumeToMesh, "Volume to Mesh", "Generate a mesh on the \"surface\" of a volume") DefNode(GeometryNode, GEO_NODE_INTERPOLATE_CURVES, 0, "INTERPOLATE_CURVES", InterpolateCurves, "Interpolate Curves", "Generate new curves on points by interpolating between existing curves") +DefNode(GeometryNode, GEO_NODE_POINTS_TO_CURVES, 0, "POINTS_TO_CURVES", PointsToCurves, "Points to Curves", "Split all points to curve by its group ID and reorder by weight") /* undefine macros */ #undef DefNode diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index 76bb8d93d3e..00ea1abbfd5 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -144,6 +144,7 @@ set(SRC nodes/node_geo_offset_point_in_curve.cc nodes/node_geo_offset_sdf_volume.cc nodes/node_geo_points.cc + nodes/node_geo_points_to_curves.cc nodes/node_geo_points_to_sdf_volume.cc nodes/node_geo_points_to_vertices.cc nodes/node_geo_points_to_volume.cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_points_to_curves.cc b/source/blender/nodes/geometry/nodes/node_geo_points_to_curves.cc new file mode 100644 index 00000000000..25836af3d8e --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_points_to_curves.cc @@ -0,0 +1,189 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "node_geometry_util.hh" + +#include "BKE_attribute.hh" +#include "BKE_attribute_math.hh" +#include "BKE_curves.hh" + +#include "BLI_array_utils.hh" + +#include "DNA_pointcloud_types.h" + +#include "BLI_sort.hh" +#include "BLI_task.hh" + +#include "BKE_geometry_set.hh" + +namespace blender::nodes::node_geo_points_to_curves_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.add_input("Points") + .supported_type(GeometryComponent::Type::PointCloud) + .description("Points to generate curves from"); + b.add_input("Curve Group ID") + .field_on_all() + .hide_value() + .description( + "A curve is created for every distinct group ID. All points with the same ID are put " + "into the same curve"); + b.add_input("Weight").field_on_all().hide_value().description( + "Determines the order of points in each curve"); + + b.add_output("Curves").propagate_all(); +} + +static void grouped_sort(const OffsetIndices offsets, + const Span weights, + MutableSpan indices) +{ + const auto comparator = [&](const int index_a, const int index_b) { + const float weight_a = weights[index_a]; + const float weight_b = weights[index_b]; + if (UNLIKELY(weight_a == weight_b)) { + /* Approach to make it stable. */ + return index_a < index_b; + } + return weight_a < weight_b; + }; + + threading::parallel_for(offsets.index_range(), 250, [&](const IndexRange range) { + for (const int group_index : range) { + MutableSpan group = indices.slice(offsets[group_index]); + parallel_sort(group.begin(), group.end(), comparator); + } + }); +} + +static void find_points_by_group_index(const Span indices_of_curves, + MutableSpan r_offsets, + MutableSpan r_indices) +{ + offset_indices::build_reverse_offsets(indices_of_curves, r_offsets); + Array counts(r_offsets.size(), 0); + + for (const int64_t index : indices_of_curves.index_range()) { + const int curve_index = indices_of_curves[index]; + r_indices[r_offsets[curve_index] + counts[curve_index]] = int(index); + counts[curve_index]++; + } +} + +static int identifiers_to_indices(MutableSpan r_identifiers_to_indices) +{ + const VectorSet deduplicated_groups(r_identifiers_to_indices); + threading::parallel_for( + r_identifiers_to_indices.index_range(), 2048, [&](const IndexRange range) { + for (int &value : r_identifiers_to_indices.slice(range)) { + value = deduplicated_groups.index_of(value); + } + }); + return deduplicated_groups.size(); +} + +static Curves *curve_from_points(const AttributeAccessor attributes, + const VArray &weights_varray, + const bke::AnonymousAttributePropagationInfo &propagation_info) +{ + const int domain_size = weights_varray.size(); + Curves *curves_id = bke::curves_new_nomain_single(domain_size, CURVE_TYPE_POLY); + bke::CurvesGeometry &curves = curves_id->geometry.wrap(); + if (weights_varray.is_single()) { + bke::copy_attributes( + attributes, ATTR_DOMAIN_POINT, propagation_info, {}, curves.attributes_for_write()); + return curves_id; + } + Array indices(domain_size); + std::iota(indices.begin(), indices.end(), 0); + const VArraySpan weights(weights_varray); + grouped_sort(OffsetIndices({0, domain_size}), weights, indices); + bke::gather_attributes( + attributes, ATTR_DOMAIN_POINT, propagation_info, {}, indices, curves.attributes_for_write()); + return curves_id; +} + +static Curves *curves_from_points(const PointCloud &points, + const Field &group_id_field, + const Field &weight_field, + const bke::AnonymousAttributePropagationInfo &propagation_info) +{ + const int domain_size = points.totpoint; + + const bke::PointCloudFieldContext context(points); + fn::FieldEvaluator evaluator(context, domain_size); + evaluator.add(group_id_field); + evaluator.add(weight_field); + evaluator.evaluate(); + + const VArray group_ids_varray = evaluator.get_evaluated(0); + const VArray weights_varray = evaluator.get_evaluated(1); + + if (group_ids_varray.is_single()) { + return curve_from_points(points.attributes(), weights_varray, propagation_info); + } + + Array group_ids(domain_size); + group_ids_varray.materialize(group_ids.as_mutable_span()); + const int total_curves = identifiers_to_indices(group_ids); + if (total_curves == 1) { + return curve_from_points(points.attributes(), weights_varray, propagation_info); + } + + Curves *curves_id = bke::curves_new_nomain(domain_size, total_curves); + bke::CurvesGeometry &curves = curves_id->geometry.wrap(); + curves.fill_curve_types(CURVE_TYPE_POLY); + MutableSpan offset = curves.offsets_for_write(); + offset.fill(0); + + Array indices(domain_size); + find_points_by_group_index(group_ids, offset, indices.as_mutable_span()); + + if (!weights_varray.is_single()) { + const VArraySpan weights(weights_varray); + grouped_sort(OffsetIndices(offset), weights, indices); + } + bke::gather_attributes(points.attributes(), + ATTR_DOMAIN_POINT, + propagation_info, + {}, + indices, + curves.attributes_for_write()); + return curves_id; +} + +static void node_geo_exec(GeoNodeExecParams params) +{ + GeometrySet geometry_set = params.extract_input("Points"); + const Field group_id_field = params.extract_input>("Curve Group ID"); + const Field weight_field = params.extract_input>("Weight"); + + const bke::AnonymousAttributePropagationInfo propagation_info = + params.get_output_propagation_info("Curves"); + geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) { + geometry_set.replace_curves(nullptr); + if (const PointCloud *points = geometry_set.get_pointcloud()) { + Curves *curves_id = curves_from_points( + *points, group_id_field, weight_field, propagation_info); + geometry_set.replace_curves(curves_id); + } + geometry_set.keep_only_during_modify({GeometryComponent::Type::Curve}); + }); + + params.set_output("Curves", std::move(geometry_set)); +} + +static void node_register() +{ + static bNodeType ntype; + + geo_node_type_base(&ntype, GEO_NODE_POINTS_TO_CURVES, "Points to Curves", NODE_CLASS_GEOMETRY); + ntype.geometry_node_execute = node_geo_exec; + ntype.declare = node_declare; + nodeRegisterType(&ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_points_to_curves_cc