Geometry Nodes: Points to Curves node

New node to converts groups of points to curves. Groups
of points defined as `Curve Group ID` attribute. `Weight` in curve
is used for sort points in each group. Points of result curves
propagate attributes from original points. Implicit conversion
of other geometry types is not supported currently.

Pull Request: https://projects.blender.org/blender/blender/pulls/109610
This commit is contained in:
Iliya Katueshenock
2023-08-29 16:52:20 +02:00
committed by Hans Goudey
parent ab67d410a9
commit 48b08199d5
7 changed files with 232 additions and 0 deletions

View File

@@ -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<std::string> &skip,
const Span<int> 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

View File

@@ -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
/** \} */

View File

@@ -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<std::string> &skip,
const Span<int> 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<typename T>
static void gather_group_to_group(const OffsetIndices<int> src_offsets,
const OffsetIndices<int> dst_offsets,

View File

@@ -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

View File

@@ -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

View File

@@ -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<decl::Geometry>("Points")
.supported_type(GeometryComponent::Type::PointCloud)
.description("Points to generate curves from");
b.add_input<decl::Int>("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<decl::Float>("Weight").field_on_all().hide_value().description(
"Determines the order of points in each curve");
b.add_output<decl::Geometry>("Curves").propagate_all();
}
static void grouped_sort(const OffsetIndices<int> offsets,
const Span<float> weights,
MutableSpan<int> 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<int> group = indices.slice(offsets[group_index]);
parallel_sort(group.begin(), group.end(), comparator);
}
});
}
static void find_points_by_group_index(const Span<int> indices_of_curves,
MutableSpan<int> r_offsets,
MutableSpan<int> r_indices)
{
offset_indices::build_reverse_offsets(indices_of_curves, r_offsets);
Array<int> 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<int> r_identifiers_to_indices)
{
const VectorSet<int> 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<float> &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<int> indices(domain_size);
std::iota(indices.begin(), indices.end(), 0);
const VArraySpan<float> weights(weights_varray);
grouped_sort(OffsetIndices<int>({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<int> &group_id_field,
const Field<float> &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<int> group_ids_varray = evaluator.get_evaluated<int>(0);
const VArray<float> weights_varray = evaluator.get_evaluated<float>(1);
if (group_ids_varray.is_single()) {
return curve_from_points(points.attributes(), weights_varray, propagation_info);
}
Array<int> 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<int> offset = curves.offsets_for_write();
offset.fill(0);
Array<int> indices(domain_size);
find_points_by_group_index(group_ids, offset, indices.as_mutable_span());
if (!weights_varray.is_single()) {
const VArraySpan<float> weights(weights_varray);
grouped_sort(OffsetIndices<int>(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<GeometrySet>("Points");
const Field<int> group_id_field = params.extract_input<Field<int>>("Curve Group ID");
const Field<float> weight_field = params.extract_input<Field<float>>("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