Curves: Add `simplify_curve_attribute` function
Compute an index masks of points to remove to simplify the curve attribute using the Ramer-Douglas-Peucker algorithm. The Ramer-Douglas-Peucker algorithm finds a set of points in a polyline to remove so that the overall shape of the polyline is similar. How similar can be controlled by the distance `epsilon`. The function takes a `GSpan` so it can be used with any attribute (that has a type `float`, `float2`, or `float3`). Pull Request: https://projects.blender.org/blender/blender/pulls/118560
This commit is contained in:
parent
3663c8147c
commit
f8ef2b3e78
|
@ -43,6 +43,7 @@ set(SRC
|
||||||
intern/reverse_uv_sampler.cc
|
intern/reverse_uv_sampler.cc
|
||||||
intern/separate_geometry.cc
|
intern/separate_geometry.cc
|
||||||
intern/set_curve_type.cc
|
intern/set_curve_type.cc
|
||||||
|
intern/simplify_curves.cc
|
||||||
intern/smooth_curves.cc
|
intern/smooth_curves.cc
|
||||||
intern/subdivide_curves.cc
|
intern/subdivide_curves.cc
|
||||||
intern/transform.cc
|
intern/transform.cc
|
||||||
|
@ -77,6 +78,7 @@ set(SRC
|
||||||
GEO_reverse_uv_sampler.hh
|
GEO_reverse_uv_sampler.hh
|
||||||
GEO_separate_geometry.hh
|
GEO_separate_geometry.hh
|
||||||
GEO_set_curve_type.hh
|
GEO_set_curve_type.hh
|
||||||
|
GEO_simplify_curves.hh
|
||||||
GEO_smooth_curves.hh
|
GEO_smooth_curves.hh
|
||||||
GEO_subdivide_curves.hh
|
GEO_subdivide_curves.hh
|
||||||
GEO_transform.hh
|
GEO_transform.hh
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BKE_curves.hh"
|
||||||
|
|
||||||
|
namespace blender::geometry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute an index masks of points to remove to simplify the curve attribute using the
|
||||||
|
* Ramer-Douglas-Peucker algorithm.
|
||||||
|
*/
|
||||||
|
IndexMask simplify_curve_attribute(const Span<float3> positions,
|
||||||
|
const IndexMask &curves_selection,
|
||||||
|
const OffsetIndices<int> points_by_curve,
|
||||||
|
const VArray<bool> &cyclic,
|
||||||
|
float epsilon,
|
||||||
|
GSpan attribute_data,
|
||||||
|
IndexMaskMemory &memory);
|
||||||
|
|
||||||
|
} // namespace blender::geometry
|
|
@ -0,0 +1,152 @@
|
||||||
|
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
|
||||||
|
#include "BLI_array_utils.hh"
|
||||||
|
#include "BLI_stack.hh"
|
||||||
|
|
||||||
|
#include "BKE_curves_utils.hh"
|
||||||
|
|
||||||
|
#include "GEO_simplify_curves.hh"
|
||||||
|
|
||||||
|
namespace blender::geometry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a "perpendicular distance" value for the generic attribute data based on the
|
||||||
|
* positions of the curve.
|
||||||
|
*
|
||||||
|
* First, we compute a lambda value that represents a factor from the first point to the last
|
||||||
|
* point of the current range. This is the projection of the point of interest onto the vector
|
||||||
|
* from the first to the last point.
|
||||||
|
*
|
||||||
|
* Then this lambda value is used to compute an interpolated value of the first and last point
|
||||||
|
* and finally we compute the distance from the interpolated value to the actual value.
|
||||||
|
* This is the "perpendicular distance".
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
float perpendicular_distance(const Span<float3> positions,
|
||||||
|
const Span<T> attribute_data,
|
||||||
|
const int64_t first_index,
|
||||||
|
const int64_t last_index,
|
||||||
|
const int64_t index)
|
||||||
|
{
|
||||||
|
const float3 ray_dir = positions[last_index] - positions[first_index];
|
||||||
|
float lambda = 0.0f;
|
||||||
|
if (!math::is_zero(ray_dir)) {
|
||||||
|
lambda = math::dot(ray_dir, positions[index] - positions[first_index]) /
|
||||||
|
math::dot(ray_dir, ray_dir);
|
||||||
|
}
|
||||||
|
const T &from = attribute_data[first_index];
|
||||||
|
const T &to = attribute_data[last_index];
|
||||||
|
const T &value = attribute_data[index];
|
||||||
|
const T &interpolated = math::interpolate(from, to, lambda);
|
||||||
|
return math::distance(value, interpolated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of the Ramer-Douglas-Peucker algorithm.
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
static void ramer_douglas_peucker(const IndexRange range,
|
||||||
|
const Span<float3> positions,
|
||||||
|
const float epsilon,
|
||||||
|
const Span<T> attribute_data,
|
||||||
|
MutableSpan<bool> points_to_delete)
|
||||||
|
{
|
||||||
|
/* Mark all points to be kept. */
|
||||||
|
points_to_delete.slice(range).fill(false);
|
||||||
|
|
||||||
|
Stack<IndexRange, 32> stack;
|
||||||
|
stack.push(range);
|
||||||
|
while (!stack.is_empty()) {
|
||||||
|
const IndexRange sub_range = stack.pop();
|
||||||
|
/* Skip ranges with less than 3 points. All points are kept. */
|
||||||
|
if (sub_range.size() < 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const IndexRange inside_range = sub_range.drop_front(1).drop_back(1);
|
||||||
|
/* Compute the maximum distance and the corresponding index. */
|
||||||
|
float max_dist = -1.0f;
|
||||||
|
int max_index = -1;
|
||||||
|
for (const int64_t index : inside_range) {
|
||||||
|
const float dist = perpendicular_distance(
|
||||||
|
positions, attribute_data, sub_range.first(), sub_range.last(), index);
|
||||||
|
if (dist > max_dist) {
|
||||||
|
max_dist = dist;
|
||||||
|
max_index = index - sub_range.first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max_dist > epsilon) {
|
||||||
|
/* Found point outside the epsilon-sized strip. The point at `max_index` will be kept, repeat
|
||||||
|
* the search on the left & right side. */
|
||||||
|
stack.push(sub_range.slice(0, max_index + 1));
|
||||||
|
stack.push(sub_range.slice(max_index, sub_range.size() - max_index));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* Points in `sub_range` are inside the epsilon-sized strip. Mark them to be deleted. */
|
||||||
|
points_to_delete.slice(inside_range).fill(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static void curve_simplify(const Span<float3> positions,
|
||||||
|
const bool cyclic,
|
||||||
|
const float epsilon,
|
||||||
|
const Span<T> attribute_data,
|
||||||
|
MutableSpan<bool> points_to_delete)
|
||||||
|
{
|
||||||
|
const Vector<IndexRange> selection_ranges = array_utils::find_all_ranges(
|
||||||
|
points_to_delete.as_span(), true);
|
||||||
|
threading::parallel_for(
|
||||||
|
selection_ranges.index_range(), 512, [&](const IndexRange range_of_ranges) {
|
||||||
|
for (const IndexRange range : selection_ranges.as_span().slice(range_of_ranges)) {
|
||||||
|
ramer_douglas_peucker(range, positions, epsilon, attribute_data, points_to_delete);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* For cyclic curves, handle the last segment separately. */
|
||||||
|
const int points_num = positions.size();
|
||||||
|
if (cyclic && points_num > 2) {
|
||||||
|
const float dist = perpendicular_distance(
|
||||||
|
positions, attribute_data, points_num - 2, 0, points_num - 1);
|
||||||
|
if (dist <= epsilon) {
|
||||||
|
points_to_delete[points_num - 1] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexMask simplify_curve_attribute(const Span<float3> positions,
|
||||||
|
const IndexMask &curves_selection,
|
||||||
|
const OffsetIndices<int> points_by_curve,
|
||||||
|
const VArray<bool> &cyclic,
|
||||||
|
const float epsilon,
|
||||||
|
const GSpan attribute_data,
|
||||||
|
IndexMaskMemory &memory)
|
||||||
|
{
|
||||||
|
Array<bool> points_to_delete(positions.size(), false);
|
||||||
|
if (epsilon <= 0.0f) {
|
||||||
|
return IndexMask::from_bools(points_to_delete, memory);
|
||||||
|
}
|
||||||
|
bke::curves::fill_points(
|
||||||
|
points_by_curve, curves_selection, true, points_to_delete.as_mutable_span());
|
||||||
|
curves_selection.foreach_index(GrainSize(512), [&](const int64_t curve_i) {
|
||||||
|
const IndexRange points = points_by_curve[curve_i];
|
||||||
|
bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) {
|
||||||
|
using T = decltype(dummy);
|
||||||
|
if constexpr (std::is_same_v<T, float> || std::is_same_v<T, float2> ||
|
||||||
|
std::is_same_v<T, float3>)
|
||||||
|
{
|
||||||
|
curve_simplify(positions.slice(points),
|
||||||
|
cyclic[curve_i],
|
||||||
|
epsilon,
|
||||||
|
attribute_data.typed<T>().slice(points),
|
||||||
|
points_to_delete.as_mutable_span().slice(points));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return IndexMask::from_bools(points_to_delete, memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace blender::geometry
|
Loading…
Reference in New Issue