Page MenuHome

Implementation: Equalize Handle Operator
ClosedPublic

Authored by Kevin C. Burke (blastframe) on Jan 1 2022, 6:30 AM.
Tokens
"Love" token, awarded by hitrpr."Love" token, awarded by gilberto_rodrigues."Love" token, awarded by Blender_Defender."Love" token, awarded by Bit."Pterodactyl" token, awarded by Gavriel5578.

Details

Summary

Big Picture
The Equalize Handles operator allows users to make selected handle lengths uniform: either respecting their original angle from the key control point or by flattening their angle (removing the overshoot sometimes produced by certain handle types).

Use cases

  • Animator working in Graph Editor wanting to match eases of multiple keyframes.

Design
T94172

Diff Detail

Event Timeline

Kevin C. Burke (blastframe) requested review of this revision.Jan 1 2022, 6:30 AM
Kevin C. Burke (blastframe) created this revision.
Kevin C. Burke (blastframe) edited the summary of this revision. (Show Details)

Hi!
I can't comment regarding code itself, but if possible there could be some consistency with code comments. Generally they should start with capitalized letter and ends with full stop.
https://wiki.blender.org/wiki/Style_Guide/C_Cpp#Comments
As far as I understand it's fine to leave them "wrong" if it's about consistency, i.e. majority of comments in file written in that "wrong" style, or comment was copied from that file or somewhere else (I noticed there are such cases in your patch).

Also when using multiline comments they should have same... offset? Don't know how to tell, I'll demonstrate. One of your comment:

/* This function is used to loop over the keyframe data in an Action
* and equalize the keyframes' handles
*/

Fixed "offset":

/* This function is used to loop over the keyframe data in an Action
*  and equalize the keyframes' handles
*/

I don't know the full context, perhaps you copied it somewhere else in that state so it might be fine, I'll leave it to your discretion.

I could have suggested fixes in inline comments but last time I did it it created a lot of noise since inline comments can't be hidden for everyone else once done, you can only hide them for yourself.

Sybren A. Stüvel (sybren) requested changes to this revision.Jan 3 2022, 11:46 AM

Congrats on your first patch!

From the animator's perspective, would it be a nice touch to initialise the "amount" parameter based on the selection? Initialising to the median of the current handle lengths should make it quite easy to filter out outliers.

I can also see you've taken the existing code as example, which has a few downsides. It's quite old code, and that means that it doesn't adhere to more modern approaches.
It's better to:

  • Use bool instead of short, and return true or false instead of 1 or 0.
  • Declare variables where you need them, not all at the top of the function.
  • Declare as much const as possible; only when a value needs to change should const be dropped. This also counts for function parameters.
  • Comments should be full sentences, so start with a capital and end with a period.
source/blender/editors/animation/keyframes_edit.c
1287

Declare the function static, so that the compiler knows it shouldn't be an "exported symbol" -- that way any other C file can also declare a function with the same name, and they won't conflict with each other.

1293–1295

Remove "This function is used to"; generally such comments are written as an imperative ("Loop over the keyframes...").

The fact that the function loops is not interesting (at this level), though, as it's pretty much an implementation detail. I think the comment can be removed, and the one at the declaration in the header file can be enhanced a little.

1296

This function only ever returns 0, so just make it void and don't return anything.

1298

There is only one call to this function, and that passes key_ok=NULL. This means the parameter can be removed, as it's not used anyway. This'll make the code a lot simpler. Yes, it will be less generic, but it's never a good idea to add code that isn't used.

1299

Configure your editor to remove trailing whitespace on save, and to use autoformatting with Clang Format. Or run make format before submitting a patch, but that'll take longer and will be more of a hassle (and easier to forget).

1304

This will get eKeyframeVertOk values; since it's a bitwise combination of those enum values and not an enum value itself, ok cannot be declared as of eKeyframeVertOk type, but at least add a comment that references eKeyframeVertOk.

1321–1324

This approach leads to lots of code duplication. Since this is run in an operator, and not on every frame change or something like that, it's fine if it's not running at 100% optimized speed. Simple, easy to understand code has less bugs, and is easier to adjust later when requirements change.

1340–1343

Move this code into the conditional that deals with the left/right sides only when they're enabled. That way it only computes the angle when it's actually going to be used.

1350

Never compare enum values to their underlying value directly, but use the symbolic names (EQUALIZE_HANDLES_LEFT etc.)

Combined with the note I left at the enum declaration (to ensure the left/right values map to individual bits), you can change this condition to:

if (mode & EQUALIZE_HANDLES_LEFT) { ... }
1351–1352

Looking at this code, amount seems to be the desired handle length. This means that the code doesn't "equalize" per se (which I would expect to investigate the current handles, and make sure that they are equal somehow), but rather just sets the handle length. handle_length would thus be a better parameter name than amount.

source/blender/editors/include/ED_keyframes_edit.h
98–100

I'd use bitfields for this, so that one bit is "left" and the other is "right", and when both are set it's naturally "both".

EQUALIZE_HANDLES_LEFT = (1 << 0),
EQUALIZE_HANDLES_RIGHT = (1 << 1),
EQUALIZE_HANDLES_BOTH = (EQUALIZE_HANDLES_LEFT | EQUALIZE_HANDLES_RIGHT),
268–270

Document the parameters. Some are quite clear, so not all of them need documenting, but at least amount and flatten should be described.

274

Declare mode as type eEditKeyframes_Equalize

source/blender/editors/space_graph/graph_edit.c
2360

Rename ..._types to ..._sides; these do not indicate "types of equalization" or "types of handles", but simply the left/right sides.

2365

Remove the trailing period (also from the other two descriptions); Blender adds them automatically.

2442

The description shouldn't just copy the name. "Ensure all handles have equal length, optionally making them all horizontal" would be a better one IMO, as that describes what's being done without having to know what "equalize" or "flatten" mean in practice.

2454

Why the default of 5.0?

2454

A zero value won't work well; handle points shouldn't be placed directly on the keyframe point itself. A minimum length of 0.1f will be fine. For the soft minimum, maybe even 1.0f would be nice? I'd certainly set the soft maximum a bit lower too, as it determines the range of the slider, and too high a maximum will make it overly sensitive and thus harder to use.

I did a quick analysis of a shot of Sprite Fright, and these were the results:

avg_length: 0.747
max_length: 9.0
min_length: 0.175
90% percentile: 1.33
95% percentile: 2.0

This means that 90% of the handles had a length of 1.33 or less, and 95% had 2.0 or less. I think having a soft max of 2.0 would likely be fine.

2454–2455

Add descriptions to these parameters to explain what they do.

This revision now requires changes to proceed.Jan 3 2022, 11:46 AM
Kevin C. Burke (blastframe) marked 19 inline comments as done.Jan 4 2022, 4:07 AM
Kevin C. Burke (blastframe) added inline comments.
source/blender/editors/space_graph/graph_edit.c
2454

I set the default to 1.0f and the min length to 0.1f, however, if it's okay with you, I'd like to set the softmax to a higher value than 2.0f. Perhaps with character animation the handles are much shorter, but something heavy like a vehicle would require an ease much longer than two frames. I currently have it set to 50.0f (much shorter than the initial 1000.0f) which seems to prevent an over-sensitivity, but won't stop someone from using the slider with an object that requires a longer ease (and is unaware of the softmax functionality of the sliders as I was).

Kevin C. Burke (blastframe) marked an inline comment as done.

Fixed a bool ok = 0; to have a value of false instead of 0.

Changed equalize property name from type to side
Fixed bug in equalize_graph_keys where mode was set to bool rather than int

Sybren A. Stüvel (sybren) requested changes to this revision.Jan 4 2022, 12:02 PM

Still lots of things are variable when they ought to be const. Here's an example:

static float bezt_handle_angle(float cx, float cy, float ex, float ey)
{
  float dy = ey - cy;
  float dx = ex - cx;
  return atan2(dy, dx);
}

this should be:

static float bezt_handle_angle(const float cx, const float cy, const float ex, const float ey)
{
  const float dy = ey - cy;
  const float dx = ex - cx;
  return atan2(dy, dx);
}

For the main part of the code, I think it would produce nicer code if it were to use vector math, rather than trigonometry:

float left_direction[2] = {-handle_length, 0.f};
float right_direction[2] = {handle_length, 0.f};

...
for (...) {

    /* Get x & y of keyframe control point. */
    const float *cp = bezt->vec[1];

    /* Perform handle equalization if mode is 'Both' or 'Left'. */
    if (mode & EQUALIZE_HANDLES_LEFT) {
      /* If retaining handles' angles, store them */
      if (!flatten) {
        sub_v2_v2v2(left_direction, bezt->vec[0], bezt->vec[1]);
        normalize_v2_length(left_direction, handle_length);
      }
      add_v2_v2v2(bezt->vec[0], cp, left_direction);
    }

    /* Perform handle equalization if mode is 'Both' or 'Right'. */
    if (mode & EQUALIZE_HANDLES_RIGHT) {
      /* If retaining handles' angles, store them. */
      if (!flatten) {
        sub_v2_v2v2(right_direction, bezt->vec[2], bezt->vec[1]);
        normalize_v2_length(right_direction, handle_length);
      }
      add_v2_v2v2(bezt->vec[2], cp, right_direction);
    }
}

This also shows the duplication of the code a little better. I think it's a good idea to extract the code inside the if (mode & EQUALIZE_HANDLES_xxx) blocks into a separate function like equalize_handle(bezt->vec, 0, left_direction, handle_length, flatten) so that what's left is:

if (mode & EQUALIZE_HANDLES_LEFT) {
  equalize_handle(bezt->vec, 0, left_direction, handle_length, flatten);
}
if (mode & EQUALIZE_HANDLES_RIGHT) {
  equalize_handle(bezt->vec, 2, right_direction, handle_length, flatten);
}
source/blender/editors/animation/keyframes_edit.c
1294

The only returned value is false, so just declare as void and don't return anything.

1294

The ked parameter was only there to pass to the key_ok function, which has been removed, so ked and all the code manipulating it can also go.

1300

Declare at the use, and not here at the top of the function.

1302

ok isn't used.

1332

Flip the condition and use continue:

if ((bezt->f2 & SELECT) == 0) {
  continue;
}

This allows you to unindent the rest of the code.

source/blender/editors/include/ED_keyframes_edit.h
271

This is tricky, as the tense in that sentence suggests it's the current handle length, rather than the resulting handle length. "Desired handle length, must be positive." looks better to me.

source/blender/editors/space_graph/graph_edit.c
2388

This should use the LISTBASE_FOREACH macro.

2411–2413

Move the declarations here, instead of declaring at the top.

This revision now requires changes to proceed.Jan 4 2022, 12:02 PM
Kevin C. Burke (blastframe) marked 7 inline comments as done and an inline comment as not done.EditedJan 5 2022, 5:14 AM

I tried creating an equalize_handle function:

void equalize_handle(const float *vec, int idx, const float direction, const float handle_length, const bool flatten)
{
  /* Get x & y of keyframe control point. */
  const float *cp = vec[1];

  /* If retaining handles' angles, store them. */
  if (!flatten) {
    sub_v2_v2v2(direction, vec[idx], vec[1]);
    normalize_v2_length(direction, handle_length);
  }
  add_v2_v2v2(vec[idx], cp, direction);
}

The first error I get is: keyframes_edit.c(1293,26): error C2440: 'function': cannot convert from 'const float' to 'float *' This error's location is the direction argument in sub_v2_v2v2. It happens with and without the const qualifier in the equalize_handle function arguments.

This is probably due to my limited knowledge of C. @Sybren A. Stüvel (sybren) , sorry to trouble you, but could you please advise why this might be happening? Thank you!

source/blender/editors/space_graph/graph_edit.c
2388

I wasn't able to find an example of this LISTBASE_FOREACH macro in the code that worked. Most of the other loops through the anim_data in this file use for (ale = anim_data.first; ale; ale = ale->next). When I use LISTBASE_FOREACH (bAnimListElem *, ale, anim_data) which is something I based on some other LISTBASE_FOREACH I found in other parts of Blender, I get the error:
error C2232: '->first': left operand has 'struct' type, use '.'

Could you please advise me on how this is used?

2454

I gave this some thought and I feel the default being set to 1.0 could give the user the impression that "Equalize Handles" minimizes the handles. 1.0 frame of ease is negligible and equalize handles wouldn't benefit curves that have keyframes closer than 3 frames . I'm not sure why the Sprite Fight animation's keyframes' handles were so short: perhaps the eases were shorter for a snappier style?

I have set the default back to 5.0 showing that there are handles and making them easier for the user to grab.

Kevin C. Burke (blastframe) marked an inline comment as not done.

@Sybren A. Stüvel (sybren) , this code is currently broken in the equalize_handles function and the LISTBASE_FOREACH loop, but just in case you'd like to see what I'm doing to more clearly diagnose.

The first error I get is: keyframes_edit.c(1293,26): error C2440: 'function': cannot convert from 'const float' to 'float *' This error's location is the direction argument in sub_v2_v2v2. It happens with and without the const qualifier in the equalize_handle function arguments.

Yeah, C is not the most accessible language out there, for sure.

There's a few things going wrong here:

  • vec is of type const float *, which means "pointer to const float memory". Because the memory is const float, it cannot be changed, so add_v2_v2v2 cannot write its result there.
  • vec should be the same type as BezTriple::vec, so float vec[3][3].
  • direction should be a vector, not a single float, and it should be modifyable as well. Make it float direction[2] and it should work.
  • The function should be marked static, as that indicates "this function belongs to this file, and shouldn't be accessible from anywhere else".

These changes make it compile again:

1diff --git a/source/blender/editors/animation/keyframes_edit.c b/source/blender/editors/animation/keyframes_edit.c
2index 06b1fb1e57f..d01f9de0d38 100644
3--- a/source/blender/editors/animation/keyframes_edit.c
4+++ b/source/blender/editors/animation/keyframes_edit.c
5@@ -1283,11 +1283,8 @@ static short set_bezt_sine(KeyframeEditData *UNUSED(ked), BezTriple *bezt)
6 return 0;
7 }
8
9-void equalize_handle(const float *vec,
10- int idx,
11- const float direction,
12- const float handle_length,
13- const bool flatten)
14+static void equalize_handle(
15+ float vec[3][3], int idx, float direction[2], const float handle_length, const bool flatten)
16 {
17 /* Get x & y of keyframe control point. */
18 const float *cp = vec[1];
19@@ -1295,7 +1292,7 @@ void equalize_handle(const float *vec,
20 /* If retaining handles' angles, store them. */
21 if (!flatten) {
22 sub_v2_v2v2(direction, vec[idx], vec[1]);
23- normalize_v2_length(direction *, handle_length);
24+ normalize_v2_length(direction, handle_length);
25 }
26 add_v2_v2v2(vec[idx], cp, direction);
27 }
28diff --git a/source/blender/editors/space_graph/graph_edit.c b/source/blender/editors/space_graph/graph_edit.c
29index 1c19c0ae8ba..181fdb3f6d2 100644
30--- a/source/blender/editors/space_graph/graph_edit.c
31+++ b/source/blender/editors/space_graph/graph_edit.c
32@@ -2370,7 +2370,6 @@ static const EnumPropertyItem prop_graphkeys_equalize_handles_sides[] = {
33 static void equalize_graph_keys(bAnimContext *ac, int mode, float handle_length, bool flatten)
34 {
35 ListBase anim_data = {NULL, NULL};
36- bAnimListElem *ale;
37 int filter;
38
39 KeyframeEditData ked;
40@@ -2385,7 +2384,7 @@ static void equalize_graph_keys(bAnimContext *ac, int mode, float handle_length,
41 ked.scene = ac->scene;
42
43 /* Equalize keyframes. */
44- LISTBASE_FOREACH (bAnimListElem *, ale, anim_data) {
45+ LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) {
46 ANIM_fcurve_equalize_keyframes_loop(&ked, ale->key_data, mode, handle_length, flatten);
47 ale->update |= ANIM_UPDATE_DEFAULT;
48 }

Thank you very much for your help getting past the previous errors. The only surprise with this latest update was that, in order to get Blender to build without warnings, I had to remove the const qualifiers from these lines in keyframes_edit.c, lines 1308:1309

float left_direction[2] = {-handle_length, 0.f};
float right_direction[2] = {handle_length, 0.f};

With const the build gives this warning:

C:\blender-git\blender\source\blender\editors\animation\keyframes_edit.c(1319,51): warning C4090: 'function': different 'const' qualifiers
C:\blender-git\blender\source\blender\editors\animation\keyframes_edit.c(1324,52): warning C4090: 'function': different 'const' qualifiers

I tried changing the equalize handle argument float direction[2] to const float direction[2], but that caused the same "different 'const' qualifiers" error with sub_v2_v2v2 at line 1294. Perhaps the first argument of the sub_v2_v2v2 function is not a const?

Kevin C. Burke (blastframe) marked an inline comment as done.Jan 8 2022, 2:35 AM

I changed some of the wording for the Equalize Handles Operator's description in graph_edit.c, line 2424.

It was:

"Ensure all handles have equal length, optionally making them all horizontal"

It is now:

"Ensure selected keyframes' handles have equal length, optionally making them horizontal"

The reason being that the operator doesn't perform the function on all keyframes: they need to be selected and also can be only those handles specified by the "Side" Enum.

  • Split equalize_handle(..., bool flatten) into handle_flatten() and handle_set_length(). This makes it clearer which function uses which parameters, and how.
  • Remove unused KeyframeEditData *ked parameter
  • More const possible by declaring the variable where it's initialised

I've updated the patch, as I want to land it today (before bcon3). It was just some more cleanups.

  • Move operator to the "Snap" menu, next to the "Flatten Handles" operator
This revision is now accepted and ready to land.Jan 25 2022, 11:42 AM
This revision was automatically updated to reflect the committed changes.