Changeset View
Changeset View
Standalone View
Standalone View
source/blender/nodes/geometry/nodes/node_geo_curve_to_points.cc
| /* SPDX-License-Identifier: GPL-2.0-or-later */ | /* SPDX-License-Identifier: GPL-2.0-or-later */ | ||||
| #include "BLI_array.hh" | #include "BLI_array.hh" | ||||
| #include "BLI_task.hh" | #include "BLI_task.hh" | ||||
| #include "BLI_timeit.hh" | #include "BLI_timeit.hh" | ||||
| #include "DNA_pointcloud_types.h" | |||||
| #include "BKE_pointcloud.h" | #include "BKE_pointcloud.h" | ||||
| #include "BKE_spline.hh" | |||||
| #include "GEO_resample_curves.hh" | |||||
| #include "UI_interface.h" | #include "UI_interface.h" | ||||
| #include "UI_resources.h" | #include "UI_resources.h" | ||||
| #include "node_geometry_util.hh" | #include "node_geometry_util.hh" | ||||
| namespace blender::nodes::node_geo_curve_to_points_cc { | namespace blender::nodes::node_geo_curve_to_points_cc { | ||||
| Show All 40 Lines | static void node_update(bNodeTree *ntree, bNode *node) | ||||
| bNodeSocket *count_socket = ((bNodeSocket *)node->inputs.first)->next; | bNodeSocket *count_socket = ((bNodeSocket *)node->inputs.first)->next; | ||||
| bNodeSocket *length_socket = count_socket->next; | bNodeSocket *length_socket = count_socket->next; | ||||
| nodeSetSocketAvailability(ntree, count_socket, mode == GEO_NODE_CURVE_RESAMPLE_COUNT); | nodeSetSocketAvailability(ntree, count_socket, mode == GEO_NODE_CURVE_RESAMPLE_COUNT); | ||||
| nodeSetSocketAvailability(ntree, length_socket, mode == GEO_NODE_CURVE_RESAMPLE_LENGTH); | nodeSetSocketAvailability(ntree, length_socket, mode == GEO_NODE_CURVE_RESAMPLE_LENGTH); | ||||
| } | } | ||||
| static void curve_create_default_rotation_attribute(Span<float3> tangents, | static void fill_rotation_attribute(const Span<float3> tangents, | ||||
| Span<float3> normals, | const Span<float3> normals, | ||||
| MutableSpan<float3> rotations) | MutableSpan<float3> rotations) | ||||
| { | { | ||||
| threading::parallel_for(IndexRange(rotations.size()), 512, [&](IndexRange range) { | threading::parallel_for(IndexRange(rotations.size()), 512, [&](IndexRange range) { | ||||
| for (const int i : range) { | for (const int i : range) { | ||||
| rotations[i] = | rotations[i] = | ||||
| float4x4::from_normalized_axis_data({0, 0, 0}, normals[i], tangents[i]).to_euler(); | float4x4::from_normalized_axis_data({0, 0, 0}, normals[i], tangents[i]).to_euler(); | ||||
| } | } | ||||
| }); | }); | ||||
| } | } | ||||
| static Array<int> calculate_spline_point_offsets(GeoNodeExecParams ¶ms, | static PointCloud *pointcloud_from_curves(bke::CurvesGeometry curves, | ||||
| const GeometryNodeCurveResampleMode mode, | const AttributeIDRef &tangent_id, | ||||
| const CurveEval &curve, | const AttributeIDRef &normal_id, | ||||
| const Span<SplinePtr> splines) | const AttributeIDRef &rotation_id) | ||||
| { | { | ||||
| const int size = curve.splines().size(); | PointCloud *pointcloud = BKE_pointcloud_new_nomain(0); | ||||
| switch (mode) { | pointcloud->totpoint = curves.points_num(); | ||||
| case GEO_NODE_CURVE_RESAMPLE_COUNT: { | |||||
| const int count = params.get_input<int>("Count"); | if (rotation_id) { | ||||
| if (count < 1) { | MutableAttributeAccessor attributes = curves.attributes_for_write(); | ||||
| return {0}; | const VArraySpan<float3> tangents = attributes.lookup<float3>(tangent_id, ATTR_DOMAIN_POINT); | ||||
| } | const VArraySpan<float3> normals = attributes.lookup<float3>(normal_id, ATTR_DOMAIN_POINT); | ||||
| Array<int> offsets(size + 1); | SpanAttributeWriter<float3> rotations = attributes.lookup_or_add_for_write_only_span<float3>( | ||||
| int offset = 0; | rotation_id, ATTR_DOMAIN_POINT); | ||||
| for (const int i : IndexRange(size)) { | fill_rotation_attribute(tangents, normals, rotations.span); | ||||
| offsets[i] = offset; | rotations.finish(); | ||||
| if (splines[i]->evaluated_points_num() > 0) { | } | ||||
| offset += count; | |||||
| } | /* Move the curve point custom data to the pointcloud, to avoid any copying. */ | ||||
| } | CustomData_free(&pointcloud->pdata, pointcloud->totpoint); | ||||
| offsets.last() = offset; | pointcloud->pdata = curves.point_data; | ||||
| return offsets; | CustomData_reset(&curves.point_data); | ||||
| } | |||||
| case GEO_NODE_CURVE_RESAMPLE_LENGTH: { | |||||
| /* Don't allow asymptotic count increase for low resolution values. */ | |||||
| const float resolution = std::max(params.get_input<float>("Length"), 0.0001f); | |||||
| Array<int> offsets(size + 1); | |||||
| int offset = 0; | |||||
| for (const int i : IndexRange(size)) { | |||||
| offsets[i] = offset; | |||||
| if (splines[i]->evaluated_points_num() > 0) { | |||||
| offset += splines[i]->length() / resolution + 1; | |||||
| } | |||||
| } | |||||
| offsets.last() = offset; | |||||
| return offsets; | |||||
| } | |||||
| case GEO_NODE_CURVE_RESAMPLE_EVALUATED: { | |||||
| return curve.evaluated_point_offsets(); | |||||
| } | |||||
| } | |||||
| BLI_assert_unreachable(); | |||||
| return {0}; | |||||
| } | |||||
| /** | return pointcloud; | ||||
| * \note Relies on the fact that all attributes on point clouds are stored contiguously. | |||||
| */ | |||||
| static GMutableSpan ensure_point_attribute(PointCloudComponent &points, | |||||
| const AttributeIDRef &attribute_id, | |||||
| const eCustomDataType data_type) | |||||
| { | |||||
| GAttributeWriter attribute = points.attributes_for_write()->lookup_or_add_for_write( | |||||
| attribute_id, ATTR_DOMAIN_POINT, data_type); | |||||
| GMutableSpan span = attribute.varray.get_internal_span(); | |||||
| attribute.finish(); | |||||
| return span; | |||||
| } | |||||
| template<typename T> | |||||
| static MutableSpan<T> ensure_point_attribute(PointCloudComponent &points, | |||||
| const AttributeIDRef &attribute_id) | |||||
| { | |||||
| AttributeWriter<T> attribute = points.attributes_for_write()->lookup_or_add_for_write<T>( | |||||
| attribute_id, ATTR_DOMAIN_POINT); | |||||
| MutableSpan<T> span = attribute.varray.get_internal_span(); | |||||
| attribute.finish(); | |||||
| return span; | |||||
| } | |||||
| namespace { | |||||
| struct AnonymousAttributeIDs { | |||||
| StrongAnonymousAttributeID tangent_id; | |||||
| StrongAnonymousAttributeID normal_id; | |||||
| StrongAnonymousAttributeID rotation_id; | |||||
| }; | |||||
| struct ResultAttributes { | |||||
| MutableSpan<float3> positions; | |||||
| MutableSpan<float> radii; | |||||
| Map<AttributeIDRef, GMutableSpan> point_attributes; | |||||
| MutableSpan<float3> tangents; | |||||
| MutableSpan<float3> normals; | |||||
| MutableSpan<float3> rotations; | |||||
| }; | |||||
| } // namespace | |||||
| static ResultAttributes create_attributes_for_transfer(PointCloudComponent &points, | |||||
| const CurveEval &curve, | |||||
| const AnonymousAttributeIDs &attributes) | |||||
| { | |||||
| ResultAttributes outputs; | |||||
| outputs.positions = ensure_point_attribute<float3>(points, "position"); | |||||
| outputs.radii = ensure_point_attribute<float>(points, "radius"); | |||||
| if (attributes.tangent_id) { | |||||
| outputs.tangents = ensure_point_attribute<float3>(points, attributes.tangent_id.get()); | |||||
| } | |||||
| if (attributes.normal_id) { | |||||
| outputs.normals = ensure_point_attribute<float3>(points, attributes.normal_id.get()); | |||||
| } | |||||
| if (attributes.rotation_id) { | |||||
| outputs.rotations = ensure_point_attribute<float3>(points, attributes.rotation_id.get()); | |||||
| } | |||||
| /* Because of the invariants of the curve component, we use the attributes of the first spline | |||||
| * as a representative for the attribute meta data all splines. Attributes from the spline domain | |||||
| * are handled separately. */ | |||||
| curve.splines().first()->attributes.foreach_attribute( | |||||
| [&](const AttributeIDRef &id, const AttributeMetaData &meta_data) { | |||||
| if (id.should_be_kept()) { | |||||
| outputs.point_attributes.add_new( | |||||
| id, ensure_point_attribute(points, id, meta_data.data_type)); | |||||
| } | |||||
| return true; | |||||
| }, | |||||
| ATTR_DOMAIN_POINT); | |||||
| return outputs; | |||||
| } | |||||
| /** | |||||
| * TODO: For non-poly splines, this has double copies that could be avoided as part | |||||
| * of a general look at optimizing uses of #Spline::interpolate_to_evaluated. | |||||
| */ | |||||
| static void copy_evaluated_point_attributes(const Span<SplinePtr> splines, | |||||
| const Span<int> offsets, | |||||
| ResultAttributes &data) | |||||
| { | |||||
| threading::parallel_for(splines.index_range(), 64, [&](IndexRange range) { | |||||
| for (const int i : range) { | |||||
| const Spline &spline = *splines[i]; | |||||
| const int offset = offsets[i]; | |||||
| const int size = offsets[i + 1] - offsets[i]; | |||||
| data.positions.slice(offset, size).copy_from(spline.evaluated_positions()); | |||||
| spline.interpolate_to_evaluated(spline.radii()).materialize(data.radii.slice(offset, size)); | |||||
| for (const Map<AttributeIDRef, GMutableSpan>::Item item : data.point_attributes.items()) { | |||||
| const AttributeIDRef attribute_id = item.key; | |||||
| const GMutableSpan dst = item.value; | |||||
| BLI_assert(spline.attributes.get_for_read(attribute_id)); | |||||
| GSpan spline_span = *spline.attributes.get_for_read(attribute_id); | |||||
| spline.interpolate_to_evaluated(spline_span).materialize(dst.slice(offset, size).data()); | |||||
| } | |||||
| if (!data.tangents.is_empty()) { | |||||
| data.tangents.slice(offset, size).copy_from(spline.evaluated_tangents()); | |||||
| } | |||||
| if (!data.normals.is_empty()) { | |||||
| data.normals.slice(offset, size).copy_from(spline.evaluated_normals()); | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| static void copy_uniform_sample_point_attributes(const Span<SplinePtr> splines, | |||||
| const Span<int> offsets, | |||||
| ResultAttributes &data) | |||||
| { | |||||
| threading::parallel_for(splines.index_range(), 64, [&](IndexRange range) { | |||||
| for (const int i : range) { | |||||
| const Spline &spline = *splines[i]; | |||||
| const int offset = offsets[i]; | |||||
| const int num = offsets[i + 1] - offsets[i]; | |||||
| if (num == 0) { | |||||
| continue; | |||||
| } | |||||
| const Array<float> uniform_samples = spline.sample_uniform_index_factors(num); | |||||
| spline.sample_with_index_factors<float3>( | |||||
| spline.evaluated_positions(), uniform_samples, data.positions.slice(offset, num)); | |||||
| spline.sample_with_index_factors<float>(spline.interpolate_to_evaluated(spline.radii()), | |||||
| uniform_samples, | |||||
| data.radii.slice(offset, num)); | |||||
| for (const Map<AttributeIDRef, GMutableSpan>::Item item : data.point_attributes.items()) { | |||||
| const AttributeIDRef attribute_id = item.key; | |||||
| const GMutableSpan dst = item.value; | |||||
| BLI_assert(spline.attributes.get_for_read(attribute_id)); | |||||
| GSpan spline_span = *spline.attributes.get_for_read(attribute_id); | |||||
| spline.sample_with_index_factors( | |||||
| spline.interpolate_to_evaluated(spline_span), uniform_samples, dst.slice(offset, num)); | |||||
| } | |||||
| if (!data.tangents.is_empty()) { | |||||
| Span<float3> src_tangents = spline.evaluated_tangents(); | |||||
| MutableSpan<float3> sampled_tangents = data.tangents.slice(offset, num); | |||||
| spline.sample_with_index_factors<float3>(src_tangents, uniform_samples, sampled_tangents); | |||||
| for (float3 &vector : sampled_tangents) { | |||||
| vector = math::normalize(vector); | |||||
| } | |||||
| } | |||||
| if (!data.normals.is_empty()) { | |||||
| Span<float3> src_normals = spline.evaluated_normals(); | |||||
| MutableSpan<float3> sampled_normals = data.normals.slice(offset, num); | |||||
| spline.sample_with_index_factors<float3>(src_normals, uniform_samples, sampled_normals); | |||||
| for (float3 &vector : sampled_normals) { | |||||
| vector = math::normalize(vector); | |||||
| } | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| static void copy_spline_domain_attributes(const CurveEval &curve, | |||||
| const Span<int> offsets, | |||||
| PointCloudComponent &points) | |||||
| { | |||||
| curve.attributes.foreach_attribute( | |||||
| [&](const AttributeIDRef &attribute_id, const AttributeMetaData &meta_data) { | |||||
| const GSpan curve_attribute = *curve.attributes.get_for_read(attribute_id); | |||||
| const CPPType &type = curve_attribute.type(); | |||||
| const GMutableSpan dst = ensure_point_attribute(points, attribute_id, meta_data.data_type); | |||||
| for (const int i : curve.splines().index_range()) { | |||||
| const int offset = offsets[i]; | |||||
| const int num = offsets[i + 1] - offsets[i]; | |||||
| type.fill_assign_n(curve_attribute[i], dst[offset], num); | |||||
| } | |||||
| return true; | |||||
| }, | |||||
| ATTR_DOMAIN_CURVE); | |||||
| } | } | ||||
| static void node_geo_exec(GeoNodeExecParams params) | static void node_geo_exec(GeoNodeExecParams params) | ||||
| { | { | ||||
| const NodeGeometryCurveToPoints &storage = node_storage(params.node()); | const NodeGeometryCurveToPoints &storage = node_storage(params.node()); | ||||
| const GeometryNodeCurveResampleMode mode = (GeometryNodeCurveResampleMode)storage.mode; | const GeometryNodeCurveResampleMode mode = (GeometryNodeCurveResampleMode)storage.mode; | ||||
| GeometrySet geometry_set = params.extract_input<GeometrySet>("Curve"); | GeometrySet geometry_set = params.extract_input<GeometrySet>("Curve"); | ||||
| AnonymousAttributeIDs attribute_outputs; | |||||
| attribute_outputs.tangent_id = StrongAnonymousAttributeID("Tangent"); | |||||
| attribute_outputs.normal_id = StrongAnonymousAttributeID("Normal"); | |||||
| attribute_outputs.rotation_id = StrongAnonymousAttributeID("Rotation"); | |||||
| GeometryComponentEditData::remember_deformed_curve_positions_if_necessary(geometry_set); | GeometryComponentEditData::remember_deformed_curve_positions_if_necessary(geometry_set); | ||||
| geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) { | StrongAnonymousAttributeID tangent_anonymous_id; | ||||
| if (!geometry_set.has_curves()) { | StrongAnonymousAttributeID normal_anonymous_id; | ||||
| geometry_set.remove_geometry_during_modify(); | StrongAnonymousAttributeID rotation_anonymous_id; | ||||
| return; | const bool rotation_required = params.output_is_required("Rotation"); | ||||
| } | if (params.output_is_required("Tangent") || rotation_required) { | ||||
| const std::unique_ptr<CurveEval> curve = curves_to_curve_eval( | tangent_anonymous_id = StrongAnonymousAttributeID("Tangent"); | ||||
| *geometry_set.get_curves_for_read()); | } | ||||
| const Span<SplinePtr> splines = curve->splines(); | if (params.output_is_required("Normal") || rotation_required) { | ||||
| curve->assert_valid_point_attributes(); | normal_anonymous_id = StrongAnonymousAttributeID("Normal"); | ||||
| } | |||||
| const Array<int> offsets = calculate_spline_point_offsets(params, mode, *curve, splines); | if (rotation_required) { | ||||
| const int total_num = offsets.last(); | rotation_anonymous_id = StrongAnonymousAttributeID("Rotation"); | ||||
| if (total_num == 0) { | } | ||||
| geometry_set.remove_geometry_during_modify(); | |||||
| return; | geometry::ResampleCurvesOutputAttributeIDs resample_attributes; | ||||
| } | resample_attributes.tangent_id = tangent_anonymous_id.get(); | ||||
| resample_attributes.normal_id = normal_anonymous_id.get(); | |||||
| geometry_set.replace_pointcloud(BKE_pointcloud_new_nomain(total_num)); | |||||
| PointCloudComponent &points = geometry_set.get_component_for_write<PointCloudComponent>(); | |||||
| ResultAttributes point_attributes = create_attributes_for_transfer( | |||||
| points, *curve, attribute_outputs); | |||||
| switch (mode) { | switch (mode) { | ||||
| case GEO_NODE_CURVE_RESAMPLE_COUNT: | case GEO_NODE_CURVE_RESAMPLE_COUNT: { | ||||
| case GEO_NODE_CURVE_RESAMPLE_LENGTH: | Field<int> count = params.extract_input<Field<int>>("Count"); | ||||
| copy_uniform_sample_point_attributes(splines, offsets, point_attributes); | geometry_set.modify_geometry_sets([&](GeometrySet &geometry) { | ||||
| if (const Curves *src_curves_id = geometry.get_curves_for_read()) { | |||||
| const bke::CurvesGeometry &src_curves = bke::CurvesGeometry::wrap( | |||||
| src_curves_id->geometry); | |||||
| bke::CurvesGeometry dst_curves = geometry::resample_to_count( | |||||
| src_curves, fn::make_constant_field<bool>(true), count, resample_attributes); | |||||
| PointCloud *pointcloud = pointcloud_from_curves(std::move(dst_curves), | |||||
| resample_attributes.tangent_id, | |||||
| resample_attributes.normal_id, | |||||
| rotation_anonymous_id.get()); | |||||
| geometry.remove_geometry_during_modify(); | |||||
| geometry.replace_pointcloud(pointcloud); | |||||
| } | |||||
| }); | |||||
| break; | break; | ||||
| case GEO_NODE_CURVE_RESAMPLE_EVALUATED: | } | ||||
| copy_evaluated_point_attributes(splines, offsets, point_attributes); | case GEO_NODE_CURVE_RESAMPLE_LENGTH: { | ||||
| Field<float> length = params.extract_input<Field<float>>("Length"); | |||||
| geometry_set.modify_geometry_sets([&](GeometrySet &geometry) { | |||||
| if (const Curves *src_curves_id = geometry.get_curves_for_read()) { | |||||
| const bke::CurvesGeometry &src_curves = bke::CurvesGeometry::wrap( | |||||
| src_curves_id->geometry); | |||||
| bke::CurvesGeometry dst_curves = geometry::resample_to_length( | |||||
| src_curves, fn::make_constant_field<bool>(true), length, resample_attributes); | |||||
| PointCloud *pointcloud = pointcloud_from_curves(std::move(dst_curves), | |||||
| resample_attributes.tangent_id, | |||||
| resample_attributes.normal_id, | |||||
| rotation_anonymous_id.get()); | |||||
| geometry.remove_geometry_during_modify(); | |||||
| geometry.replace_pointcloud(pointcloud); | |||||
| } | |||||
| }); | |||||
| break; | break; | ||||
| } | } | ||||
| case GEO_NODE_CURVE_RESAMPLE_EVALUATED: | |||||
| copy_spline_domain_attributes(*curve, offsets, points); | geometry_set.modify_geometry_sets([&](GeometrySet &geometry) { | ||||
| if (const Curves *src_curves_id = geometry.get_curves_for_read()) { | |||||
| if (!point_attributes.rotations.is_empty()) { | const bke::CurvesGeometry &src_curves = bke::CurvesGeometry::wrap( | ||||
| curve_create_default_rotation_attribute( | src_curves_id->geometry); | ||||
| point_attributes.tangents, point_attributes.normals, point_attributes.rotations); | bke::CurvesGeometry dst_curves = geometry::resample_to_evaluated( | ||||
| src_curves, fn::make_constant_field<bool>(true), resample_attributes); | |||||
| PointCloud *pointcloud = pointcloud_from_curves(std::move(dst_curves), | |||||
| resample_attributes.tangent_id, | |||||
| resample_attributes.normal_id, | |||||
| rotation_anonymous_id.get()); | |||||
| geometry.remove_geometry_during_modify(); | |||||
| geometry.replace_pointcloud(pointcloud); | |||||
| } | } | ||||
| geometry_set.keep_only_during_modify({GEO_COMPONENT_TYPE_POINT_CLOUD}); | |||||
| }); | }); | ||||
| break; | |||||
| } | |||||
| params.set_output("Points", std::move(geometry_set)); | params.set_output("Points", std::move(geometry_set)); | ||||
| if (attribute_outputs.tangent_id) { | if (tangent_anonymous_id) { | ||||
| params.set_output( | params.set_output("Tangent", | ||||
| "Tangent", | AnonymousAttributeFieldInput::Create<float3>( | ||||
| AnonymousAttributeFieldInput::Create<float3>(std::move(attribute_outputs.tangent_id), | std::move(tangent_anonymous_id), params.attribute_producer_name())); | ||||
| params.attribute_producer_name())); | } | ||||
| } | if (normal_anonymous_id) { | ||||
| if (attribute_outputs.normal_id) { | params.set_output("Normal", | ||||
| params.set_output( | AnonymousAttributeFieldInput::Create<float3>( | ||||
| "Normal", | std::move(normal_anonymous_id), params.attribute_producer_name())); | ||||
| AnonymousAttributeFieldInput::Create<float3>(std::move(attribute_outputs.normal_id), | } | ||||
| params.attribute_producer_name())); | if (rotation_anonymous_id) { | ||||
| } | params.set_output("Rotation", | ||||
| if (attribute_outputs.rotation_id) { | AnonymousAttributeFieldInput::Create<float3>( | ||||
| params.set_output( | std::move(rotation_anonymous_id), params.attribute_producer_name())); | ||||
| "Rotation", | |||||
| AnonymousAttributeFieldInput::Create<float3>(std::move(attribute_outputs.rotation_id), | |||||
| params.attribute_producer_name())); | |||||
| } | } | ||||
| } | } | ||||
| } // namespace blender::nodes::node_geo_curve_to_points_cc | } // namespace blender::nodes::node_geo_curve_to_points_cc | ||||
| void register_node_type_geo_curve_to_points() | void register_node_type_geo_curve_to_points() | ||||
| { | { | ||||
| namespace file_ns = blender::nodes::node_geo_curve_to_points_cc; | namespace file_ns = blender::nodes::node_geo_curve_to_points_cc; | ||||
| Show All 13 Lines | |||||