diff --git a/scripts/startup/bl_ui/space_graph.py b/scripts/startup/bl_ui/space_graph.py index 21c0c1c5e12..1334550b752 100644 --- a/scripts/startup/bl_ui/space_graph.py +++ b/scripts/startup/bl_ui/space_graph.py @@ -295,6 +295,7 @@ class GRAPH_MT_channel(Menu): layout.operator("graph.keys_to_samples") layout.operator("graph.samples_to_keys") layout.operator("graph.sound_to_samples") + layout.operator("anim.channels_bake") layout.separator() layout.operator("graph.euler_filter", text="Discontinuity (Euler) Filter") diff --git a/source/blender/blenkernel/BKE_fcurve.h b/source/blender/blenkernel/BKE_fcurve.h index 026680e6df1..ff4ab3a0d76 100644 --- a/source/blender/blenkernel/BKE_fcurve.h +++ b/source/blender/blenkernel/BKE_fcurve.h @@ -8,6 +8,7 @@ * \ingroup bke */ +#include "BLI_math_vector_types.hh" #include "DNA_curve_types.h" #ifdef __cplusplus @@ -483,11 +484,31 @@ bool BKE_fcurve_bezt_subdivide_handles(struct BezTriple *bezt, */ void BKE_fcurve_bezt_shrink(struct FCurve *fcu, int new_totvert); +/** + * Merge the two given BezTriple arrays `a` and `b` into a newly allocated BezTriple array of size + * `r_merged_size`. In case of keys on identical frames, `a` takes precedence. + * Does not free `a` or `b`. + * Assumes that both arrays are sorted for the x-position. + * Has a complexity of O(N) with respect to the length of `size_a` + `size_b`. + * + * \return The merged BezTriple array of length `r_merged_size`. + */ +BezTriple *BKE_bezier_array_merge( + const BezTriple *a, int size_a, const BezTriple *b, int size_b, int *r_merged_size); + /** * Delete a keyframe from an F-curve at a specific index. */ void BKE_fcurve_delete_key(struct FCurve *fcu, int index); +/** Delete an index range of keyframes from an F-curve. This is more performant than individually + * removing keys. + * Has a complexity of O(N) with respect to number of keys in `fcu`. + * + * \param index_range is right exclusive. + */ +void BKE_fcurve_delete_keys(FCurve *fcu, blender::uint2 index_range); + /** * Delete selected keyframes from an F-curve. */ diff --git a/source/blender/blenkernel/intern/fcurve.cc b/source/blender/blenkernel/intern/fcurve.cc index c61cb84715a..8a54ffaa298 100644 --- a/source/blender/blenkernel/intern/fcurve.cc +++ b/source/blender/blenkernel/intern/fcurve.cc @@ -22,6 +22,7 @@ #include "BLI_easing.h" #include "BLI_ghash.h" #include "BLI_math_vector.h" +#include "BLI_math_vector_types.hh" #include "BLI_sort_utils.h" #include "BLI_string_utils.hh" @@ -1705,6 +1706,81 @@ void BKE_fcurve_delete_key(FCurve *fcu, int index) } } +void BKE_fcurve_delete_keys(FCurve *fcu, blender::uint2 index_range) +{ + BLI_assert(fcu != nullptr); + BLI_assert(fcu->bezt != nullptr); + BLI_assert(index_range[1] > index_range[0]); + BLI_assert(index_range[1] <= fcu->totvert); + + const int removed_index_count = index_range[1] - index_range[0]; + memmove(&fcu->bezt[index_range[0]], + &fcu->bezt[index_range[1]], + sizeof(BezTriple) * (fcu->totvert - index_range[1])); + fcu->totvert -= removed_index_count; + + if (fcu->totvert == 0) { + fcurve_bezt_free(fcu); + } +} + +BezTriple *BKE_bezier_array_merge( + const BezTriple *a, const int size_a, const BezTriple *b, const int size_b, int *r_merged_size) +{ + BezTriple *large_array = static_cast( + MEM_callocN((size_a + size_b) * sizeof(BezTriple), "beztriple")); + + int iterator_a = 0; + int iterator_b = 0; + *r_merged_size = 0; + + /* For comparing if keyframes are at the same x-value. */ + const int max_ulps = 32; + + while (iterator_a < size_a || iterator_b < size_b) { + if (iterator_a >= size_a) { + const int remaining_keys = size_b - iterator_b; + memcpy(&large_array[*r_merged_size], &b[iterator_b], sizeof(BezTriple) * remaining_keys); + (*r_merged_size) += remaining_keys; + break; + } + if (iterator_b >= size_b) { + const int remaining_keys = size_a - iterator_a; + memcpy(&large_array[*r_merged_size], &a[iterator_a], sizeof(BezTriple) * remaining_keys); + (*r_merged_size) += remaining_keys; + break; + } + + if (compare_ff_relative( + a[iterator_a].vec[1][0], b[iterator_b].vec[1][0], BEZT_BINARYSEARCH_THRESH, max_ulps)) + { + memcpy(&large_array[*r_merged_size], &a[iterator_a], sizeof(BezTriple)); + iterator_a++; + iterator_b++; + } + else if (a[iterator_a].vec[1][0] < b[iterator_b].vec[1][0]) { + memcpy(&large_array[*r_merged_size], &a[iterator_a], sizeof(BezTriple)); + iterator_a++; + } + else { + memcpy(&large_array[*r_merged_size], &b[iterator_b], sizeof(BezTriple)); + iterator_b++; + } + (*r_merged_size)++; + } + + BezTriple *minimal_array; + if (*r_merged_size < size_a + size_b) { + minimal_array = static_cast( + MEM_reallocN(large_array, sizeof(BezTriple) * (*r_merged_size))); + } + else { + minimal_array = large_array; + } + + return minimal_array; +} + bool BKE_fcurve_delete_keys_selected(FCurve *fcu) { if (fcu->bezt == nullptr) { /* ignore baked curves */ diff --git a/source/blender/editors/animation/anim_channels_edit.cc b/source/blender/editors/animation/anim_channels_edit.cc index 808432ef985..e18e529802f 100644 --- a/source/blender/editors/animation/anim_channels_edit.cc +++ b/source/blender/editors/animation/anim_channels_edit.cc @@ -4310,6 +4310,189 @@ static void ANIM_OT_channel_view_pick(wmOperatorType *ot) "Ignore frames outside of the preview range"); } +static const EnumPropertyItem channel_bake_key_options[] = { + {BEZT_IPO_BEZ, "BEZIER", 0, "Bezier", "New keys will be beziers"}, + {BEZT_IPO_LIN, "LIN", 0, "Linear", "New keys will be linear"}, + {BEZT_IPO_CONST, "CONST", 0, "Constant", "New keys will be constant"}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +static const EnumPropertyItem channel_bake_remove_options[] = { + {int(BakeCurveRemove::REMOVE_NONE), "NONE", 0, "None", "Keep all keys"}, + {int(BakeCurveRemove::REMOVE_IN_RANGE), + "IN_RANGE", + 0, + "In Range", + "Remove all keys within the defined range"}, + {int(BakeCurveRemove::REMOVE_OUT_RANGE), + "OUT_RANGE", + 0, + "Outside Range", + "Remove all keys outside the defined range"}, + {int(BakeCurveRemove::REMOVE_ALL), "ALL", 0, "All", "Remove all existing keys"}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +static int channels_bake_exec(bContext *C, wmOperator *op) +{ + bAnimContext ac; + + /* Get editor data. */ + if (ANIM_animdata_get_context(C, &ac) == 0) { + return OPERATOR_CANCELLED; + } + + ListBase anim_data = {nullptr, nullptr}; + const int filter = (ANIMFILTER_SEL | ANIMFILTER_NODUPLIS | ANIMFILTER_DATA_VISIBLE | + ANIMFILTER_LIST_VISIBLE | ANIMFILTER_FCURVESONLY); + size_t anim_data_length = ANIM_animdata_filter( + &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype)); + + if (anim_data_length == 0) { + WM_report(RPT_WARNING, "No channels to operate on"); + return OPERATOR_CANCELLED; + } + + Scene *scene = CTX_data_scene(C); + + /* The range will default to the scene or preview range, but only if it hasn't been set before. + * If a range is set here, the redo panel wouldn't work properly because the range would + * constantly be overridden. */ + blender::int2 frame_range; + RNA_int_get_array(op->ptr, "range", frame_range); + if (frame_range[1] < frame_range[0]) { + frame_range[1] = frame_range[0]; + } + const float step = RNA_float_get(op->ptr, "step"); + if (frame_range[0] == 0 && frame_range[1] == 0) { + if (scene->r.flag & SCER_PRV_RANGE) { + frame_range = {scene->r.psfra, scene->r.pefra}; + } + else { + frame_range = {scene->r.sfra, scene->r.efra}; + } + RNA_int_set_array(op->ptr, "range", frame_range); + } + + const bool remove_outside_range = RNA_boolean_get(op->ptr, "remove_outside_range"); + const BakeCurveRemove remove_existing = remove_outside_range ? BakeCurveRemove::REMOVE_ALL : + BakeCurveRemove::REMOVE_IN_RANGE; + const int interpolation_type = RNA_enum_get(op->ptr, "interpolation_type"); + const bool bake_modifiers = RNA_boolean_get(op->ptr, "bake_modifiers"); + + LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) { + FCurve *fcu = static_cast(ale->data); + if (!fcu->bezt) { + continue; + } + AnimData *adt = ANIM_nla_mapping_get(&ac, ale); + blender::int2 nla_mapped_range; + nla_mapped_range[0] = int(BKE_nla_tweakedit_remap(adt, frame_range[0], NLATIME_CONVERT_UNMAP)); + nla_mapped_range[1] = int(BKE_nla_tweakedit_remap(adt, frame_range[1], NLATIME_CONVERT_UNMAP)); + /* Save current state of modifier flags so they can be reapplied after baking. */ + blender::Vector modifier_flags; + if (!bake_modifiers) { + LISTBASE_FOREACH (FModifier *, modifier, &fcu->modifiers) { + modifier_flags.append(modifier->flag); + modifier->flag |= FMODIFIER_FLAG_MUTED; + } + } + + bool replace; + const int last_index = BKE_fcurve_bezt_binarysearch_index( + fcu->bezt, nla_mapped_range[1], fcu->totvert, &replace); + + /* Since the interpolation of a key defines the curve following it, the last key in the baked + * segment needs to keep the interpolation mode that existed previously so the curve isn't + * changed. */ + const char segment_end_interpolation = fcu->bezt[min_ii(last_index, fcu->totvert - 1)].ipo; + + bake_fcurve(fcu, nla_mapped_range, step, remove_existing); + + if (bake_modifiers) { + free_fmodifiers(&fcu->modifiers); + } + else { + int modifier_index = 0; + LISTBASE_FOREACH (FModifier *, modifier, &fcu->modifiers) { + modifier->flag = modifier_flags[modifier_index]; + modifier_index++; + } + } + + for (int i = 0; i < fcu->totvert; i++) { + BezTriple *key = &fcu->bezt[i]; + if (key->vec[1][0] < nla_mapped_range[0]) { + continue; + } + if (key->vec[1][0] > nla_mapped_range[1]) { + fcu->bezt[max_ii(i - 1, 0)].ipo = segment_end_interpolation; + break; + } + key->ipo = interpolation_type; + } + } + + ANIM_animdata_freelist(&anim_data); + WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_SELECTED, nullptr); + + return OPERATOR_FINISHED; +} + +static void ANIM_OT_channels_bake(wmOperatorType *ot) +{ + /* Identifiers */ + ot->name = "Bake Channels"; + ot->idname = "ANIM_OT_channels_bake"; + ot->description = + "Create keyframes following the current shape of F-Curves of selected channels"; + + /* API callbacks */ + ot->exec = channels_bake_exec; + ot->poll = channel_view_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + RNA_def_int_array(ot->srna, + "range", + 2, + nullptr, + INT_MIN, + INT_MAX, + "Frame Range", + "The range in which to create new keys", + 0, + INT_MAX); + + RNA_def_float(ot->srna, + "step", + 1.0f, + 0.01f, + FLT_MAX, + "Frame Step", + "At which interval to add keys", + 1.0f, + 16.0f); + + RNA_def_boolean(ot->srna, + "remove_outside_range", + false, + "Remove Outside Range", + "Removes keys outside the given range, leaving only the newly baked"); + + RNA_def_enum(ot->srna, + "interpolation_type", + channel_bake_key_options, + BEZT_IPO_BEZ, + "Interpolation Type", + "Choose the interpolation type with which new keys will be added"); + + RNA_def_boolean(ot->srna, + "bake_modifiers", + true, + "Bake Modifiers", + "Bake Modifiers into keyframes and delete them after"); +} + /* Find a Graph Editor area and modify the given context to be the window region of it. */ static bool move_context_to_graph_editor(bContext *C) { @@ -4584,6 +4767,8 @@ void ED_operatortypes_animchannels() WM_operatortype_append(ANIM_OT_channels_group); WM_operatortype_append(ANIM_OT_channels_ungroup); + + WM_operatortype_append(ANIM_OT_channels_bake); } void ED_keymap_animchannels(wmKeyConfig *keyconf) diff --git a/source/blender/editors/animation/keyframes_general.cc b/source/blender/editors/animation/keyframes_general.cc index 21ea311e46f..7a36be279fd 100644 --- a/source/blender/editors/animation/keyframes_general.cc +++ b/source/blender/editors/animation/keyframes_general.cc @@ -15,6 +15,7 @@ #include "BLI_blenlib.h" #include "BLI_math_vector.h" +#include "BLI_math_vector_types.hh" #include "BLI_string_utils.hh" #include "BLI_utildefines.h" @@ -1211,7 +1212,7 @@ struct TempFrameValCache { void sample_fcurve_segment(FCurve *fcu, const float start_frame, - const int sample_rate, + const float sample_rate, float *samples, const int sample_count) { @@ -1221,6 +1222,104 @@ void sample_fcurve_segment(FCurve *fcu, } } +static void remove_fcurve_key_range(FCurve *fcu, + const blender::int2 range, + const BakeCurveRemove removal_mode) +{ + switch (removal_mode) { + + case BakeCurveRemove::REMOVE_ALL: { + BKE_fcurve_delete_keys_all(fcu); + break; + } + + case BakeCurveRemove::REMOVE_OUT_RANGE: { + bool replace; + + int before_index = BKE_fcurve_bezt_binarysearch_index( + fcu->bezt, range[0], fcu->totvert, &replace); + + if (before_index > 0) { + BKE_fcurve_delete_keys(fcu, {0, uint(before_index)}); + } + + int after_index = BKE_fcurve_bezt_binarysearch_index( + fcu->bezt, range[1], fcu->totvert, &replace); + /* REMOVE_OUT_RANGE is treated as exlusive on both ends. */ + if (replace) { + after_index++; + } + if (after_index < fcu->totvert) { + BKE_fcurve_delete_keys(fcu, {uint(after_index), fcu->totvert}); + } + break; + } + + case BakeCurveRemove::REMOVE_IN_RANGE: { + bool replace; + const int range_start_index = BKE_fcurve_bezt_binarysearch_index( + fcu->bezt, range[0], fcu->totvert, &replace); + int range_end_index = BKE_fcurve_bezt_binarysearch_index( + fcu->bezt, range[1], fcu->totvert, &replace); + if (replace) { + range_end_index++; + } + + if (range_end_index > range_start_index) { + BKE_fcurve_delete_keys(fcu, {uint(range_start_index), uint(range_end_index)}); + } + break; + } + + default: + break; + } +} + +void bake_fcurve(FCurve *fcu, + const blender::int2 range, + const float step, + const BakeCurveRemove remove_existing) +{ + using namespace blender::animrig; + BLI_assert(step > 0); + const int sample_count = (range[1] - range[0]) / step + 1; + float *samples = static_cast( + MEM_callocN(sample_count * sizeof(float), "Channel Bake Samples")); + const float sample_rate = 1.0f / step; + sample_fcurve_segment(fcu, range[0], sample_rate, samples, sample_count); + + if (remove_existing != BakeCurveRemove::REMOVE_NONE) { + remove_fcurve_key_range(fcu, range, remove_existing); + } + + BezTriple *baked_keys = static_cast( + MEM_callocN(sample_count * sizeof(BezTriple), "beztriple")); + + const KeyframeSettings settings = get_keyframe_settings(true); + + for (int i = 0; i < sample_count; i++) { + BezTriple *key = &baked_keys[i]; + blender::float2 key_position = {range[0] + i * step, samples[i]}; + initialize_bezt(key, key_position, settings, eFCurve_Flags(fcu->flag)); + } + + int merged_size; + BezTriple *merged_bezt = BKE_bezier_array_merge( + baked_keys, sample_count, fcu->bezt, fcu->totvert, &merged_size); + + if (fcu->bezt != nullptr) { + /* Can happen if we removed all keys beforehand. */ + MEM_freeN(fcu->bezt); + } + MEM_freeN(baked_keys); + fcu->bezt = merged_bezt; + fcu->totvert = merged_size; + + MEM_freeN(samples); + BKE_fcurve_handles_recalc(fcu); +} + void bake_fcurve_segments(FCurve *fcu) { using namespace blender::animrig; diff --git a/source/blender/editors/include/ED_keyframes_edit.hh b/source/blender/editors/include/ED_keyframes_edit.hh index e8c7289acb7..c623ae31cbf 100644 --- a/source/blender/editors/include/ED_keyframes_edit.hh +++ b/source/blender/editors/include/ED_keyframes_edit.hh @@ -8,6 +8,7 @@ #pragma once +#include "BLI_math_vector_types.hh" #include "ED_anim_api.hh" /* for enum eAnimFilter_Flags */ struct BezTriple; @@ -497,7 +498,14 @@ void bake_fcurve_segments(FCurve *fcu); * \param sample_rate: indicates how many samples per frame should be generated. */ void sample_fcurve_segment( - FCurve *fcu, float start_frame, int sample_rate, float *r_samples, int sample_count); + FCurve *fcu, float start_frame, float sample_rate, float *r_samples, int sample_count); + +enum class BakeCurveRemove { REMOVE_NONE, REMOVE_IN_RANGE, REMOVE_OUT_RANGE, REMOVE_ALL }; +/** Creates keyframes in the given range at the given step interval. + * \param range: start and end frame to bake. Is inclusive on both ends. + * \param remove_existing: choice which keys to remove in relation to the given range. + */ +void bake_fcurve(FCurve *fcu, blender::int2 range, float step, BakeCurveRemove remove_existing); /* ----------- */