From a5b47383c76517cdab2263d9ffe4090a229c818d Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Sun, 29 Oct 2023 18:27:50 +0000 Subject: [PATCH 1/2] FBX IO: Fix animation import when the newer opt-in FBX_KTIME is used FBX 2019.5 (file version 7700) introduced a new number of "ktimes" per second to more accurately support some animation frame rates. The new value is an opt-in until FBX version 8000, where it should then become the default. The importer will now look for new the header version and flag specifying to opt in to the new FBX_KTIME value. The exporter is unaffected because it always writes an FBX version 7400 binary. --- io_scene_fbx/fbx_utils.py | 17 +++++++++++-- io_scene_fbx/import_fbx.py | 51 +++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/io_scene_fbx/fbx_utils.py b/io_scene_fbx/fbx_utils.py index fd3d37db7..1b8a08ca3 100644 --- a/io_scene_fbx/fbx_utils.py +++ b/io_scene_fbx/fbx_utils.py @@ -23,6 +23,7 @@ from . import encode_bin, data_types # "Constants" FBX_VERSION = 7400 +# 1004 adds use of "OtherFlags"->"TCDefinition" to control the FBX_KTIME opt-in in FBX version 7700. FBX_HEADER_VERSION = 1003 FBX_SCENEINFO_VERSION = 100 FBX_TEMPLATES_VERSION = 100 @@ -54,7 +55,19 @@ FBX_ANIM_KEY_VERSION = 4008 FBX_NAME_CLASS_SEP = b"\x00\x01" FBX_ANIM_PROPSGROUP_NAME = "d" -FBX_KTIME = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...) +FBX_KTIME_V7 = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...) +# FBX 2019.5 (FBX version 7700) changed the number of "ktimes" per second, however, the new value is opt-in until FBX +# version 8000 where it will probably become opt-out. +FBX_KTIME_V8 = 141120000 +# To explicitly use the V7 value in FBX versions 7700-7XXX: fbx_root->"FBXHeaderExtension"->"OtherFlags"->"TCDefinition" +# is set to 127. +# To opt in to the V8 value in FBX version 7700-7XXX: "TCDefinition" is set to 0. +FBX_TIMECODE_DEFINITION_TO_KTIME_PER_SECOND = { + 0: FBX_KTIME_V8, + 127: FBX_KTIME_V7, +} +# The "ktimes" per second for Blender exported FBX is constant because the exported `FBX_VERSION` is constant. +FBX_KTIME = FBX_KTIME_V8 if FBX_VERSION >= 8000 else FBX_KTIME_V7 MAT_CONVERT_LIGHT = Matrix.Rotation(math.pi / 2.0, 4, 'X') # Blender is -Z, FBX is -Y. @@ -216,7 +229,7 @@ UNITS = { "degree": 360.0, "radian": math.pi * 2.0, "second": 1.0, # Ref unit! - "ktime": FBX_KTIME, + "ktime": FBX_KTIME, # For export use only because the imported "ktimes" per second may vary. } diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index 044f95d35..894f6888a 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -48,6 +48,9 @@ from .fbx_utils import ( MESH_ATTRIBUTE_SHARP_FACE, MESH_ATTRIBUTE_SHARP_EDGE, expand_shape_key_range, + FBX_KTIME_V7, + FBX_KTIME_V8, + FBX_TIMECODE_DEFINITION_TO_KTIME_PER_SECOND, ) LINEAR_INTERPOLATION_VALUE = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items['LINEAR'].value @@ -803,9 +806,8 @@ def blen_read_invalid_animation_curve(key_times, key_values): return key_times, key_values -def _convert_fbx_time_to_blender_time(key_times, blen_start_offset, fbx_start_offset, fps): - from .fbx_utils import FBX_KTIME - timefac = fps / FBX_KTIME +def _convert_fbx_time_to_blender_time(key_times, blen_start_offset, fbx_start_offset, fps, fbx_ktime): + timefac = fps / fbx_ktime # Convert from FBX timing to Blender timing. # Cannot subtract in-place because key_times could be read directly from FBX and could be used by multiple Actions. @@ -838,19 +840,21 @@ def blen_read_animation_curve(fbx_curve): return blen_read_invalid_animation_curve(key_times, key_values) -def blen_store_keyframes(fbx_key_times, blen_fcurve, key_values, blen_start_offset, fps, fbx_start_offset=0): +def blen_store_keyframes(fbx_key_times, blen_fcurve, key_values, blen_start_offset, fps, fbx_ktime, fbx_start_offset=0): """Set all keyframe times and values for a newly created FCurve. Linear interpolation is currently assumed. This is a convenience function for calling blen_store_keyframes_multi with only a single fcurve and values array.""" - blen_store_keyframes_multi(fbx_key_times, [(blen_fcurve, key_values)], blen_start_offset, fps, fbx_start_offset) + blen_store_keyframes_multi(fbx_key_times, [(blen_fcurve, key_values)], blen_start_offset, fps, fbx_ktime, + fbx_start_offset) -def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_start_offset, fps, fbx_start_offset=0): +def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_start_offset, fps, fbx_ktime, + fbx_start_offset=0): """Set all keyframe times and values for multiple pairs of newly created FCurves and keyframe values arrays, where each pair has the same keyframe times. Linear interpolation is currently assumed.""" - bl_key_times = _convert_fbx_time_to_blender_time(fbx_key_times, blen_start_offset, fbx_start_offset, fps) + bl_key_times = _convert_fbx_time_to_blender_time(fbx_key_times, blen_start_offset, fbx_start_offset, fps, fbx_ktime) num_keys = len(bl_key_times) # Compatible with C float type @@ -883,7 +887,8 @@ 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(action, item, cnodes, fps, anim_offset, global_scale, shape_key_deforms, + fbx_ktime): """ 'Bake' loc/rot/scale into the action, taking any pre_ and post_ matrix into account to transform from fbx into blender space. @@ -947,7 +952,7 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo assert(channel in {0, 1, 2}) blen_curve = blen_curves[channel] fbx_key_times, values = blen_read_animation_curve(curve) - blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps) + blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps, fbx_ktime) elif isinstance(item, ShapeKey): deform_values = shape_key_deforms.setdefault(item, []) @@ -960,7 +965,7 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo fbx_key_times, values = blen_read_animation_curve(curve) # A fully activated shape key in FBX DeformPercent is 100.0 whereas it is 1.0 in Blender. values = values / 100.0 - blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps) + blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps, fbx_ktime) # Store the minimum and maximum shape key values, so that the shape key's slider range can be expanded # if necessary after reading all animations. @@ -981,7 +986,7 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo # Remap the imported values from FBX to Blender. values = values / 1000.0 values *= global_scale - blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps) + blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps, fbx_ktime) else: # Object or PoseBone: transform_data = item.fbx_transform_data @@ -1042,10 +1047,10 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo # Each channel has the same keyframe times, so the combined times can be passed once along with all the curves # and values arrays. - blen_store_keyframes_multi(combined_fbx_times, zip(blen_curves, channel_values), anim_offset, fps) + blen_store_keyframes_multi(combined_fbx_times, zip(blen_curves, channel_values), anim_offset, fps, fbx_ktime) -def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_offset, global_scale): +def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_offset, global_scale, fbx_ktime): """ Recreate an action per stack/layer/object combinations. Only the first found action is linked to objects, more complex setups are not handled, @@ -1092,7 +1097,7 @@ def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_o id_data.animation_data.action = action # And actually populate the action! blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset, global_scale, - shape_key_values) + 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 # range until the animated range fits and has extra room to be decreased or increased further. @@ -3620,6 +3625,21 @@ def load(operator, context, filepath="", # Animation! def _(): + # Find the number of "ktimes" per second for this file. + # Start with the default for this FBX version. + fbx_ktime = FBX_KTIME_V8 if version >= 8000 else FBX_KTIME_V7 + # Try to find the value of the nested elem_root->'FBXHeaderExtension'->'OtherFlags'->'TCDefinition' element + # and look up the "ktimes" per second for its value. + if header := elem_find_first(elem_root, b'FBXHeaderExtension'): + # The header version that added TCDefinition support is 1004. + if elem_prop_first(elem_find_first(header, b'FBXHeaderVersion'), default=0) >= 1004: + if other_flags := elem_find_first(header, b'OtherFlags'): + if timecode_definition := elem_find_first(other_flags, b'TCDefinition'): + timecode_definition_value = elem_prop_first(timecode_definition) + # If its value is unknown or missing, default to FBX_KTIME_V8. + fbx_ktime = FBX_TIMECODE_DEFINITION_TO_KTIME_PER_SECOND.get(timecode_definition_value, + FBX_KTIME_V8) + fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack')) fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer')) stacks = {} @@ -3733,7 +3753,8 @@ def load(operator, context, filepath="", curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel) # And now that we have sorted all this, apply animations! - blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, settings.anim_offset, global_scale) + blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, settings.anim_offset, global_scale, + fbx_ktime) _(); del _ -- 2.30.2 From 032ae1f6018df5f4778b1c18058392fc98681e33 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Thu, 30 Nov 2023 23:51:15 +0000 Subject: [PATCH 2/2] Increase FBX version --- io_scene_fbx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_fbx/__init__.py b/io_scene_fbx/__init__.py index c6e56e1bc..a3850aed8 100644 --- a/io_scene_fbx/__init__.py +++ b/io_scene_fbx/__init__.py @@ -5,7 +5,7 @@ bl_info = { "name": "FBX format", "author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem", - "version": (5, 10, 2), + "version": (5, 10, 3), "blender": (4, 1, 0), "location": "File > Import-Export", "description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions", -- 2.30.2