From 4ddb52a775950fe14733f675a4804883a2e4dce3 Mon Sep 17 00:00:00 2001 From: Nate Rupsis Date: Fri, 29 Dec 2023 17:59:24 +0100 Subject: [PATCH] Anim: Action bake custom properties Add custom properties to Action Bake. objects will bake all animatable custom properties. Armatures will bake all bone custom properties, as well as object (armature) custom properties. Pull Request: https://projects.blender.org/blender/blender/pulls/113208 --- scripts/modules/bpy_extras/anim_utils.py | 68 ++++++++++++++++++++---- scripts/startup/bl_operators/anim.py | 4 +- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/scripts/modules/bpy_extras/anim_utils.py b/scripts/modules/bpy_extras/anim_utils.py index f0db486f07e..0a38e942392 100644 --- a/scripts/modules/bpy_extras/anim_utils.py +++ b/scripts/modules/bpy_extras/anim_utils.py @@ -21,6 +21,10 @@ from typing import ( Tuple, ) +from rna_prop_ui import ( + rna_idprop_value_to_python, +) + FCurveKey = Tuple[ # `fcurve.data_path`. str, @@ -67,6 +71,9 @@ class BakeOptions: do_bbone: bool """Bake b-bone channels""" + do_custom_props: bool + """Bake custom properties.""" + def bake_action( obj, @@ -195,9 +202,29 @@ def bake_action_iter( "bbone_easeout": 1, } + # Convert rna_prop types (IDPropertyArray, etc) to python types. + def clean_custom_properties(obj): + clean_props = { + key: rna_idprop_value_to_python(value) + for key, value in obj.items() + } + return clean_props + + def bake_custom_properties(obj, *, custom_props, frame, group_name=""): + if frame is None or not custom_props: + return + for key, value in custom_props.items(): + obj[key] = value + try: + obj.keyframe_insert(f'["{bpy.utils.escape_identifier(key)}"]', frame=frame, group=group_name) + except TypeError: + # Non animatable properties (datablocks, etc) cannot be keyed. + continue + def pose_frame_info(obj): matrix = {} bbones = {} + custom_props = {} for name, pbone in obj.pose.bones.items(): if bake_options.do_visual_keying: # Get the final transform of the bone in its own local space... @@ -209,32 +236,41 @@ def bake_action_iter( # Bendy Bones if pbone.bone.bbone_segments > 1: bbones[name] = {bb_prop: getattr(pbone, bb_prop) for bb_prop in BBONE_PROPS} - return matrix, bbones + + # Custom Properties + custom_props[name] = clean_custom_properties(pbone) + + return matrix, bbones, custom_props + + def armature_frame_info(obj): + if obj.type != 'ARMATURE': + return {} + return clean_custom_properties(obj) if bake_options.do_parents_clear: if bake_options.do_visual_keying: def obj_frame_info(obj): - return obj.matrix_world.copy() + return obj.matrix_world.copy(), clean_custom_properties(obj) else: def obj_frame_info(obj): parent = obj.parent matrix = obj.matrix_basis if parent: - return parent.matrix_world @ matrix + return parent.matrix_world @ matrix, clean_custom_properties(obj) else: - return matrix.copy() + return matrix.copy(), clean_custom_properties(obj) else: if bake_options.do_visual_keying: def obj_frame_info(obj): parent = obj.parent matrix = obj.matrix_world if parent: - return parent.matrix_world.inverted_safe() @ matrix + return parent.matrix_world.inverted_safe() @ matrix, clean_custom_properties(obj) else: - return matrix.copy() + return matrix.copy(), clean_custom_properties(obj) else: def obj_frame_info(obj): - return obj.matrix_basis.copy() + return obj.matrix_basis.copy(), clean_custom_properties(obj) # ------------------------------------------------------------------------- # Setup the Context @@ -246,6 +282,7 @@ def bake_action_iter( raise Exception("Pose and object baking is disabled, no action needed") pose_info = [] + armature_info = [] obj_info = [] # ------------------------------------------------------------------------- @@ -258,11 +295,11 @@ def bake_action_iter( # Signal we're done! if frame is None: break - if bake_options.do_pose: pose_info.append((frame, *pose_frame_info(obj))) + armature_info.append((frame, armature_frame_info(obj))) if bake_options.do_object: - obj_info.append((frame, obj_frame_info(obj))) + obj_info.append((frame, *obj_frame_info(obj))) # ------------------------------------------------------------------------- # Clean (store initial data) @@ -298,6 +335,9 @@ def bake_action_iter( # pose lookup_fcurves = {(fcurve.data_path, fcurve.array_index): fcurve for fcurve in action.fcurves} if bake_options.do_pose: + for f, armature_custom_properties in armature_info: + bake_custom_properties(obj, custom_props = armature_custom_properties, frame = f) + for name, pbone in obj.pose.bones.items(): if bake_options.only_selected and not pbone.bone.select: continue @@ -335,7 +375,7 @@ def bake_action_iter( rotation_mode = pbone.rotation_mode total_new_keys = len(pose_info) - for (f, matrix, bbones) in pose_info: + for (f, matrix, bbones, custom_props) in pose_info: pbone.matrix_basis = matrix[name].copy() if bake_options.do_location: @@ -378,6 +418,9 @@ def bake_action_iter( keyframes.extend_co_value( paths_bbprops[prop_index], f, bbone_shape[prop_name] ) + # Custom Properties + if bake_options.do_custom_props: + bake_custom_properties(pbone, custom_props = custom_props[name], frame = f, group_name = name) if is_new_action: keyframes.insert_keyframes_into_new_action(total_new_keys, action, name) @@ -412,7 +455,7 @@ def bake_action_iter( rotation_mode = obj.rotation_mode total_new_keys = len(obj_info) - for (f, matrix) in obj_info: + for (f, matrix, custom_props) in obj_info: name = "Action Bake" # XXX: placeholder obj.matrix_basis = matrix @@ -442,6 +485,9 @@ def bake_action_iter( if bake_options.do_scale: keyframes.extend_co_values(path_scale, 3, f, obj.scale) + if bake_options.do_custom_props: + bake_custom_properties(obj, custom_props = custom_props, frame = f, group_name = name) + if is_new_action: keyframes.insert_keyframes_into_new_action(total_new_keys, action, name) else: diff --git a/scripts/startup/bl_operators/anim.py b/scripts/startup/bl_operators/anim.py index 25bebe3d781..66703c7528c 100644 --- a/scripts/startup/bl_operators/anim.py +++ b/scripts/startup/bl_operators/anim.py @@ -259,8 +259,9 @@ class NLA_OT_bake(Operator): ('ROTATION', "Rotation", "Bake rotation channels"), ('SCALE', "Scale", "Bake scale channels"), ('BBONE', "B-Bone", "Bake B-Bone channels"), + ('PROPS', "Custom Properties", "Bake custom properties") ), - default={'LOCATION', 'ROTATION', 'SCALE', 'BBONE'}, + default={'LOCATION', 'ROTATION', 'SCALE', 'BBONE', 'PROPS'}, ) def execute(self, context): @@ -278,6 +279,7 @@ class NLA_OT_bake(Operator): do_rotation='ROTATION' in self.channel_types, do_scale='SCALE' in self.channel_types, do_bbone='BBONE' in self.channel_types, + do_custom_props='PROPS' in self.channel_types ) if bake_options.do_pose and self.only_selected: