Fix: keyframe values with additive NLA stack #118053

Merged
Christoph Lendenfeld merged 6 commits from ChrisLend/blender:fix_nla_stack_keying into blender-v4.1-release 2024-02-13 11:10:26 +01:00
5 changed files with 184 additions and 9 deletions

View File

@ -12,6 +12,7 @@
#include <string>
#include "BLI_bitmap.h"
#include "BLI_vector.hh"
#include "DNA_anim_types.h"
#include "ED_transform.hh"
@ -192,6 +193,8 @@ bool autokeyframe_property(bContext *C,
* expected to be the size of the property array.
* \param frame: is expected to be in the local time of the action, meaning it has to be NLA mapped
* already.
* \param keying_mask is expected to have the same size as `rna_path`. A false bit means that index
dr.sybren marked this conversation as resolved

This doc only mentions keying_mask as an input parameter. Yet, it is not const and thus the function changes its values. What do they mean as return parameter? Or should it just be const?

This doc only mentions `keying_mask` as an input parameter. Yet, it is not `const` and thus the function changes its values. What do they mean as return parameter? Or should it just be `const`?

you were right it should have been const. changed that now

you were right it should have been const. changed that now
* will be skipped.
* \returns The number of keys inserted.
*/
int insert_key_action(Main *bmain,
@ -202,7 +205,8 @@ int insert_key_action(Main *bmain,
float frame,
Span<float> values,
eInsertKeyFlags insert_key_flag,
eBezTriple_KeyframeType key_type);
eBezTriple_KeyframeType key_type,
const BLI_bitmap *keying_mask);
/**
* Insert keys to the ID of the given PointerRNA for the given RNA paths. Tries to create an
@ -215,6 +219,7 @@ void insert_key_rna(PointerRNA *rna_pointer,
eInsertKeyFlags insert_key_flags,
eBezTriple_KeyframeType key_type,
Main *bmain,
ReportList *reports);
ReportList *reports,
const AnimationEvalContext &anim_eval_context);

As discussed in the chat, this is to become a reference instead of a pointer.

As discussed in the chat, this is to become a reference instead of a pointer.
} // namespace blender::animrig

View File

@ -863,7 +863,8 @@ int insert_key_action(Main *bmain,
const float frame,
const Span<float> values,
eInsertKeyFlags insert_key_flag,
eBezTriple_KeyframeType key_type)
eBezTriple_KeyframeType key_type,
const BLI_bitmap *keying_mask)
{
BLI_assert(bmain != nullptr);
BLI_assert(action != nullptr);
@ -880,6 +881,10 @@ int insert_key_action(Main *bmain,
int property_array_index = 0;
int inserted_keys = 0;
for (float value : values) {
if (!BLI_BITMAP_TEST_BOOL(keying_mask, property_array_index)) {
property_array_index++;
continue;
}
const bool inserted_key = insert_keyframe_fcurve_value(bmain,
nullptr,
ptr,
@ -925,7 +930,8 @@ void insert_key_rna(PointerRNA *rna_pointer,
const eInsertKeyFlags insert_key_flags,
const eBezTriple_KeyframeType key_type,
Main *bmain,
ReportList *reports)
ReportList *reports,
const AnimationEvalContext &anim_eval_context)
{
ID *id = rna_pointer->owner_id;
bAction *action = id_action_ensure(bmain, id);
@ -939,6 +945,15 @@ void insert_key_rna(PointerRNA *rna_pointer,
}
AnimData *adt = BKE_animdata_from_id(id);
/* Keyframing functions can deal with the nla_context being a nullptr. */
ListBase nla_cache = {nullptr, nullptr};
NlaKeyframingContext *nla_context = nullptr;
if (adt && adt->action == action) {
nla_context = BKE_animsys_get_nla_keyframing_context(
&nla_cache, rna_pointer, adt, &anim_eval_context);
}
const float nla_frame = BKE_nla_tweakedit_remap(adt, scene_frame, NLATIME_CONVERT_UNMAP);
const bool visual_keyframing = insert_key_flags & INSERTKEY_MATRIX;
@ -961,6 +976,16 @@ void insert_key_rna(PointerRNA *rna_pointer,
prop);
Vector<float> rna_values = get_keyframe_values(&ptr, prop, visual_keyframing);
BLI_bitmap *successful_remaps = BLI_BITMAP_NEW(rna_values.size(), __func__);
BKE_animsys_nla_remap_keyframe_values(nla_context,
rna_pointer,
prop,
rna_values.as_mutable_span(),
-1,
&anim_eval_context,
nullptr,
successful_remaps);
insert_key_count += insert_key_action(bmain,
action,
rna_pointer,
@ -969,7 +994,9 @@ void insert_key_rna(PointerRNA *rna_pointer,
nla_frame,
rna_values.as_span(),
insert_key_flags,
key_type);
key_type,
successful_remaps);
MEM_freeN(successful_remaps);
}
if (insert_key_count == 0) {

View File

@ -165,7 +165,8 @@ void autokeyframe_object(bContext *C, Scene *scene, Object *ob, Span<std::string
flag,
eBezTriple_KeyframeType(scene->toolsettings->keyframe_type),
bmain,
reports);
reports,
anim_eval_context);
}
}
@ -290,7 +291,8 @@ void autokeyframe_pose_channel(bContext *C,
flag,
eBezTriple_KeyframeType(scene->toolsettings->keyframe_type),
bmain,
reports);
reports,
anim_eval_context);
}
}

View File

@ -350,6 +350,9 @@ static int insert_key(bContext *C, wmOperator *op)
const eInsertKeyFlags insert_key_flags = ANIM_get_keyframing_flags(scene);
const eBezTriple_KeyframeType key_type = eBezTriple_KeyframeType(
scene->toolsettings->keyframe_type);
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(
depsgraph, BKE_scene_frame_get(scene));
LISTBASE_FOREACH (CollectionPointerLink *, collection_ptr_link, &selection) {
ID *selected_id = collection_ptr_link->ptr.owner_id;
@ -360,8 +363,14 @@ static int insert_key(bContext *C, wmOperator *op)
PointerRNA id_ptr = collection_ptr_link->ptr;
Vector<std::string> rna_paths = construct_rna_paths(&collection_ptr_link->ptr);
animrig::insert_key_rna(
&id_ptr, rna_paths.as_span(), scene_frame, insert_key_flags, key_type, bmain, op->reports);
animrig::insert_key_rna(&id_ptr,
rna_paths.as_span(),
scene_frame,
insert_key_flags,
key_type,
bmain,
op->reports,
anim_eval_context);
}
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_ADDED, nullptr);

View File

@ -36,6 +36,19 @@ def _get_view3d_context():
return ctx
def _get_nla_context():
ctx = bpy.context.copy()
for area in bpy.context.window.screen.areas:
if area.type != 'NLA_EDITOR':
continue
ctx['area'] = area
ctx['space'] = area.spaces.active
break
return ctx
def _create_animation_object():
anim_object = bpy.data.objects.new("anim_object", None)
# Ensure that the rotation mode is correct so we can check against rotation_euler
@ -487,6 +500,125 @@ class InsertNeededTest(AbstractKeyframingTest, unittest.TestCase):
self.assertEqual(expected_keys[fcurve.data_path][fcurve.array_index], len(fcurve.keyframe_points))
def _create_nla_anim_object():
"""

Use a """docstring""", not a comment, for documenting what a function does.

Also include info of which keys are created, as that's kinda necessary for following the tests that use this function.

Use a `"""docstring"""`, not a comment, for documenting what a function does. Also include info of which keys are created, as that's kinda necessary for following the tests that use this function.
Creates an object with 3 NLA tracks each with a strip that has its own action.
The middle layer is additive.
Creates a key on frame 0 and frame 10 for each of them.
The values are:
top: 0, 0
add: 0, 1
base: 0, 1
"""
anim_object = bpy.data.objects.new("anim_object", None)
bpy.context.scene.collection.objects.link(anim_object)
bpy.context.view_layer.objects.active = anim_object
anim_object.select_set(True)
anim_object.animation_data_create()
track = anim_object.animation_data.nla_tracks.new()
track.name = "base"
action_base = bpy.data.actions.new(name="action_base")
fcu = action_base.fcurves.new(data_path="location", index=0)
fcu.keyframe_points.insert(0, value=0).interpolation = 'LINEAR'
fcu.keyframe_points.insert(10, value=1).interpolation = 'LINEAR'
track.strips.new("base_strip", 0, action_base)
track = anim_object.animation_data.nla_tracks.new()
track.name = "add"
action_add = bpy.data.actions.new(name="action_add")
fcu = action_add.fcurves.new(data_path="location", index=0)
fcu.keyframe_points.insert(0, value=0).interpolation = 'LINEAR'
fcu.keyframe_points.insert(10, value=1).interpolation = 'LINEAR'
strip = track.strips.new("add_strip", 0, action_add)
strip.blend_type = "ADD"
track = anim_object.animation_data.nla_tracks.new()
track.name = "top"
action_top = bpy.data.actions.new(name="action_top")
fcu = action_top.fcurves.new(data_path="location", index=0)
fcu.keyframe_points.insert(0, value=0).interpolation = 'LINEAR'
fcu.keyframe_points.insert(10, value=0).interpolation = 'LINEAR'
track.strips.new("top_strip", 0, action_top)
return anim_object
class NlaInsertTest(AbstractKeyframingTest, unittest.TestCase):
"""

window → editor

window → editor
Testing inserting keys into an NLA stack.
The system is expected to remap the inserted values based on the strips blend_type.
"""
def setUp(self):
super().setUp()
bpy.context.preferences.edit.key_insert_channels = {'LOCATION'}
# Change one area to the NLA so we can call operators in it.
# Assumes there is at least one editor in the blender default startup file that is not the 3D viewport.
for area in bpy.context.window.screen.areas:
if area.type == 'VIEW_3D':
continue
area.type = "NLA_EDITOR"
break
def test_insert_failure(self):
# If the topmost track is set to "REPLACE" the system will fail
# when trying to insert keys into a layer beneath.
nla_anim_object = _create_nla_anim_object()
tracks = nla_anim_object.animation_data.nla_tracks
with bpy.context.temp_override(**_get_nla_context()):
bpy.ops.nla.select_all(action="DESELECT")
tracks.active = tracks["base"]
tracks["base"].strips[0].select = True
bpy.ops.nla.tweakmode_enter(use_upper_stack_evaluation=True)
with bpy.context.temp_override(**_get_view3d_context()):
bpy.context.scene.frame_set(5)
bpy.ops.anim.keyframe_insert()
base_action = bpy.data.actions["action_base"]
# Location X should not have been able to insert a keyframe because the top strip is overriding the result completely,
# making it impossible to calculate which value should be inserted.
self.assertEqual(len(base_action.fcurves.find("location", index=0).keyframe_points), 2)
# Location Y and Z will go through since they have not been defined in the action of the top strip.
self.assertEqual(len(base_action.fcurves.find("location", index=1).keyframe_points), 1)
self.assertEqual(len(base_action.fcurves.find("location", index=2).keyframe_points), 1)
def test_insert_additive(self):
nla_anim_object = _create_nla_anim_object()
tracks = nla_anim_object.animation_data.nla_tracks
# This leaves the additive track as the topmost track with influence
tracks["top"].mute = True
with bpy.context.temp_override(**_get_nla_context()):
bpy.ops.nla.select_all(action="DESELECT")
tracks.active = tracks["base"]
tracks["base"].strips[0].select = True

should add → should have added

I'm also not sure why "added keys to Y and Z but not X" is followed by 2 keys for X but only 1 for Y and Z.

should add → should have added I'm also not sure why "added keys to Y and Z but not X" is followed by 2 keys for X but only 1 for Y and Z.

I added an extra comment line to mention why X already has had keys

I added an extra comment line to mention why X already has had keys
bpy.ops.nla.tweakmode_enter(use_upper_stack_evaluation=True)
# Inserting over the existing keyframe.
bpy.context.scene.frame_set(10)
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert()
base_action = bpy.data.actions["action_base"]
# This should have added keys to Y and Z but not X.
# X already had two keys from the file setup.
self.assertEqual(len(base_action.fcurves.find("location", index=0).keyframe_points), 2)
self.assertEqual(len(base_action.fcurves.find("location", index=1).keyframe_points), 1)
self.assertEqual(len(base_action.fcurves.find("location", index=2).keyframe_points), 1)
# The keyframe value should not be changed even though the position of the
# object is modified by the additive layer.
self.assertAlmostEqual(nla_anim_object.location.x, 2.0, 8)
fcurve_loc_x = base_action.fcurves.find("location", index=0)
self.assertAlmostEqual(fcurve_loc_x.keyframe_points[-1].co[1], 1.0, 8)
def main():
global args
import argparse