IO: update FBX im-/exporter to use the current Action API

Use the current Action API (i.e. move away from the to-be-deleted-in-5.0
one) to import and export F-Curves from/to FBX files.

There is a slight difference in functionality for the exporter, in the
selection of which Actions to export for the "All Actions" option. This
is just a minimal change to ensure the legacy API is no longer used.

Old: `action.fcurves` was iterated, and if all FCurves could resolve to
existing properties, the Action was exported. This would only work
reliably for single-slotted Actions, due to the use of the deprecated
`action.fcurves` property.

New: the above check is done for each Channelbag in the Action. The
first Channelbag that match the above check is exported. This does _not_
export all suitable channelbags; it merely improves on the old behaviour
slightly.

This is part of #146586

Pull Request: https://projects.blender.org/blender/blender/pulls/146980
This commit is contained in:
Sybren A. Stüvel
2025-10-02 10:25:50 +02:00
parent 1aaa540763
commit 6c011cfed8
2 changed files with 34 additions and 15 deletions

View File

@@ -2491,16 +2491,25 @@ def fbx_animations(scene_data):
# All actions.
if scene_data.settings.bake_anim_use_all_actions:
def validate_actions(act, path_resolve):
for fc in act.fcurves:
data_path = fc.data_path
if fc.array_index:
data_path = data_path + "[%d]" % fc.array_index
try:
path_resolve(data_path)
except ValueError:
return False # Invalid.
return True # Valid.
def find_validate_action_slot(act, path_resolve) -> bpy.types.ActionSlot | None:
for layer in act.layers:
for strip in layer.strips:
for channelbag in strip.channelbags:
if not channelbag.fcurves:
# Do not export empty Channelbags.
continue
for fc in channelbag.fcurves:
data_path = fc.data_path
if fc.array_index:
data_path = data_path + "[%d]" % fc.array_index
try:
path_resolve(data_path)
except ValueError:
break # Invalid, go to next strip.
else:
# Did not 'break', so all F-Curves are valid.
return channelbag.slot
return None # Found nothing to return.
def restore_object(ob_to, ob_from):
# Restore org state of object (ugh :/ ).
@@ -2540,14 +2549,20 @@ def fbx_animations(scene_data):
pbones_matrices = [pbo.matrix_basis.copy() for pbo in ob.pose.bones] if ob.type == 'ARMATURE' else ...
org_act = ob.animation_data.action
org_act_slot = ob.animation_data.action_slot
path_resolve = ob.path_resolve
for act in bpy.data.actions:
# For now, *all* paths in the action must be valid for the object, to validate the action.
# Unless that action was already assigned to the object!
if act != org_act and not validate_actions(act, path_resolve):
if act == org_act:
act_slot = org_act_slot
else:
act_slot = find_validate_action_slot(act, path_resolve)
if not act_slot:
continue
ob.animation_data.action = act
ob.animation_data.action_slot = act_slot
frame_start, frame_end = act.frame_range # sic!
add_anim(animations, animated,
fbx_animations_do(scene_data, (ob, act), frame_start, frame_end, True,
@@ -2557,6 +2572,7 @@ def fbx_animations(scene_data):
for pbo, mat in zip(ob.pose.bones, pbones_matrices):
pbo.matrix_basis = mat.copy()
ob.animation_data.action = org_act
ob.animation_data.action_slot = org_act_slot
restore_object(ob, ob_copy)
scene.frame_set(scene.frame_current, subframe=0.0)
@@ -2564,6 +2580,7 @@ def fbx_animations(scene_data):
for pbo, mat in zip(ob.pose.bones, pbones_matrices):
pbo.matrix_basis = mat.copy()
ob.animation_data.action = org_act
ob.animation_data.action_slot = org_act_slot
bpy.data.objects.remove(ob_copy)
scene.frame_set(scene.frame_current, subframe=0.0)

View File

@@ -17,6 +17,7 @@ if "bpy" in locals():
import bpy
from bpy.app.translations import pgettext_tip as tip_
from mathutils import Matrix, Euler, Vector, Quaternion
from bpy_extras import anim_utils
# Also imported in .fbx_utils, so importing here is unlikely to further affect Blender startup time.
import numpy as np
@@ -893,10 +894,10 @@ def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_
blen_fcurve.update()
def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, global_scale, shape_key_deforms,
def blen_read_animations_action_item(channelbag, item, cnodes, fps, anim_offset, global_scale, shape_key_deforms,
fbx_ktime):
"""
'Bake' loc/rot/scale into the action,
'Bake' loc/rot/scale into the channelbag,
taking any pre_ and post_ matrix into account to transform from fbx into blender space.
"""
from bpy.types import ShapeKey, Material, Camera
@@ -948,7 +949,7 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
else: # Euler
props[1] = (bl_obj.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation")
blen_curves = [action.fcurves.new(prop, index=channel, action_group=grpname)
blen_curves = [channelbag.fcurves.new(prop, index=channel, group_name=grpname)
for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
if isinstance(item, Material):
@@ -1115,7 +1116,8 @@ def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_o
id_data.animation_data.action_slot = action.slots[0]
# And actually populate the action!
blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset, global_scale,
channelbag = anim_utils.action_ensure_channelbag_for_slot(action, action.slots[0])
blen_read_animations_action_item(channelbag, item, cnodes, scene.render.fps, anim_offset, global_scale,
shape_key_values, fbx_ktime)
# If the minimum/maximum animated value is outside the slider range of the shape key, attempt to expand the slider