GPv3: Interpolate Tool

Interpolation tool for strokes ported from GPv2.

Adds a new operator that inserts a new frame with interpolated curves.
The source curves are taken from the previous/next keyframe.

Co-authored-by: Hans Goudey <hans@blender.org>
Pull Request: https://projects.blender.org/blender/blender/pulls/122155
This commit is contained in:
Lukas Tönne
2024-07-12 15:59:56 +02:00
parent e576c19097
commit 0a70096a5b
13 changed files with 1427 additions and 10 deletions

View File

@@ -4578,7 +4578,7 @@ def km_gpencil_legacy_stroke_vertex_replace(_params):
# Grease Pencil v3
def km_grease_pencil_paint_mode(_params):
def km_grease_pencil_paint_mode(params):
items = []
keymap = (
"Grease Pencil Paint Mode",
@@ -4600,6 +4600,10 @@ def km_grease_pencil_paint_mode(_params):
# Isolate Layer
("grease_pencil.layer_isolate", {"type": 'NUMPAD_ASTERIX', "value": 'PRESS'}, None),
op_tool_optional(
("grease_pencil.interpolate", {"type": 'E', "value": 'PRESS', "ctrl": True}, None),
(op_tool_cycle, "builtin.interpolate"), params),
op_asset_shelf_popup(
"VIEW3D_AST_brush_gpencil_paint",
{"type": 'SPACE', "value": 'PRESS', "shift": True}
@@ -4730,6 +4734,9 @@ def km_grease_pencil_edit_mode(params):
# Set Handle Type
("grease_pencil.set_handle_type", {"type": 'V', "value": 'PRESS'}, None),
op_tool_optional(
("grease_pencil.interpolate", {"type": 'E', "value": 'PRESS', "ctrl": True}, None),
(op_tool_cycle, "builtin.interpolate"), params),
])
return keymap
@@ -8626,6 +8633,27 @@ def km_3d_view_tool_paint_gpencil_interpolate(params):
)
def km_grease_pencil_interpolate_tool_modal_map(params):
items = []
keymap = (
"Interpolate Tool Modal Map",
{"space_type": 'EMPTY', "region_type": 'WINDOW', "modal": True},
{"items": items},
)
items.extend([
("CANCEL", {"type": 'ESC', "value": 'PRESS', "any": True}, None),
("CANCEL", {"type": 'RIGHTMOUSE', "value": 'PRESS', "any": True}, None),
("CONFIRM", {"type": 'RET', "value": 'PRESS', "any": True}, None),
("CONFIRM", {"type": 'NUMPAD_ENTER', "value": 'PRESS', "any": True}, None),
("CONFIRM", {"type": 'LEFTMOUSE', "value": 'RELEASE', "any": True}, None),
("INCREASE", {"type": 'WHEELUPMOUSE', "value": 'PRESS'}, None),
("DECREASE", {"type": 'WHEELDOWNMOUSE', "value": 'PRESS'}, None),
])
return keymap
# ------------------------------------------------------------------------------
# Tool System (3D View, Grease Pencil, Edit)
@@ -8967,6 +8995,28 @@ def km_sequencer_editor_tool_scale(params):
)
def km_3d_view_tool_edit_grease_pencil_interpolate(params):
return (
"3D View Tool: Edit Grease Pencil, Interpolate",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
{"items": [
("grease_pencil.interpolate", params.tool_maybe_tweak_event,
None),
]},
)
def km_3d_view_tool_paint_grease_pencil_interpolate(params):
return (
"3D View Tool: Paint Grease Pencil, Interpolate",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
{"items": [
("grease_pencil.interpolate", params.tool_maybe_tweak_event,
None),
]},
)
# ------------------------------------------------------------------------------
# Full Configuration
@@ -9116,6 +9166,7 @@ def generate_keymaps(params=None):
km_node_link_modal_map(params),
km_grease_pencil_primitive_tool_modal_map(params),
km_grease_pencil_fill_tool_modal_map(params),
km_grease_pencil_interpolate_tool_modal_map(params),
# Gizmos.
km_generic_gizmo(params),
@@ -9265,6 +9316,8 @@ def generate_keymaps(params=None):
km_sequencer_editor_tool_move(params),
km_sequencer_editor_tool_rotate(params),
km_sequencer_editor_tool_scale(params),
km_3d_view_tool_edit_grease_pencil_interpolate(params),
km_3d_view_tool_paint_grease_pencil_interpolate(params),
]
# ------------------------------------------------------------------------------

View File

@@ -2174,6 +2174,48 @@ class _defs_grease_pencil_paint:
draw_settings=draw_settings,
)
@ToolDef.from_fn
def interpolate():
def draw_settings(_context, layout, tool):
props = tool.operator_properties("grease_pencil.interpolate")
layout.prop(props, "layers")
layout.prop(props, "exclude_breakdowns")
layout.prop(props, "flip")
layout.prop(props, "smooth_factor")
layout.prop(props, "smooth_steps")
return dict(
idname="builtin.interpolate",
label="Interpolate",
icon="ops.pose.breakdowner",
cursor='DEFAULT',
widget=None,
keymap=(),
draw_settings=draw_settings,
)
class _defs_grease_pencil_edit:
@ToolDef.from_fn
def interpolate():
def draw_settings(_context, layout, tool):
props = tool.operator_properties("grease_pencil.interpolate")
layout.prop(props, "layers")
layout.prop(props, "exclude_breakdowns")
layout.prop(props, "flip")
layout.prop(props, "smooth_factor")
layout.prop(props, "smooth_steps")
return dict(
idname="builtin.interpolate",
label="Interpolate",
icon="ops.pose.breakdowner",
cursor='DEFAULT',
widget=None,
keymap=(),
draw_settings=draw_settings,
)
class _defs_image_generic:
@@ -3610,6 +3652,8 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel):
_defs_edit_mesh.tosphere,
),
None,
_defs_grease_pencil_edit.interpolate,
None,
*_tools_annotate,
],
'PARTICLE': [
@@ -3725,6 +3769,8 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel):
_defs_grease_pencil_paint.curve,
_defs_grease_pencil_paint.box,
_defs_grease_pencil_paint.circle,
None,
_defs_grease_pencil_paint.interpolate,
],
'PAINT_GPENCIL': [
_defs_view3d_generic.cursor,

View File

@@ -403,12 +403,13 @@ class LayerRuntime {
*/
class Layer : public ::GreasePencilLayer {
public:
using SortedKeysIterator = const int *;
Layer();
explicit Layer(StringRefNull name);
Layer(const Layer &other);
~Layer();
public:
/* Define the common functions for #TreeNode. */
TREENODE_COMMON_METHODS;
/**
@@ -486,6 +487,11 @@ class Layer : public ::GreasePencilLayer {
* exists.
*/
int sorted_keys_index_at(int frame_number) const;
/**
* \returns an iterator into the `sorted_keys` span to the frame at \a frame_number or nullptr if
* no such frame exists.
*/
SortedKeysIterator sorted_keys_iterator_at(int frame_number) const;
/**
* \returns a pointer to the active frame at \a frame_number or nullptr if there is no frame.
@@ -557,14 +563,6 @@ class Layer : public ::GreasePencilLayer {
void set_view_layer_name(const char *new_name);
private:
using SortedKeysIterator = const int *;
private:
/**
* \returns an iterator into the `sorted_keys` span to the frame at \a frame_number or nullptr if
* no such frame exists.
*/
SortedKeysIterator sorted_keys_iterator_at(int frame_number) const;
/**
* \returns the key of the active frame at \a frame_number or #std::nullopt if no such frame
* exists.

View File

@@ -157,6 +157,20 @@ void sample_uniform(Span<float> accumulated_segment_lengths,
MutableSpan<int> r_segment_indices,
MutableSpan<float> r_factors);
/**
* Find evenly spaced samples along the lengths, starting at the end.
*
* \param accumulated_segment_lengths: The accumulated lengths of the original elements being
* sampled. Could be calculated by #accumulate_lengths.
* \param include_first_point: Generally false for cyclic sequences and true otherwise.
* \param r_segment_indices: The index of the previous point at each sample.
* \param r_factors: The portion of the length in each segment at each sample.
*/
void sample_uniform_reverse(Span<float> accumulated_segment_lengths,
bool include_first_point,
MutableSpan<int> r_segment_indices,
MutableSpan<float> r_factors);
/**
* For each provided sample length, find the segment index and interpolation factor.
*

View File

@@ -4,6 +4,7 @@
#include "BLI_length_parameterize.hh"
#include "BLI_task.hh"
#include <algorithm>
namespace blender::length_parameterize {
@@ -36,6 +37,35 @@ void sample_uniform(const Span<float> accumulated_segment_lengths,
});
}
void sample_uniform_reverse(const Span<float> accumulated_segment_lengths,
const bool include_first_point,
MutableSpan<int> r_segment_indices,
MutableSpan<float> r_factors)
{
const int count = r_segment_indices.size();
BLI_assert(count > 0);
BLI_assert(accumulated_segment_lengths.size() >= 1);
BLI_assert(
std::is_sorted(accumulated_segment_lengths.begin(), accumulated_segment_lengths.end()));
if (count == 1) {
r_segment_indices[0] = accumulated_segment_lengths.size() - 1;
r_factors[0] = 1.0f;
return;
}
const float total_length = accumulated_segment_lengths.last();
const float step_length = total_length / (count - include_first_point);
threading::parallel_for(IndexRange(count), 512, [&](const IndexRange range) {
SampleSegmentHint hint;
for (const int i : range) {
/* Use maximum to avoid issues with floating point accuracy. */
const float sample_length = std::max(0.0f, total_length - i * step_length);
sample_at_length(
accumulated_segment_lengths, sample_length, r_segment_indices[i], r_factors[i], &hint);
}
});
}
void sample_at_lengths(const Span<float> accumulated_segment_lengths,
const Span<float> sample_lengths,
MutableSpan<int> r_segment_indices,

View File

@@ -199,6 +199,7 @@ void ED_operatortypes_grease_pencil()
ED_operatortypes_grease_pencil_material();
ED_operatortypes_grease_pencil_primitives();
ED_operatortypes_grease_pencil_weight_paint();
ED_operatortypes_grease_pencil_interpolate();
}
void ED_operatormacros_grease_pencil()
@@ -247,4 +248,5 @@ void ED_keymap_grease_pencil(wmKeyConfig *keyconf)
keymap_grease_pencil_fill_tool(keyconf);
ED_primitivetool_modal_keymap(keyconf);
ED_filltool_modal_keymap(keyconf);
ED_interpolatetool_modal_keymap(keyconf);
}

View File

@@ -23,6 +23,7 @@
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DNA_grease_pencil_types.h"
#include "ED_grease_pencil.hh"
#include "ED_undo.hh"

View File

@@ -58,10 +58,12 @@ void ED_operatortypes_grease_pencil_edit();
void ED_operatortypes_grease_pencil_material();
void ED_operatortypes_grease_pencil_primitives();
void ED_operatortypes_grease_pencil_weight_paint();
void ED_operatortypes_grease_pencil_interpolate();
void ED_operatormacros_grease_pencil();
void ED_keymap_grease_pencil(wmKeyConfig *keyconf);
void ED_primitivetool_modal_keymap(wmKeyConfig *keyconf);
void ED_filltool_modal_keymap(wmKeyConfig *keyconf);
void ED_interpolatetool_modal_keymap(wmKeyConfig *keyconf);
void GREASE_PENCIL_OT_stroke_cutter(wmOperatorType *ot);
@@ -606,6 +608,22 @@ void draw_grease_pencil_strokes(const RegionView3D &rv3d,
} // namespace image_render
enum class InterpolateFlipMode : int8_t {
/* No flip. */
None = 0,
/* Flip always. */
Flip,
/* Flip if needed. */
FlipAuto,
};
enum class InterpolateLayerMode : int8_t {
/* Only interpolate on the active layer. */
Active = 0,
/* Interpolate strokes on every layer. */
All,
};
/**
* Create new strokes tracing the rendered outline of existing strokes.
* \param drawing: Drawing with input strokes.

View File

@@ -50,6 +50,7 @@ set(SRC
grease_pencil_draw_ops.cc
grease_pencil_erase.cc
grease_pencil_fill.cc
grease_pencil_interpolate.cc
grease_pencil_paint.cc
grease_pencil_sculpt_clone.cc
grease_pencil_sculpt_common.cc

View File

@@ -0,0 +1,828 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_context.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BLI_array_utils.hh"
#include "BLI_index_mask.hh"
#include "BLI_math_angle_types.hh"
#include "BLI_math_geom.h"
#include "BLI_math_rotation.h"
#include "BLI_math_rotation.hh"
#include "BLI_math_vector.hh"
#include "BLI_offset_indices.hh"
#include "BLI_task.hh"
#include "BLT_translation.hh"
#include "DEG_depsgraph.hh"
#include "DNA_grease_pencil_types.h"
#include "ED_grease_pencil.hh"
#include "ED_numinput.hh"
#include "ED_screen.hh"
#include "GEO_interpolate_curves.hh"
#include "GEO_smooth_curves.hh"
#include "MEM_guardedalloc.h"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include <climits>
namespace blender::ed::sculpt_paint::greasepencil {
using ed::greasepencil::InterpolateFlipMode;
using ed::greasepencil::InterpolateLayerMode;
/* -------------------------------------------------------------------- */
/** \name Interpolate Operator
* \{ */
constexpr float interpolate_factor_min = -1.0f;
constexpr float interpolate_factor_max = 2.0f;
/* Pair of curves in a layer that get interpolated. */
struct InterpolationPairs {
Vector<int> from_frames;
Vector<int> to_frames;
Vector<int> from_curves;
Vector<int> to_curves;
};
struct InterpolateOpData {
struct LayerData {
/* Curve pairs to interpolate from this layer. */
InterpolationPairs curve_pairs;
/* Geometry of the target frame before interpolation for restoring on cancel. */
std::optional<bke::CurvesGeometry> orig_curves;
};
/* Layers to include. */
IndexMaskMemory layer_mask_memory;
IndexMask layer_mask;
/* Exclude breakdown keyframes when finding intervals. */
bool exclude_breakdowns;
/* Interpolation factor bias controlled by the user. */
float shift;
/* Interpolation base factor for the active layer. */
float init_factor;
InterpolateFlipMode flipmode;
float smooth_factor;
int smooth_steps;
NumInput numeric_input;
Array<LayerData> layer_data;
int active_layer_index;
};
using FramesMapKeyIntervalT = std::pair<int, int>;
static std::optional<FramesMapKeyIntervalT> find_frames_interval(
const bke::greasepencil::Layer &layer, const int frame_number, const bool exclude_breakdowns)
{
using Layer = bke::greasepencil::Layer;
using bke::greasepencil::FramesMapKeyT;
using SortedKeysIterator = Layer::SortedKeysIterator;
const Span<FramesMapKeyT> sorted_keys = layer.sorted_keys();
SortedKeysIterator prev_key_it = layer.sorted_keys_iterator_at(frame_number);
if (!prev_key_it) {
return std::nullopt;
}
SortedKeysIterator next_key_it = std::next(prev_key_it);
/* Skip over invalid keyframes on either side. */
auto is_valid_keyframe = [&](const FramesMapKeyT key) {
const GreasePencilFrame *frame = layer.frame_at(key);
if (!frame || frame->is_end()) {
return false;
}
if (exclude_breakdowns && frame->type == BEZT_KEYTYPE_BREAKDOWN) {
return false;
}
return true;
};
for (; next_key_it != sorted_keys.end(); ++next_key_it) {
if (is_valid_keyframe(*next_key_it)) {
break;
}
}
for (; prev_key_it != sorted_keys.begin(); --prev_key_it) {
if (is_valid_keyframe(*prev_key_it)) {
break;
}
}
if (next_key_it == sorted_keys.end() || !is_valid_keyframe(*prev_key_it)) {
return std::nullopt;
}
return std::make_pair(*prev_key_it, *next_key_it);
}
/* Build index lists for curve interpolation using index. */
static void find_curve_mapping_from_index(const GreasePencil &grease_pencil,
const bke::greasepencil::Layer &layer,
const int current_frame,
const bool exclude_breakdowns,
InterpolationPairs &pairs)
{
using bke::greasepencil::Drawing;
const std::optional<FramesMapKeyIntervalT> interval = find_frames_interval(
layer, current_frame, exclude_breakdowns);
if (!interval) {
return;
}
BLI_assert(layer.has_drawing_at(interval->first));
BLI_assert(layer.has_drawing_at(interval->second));
const Drawing &from_drawing = *grease_pencil.get_drawing_at(layer, interval->first);
const Drawing &to_drawing = *grease_pencil.get_drawing_at(layer, interval->second);
const int pairs_num = std::min(from_drawing.strokes().curves_num(),
to_drawing.strokes().curves_num());
const int old_pairs_num = pairs.from_frames.size();
pairs.from_frames.append_n_times(interval->first, pairs_num);
pairs.to_frames.append_n_times(interval->second, pairs_num);
pairs.from_curves.resize(old_pairs_num + pairs_num);
pairs.to_curves.resize(old_pairs_num + pairs_num);
array_utils::fill_index_range(
pairs.from_curves.as_mutable_span().slice(old_pairs_num, pairs_num));
array_utils::fill_index_range(pairs.to_curves.as_mutable_span().slice(old_pairs_num, pairs_num));
}
static bool compute_auto_flip(const Span<float3> from_positions, const Span<float3> to_positions)
{
if (from_positions.size() < 2 || to_positions.size() < 2) {
return false;
}
constexpr float min_angle = DEG2RADF(15);
const float3 &from_first = from_positions.first();
const float3 &from_last = from_positions.last();
const float3 &to_first = to_positions.first();
const float3 &to_last = to_positions.last();
/* If lines intersect at a sharp angle check distances. */
if (isect_seg_seg_v2(from_first, to_first, from_last, to_last) == ISECT_LINE_LINE_CROSS) {
if (math::angle_between(math::normalize(to_first - from_first),
math::normalize(to_last - from_last))
.radian() < min_angle)
{
if (math::distance_squared(from_first, to_first) >=
math::distance_squared(from_last, to_first))
{
return math::distance_squared(from_last, to_first) >=
math::distance_squared(from_last, to_last);
}
return math::distance_squared(from_first, to_first) <
math::distance_squared(from_first, to_last);
}
return true;
}
return math::dot(from_last - from_first, to_last - to_first) < 0.0f;
}
static bke::CurvesGeometry interpolate_between_curves(const GreasePencil &grease_pencil,
const bke::greasepencil::Layer &layer,
const InterpolationPairs &curve_pairs,
const float mix_factor,
const InterpolateFlipMode flip_mode)
{
using bke::greasepencil::Drawing;
const int dst_curve_num = curve_pairs.from_curves.size();
BLI_assert(curve_pairs.to_curves.size() == dst_curve_num);
BLI_assert(curve_pairs.from_frames.size() == dst_curve_num);
BLI_assert(curve_pairs.to_frames.size() == dst_curve_num);
/* Sort pairs by unique to/from frame combinations.
* Curves for each frame pair are then interpolated together.
* Map entries are indices into the original curve_pairs array,
* so the order of strokes can be maintained. */
Array<int> sorted_pairs(dst_curve_num);
array_utils::fill_index_range(sorted_pairs.as_mutable_span());
std::sort(sorted_pairs.begin(), sorted_pairs.end(), [&](const int a, const int b) {
const int from_frame_a = curve_pairs.from_frames[a];
const int to_frame_a = curve_pairs.to_frames[a];
const int from_frame_b = curve_pairs.from_frames[b];
const int to_frame_b = curve_pairs.to_frames[b];
return from_frame_a < from_frame_b ||
(from_frame_a == from_frame_b && to_frame_a < to_frame_b);
});
/* Find ranges of sorted pairs with the same from/to frame intervals. */
Vector<int> pair_offsets;
const OffsetIndices curves_by_pair = [&]() {
int prev_from_frame = INT_MIN;
int prev_to_frame = INT_MIN;
int current_count = 0;
for (const int sorted_index : IndexRange(dst_curve_num)) {
const int pair_index = sorted_pairs[sorted_index];
const int from_frame = curve_pairs.from_frames[pair_index];
const int to_frame = curve_pairs.to_frames[pair_index];
if (from_frame != prev_from_frame || to_frame != prev_to_frame) {
/* New pair. */
if (current_count > 0) {
pair_offsets.append(current_count);
}
current_count = 0;
}
++current_count;
}
if (current_count > 0) {
pair_offsets.append(current_count);
}
/* Last entry for overall size. */
if (pair_offsets.is_empty()) {
return OffsetIndices<int>{};
}
pair_offsets.append(0);
return offset_indices::accumulate_counts_to_offsets(pair_offsets);
}();
/* Compute curve length and flip mode for each pair. */
Vector<int> dst_curve_offsets;
Vector<bool> dst_curve_flip;
const OffsetIndices dst_points_by_curve = [&]() {
for (const int pair_range_i : curves_by_pair.index_range()) {
const IndexRange pair_range = curves_by_pair[pair_range_i];
BLI_assert(!pair_range.is_empty());
const int first_pair_index = sorted_pairs[pair_range.first()];
const int from_frame = curve_pairs.from_frames[first_pair_index];
const int to_frame = curve_pairs.to_frames[first_pair_index];
const Drawing *from_drawing = grease_pencil.get_drawing_at(layer, from_frame);
const Drawing *to_drawing = grease_pencil.get_drawing_at(layer, to_frame);
if (!from_drawing || !to_drawing) {
continue;
}
const OffsetIndices from_points_by_curve = from_drawing->strokes().points_by_curve();
const OffsetIndices to_points_by_curve = to_drawing->strokes().points_by_curve();
const Span<float3> from_positions = from_drawing->strokes().positions();
const Span<float3> to_positions = to_drawing->strokes().positions();
for (const int sorted_index : pair_range) {
const int pair_index = sorted_pairs[sorted_index];
const int from_curve = curve_pairs.from_curves[pair_index];
const int to_curve = curve_pairs.to_curves[pair_index];
const IndexRange from_points = from_points_by_curve[from_curve];
const IndexRange to_points = to_points_by_curve[to_curve];
dst_curve_offsets.append(std::max(from_points.size(), to_points.size()));
switch (flip_mode) {
case InterpolateFlipMode::None:
dst_curve_flip.append(false);
break;
case InterpolateFlipMode::Flip:
dst_curve_flip.append(true);
break;
case InterpolateFlipMode::FlipAuto: {
dst_curve_flip.append(compute_auto_flip(from_positions.slice(from_points),
to_positions.slice(to_points)));
break;
}
}
}
}
/* Last entry for overall size. */
if (dst_curve_offsets.is_empty()) {
return OffsetIndices<int>{};
}
dst_curve_offsets.append(0);
return offset_indices::accumulate_counts_to_offsets(dst_curve_offsets);
}();
const int dst_point_num = dst_points_by_curve.total_size();
bke::CurvesGeometry dst_curves(dst_point_num, dst_curve_num);
/* Offsets are empty when there are no curves. */
if (dst_curve_num > 0) {
dst_curves.offsets_for_write().copy_from(dst_curve_offsets);
}
/* Sorted map arrays that can be passed to the interpolation function directly.
* These index maps have the same order as the sorted indices, so slices of indices can be used
* for interpolating all curves of a frame pair at once. */
Array<int> sorted_from_curve_indices(dst_curve_num);
Array<int> sorted_to_curve_indices(dst_curve_num);
for (const int pair_range_i : curves_by_pair.index_range()) {
const IndexRange pair_range = curves_by_pair[pair_range_i];
const int first_pair_index = sorted_pairs[pair_range.first()];
const int from_frame = curve_pairs.from_frames[first_pair_index];
const int to_frame = curve_pairs.to_frames[first_pair_index];
const Drawing *from_drawing = grease_pencil.get_drawing_at(layer, from_frame);
const Drawing *to_drawing = grease_pencil.get_drawing_at(layer, to_frame);
if (!from_drawing || !to_drawing) {
continue;
}
const IndexRange from_curves = from_drawing->strokes().curves_range();
const IndexRange to_curves = to_drawing->strokes().curves_range();
/* Subset of target curves that are filled by this frame pair. */
IndexMaskMemory selection_memory;
const IndexMask selection = IndexMask::from_indices(sorted_pairs.as_span().slice(pair_range),
selection_memory);
MutableSpan<int> pair_from_indices = sorted_from_curve_indices.as_mutable_span().slice(
pair_range);
MutableSpan<int> pair_to_indices = sorted_to_curve_indices.as_mutable_span().slice(pair_range);
for (const int i : pair_range) {
const int pair_index = sorted_pairs[i];
sorted_from_curve_indices[i] = std::clamp(
curve_pairs.from_curves[pair_index], 0, int(from_curves.last()));
sorted_to_curve_indices[i] = std::clamp(
curve_pairs.to_curves[pair_index], 0, int(to_curves.last()));
}
geometry::interpolate_curves(from_drawing->strokes(),
to_drawing->strokes(),
pair_from_indices,
pair_to_indices,
selection,
dst_curve_flip,
mix_factor,
dst_curves);
}
return dst_curves;
}
static void grease_pencil_interpolate_status_indicators(bContext &C,
const InterpolateOpData &opdata)
{
Scene &scene = *CTX_data_scene(&C);
ScrArea &area = *CTX_wm_area(&C);
const StringRef msg = IFACE_("GPencil Interpolation: ");
std::string status;
if (hasNumInput(&opdata.numeric_input)) {
char str_ofs[NUM_STR_REP_LEN];
outputNumInput(&const_cast<NumInput &>(opdata.numeric_input), str_ofs, &scene.unit);
status = msg + std::string(str_ofs);
}
else {
status = msg + std::to_string(int((opdata.init_factor + opdata.shift) * 100.0f)) + " %";
}
ED_area_status_text(&area, status.c_str());
ED_workspace_status_text(
&C, IFACE_("ESC/RMB to cancel, Enter/LMB to confirm, WHEEL/MOVE to adjust factor"));
}
/* Utility function to get a drawing at the exact frame number. */
static bke::greasepencil::Drawing *get_drawing_at_exact_frame(GreasePencil &grease_pencil,
bke::greasepencil::Layer &layer,
const int frame_number)
{
using bke::greasepencil::Drawing;
const std::optional<int> start_frame = layer.start_frame_at(frame_number);
if (start_frame && *start_frame == frame_number) {
return grease_pencil.get_editable_drawing_at(layer, frame_number);
}
return nullptr;
}
static bke::greasepencil::Drawing *ensure_drawing_at_exact_frame(
GreasePencil &grease_pencil,
bke::greasepencil::Layer &layer,
InterpolateOpData::LayerData &layer_data,
const int frame_number)
{
using bke::greasepencil::Drawing;
if (Drawing *drawing = get_drawing_at_exact_frame(grease_pencil, layer, frame_number)) {
layer_data.orig_curves = drawing->strokes();
return drawing;
}
return grease_pencil.insert_frame(layer, frame_number);
}
static void grease_pencil_interpolate_update(bContext &C, const wmOperator &op)
{
using bke::greasepencil::Drawing;
using bke::greasepencil::Layer;
const auto &opdata = *static_cast<InterpolateOpData *>(op.customdata);
const Scene &scene = *CTX_data_scene(&C);
const int current_frame = scene.r.cfra;
Object &object = *CTX_data_active_object(&C);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
const auto flip_mode = InterpolateFlipMode(RNA_enum_get(op.ptr, "flip"));
opdata.layer_mask.foreach_index([&](const int layer_index) {
Layer &layer = *grease_pencil.layer(layer_index);
const InterpolateOpData::LayerData &layer_data = opdata.layer_data[layer_index];
/* Drawings must be created on operator invoke. */
Drawing *dst_drawing = get_drawing_at_exact_frame(grease_pencil, layer, current_frame);
if (dst_drawing == nullptr) {
return;
}
const float mix_factor = opdata.init_factor + opdata.shift;
bke::CurvesGeometry interpolated_curves = interpolate_between_curves(
grease_pencil, layer, layer_data.curve_pairs, mix_factor, flip_mode);
if (opdata.smooth_factor > 0.0f && opdata.smooth_steps > 0) {
MutableSpan<float3> positions = interpolated_curves.positions_for_write();
geometry::smooth_curve_attribute(
interpolated_curves.curves_range(),
interpolated_curves.points_by_curve(),
VArray<bool>::ForSingle(true, interpolated_curves.points_num()),
interpolated_curves.cyclic(),
opdata.smooth_steps,
opdata.smooth_factor,
false,
false,
positions);
interpolated_curves.tag_positions_changed();
}
dst_drawing->strokes_for_write() = std::move(interpolated_curves);
dst_drawing->tag_topology_changed();
});
grease_pencil_interpolate_status_indicators(C, opdata);
DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GPENCIL | NA_EDITED, nullptr);
}
/* Restore timeline changes when cancelled. */
static void grease_pencil_interpolate_restore(bContext &C, wmOperator &op)
{
using bke::greasepencil::Drawing;
using bke::greasepencil::Layer;
if (op.customdata == nullptr) {
return;
}
const auto &opdata = *static_cast<InterpolateOpData *>(op.customdata);
const Scene &scene = *CTX_data_scene(&C);
const int current_frame = scene.r.cfra;
Object &object = *CTX_data_active_object(&C);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
opdata.layer_mask.foreach_index([&](const int layer_index) {
Layer &layer = *grease_pencil.layer(layer_index);
const InterpolateOpData::LayerData &layer_data = opdata.layer_data[layer_index];
if (layer_data.orig_curves) {
/* Keyframe existed before the operator, restore geometry. */
Drawing *drawing = grease_pencil.get_editable_drawing_at(layer, current_frame);
if (drawing) {
drawing->strokes_for_write() = *layer_data.orig_curves;
drawing->tag_topology_changed();
DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GPENCIL | NA_EDITED, nullptr);
}
}
else {
/* Frame was empty, remove the added drawing. */
grease_pencil.remove_frames(layer, {current_frame});
DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GPENCIL | NA_EDITED, nullptr);
}
});
}
static bool grease_pencil_interpolate_init(const bContext &C, wmOperator &op)
{
using bke::greasepencil::Drawing;
using bke::greasepencil::Layer;
const Scene &scene = *CTX_data_scene(&C);
const int current_frame = scene.r.cfra;
Object &object = *CTX_data_active_object(&C);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
BLI_assert(grease_pencil.has_active_layer());
const Layer &active_layer = *grease_pencil.get_active_layer();
op.customdata = MEM_new<InterpolateOpData>(__func__);
InterpolateOpData &data = *static_cast<InterpolateOpData *>(op.customdata);
data.shift = RNA_float_get(op.ptr, "shift");
data.exclude_breakdowns = RNA_boolean_get(op.ptr, "exclude_breakdowns");
data.flipmode = InterpolateFlipMode(RNA_enum_get(op.ptr, "flip"));
data.smooth_factor = RNA_float_get(op.ptr, "smooth_factor");
data.smooth_steps = RNA_int_get(op.ptr, "smooth_steps");
data.active_layer_index = *grease_pencil.get_layer_index(active_layer);
const auto layer_mode = InterpolateLayerMode(RNA_enum_get(op.ptr, "layers"));
switch (layer_mode) {
case InterpolateLayerMode::Active:
data.layer_mask = IndexRange::from_single(data.active_layer_index);
break;
case InterpolateLayerMode::All:
data.layer_mask = IndexMask::from_predicate(
grease_pencil.layers().index_range(),
GrainSize(1024),
data.layer_mask_memory,
[&](const int layer_index) { return grease_pencil.layer(layer_index)->is_editable(); });
break;
}
data.layer_data.reinitialize(grease_pencil.layers().size());
data.layer_mask.foreach_index([&](const int layer_index) {
Layer &layer = *grease_pencil.layer(layer_index);
InterpolateOpData::LayerData &layer_data = data.layer_data[layer_index];
/* Pair from/to curves by index. */
find_curve_mapping_from_index(
grease_pencil, layer, current_frame, data.exclude_breakdowns, layer_data.curve_pairs);
ensure_drawing_at_exact_frame(grease_pencil, layer, layer_data, current_frame);
});
const std::optional<FramesMapKeyIntervalT> active_layer_interval = find_frames_interval(
active_layer, current_frame, data.exclude_breakdowns);
data.init_factor = active_layer_interval ?
float(current_frame - active_layer_interval->first) /
(active_layer_interval->second - active_layer_interval->first + 1) :
0.5f;
return true;
}
/* Exit and free memory. */
static void grease_pencil_interpolate_exit(bContext &C, wmOperator &op)
{
ScrArea &area = *CTX_wm_area(&C);
if (op.customdata == nullptr) {
return;
}
ED_area_status_text(&area, nullptr);
ED_workspace_status_text(&C, nullptr);
MEM_delete(static_cast<InterpolateOpData *>(op.customdata));
op.customdata = nullptr;
}
static bool grease_pencil_interpolate_poll(bContext *C)
{
if (!ed::greasepencil::active_grease_pencil_poll(C)) {
return false;
}
ToolSettings *ts = CTX_data_tool_settings(C);
if (!ts || !ts->gp_paint) {
return false;
}
/* Only 3D view */
ScrArea *area = CTX_wm_area(C);
if (area && area->spacetype != SPACE_VIEW3D) {
return false;
}
return true;
}
/* Invoke handler: Initialize the operator */
static int grease_pencil_interpolate_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
wmWindow &win = *CTX_wm_window(C);
if (!grease_pencil_interpolate_init(*C, *op)) {
grease_pencil_interpolate_exit(*C, *op);
return OPERATOR_CANCELLED;
}
InterpolateOpData &opdata = *static_cast<InterpolateOpData *>(op->customdata);
/* Set cursor to indicate modal operator. */
WM_cursor_modal_set(&win, WM_CURSOR_EW_SCROLL);
grease_pencil_interpolate_status_indicators(*C, opdata);
WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, nullptr);
WM_event_add_modal_handler(C, op);
return OPERATOR_RUNNING_MODAL;
}
enum class InterpolateToolModalEvent : int8_t {
Cancel = 1,
Confirm,
Increase,
Decrease,
};
/* Modal handler: Events handling during interactive part */
static int grease_pencil_interpolate_modal(bContext *C, wmOperator *op, const wmEvent *event)
{
wmWindow &win = *CTX_wm_window(C);
const ARegion &region = *CTX_wm_region(C);
ScrArea &area = *CTX_wm_area(C);
InterpolateOpData &opdata = *static_cast<InterpolateOpData *>(op->customdata);
const bool has_numinput = hasNumInput(&opdata.numeric_input);
switch (event->type) {
case EVT_MODAL_MAP: {
switch (InterpolateToolModalEvent(event->val)) {
case InterpolateToolModalEvent::Cancel:
ED_area_status_text(&area, nullptr);
ED_workspace_status_text(C, nullptr);
WM_cursor_modal_restore(&win);
grease_pencil_interpolate_restore(*C, *op);
grease_pencil_interpolate_exit(*C, *op);
return OPERATOR_CANCELLED;
case InterpolateToolModalEvent::Confirm:
ED_area_status_text(&area, nullptr);
ED_workspace_status_text(C, nullptr);
WM_cursor_modal_restore(&win);
/* Write current factor to properties for the next execution. */
RNA_float_set(op->ptr, "shift", opdata.shift);
grease_pencil_interpolate_exit(*C, *op);
return OPERATOR_FINISHED;
case InterpolateToolModalEvent::Increase:
opdata.shift = std::clamp(opdata.init_factor + opdata.shift + 0.01f,
interpolate_factor_min,
interpolate_factor_max) -
opdata.init_factor;
grease_pencil_interpolate_update(*C, *op);
break;
case InterpolateToolModalEvent::Decrease:
opdata.shift = std::clamp(opdata.init_factor + opdata.shift - 0.01f,
interpolate_factor_min,
interpolate_factor_max) -
opdata.init_factor;
grease_pencil_interpolate_update(*C, *op);
break;
}
break;
}
case MOUSEMOVE:
/* Only handle mouse-move if not doing numeric-input. */
if (!has_numinput) {
const float mouse_pos = event->mval[0];
const float factor = std::clamp(
mouse_pos / region.winx, interpolate_factor_min, interpolate_factor_max);
opdata.shift = factor - opdata.init_factor;
grease_pencil_interpolate_update(*C, *op);
}
break;
default: {
if ((event->val == KM_PRESS) && handleNumInput(C, &opdata.numeric_input, event)) {
float value = (opdata.init_factor + opdata.shift) * 100.0f;
applyNumInput(&opdata.numeric_input, &value);
opdata.shift = std::clamp(value * 0.01f, interpolate_factor_min, interpolate_factor_max) -
opdata.init_factor;
grease_pencil_interpolate_update(*C, *op);
break;
}
/* Unhandled event, allow to pass through. */
return OPERATOR_RUNNING_MODAL | OPERATOR_PASS_THROUGH;
}
}
return OPERATOR_RUNNING_MODAL;
}
static void grease_pencil_interpolate_cancel(bContext *C, wmOperator *op)
{
grease_pencil_interpolate_restore(*C, *op);
grease_pencil_interpolate_exit(*C, *op);
}
static void GREASE_PENCIL_OT_interpolate(wmOperatorType *ot)
{
static const EnumPropertyItem flip_modes[] = {
{int(InterpolateFlipMode::None), "NONE", 0, "No Flip", ""},
{int(InterpolateFlipMode::Flip), "FLIP", 0, "Flip", ""},
{int(InterpolateFlipMode::FlipAuto), "AUTO", 0, "Automatic", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem gpencil_interpolation_layer_items[] = {
{int(InterpolateLayerMode::Active), "ACTIVE", 0, "Active", ""},
{int(InterpolateLayerMode::All), "ALL", 0, "All Layers", ""},
{0, nullptr, 0, nullptr, nullptr},
};
ot->name = "Grease Pencil Interpolation";
ot->idname = "GREASE_PENCIL_OT_interpolate";
ot->description = "Interpolate grease pencil strokes between frames";
ot->invoke = grease_pencil_interpolate_invoke;
ot->modal = grease_pencil_interpolate_modal;
ot->cancel = grease_pencil_interpolate_cancel;
ot->poll = grease_pencil_interpolate_poll;
ot->flag = OPTYPE_UNDO | OPTYPE_BLOCKING;
RNA_def_float_factor(
ot->srna,
"shift",
0.0f,
-1.0f,
1.0f,
"Shift",
"Bias factor for which frame has more influence on the interpolated strokes",
-0.9f,
0.9f);
RNA_def_enum(ot->srna,
"layers",
gpencil_interpolation_layer_items,
0,
"Layer",
"Layers included in the interpolation");
RNA_def_boolean(ot->srna,
"exclude_breakdowns",
false,
"Exclude Breakdowns",
"Exclude existing Breakdowns keyframes as interpolation extremes");
RNA_def_enum(ot->srna,
"flip",
flip_modes,
int(InterpolateFlipMode::FlipAuto),
"Flip Mode",
"Invert destination stroke to match start and end with source stroke");
RNA_def_int(ot->srna,
"smooth_steps",
1,
1,
3,
"Iterations",
"Number of times to smooth newly created strokes",
1,
3);
RNA_def_float(ot->srna,
"smooth_factor",
0.0f,
0.0f,
2.0f,
"Smooth",
"Amount of smoothing to apply to interpolated strokes, to reduce jitter/noise",
0.0f,
2.0f);
}
/** \} */
} // namespace blender::ed::sculpt_paint::greasepencil
/* -------------------------------------------------------------------- */
/** \name Registration
* \{ */
void ED_operatortypes_grease_pencil_interpolate()
{
using namespace blender::ed::sculpt_paint::greasepencil;
WM_operatortype_append(GREASE_PENCIL_OT_interpolate);
}
void ED_interpolatetool_modal_keymap(wmKeyConfig *keyconf)
{
using namespace blender::ed::sculpt_paint::greasepencil;
static const EnumPropertyItem modal_items[] = {
{int(InterpolateToolModalEvent::Cancel), "CANCEL", 0, "Cancel", ""},
{int(InterpolateToolModalEvent::Confirm), "CONFIRM", 0, "Confirm", ""},
{int(InterpolateToolModalEvent::Increase), "INCREASE", 0, "Increase", ""},
{int(InterpolateToolModalEvent::Decrease), "DECREASE", 0, "Decrease", ""},
{0, nullptr, 0, nullptr, nullptr},
};
wmKeyMap *keymap = WM_modalkeymap_find(keyconf, "Interpolate Tool Modal Map");
/* This function is called for each space-type, only needs to add map once. */
if (keymap && keymap->modal_items) {
return;
}
keymap = WM_modalkeymap_ensure(keyconf, "Interpolate Tool Modal Map", modal_items);
WM_modalkeymap_assign(keymap, "GREASE_PENCIL_OT_interpolate");
}
/** \} */

View File

@@ -20,6 +20,7 @@ set(SRC
intern/curve_constraints.cc
intern/extend_curves.cc
intern/fillet_curves.cc
intern/interpolate_curves.cc
intern/join_geometries.cc
intern/merge_curves.cc
intern/mesh_boolean.cc
@@ -57,6 +58,7 @@ set(SRC
GEO_curve_constraints.hh
GEO_extend_curves.hh
GEO_fillet_curves.hh
GEO_interpolate_curves.hh
GEO_join_geometries.hh
GEO_merge_curves.hh
GEO_mesh_boolean.hh

View File

@@ -0,0 +1,27 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "FN_field.hh"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
namespace blender::geometry {
/**
* Create new curves that are interpolated between "from" and "to" curves.
* \param selection: Selection of curves in \a dst_curves that are being filled.
*/
void interpolate_curves(const bke::CurvesGeometry &from_curves,
const bke::CurvesGeometry &to_curves,
Span<int> from_curve_indices,
Span<int> to_curve_indices,
const IndexMask &selection,
Span<bool> curve_flip_direction,
const float mix_factor,
bke::CurvesGeometry &dst_curves);
} // namespace blender::geometry

View File

@@ -0,0 +1,397 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_attribute_math.hh"
#include "BKE_curves.hh"
#include "BLI_assert.h"
#include "BLI_length_parameterize.hh"
#include "BLI_math_vector.hh"
#include "BLI_offset_indices.hh"
#include "BLI_task.hh"
#include "DNA_customdata_types.h"
#include "GEO_interpolate_curves.hh"
#include "GEO_resample_curves.hh"
namespace blender::geometry {
using bke::CurvesGeometry;
/**
* Return true if the attribute should be copied/interpolated to the result curves.
* Don't output attributes that correspond to curve types that have no curves in the result.
*/
static bool interpolate_attribute_to_curves(const bke::AttributeIDRef &attribute_id,
const std::array<int, CURVE_TYPES_NUM> &type_counts)
{
if (attribute_id.is_anonymous()) {
return true;
}
if (ELEM(attribute_id.name(),
"handle_type_left",
"handle_type_right",
"handle_left",
"handle_right"))
{
return type_counts[CURVE_TYPE_BEZIER] != 0;
}
if (ELEM(attribute_id.name(), "nurbs_weight")) {
return type_counts[CURVE_TYPE_NURBS] != 0;
}
return true;
}
/**
* Return true if the attribute should be copied to poly curves.
*/
static bool interpolate_attribute_to_poly_curve(const bke::AttributeIDRef &attribute_id)
{
static const Set<StringRef> no_interpolation{{
"handle_type_left",
"handle_type_right",
"handle_right",
"handle_left",
"nurbs_weight",
}};
return !no_interpolation.contains(attribute_id.name());
}
struct AttributesForInterpolation {
Vector<GVArraySpan> src_from;
Vector<GVArraySpan> src_to;
Vector<bke::GSpanAttributeWriter> dst;
};
/**
* Retrieve spans from source and result attributes.
*/
static AttributesForInterpolation retrieve_attribute_spans(const Span<bke::AttributeIDRef> ids,
const CurvesGeometry &src_from_curves,
const CurvesGeometry &src_to_curves,
CurvesGeometry &dst_curves)
{
AttributesForInterpolation result;
const bke::AttributeAccessor src_from_attributes = src_from_curves.attributes();
const bke::AttributeAccessor src_to_attributes = src_to_curves.attributes();
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
for (const int i : ids.index_range()) {
eCustomDataType data_type;
const GVArray src_from_attribute = *src_from_attributes.lookup(ids[i], bke::AttrDomain::Point);
if (src_from_attribute) {
data_type = bke::cpp_type_to_custom_data_type(src_from_attribute.type());
const GVArray src_to_attribute = *src_to_attributes.lookup(
ids[i], bke::AttrDomain::Point, data_type);
result.src_from.append(src_from_attribute);
result.src_to.append(src_to_attribute ? src_to_attribute : GVArraySpan{});
}
else {
const GVArray src_to_attribute = *src_to_attributes.lookup(ids[i], bke::AttrDomain::Point);
/* Attribute should exist on at least one of the geometries. */
BLI_assert(src_to_attribute);
data_type = bke::cpp_type_to_custom_data_type(src_to_attribute.type());
result.src_from.append(GVArraySpan{});
result.src_to.append(src_to_attribute);
}
bke::GSpanAttributeWriter dst_attribute = dst_attributes.lookup_or_add_for_write_only_span(
ids[i], bke::AttrDomain::Point, data_type);
result.dst.append(std::move(dst_attribute));
}
return result;
}
/**
* Gather a set of all generic attribute IDs to copy to the result curves.
*/
static AttributesForInterpolation gather_point_attributes_to_interpolate(
const CurvesGeometry &from_curves, const CurvesGeometry &to_curves, CurvesGeometry &dst_curves)
{
VectorSet<bke::AttributeIDRef> ids;
auto add_attribute = [&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) {
if (meta_data.domain != bke::AttrDomain::Point) {
return true;
}
if (meta_data.data_type == CD_PROP_STRING) {
return true;
}
if (!interpolate_attribute_to_curves(id, dst_curves.curve_type_counts())) {
return true;
}
if (interpolate_attribute_to_poly_curve(id)) {
ids.add(id);
}
return true;
};
from_curves.attributes().for_all(add_attribute);
to_curves.attributes().for_all(add_attribute);
/* Position is handled differently since it has non-generic interpolation for Bezier
* curves and because the evaluated positions are cached for each evaluated point. */
ids.remove_contained("position");
return retrieve_attribute_spans(ids, from_curves, to_curves, dst_curves);
}
/* Resample a span of attribute values from source curves to a destination buffer. */
static void sample_curve_attribute(const bke::CurvesGeometry &src_curves,
const OffsetIndices<int> dst_points_by_curve,
const GSpan src_data,
const IndexMask &curve_selection,
const Span<int> sample_indices,
const Span<float> sample_factors,
GMutableSpan dst_data)
{
const CPPType &type = src_data.type();
BLI_assert(dst_data.type() == type);
const OffsetIndices<int> src_points_by_curve = src_curves.points_by_curve();
const OffsetIndices<int> src_evaluated_points_by_curve = src_curves.evaluated_points_by_curve();
const VArray<int8_t> curve_types = src_curves.curve_types();
#ifndef NDEBUG
const int dst_points_num = dst_data.size();
BLI_assert(sample_indices.size() == dst_points_num);
BLI_assert(sample_factors.size() == dst_points_num);
#endif
bke::attribute_math::convert_to_static_type(type, [&](auto dummy) {
using T = decltype(dummy);
Span<T> src = src_data.typed<T>();
MutableSpan<T> dst = dst_data.typed<T>();
Vector<T> evaluated_data;
curve_selection.foreach_index([&](const int i_curve) {
const IndexRange src_points = src_points_by_curve[i_curve];
const IndexRange dst_points = dst_points_by_curve[i_curve];
if (curve_types[i_curve] == CURVE_TYPE_POLY) {
length_parameterize::interpolate(src.slice(src_points),
sample_indices.slice(dst_points),
sample_factors.slice(dst_points),
dst.slice(dst_points));
}
else {
const IndexRange src_evaluated_points = src_evaluated_points_by_curve[i_curve];
evaluated_data.reinitialize(src_evaluated_points.size());
src_curves.interpolate_to_evaluated(
i_curve, src.slice(src_points), evaluated_data.as_mutable_span());
length_parameterize::interpolate(evaluated_data.as_span(),
sample_indices.slice(dst_points),
sample_factors.slice(dst_points),
dst.slice(dst_points));
}
});
});
}
template<typename T>
static void mix_arrays(const Span<T> from,
const Span<T> to,
const float mix_factor,
const MutableSpan<T> dst)
{
for (const int i : dst.index_range()) {
dst[i] = math::interpolate(from[i], to[i], mix_factor);
}
}
static void mix_arrays(const GSpan src_from,
const GSpan src_to,
const float mix_factor,
const IndexMask &group_selection,
const OffsetIndices<int> groups,
const GMutableSpan dst)
{
group_selection.foreach_index(GrainSize(32), [&](const int curve) {
const IndexRange range = groups[curve];
bke::attribute_math::convert_to_static_type(dst.type(), [&](auto dummy) {
using T = decltype(dummy);
const Span<T> from = src_from.typed<T>();
const Span<T> to = src_to.typed<T>();
const MutableSpan<T> dst_typed = dst.typed<T>();
mix_arrays(from.slice(range), to.slice(range), mix_factor, dst_typed.slice(range));
});
});
}
void interpolate_curves(const CurvesGeometry &from_curves,
const CurvesGeometry &to_curves,
const Span<int> from_curve_indices,
const Span<int> to_curve_indices,
const IndexMask &selection,
const Span<bool> curve_flip_direction,
const float mix_factor,
CurvesGeometry &dst_curves)
{
BLI_assert(from_curve_indices.size() == selection.size());
BLI_assert(to_curve_indices.size() == selection.size());
if (from_curves.curves_num() == 0 || to_curves.curves_num() == 0) {
return;
}
const VArray<bool> from_curves_cyclic = from_curves.cyclic();
const VArray<bool> to_curves_cyclic = to_curves.cyclic();
const Span<float3> from_evaluated_positions = from_curves.evaluated_positions();
const Span<float3> to_evaluated_positions = to_curves.evaluated_positions();
/* All resampled curves are poly curves. */
dst_curves.fill_curve_types(selection, CURVE_TYPE_POLY);
MutableSpan<float3> dst_positions = dst_curves.positions_for_write();
AttributesForInterpolation attributes = gather_point_attributes_to_interpolate(
from_curves, to_curves, dst_curves);
from_curves.ensure_evaluated_lengths();
to_curves.ensure_evaluated_lengths();
/* Sampling arbitrary attributes works by first interpolating them to the curve's standard
* "evaluated points" and then interpolating that result with the uniform samples. This is
* potentially wasteful when down-sampling a curve to many fewer points. There are two possible
* solutions: only sample the necessary points for interpolation, or first sample curve
* parameter/segment indices and evaluate the curve directly. */
Array<int> from_sample_indices(dst_curves.points_num());
Array<int> to_sample_indices(dst_curves.points_num());
Array<float> from_sample_factors(dst_curves.points_num());
Array<float> to_sample_factors(dst_curves.points_num());
const OffsetIndices dst_points_by_curve = dst_curves.points_by_curve();
/* Gather uniform samples based on the accumulated lengths of the original curve. */
selection.foreach_index(GrainSize(32), [&](const int i_dst_curve, const int pos) {
const int i_from_curve = from_curve_indices[pos];
const int i_to_curve = to_curve_indices[pos];
const IndexRange dst_points = dst_points_by_curve[i_dst_curve];
const Span<float> from_lengths = from_curves.evaluated_lengths_for_curve(
i_from_curve, from_curves_cyclic[i_from_curve]);
const Span<float> to_lengths = to_curves.evaluated_lengths_for_curve(
i_to_curve, to_curves_cyclic[i_to_curve]);
if (from_lengths.is_empty()) {
/* Handle curves with only one evaluated point. */
from_sample_indices.as_mutable_span().slice(dst_points).fill(0);
from_sample_factors.as_mutable_span().slice(dst_points).fill(0.0f);
}
else {
length_parameterize::sample_uniform(from_lengths,
!from_curves_cyclic[i_from_curve],
from_sample_indices.as_mutable_span().slice(dst_points),
from_sample_factors.as_mutable_span().slice(dst_points));
}
if (to_lengths.is_empty()) {
/* Handle curves with only one evaluated point. */
to_sample_indices.as_mutable_span().slice(dst_points).fill(0);
to_sample_factors.as_mutable_span().slice(dst_points).fill(0.0f);
}
else {
if (curve_flip_direction[i_dst_curve]) {
length_parameterize::sample_uniform_reverse(
to_lengths,
!to_curves_cyclic[i_to_curve],
to_sample_indices.as_mutable_span().slice(dst_points),
to_sample_factors.as_mutable_span().slice(dst_points));
}
else {
length_parameterize::sample_uniform(to_lengths,
!to_curves_cyclic[i_to_curve],
to_sample_indices.as_mutable_span().slice(dst_points),
to_sample_factors.as_mutable_span().slice(dst_points));
}
}
});
/* For every attribute, evaluate attributes from every curve in the range in the original
* curve's "evaluated points", then use linear interpolation to sample to the result. */
for (const int i_attribute : attributes.dst.index_range()) {
const GSpan src_from = attributes.src_from[i_attribute];
const GSpan src_to = attributes.src_to[i_attribute];
GMutableSpan dst = attributes.dst[i_attribute].span;
/* Mix factors depend on which of the from/to curves geometries has attribute data. If
* only one geometry has attribute data it gets the full mix weight. */
if (!src_from.is_empty() && !src_to.is_empty()) {
GArray<> from_samples(dst.type(), dst.size());
GArray<> to_samples(dst.type(), dst.size());
sample_curve_attribute(from_curves,
dst_points_by_curve,
src_from,
selection,
from_sample_indices,
from_sample_factors,
from_samples);
sample_curve_attribute(to_curves,
dst_points_by_curve,
src_to,
selection,
to_sample_indices,
to_sample_factors,
to_samples);
mix_arrays(from_samples, to_samples, mix_factor, selection, dst_points_by_curve, dst);
}
else if (!src_from.is_empty()) {
sample_curve_attribute(from_curves,
dst_points_by_curve,
src_from,
selection,
from_sample_indices,
from_sample_factors,
dst);
}
else if (!src_to.is_empty()) {
sample_curve_attribute(to_curves,
dst_points_by_curve,
src_to,
selection,
to_sample_indices,
to_sample_factors,
dst);
}
}
{
Array<float3> from_samples(dst_positions.size());
Array<float3> to_samples(dst_positions.size());
/* Interpolate the evaluated positions to the resampled curves. */
sample_curve_attribute(from_curves,
dst_points_by_curve,
from_evaluated_positions,
selection,
from_sample_indices,
from_sample_factors,
from_samples.as_mutable_span());
sample_curve_attribute(to_curves,
dst_points_by_curve,
to_evaluated_positions,
selection,
to_sample_indices,
to_sample_factors,
to_samples.as_mutable_span());
mix_arrays(from_samples.as_span(),
to_samples.as_span(),
mix_factor,
selection,
dst_points_by_curve,
dst_positions);
}
for (bke::GSpanAttributeWriter &attribute : attributes.dst) {
attribute.finish();
}
}
} // namespace blender::geometry