Anim: add low-level function for simple FCurve key deduplication #107089

Manually merged
Sybren A. Stüvel merged 3 commits from dr.sybren/blender:anim/deduplicate-keys into main 2023-04-24 12:34:28 +02:00
7 changed files with 331 additions and 18 deletions

View File

@ -571,4 +571,8 @@ class KeyframesCo:
keyframe_points.foreach_set("co", co_buffer) keyframe_points.foreach_set("co", co_buffer)
keyframe_points.foreach_set("interpolation", ipo_buffer) keyframe_points.foreach_set("interpolation", ipo_buffer)
fcurve.update() # TODO: in Blender 4.0 the next lines can be replaced with one call to `fcurve.update()`.
# See https://projects.blender.org/blender/blender/issues/107126 for more info.
keyframe_points.sort()
keyframe_points.deduplicate()
keyframe_points.handles_recalc()

View File

@ -416,6 +416,7 @@ int BKE_fcurve_active_keyframe_index(const struct FCurve *fcu);
* Move the indexed keyframe to the given value, * Move the indexed keyframe to the given value,
* and move the handles with it to ensure the slope remains the same. * and move the handles with it to ensure the slope remains the same.
*/ */
void BKE_fcurve_keyframe_move_time_with_handles(BezTriple *keyframe, const float new_time);
void BKE_fcurve_keyframe_move_value_with_handles(struct BezTriple *keyframe, float new_value); void BKE_fcurve_keyframe_move_value_with_handles(struct BezTriple *keyframe, float new_value);
/* .............. */ /* .............. */
@ -472,6 +473,14 @@ bool BKE_fcurve_bezt_subdivide_handles(struct BezTriple *bezt,
struct BezTriple *next, struct BezTriple *next,
float *r_pdelta); float *r_pdelta);
/**
* Resize the FCurve 'bezt' array to fit the given length.
*
* \param new_totvert new number of elements in the FCurve's `bezt` array.
* Constraint: `0 <= new_totvert <= fcu->totvert`
*/
void BKE_fcurve_bezt_shrink(struct FCurve *fcu, int new_totvert);
/** /**
* Delete a keyframe from an F-curve at a specific index. * Delete a keyframe from an F-curve at a specific index.
*/ */
@ -499,6 +508,21 @@ void BKE_fcurve_merge_duplicate_keys(struct FCurve *fcu,
const int sel_flag, const int sel_flag,
const bool use_handle); const bool use_handle);
/**
* Ensure the FCurve is a proper function, such that every X-coordinate of the
* timeline has only one value of the FCurve. In other words, removes duplicate
* keyframes.
*
* Contrary to #BKE_fcurve_merge_duplicate_keys, which is intended for
* interactive use, and where selection matters, this is a simpler deduplication
* where the last duplicate "wins".
*
* Assumes the keys are sorted (see #sort_time_fcurve).
*
* After deduplication, call `BKE_fcurve_handles_recalc(fcu);`
*/
void BKE_fcurve_deduplicate_keys(struct FCurve *fcu);
/* -------- Curve Sanity -------- */ /* -------- Curve Sanity -------- */
/** /**

View File

@ -883,6 +883,14 @@ int BKE_fcurve_active_keyframe_index(const FCurve *fcu)
/** \} */ /** \} */
void BKE_fcurve_keyframe_move_time_with_handles(BezTriple *keyframe, const float new_time)
{
const float time_delta = new_time - keyframe->vec[1][0];
keyframe->vec[0][0] += time_delta;
keyframe->vec[1][0] = new_time;
keyframe->vec[2][0] += time_delta;
}
void BKE_fcurve_keyframe_move_value_with_handles(struct BezTriple *keyframe, const float new_value) void BKE_fcurve_keyframe_move_value_with_handles(struct BezTriple *keyframe, const float new_value)
{ {
const float value_delta = new_value - keyframe->vec[1][1]; const float value_delta = new_value - keyframe->vec[1][1];
@ -1659,6 +1667,24 @@ bool BKE_fcurve_bezt_subdivide_handles(struct BezTriple *bezt,
return true; return true;
} }
void BKE_fcurve_bezt_shrink(struct FCurve *fcu, const int new_totvert)
{
BLI_assert(new_totvert >= 0);
BLI_assert(new_totvert <= fcu->totvert);
/* No early return when new_totvert == fcu->totvert. There is no way to know the intention of the
* caller, nor the history of the FCurve so far, so `fcu->bezt` may actually have allocated space
* for more than `fcu->totvert` keys. */
if (new_totvert == 0) {
fcurve_bezt_free(fcu);
return;
}
fcu->bezt = MEM_reallocN(fcu->bezt, new_totvert * sizeof(*(fcu->bezt)));
fcu->totvert = new_totvert;
}
void BKE_fcurve_delete_key(FCurve *fcu, int index) void BKE_fcurve_delete_key(FCurve *fcu, int index)
{ {
/* sanity check */ /* sanity check */
@ -1849,6 +1875,52 @@ void BKE_fcurve_merge_duplicate_keys(FCurve *fcu, const int sel_flag, const bool
BLI_freelistN(&retained_keys); BLI_freelistN(&retained_keys);
} }
void BKE_fcurve_deduplicate_keys(FCurve *fcu)
{
BLI_assert_msg(fcu->bezt, "this function only works with regular (non-sampled) FCurves");
if (fcu->totvert < 2 || fcu->bezt == NULL) {
return;
}
int prev_bezt_index = 0;
for (int i = 1; i < fcu->totvert; i++) {
BezTriple *bezt = &fcu->bezt[i];
BezTriple *prev_bezt = &fcu->bezt[prev_bezt_index];
const float bezt_x = bezt->vec[1][0];
const float prev_x = prev_bezt->vec[1][0];
if (bezt_x - prev_x <= BEZT_BINARYSEARCH_THRESH) {
/* Replace 'prev_bezt', as it has the same X-coord as 'bezt' and the last one wins. */
*prev_bezt = *bezt;
if (floor(bezt_x) == bezt_x) {
/* Keep the 'bezt_x' coordinate, as being on a frame is more desirable
* than being ever so slightly off. */
}
else {
/* Move the retained key to the old X-coordinate, to 'anchor' the X-coordinate used for
* subsequente comparisons. Without this, the reference X-coordinate would keep moving
* forward in time, potentially merging in more keys than desired. */
BKE_fcurve_keyframe_move_time_with_handles(prev_bezt, prev_x);
}
continue;
}
/* Next iteration should look at the current element. However, because of the deletions, that
* may not be at index 'i'; after this increment, `prev_bezt_index` points at where the current
* element should go. */
prev_bezt_index++;
if (prev_bezt_index != i) {
/* This bezt should be kept, so copy it to its new location in the array. */
fcu->bezt[prev_bezt_index] = *bezt;
}
}
BKE_fcurve_bezt_shrink(fcu, prev_bezt_index + 1);
}
/** \} */ /** \} */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */

View File

@ -10,6 +10,8 @@
#include "DNA_anim_types.h" #include "DNA_anim_types.h"
#include "BLI_math_vector_types.hh"
namespace blender::bke::tests { namespace blender::bke::tests {
/* Epsilon for floating point comparisons. */ /* Epsilon for floating point comparisons. */
@ -346,6 +348,37 @@ TEST(BKE_fcurve, BKE_fcurve_keyframe_move_value_with_handles)
BKE_fcurve_free(fcu); BKE_fcurve_free(fcu);
} }
TEST(BKE_fcurve, BKE_fcurve_keyframe_move_time_with_handles)
{
FCurve *fcu = BKE_fcurve_create();
insert_vert_fcurve(fcu, 1.0f, 7.5f, BEZT_KEYTYPE_KEYFRAME, INSERTKEY_NO_USERPREF);
insert_vert_fcurve(fcu, 8.0f, 15.0f, BEZT_KEYTYPE_KEYFRAME, INSERTKEY_NO_USERPREF);
insert_vert_fcurve(fcu, 14.0f, 8.2f, BEZT_KEYTYPE_KEYFRAME, INSERTKEY_NO_USERPREF);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[0][0], 5.2671194f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[0][1], 15.0f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[1][0], 8.0f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[1][1], 15.0f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[2][0], 10.342469f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[2][1], 15.0f);
BKE_fcurve_keyframe_move_time_with_handles(&fcu->bezt[1], 47.0f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[0][0], 44.2671194f) << "Left handle time should be updated";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[0][1], 15.0f) << "Left handle should not move in value";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[1][0], 47.0f) << "Frame time should have been updated";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[1][1], 15.0f) << "Frame should not move in value";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[2][0], 49.342469f) << "Right handle time should be updated";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[2][1], 15.0f) << "Right handle should not move in value";
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_calc_range) TEST(BKE_fcurve, BKE_fcurve_calc_range)
{ {
FCurve *fcu = BKE_fcurve_create(); FCurve *fcu = BKE_fcurve_create();
@ -546,4 +579,127 @@ TEST(BKE_fcurve, BKE_fcurve_calc_bounds)
BKE_fcurve_free(fcu); BKE_fcurve_free(fcu);
} }
static void set_key(FCurve *fcu, const int index, const float x, const float y)
{
fcu->bezt[index].vec[0][0] = x - 0.5f;
fcu->bezt[index].vec[1][0] = x;
fcu->bezt[index].vec[2][0] = x + 0.5f;
fcu->bezt[index].vec[0][1] = y;
fcu->bezt[index].vec[1][1] = y;
fcu->bezt[index].vec[2][1] = y;
}
static FCurve *testcurve_with_duplicates()
{
/* Create a curve with some duplicate keys. The first ones are all with Y=1, the later repeats
* increase Y-coordinates on every repeat. */
FCurve *fcu = BKE_fcurve_create();
ED_keyframes_add(fcu, 10); /* Avoid `insert_vert_fcurve`, that deduplicates the keys. */
set_key(fcu, 0, 1.0f, 1.0f);
set_key(fcu, 1, 327.16f, 1.0f);
set_key(fcu, 2, 7.0f, 1.0f);
set_key(fcu, 3, 47.0f, 1.0f);
set_key(fcu, 4, 7.0f, 2.0f);
set_key(fcu, 5, 47.0f, 2.0f);
set_key(fcu, 6, 47.0f + BEZT_BINARYSEARCH_THRESH, 3.0f);
set_key(fcu, 7, 7.0f, 3.0f);
set_key(fcu, 8, 3.0f, 1.0f);
set_key(fcu, 9, 2.0f, 1.0f);
return fcu;
}
TEST(BKE_fcurve, sort_time_fcurve_stability)
{
FCurve *fcu = testcurve_with_duplicates();
ASSERT_EQ(fcu->totvert, 10);
sort_time_fcurve(fcu);
/* The sorting should be stable, i.e. retain the original order when the
* X-coordinates are identical. */
ASSERT_EQ(fcu->totvert, 10) << "sorting should not influence number of keys";
EXPECT_V2_NEAR(fcu->bezt[0].vec[1], float2(1.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[1].vec[1], float2(2.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[2].vec[1], float2(3.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[3].vec[1], float2(7.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[4].vec[1], float2(7.0f, 2.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[5].vec[1], float2(7.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[6].vec[1], float2(47.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[7].vec[1], float2(47.0f, 2.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[8].vec[1], float2(47.0f + BEZT_BINARYSEARCH_THRESH, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[9].vec[1], float2(327.16f, 1.0f), 1e-3);
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_deduplicate_keys)
{
FCurve *fcu = testcurve_with_duplicates();
ASSERT_EQ(fcu->totvert, 10);
sort_time_fcurve(fcu);
BKE_fcurve_deduplicate_keys(fcu);
ASSERT_GE(fcu->totvert, 6); /* Protect against out-of-bounds access. */
EXPECT_EQ(fcu->totvert, 6); /* The actual expected value. */
EXPECT_V2_NEAR(fcu->bezt[0].vec[1], float2(1.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[1].vec[1], float2(2.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[2].vec[1], float2(3.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[3].vec[1], float2(7.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[4].vec[1], float2(47.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[5].vec[1], float2(327.16f, 1.0f), 1e-3);
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_deduplicate_keys_edge_cases)
{
FCurve *fcu = testcurve_with_duplicates();
ASSERT_EQ(fcu->totvert, 10);
/* Update the 2nd and 2nd-to-last keys to test the edge cases. */
set_key(fcu, 0, 1, 1);
set_key(fcu, 1, 1, 2);
set_key(fcu, 8, 327.16f, 1);
set_key(fcu, 9, 327.16f, 2);
sort_time_fcurve(fcu);
BKE_fcurve_deduplicate_keys(fcu);
ASSERT_EQ(fcu->totvert, 4);
EXPECT_V2_NEAR(fcu->bezt[0].vec[1], float2(1.0f, 2.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[1].vec[1], float2(7.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[2].vec[1], float2(47.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[3].vec[1], float2(327.16f, 2.0f), 1e-3);
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_deduplicate_keys_prefer_whole_frames)
{
FCurve *fcu = testcurve_with_duplicates();
ASSERT_EQ(fcu->totvert, 10);
/* Update the first key around 47.0 to be slightly before the frame. This gives us three keys on
* 47-epsilon, 47, and 47+epsilon. The keys at index 5 and 6 already have this value, so the
* `set_key` calls are unnecessary, but this way this test has a more local overview of the
* situation under test. */
set_key(fcu, 3, 47.0f - BEZT_BINARYSEARCH_THRESH, 1.0f);
set_key(fcu, 5, 47.0f, 2.0f);
set_key(fcu, 6, 47.0f + BEZT_BINARYSEARCH_THRESH, 3.0f);
sort_time_fcurve(fcu);
BKE_fcurve_deduplicate_keys(fcu);
ASSERT_EQ(fcu->totvert, 6);
EXPECT_V2_NEAR(fcu->bezt[0].vec[1], float2(1.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[1].vec[1], float2(2.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[2].vec[1], float2(3.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[3].vec[1], float2(7.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[4].vec[1], float2(47.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[5].vec[1], float2(327.16f, 1.0f), 1e-3);
BKE_fcurve_free(fcu);
}
} // namespace blender::bke::tests } // namespace blender::bke::tests

View File

@ -1659,6 +1659,29 @@ int insert_keyframe(Main *bmain,
return ret; return ret;
} }
void ED_keyframes_add(FCurve *fcu, int num_keys_to_add)
{
BLI_assert_msg(num_keys_to_add >= 0, "cannot remove keyframes with this function");
if (num_keys_to_add == 0) {
return;
}
fcu->bezt = MEM_recallocN(fcu->bezt, sizeof(BezTriple) * (fcu->totvert + num_keys_to_add));
BezTriple *bezt = fcu->bezt + fcu->totvert; /* Pointer to the first new one. '*/
fcu->totvert += num_keys_to_add;
/* Iterate over the new keys to update their settings. */
while (num_keys_to_add--) {
/* Defaults, ignoring user-preference gives predictable results for API. */
bezt->f1 = bezt->f2 = bezt->f3 = SELECT;
bezt->ipo = BEZT_IPO_BEZ;
bezt->h1 = bezt->h2 = HD_AUTO_ANIM;
bezt++;
}
}
/* ************************************************** */ /* ************************************************** */
/* KEYFRAME DELETION */ /* KEYFRAME DELETION */

View File

@ -123,6 +123,17 @@ int insert_vert_fcurve(struct FCurve *fcu,
eBezTriple_KeyframeType keyframe_type, eBezTriple_KeyframeType keyframe_type,
eInsertKeyFlags flag); eInsertKeyFlags flag);
/**
* Add the given number of keyframes to the FCurve. Their coordinates are
* uninitialized, so the curve should not be used without further attention.
*
* The newly created keys are selected, existing keys are not touched.
*
* This can be used to allocate all the keys at once, and then update them
* afterwards.
*/
void ED_keyframes_add(struct FCurve *fcu, int num_keys_to_add);
/* -------- */ /* -------- */
/** /**

View File

@ -692,6 +692,7 @@ static void rna_tag_animation_update(Main *bmain, ID *id)
static void rna_FCurve_update_data_ex(ID *id, FCurve *fcu, Main *bmain) static void rna_FCurve_update_data_ex(ID *id, FCurve *fcu, Main *bmain)
{ {
sort_time_fcurve(fcu); sort_time_fcurve(fcu);
/* TODO: Blender 4.0 should call BKE_fcurve_deduplicate_keys(fcu) here. */
BKE_fcurve_handles_recalc(fcu); BKE_fcurve_handles_recalc(fcu);
rna_tag_animation_update(bmain, id); rna_tag_animation_update(bmain, id);
@ -1071,24 +1072,12 @@ static BezTriple *rna_FKeyframe_points_insert(
static void rna_FKeyframe_points_add(ID *id, FCurve *fcu, Main *bmain, int tot) static void rna_FKeyframe_points_add(ID *id, FCurve *fcu, Main *bmain, int tot)
{ {
if (tot > 0) { if (tot <= 0) {
BezTriple *bezt; return;
fcu->bezt = MEM_recallocN(fcu->bezt, sizeof(BezTriple) * (fcu->totvert + tot));
bezt = fcu->bezt + fcu->totvert;
fcu->totvert += tot;
while (tot--) {
/* Defaults, ignoring user-preference gives predictable results for API. */
bezt->f1 = bezt->f2 = bezt->f3 = SELECT;
bezt->ipo = BEZT_IPO_BEZ;
bezt->h1 = bezt->h2 = HD_AUTO_ANIM;
bezt++;
}
rna_tag_animation_update(bmain, id);
} }
ED_keyframes_add(fcu, tot);
rna_tag_animation_update(bmain, id);
} }
static void rna_FKeyframe_points_remove( static void rna_FKeyframe_points_remove(
@ -1118,6 +1107,24 @@ static void rna_FKeyframe_points_clear(ID *id, FCurve *fcu, Main *bmain)
rna_tag_animation_update(bmain, id); rna_tag_animation_update(bmain, id);
} }
static void rna_FKeyframe_points_sort(ID *id, FCurve *fcu, Main *bmain)
{
sort_time_fcurve(fcu);
rna_tag_animation_update(bmain, id);
}
static void rna_FKeyframe_points_deduplicate(ID *id, FCurve *fcu, Main *bmain)
{
BKE_fcurve_deduplicate_keys(fcu);
rna_tag_animation_update(bmain, id);
}
static void rna_FKeyframe_points_handles_recalc(ID *id, FCurve *fcu, Main *bmain)
{
BKE_fcurve_handles_recalc(fcu);
rna_tag_animation_update(bmain, id);
}
static FCM_EnvelopeData *rna_FModifierEnvelope_points_add( static FCM_EnvelopeData *rna_FModifierEnvelope_points_add(
ID *id, FModifier *fmod, Main *bmain, ReportList *reports, float frame) ID *id, FModifier *fmod, Main *bmain, ReportList *reports, float frame)
{ {
@ -2414,6 +2421,22 @@ static void rna_def_fcurve_keyframe_points(BlenderRNA *brna, PropertyRNA *cprop)
func = RNA_def_function(srna, "clear", "rna_FKeyframe_points_clear"); func = RNA_def_function(srna, "clear", "rna_FKeyframe_points_clear");
RNA_def_function_ui_description(func, "Remove all keyframes from an F-Curve"); RNA_def_function_ui_description(func, "Remove all keyframes from an F-Curve");
RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN); RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN);
func = RNA_def_function(srna, "sort", "rna_FKeyframe_points_sort");
RNA_def_function_ui_description(func, "Ensure all keyframe points are chronologically sorted");
RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN);
func = RNA_def_function(srna, "deduplicate", "rna_FKeyframe_points_deduplicate");
RNA_def_function_ui_description(
func,
"Ensure there are no duplicate keys. Assumes that the points have already been sorted");
RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN);
func = RNA_def_function(srna, "handles_recalc", "rna_FKeyframe_points_handles_recalc");
RNA_def_function_ui_description(func,
"Update handles after modifications to the keyframe points, to "
"update things like auto-clamping");
RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN);
} }
static void rna_def_fcurve(BlenderRNA *brna) static void rna_def_fcurve(BlenderRNA *brna)