Animation: time offset slider #106520
|
@ -330,6 +330,7 @@ class GRAPH_MT_slider(Menu):
|
||||||
layout.operator("graph.blend_to_neighbor", text="Blend to Neighbor")
|
layout.operator("graph.blend_to_neighbor", text="Blend to Neighbor")
|
||||||
layout.operator("graph.blend_to_default", text="Blend to Default Value")
|
layout.operator("graph.blend_to_default", text="Blend to Default Value")
|
||||||
layout.operator("graph.ease", text="Ease")
|
layout.operator("graph.ease", text="Ease")
|
||||||
|
layout.operator("graph.time_offset", text="Time Offset")
|
||||||
layout.operator("graph.gaussian_smooth", text="Smooth")
|
layout.operator("graph.gaussian_smooth", text="Smooth")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -491,6 +491,50 @@ void ease_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor
|
||||||
|
|
||||||
/* ---------------- */
|
/* ---------------- */
|
||||||
|
|
||||||
|
void time_offset_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor)
|
||||||
|
|||||||
|
{
|
||||||
|
/* Two bookend keys of the fcurve are needed to be able to cycle the values. */
|
||||||
|
const BezTriple *last_key = &fcu->bezt[fcu->totvert - 1];
|
||||||
|
const BezTriple *first_key = &fcu->bezt[0];
|
||||||
|
|
||||||
|
const int fcu_x_range = last_key->vec[1][0] - first_key->vec[1][0];
|
||||||
Sybren A. Stüvel
commented
The X-coordinate of keys is also a The X-coordinate of keys is also a `float`, as keys can be placed on sub-frames as well (to control things like motion blur).
|
|||||||
|
const float fcu_y_range = last_key->vec[1][1] - first_key->vec[1][1];
|
||||||
|
|
||||||
|
const float first_key_x = first_key->vec[1][0];
|
||||||
|
|
||||||
|
/* If we operate directly on the fcurve there will be a feedback loop
|
||||||
|
* so we need to capture the "y" values on an array to then apply them on a second loop. */
|
||||||
|
float *y_values = MEM_callocN(sizeof(float) * segment->length, "Time Offset Samples");
|
||||||
|
|
||||||
this doesn't compile for me. C doesn't allow variable length arrays. you'd need to do this doesn't compile for me. C doesn't allow variable length arrays.
you'd need to do `MEM_callocN(sizeof(float) * segment->length, "Time Offset Samples");`
and at the end of the function `MEM_freeN(y_values);` so to not have a memory leak
AresDeveaux
commented
Why can I compile it? do I need to set something I haven't for When you say C doesn't allow variable length array, I'm not sure I follow. I thought by declaring an array with a fixed value, C could handle it. Isn't
Didn't know we could overshoot. Where is that set in the code, and how can I test it on the UI? In regard to the memory leak, should I be adding those types of lines to the other sliders too? Why can I compile it? do I need to set something I haven't for `make`?
When you say C doesn't allow variable length array, I'm not sure I follow. I thought by declaring an array with a fixed value, C could handle it. Isn't `segment->length` an unchanged value for that segment?
```
Also the slider currently allows an overshoot in the positive axis, but doesn't allow to go below 0.
When overshooting on positive x it doesn't loop infinitely as you'd expect
```
Didn't know we could overshoot. Where is that set in the code, and how can I test it on the UI?
In regard to the memory leak, should I be adding those types of lines to the other sliders too?
variable length arrays are a C99 feature, but I think Blender runs on a different standard. But honestly I can't find in the wiki where that is. A lot of the slider features are controller by the slider itself. You can see the shortcuts in the Status Bar variable length arrays are a C99 feature, but I think Blender runs on a different standard. But honestly I can't find in the wiki where that is.
A lot of the slider features are controller by the slider itself. You can see the shortcuts in the Status Bar
`ED_slider_allow_overshoot_set` allows you to change the behaviour
AresDeveaux
commented
Interesting! I didn't see those before. I tried overshoot in Interesting! I didn't see those before.
I tried overshoot in `time offset slider` and as you say, to the right it works as expected, but to the left stops at 0. I then tried the `ease slider`, the `neighbor slider` and the `default slider`, and all of them stop at 0. I could try to find the problem, but it seems the issue is not in my function but in the slider itself and that might be beyond my understanding at this point.
|
|||||||
|
for (int i = segment->start_index; i < segment->start_index + segment->length; i++) {
|
||||||
|
|
||||||
|
/* This simulates the fcu curve moving in time. */
|
||||||
|
const float time = fcu->bezt[i].vec[1][0] + fcu_x_range * factor;
|
||||||
|
/* Need to normalize time to first_key to specify that as the wrapping point. */
|
||||||
|
float wrapped_time = fmod(time - first_key_x, fcu_x_range) + first_key_x;
|
||||||
|
float delta_y = fcu_y_range * (int)((time - first_key_x) / fcu_x_range);
|
||||||
|
if (time - first_key_x < 0) {
|
||||||
|
/* Offset negative values since the modulo at fmod(-0.1, 1) would produce -0.1 instead of
|
||||||
|
* 0.9, as we'd need it to. */
|
||||||
|
wrapped_time += fcu_x_range;
|
||||||
Sybren A. Stüvel
commented
(please read on to the end of the comment, I'm asking less of you than apparent at first sight) This code (adjusting the behaviour of Another way of making the same point: 'computing the modulo in an always-positive manner' is not the same job as as 'offsetting an fcurve segment', and thus it belongs in a different function. I think it would be best to add a function declaration to
The implementation can then be added to
Note that I've also used TL;DR: And yeah, then I figured that adding such a function would also require writing a unit test for it, and that's getting too much for this PR. So I just went ahead and implemented that. It's now in the _(please read on to the end of the comment, I'm asking less of you than apparent at first sight)_
This code (adjusting the behaviour of `fmod` to match what you need) shouldn't be here. The fact that you need to write it means that you need another modulo function that behaves correctly.
Another way of making the same point: 'computing the modulo in an always-positive manner' is not the same job as as 'offsetting an fcurve segment', and thus it belongs in a different function.
I think it would be best to add a function declaration to `source/blender/blenlib/BLI_math_base.h`, underneath `mod_i()`:
```c
MINLINE float mod_f_positive(float f, float n);
```
The implementation can then be added to `source/blender/blenlib/intern/math_base_inline.c`, again underneath `mod_i()`. Don't forget to make the parameters `const` in the function implementation (the surrounding code is old, and doesn't follow this new-ish style rule yet). Something like this likely works:
```c
MINLINE float mod_f_positive(const float f, const float n)
{
const float modulo = fmodf(f, n);
if (modulo < 0) {
return modulo + n;
}
return modulo;
}
```
Note that I've also used `fmodf()` here instead of `fmod()`. The latter works fine, but converts the `float` paramters to `double`, computes the `double` result, which is then put back into a `float` again. `fmodf()` just operates on `float` directly.
**TL;DR:** And yeah, then I figured that adding such a function would also require writing a unit test for it, and that's getting too much for this PR. So I just went ahead and implemented that. It's now in the `main` branch and you can replace the call to `fmod()` with `mod_f_positive()`.
|
|||||||
|
/* Similarly, (int)-0.1 produces 0 while we need it to be -1. */
|
||||||
Sybren A. Stüvel
commented
Use Use `floor(x)` if you want to always round down (instead of to zero). That way you don't have to correct the result of the computation.
|
|||||||
|
delta_y -= fcu_y_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float key_y_value = evaluate_fcurve(fcu, wrapped_time) + delta_y;
|
||||||
|
const int index_from_zero = i - segment->start_index;
|
||||||
|
y_values[index_from_zero] = key_y_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = segment->start_index; i < segment->start_index + segment->length; i++) {
|
||||||
|
const int index_from_zero = i - segment->start_index;
|
||||||
|
move_key(&fcu->bezt[i], y_values[index_from_zero]);
|
||||||
|
}
|
||||||
|
MEM_freeN(y_values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- */
|
||||||
|
|
||||||
void breakdown_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor)
|
void breakdown_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor)
|
||||||
{
|
{
|
||||||
const BezTriple *left_bezt = fcurve_segment_start_get(fcu, segment->start_index);
|
const BezTriple *left_bezt = fcurve_segment_start_get(fcu, segment->start_index);
|
||||||
|
|
|
@ -437,6 +437,7 @@ void smooth_fcurve_segment(struct FCurve *fcu,
|
||||||
int kernel_size,
|
int kernel_size,
|
||||||
double *kernel);
|
double *kernel);
|
||||||
void ease_fcurve_segment(struct FCurve *fcu, struct FCurveSegment *segment, float factor);
|
void ease_fcurve_segment(struct FCurve *fcu, struct FCurveSegment *segment, float factor);
|
||||||
|
void time_offset_fcurve_segment(struct FCurve *fcu, struct FCurveSegment *segment, float factor);
|
||||||
bool decimate_fcurve(struct bAnimListElem *ale, float remove_ratio, float error_sq_max);
|
bool decimate_fcurve(struct bAnimListElem *ale, float remove_ratio, float error_sq_max);
|
||||||
void blend_to_default_fcurve(struct PointerRNA *id_ptr, struct FCurve *fcu, float factor);
|
void blend_to_default_fcurve(struct PointerRNA *id_ptr, struct FCurve *fcu, float factor);
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -114,6 +114,7 @@ void GRAPH_OT_clean(struct wmOperatorType *ot);
|
||||||
void GRAPH_OT_blend_to_neighbor(struct wmOperatorType *ot);
|
void GRAPH_OT_blend_to_neighbor(struct wmOperatorType *ot);
|
||||||
void GRAPH_OT_breakdown(struct wmOperatorType *ot);
|
void GRAPH_OT_breakdown(struct wmOperatorType *ot);
|
||||||
void GRAPH_OT_ease(struct wmOperatorType *ot);
|
void GRAPH_OT_ease(struct wmOperatorType *ot);
|
||||||
|
void GRAPH_OT_time_offset(struct wmOperatorType *ot);
|
||||||
void GRAPH_OT_decimate(struct wmOperatorType *ot);
|
void GRAPH_OT_decimate(struct wmOperatorType *ot);
|
||||||
void GRAPH_OT_blend_to_default(struct wmOperatorType *ot);
|
void GRAPH_OT_blend_to_default(struct wmOperatorType *ot);
|
||||||
void GRAPH_OT_gaussian_smooth(struct wmOperatorType *ot);
|
void GRAPH_OT_gaussian_smooth(struct wmOperatorType *ot);
|
||||||
|
|
|
@ -463,6 +463,7 @@ void graphedit_operatortypes(void)
|
||||||
WM_operatortype_append(GRAPH_OT_blend_to_neighbor);
|
WM_operatortype_append(GRAPH_OT_blend_to_neighbor);
|
||||||
WM_operatortype_append(GRAPH_OT_breakdown);
|
WM_operatortype_append(GRAPH_OT_breakdown);
|
||||||
WM_operatortype_append(GRAPH_OT_ease);
|
WM_operatortype_append(GRAPH_OT_ease);
|
||||||
|
WM_operatortype_append(GRAPH_OT_time_offset);
|
||||||
WM_operatortype_append(GRAPH_OT_blend_to_default);
|
WM_operatortype_append(GRAPH_OT_blend_to_default);
|
||||||
WM_operatortype_append(GRAPH_OT_gaussian_smooth);
|
WM_operatortype_append(GRAPH_OT_gaussian_smooth);
|
||||||
WM_operatortype_append(GRAPH_OT_euler_filter);
|
WM_operatortype_append(GRAPH_OT_euler_filter);
|
||||||
|
|
|
@ -1060,6 +1060,132 @@ void GRAPH_OT_ease(wmOperatorType *ot)
|
||||||
1.0f);
|
1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/** \name Time Offset
|
||||||
|
* \{ */
|
||||||
|
|
||||||
|
static void time_offset_graph_keys(bAnimContext *ac, const float factor)
|
||||||
|
{
|
||||||
|
ListBase anim_data = {NULL, NULL};
|
||||||
|
|
||||||
|
ANIM_animdata_filter(ac, &anim_data, OPERATOR_DATA_FILTER, ac->data, ac->datatype);
|
||||||
|
LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) {
|
||||||
|
FCurve *fcu = (FCurve *)ale->key_data;
|
||||||
|
ListBase segments = find_fcurve_segments(fcu);
|
||||||
|
|
||||||
|
LISTBASE_FOREACH (FCurveSegment *, segment, &segments) {
|
||||||
|
time_offset_fcurve_segment(fcu, segment, factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
ale->update |= ANIM_UPDATE_DEFAULT;
|
||||||
|
BLI_freelistN(&segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
ANIM_animdata_update(ac, &anim_data);
|
||||||
|
ANIM_animdata_freelist(&anim_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void time_offset_draw_status_header(bContext *C, tGraphSliderOp *gso)
|
||||||
|
{
|
||||||
|
char status_str[UI_MAX_DRAW_STR];
|
||||||
|
char mode_str[32];
|
||||||
|
char slider_string[UI_MAX_DRAW_STR];
|
||||||
|
|
||||||
|
ED_slider_status_string_get(gso->slider, slider_string, UI_MAX_DRAW_STR);
|
||||||
|
|
||||||
|
strcpy(mode_str, TIP_("Time Offset Keys"));
|
||||||
|
|
||||||
|
if (hasNumInput(&gso->num)) {
|
||||||
|
char str_ofs[NUM_STR_REP_LEN];
|
||||||
|
|
||||||
|
outputNumInput(&gso->num, str_ofs, &gso->scene->unit);
|
||||||
|
|
||||||
|
BLI_snprintf(status_str, sizeof(status_str), "%s: %s", mode_str, str_ofs);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BLI_snprintf(status_str, sizeof(status_str), "%s: %s", mode_str, slider_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
ED_workspace_status_text(C, status_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void time_offset_modal_update(bContext *C, wmOperator *op)
|
||||||
|
{
|
||||||
|
tGraphSliderOp *gso = op->customdata;
|
||||||
|
|
||||||
|
time_offset_draw_status_header(C, gso);
|
||||||
|
|
||||||
|
/* Reset keyframes to the state at invoke. */
|
||||||
|
reset_bezts(gso);
|
||||||
|
const float factor = slider_factor_get_and_remember(op);
|
||||||
|
time_offset_graph_keys(&gso->ac, factor);
|
||||||
|
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_EDITED, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int time_offset_invoke(bContext *C, wmOperator *op, const wmEvent *event)
|
||||||
|
{
|
||||||
|
const int invoke_result = graph_slider_invoke(C, op, event);
|
||||||
|
|
||||||
|
if (invoke_result == OPERATOR_CANCELLED) {
|
||||||
|
return invoke_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
tGraphSliderOp *gso = op->customdata;
|
||||||
|
gso->modal_update = time_offset_modal_update;
|
||||||
|
gso->factor_prop = RNA_struct_find_property(op->ptr, "factor");
|
||||||
|
time_offset_draw_status_header(C, gso);
|
||||||
|
ED_slider_is_bidirectional_set(gso->slider, true);
|
||||||
|
ED_slider_factor_set(gso->slider, 0.0f);
|
||||||
|
|
||||||
|
return invoke_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int time_offset_exec(bContext *C, wmOperator *op)
|
||||||
|
{
|
||||||
|
bAnimContext ac;
|
||||||
|
|
||||||
|
/* Get editor data. */
|
||||||
|
if (ANIM_animdata_get_context(C, &ac) == 0) {
|
||||||
|
return OPERATOR_CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float factor = RNA_float_get(op->ptr, "factor");
|
||||||
|
|
||||||
|
time_offset_graph_keys(&ac, factor);
|
||||||
|
|
||||||
|
/* Set notifier that keyframes have changed. */
|
||||||
|
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_EDITED, NULL);
|
||||||
|
|
||||||
|
return OPERATOR_FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GRAPH_OT_time_offset(wmOperatorType *ot)
|
||||||
|
{
|
||||||
|
/* Identifiers. */
|
||||||
|
ot->name = "Time Offset Keyframes";
|
||||||
|
ot->idname = "GRAPH_OT_time_offset";
|
||||||
|
ot->description = "Shifts the value of selected keys in time";
|
||||||
|
|
||||||
|
/* API callbacks. */
|
||||||
|
ot->invoke = time_offset_invoke;
|
||||||
|
ot->modal = graph_slider_modal;
|
||||||
|
ot->exec = time_offset_exec;
|
||||||
|
ot->poll = graphop_editable_keyframes_poll;
|
||||||
|
|
||||||
|
/* Flags. */
|
||||||
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
||||||
|
|
||||||
|
RNA_def_float_factor(ot->srna,
|
||||||
|
"factor",
|
||||||
|
0.0f,
|
||||||
|
-FLT_MAX,
|
||||||
|
FLT_MAX,
|
||||||
|
"Curve Bend",
|
||||||
|
"Control the bend of the curve",
|
||||||
|
-1.0f,
|
||||||
|
1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
/** \} */
|
/** \} */
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
/** \name Gauss Smooth Operator
|
/** \name Gauss Smooth Operator
|
||||||
|
|
An offset usually is just, mathematically speaking, an addition of a constant value. So shifting all the keys into a certain direction. A comment below mentions "the wrapping point", which suggests that more is going on than just applying an offset.
This should be reflected in the name,. If that's not possible, at least it should be documented.