FBX IO: Fix animation import when the newer opt-in FBX_KTIME is used #105019

Merged
Thomas Barlow merged 3 commits from Mysteryem/blender-addons:fbx_new_ktime into main 2023-12-01 01:00:26 +01:00
2 changed files with 51 additions and 17 deletions
Showing only changes of commit a5b47383c7 - Show all commits

View File

@ -23,6 +23,7 @@ from . import encode_bin, data_types
# "Constants" # "Constants"
FBX_VERSION = 7400 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_HEADER_VERSION = 1003
FBX_SCENEINFO_VERSION = 100 FBX_SCENEINFO_VERSION = 100
FBX_TEMPLATES_VERSION = 100 FBX_TEMPLATES_VERSION = 100
@ -54,7 +55,19 @@ FBX_ANIM_KEY_VERSION = 4008
FBX_NAME_CLASS_SEP = b"\x00\x01" FBX_NAME_CLASS_SEP = b"\x00\x01"
FBX_ANIM_PROPSGROUP_NAME = "d" 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. 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, "degree": 360.0,
"radian": math.pi * 2.0, "radian": math.pi * 2.0,
"second": 1.0, # Ref unit! "second": 1.0, # Ref unit!
"ktime": FBX_KTIME, "ktime": FBX_KTIME, # For export use only because the imported "ktimes" per second may vary.
} }

View File

@ -48,6 +48,9 @@ from .fbx_utils import (
MESH_ATTRIBUTE_SHARP_FACE, MESH_ATTRIBUTE_SHARP_FACE,
MESH_ATTRIBUTE_SHARP_EDGE, MESH_ATTRIBUTE_SHARP_EDGE,
expand_shape_key_range, 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 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 return key_times, key_values
def _convert_fbx_time_to_blender_time(key_times, blen_start_offset, fbx_start_offset, fps): def _convert_fbx_time_to_blender_time(key_times, blen_start_offset, fbx_start_offset, fps, fbx_ktime):
from .fbx_utils import FBX_KTIME timefac = fps / fbx_ktime
timefac = fps / FBX_KTIME
# Convert from FBX timing to Blender timing. # 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. # 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) 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. """Set all keyframe times and values for a newly created FCurve.
Linear interpolation is currently assumed. Linear interpolation is currently assumed.
This is a convenience function for calling blen_store_keyframes_multi with only a single fcurve and values array.""" 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 """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. each pair has the same keyframe times.
Linear interpolation is currently assumed.""" 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) num_keys = len(bl_key_times)
# Compatible with C float type # 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() 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, 'Bake' loc/rot/scale into the action,
taking any pre_ and post_ matrix into account to transform from fbx into blender space. 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}) assert(channel in {0, 1, 2})
blen_curve = blen_curves[channel] blen_curve = blen_curves[channel]
fbx_key_times, values = blen_read_animation_curve(curve) 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): elif isinstance(item, ShapeKey):
deform_values = shape_key_deforms.setdefault(item, []) 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) 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. # A fully activated shape key in FBX DeformPercent is 100.0 whereas it is 1.0 in Blender.
values = values / 100.0 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 # 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. # 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. # Remap the imported values from FBX to Blender.
values = values / 1000.0 values = values / 1000.0
values *= global_scale 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: else: # Object or PoseBone:
transform_data = item.fbx_transform_data 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 # Each channel has the same keyframe times, so the combined times can be passed once along with all the curves
# and values arrays. # 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. Recreate an action per stack/layer/object combinations.
Only the first found action is linked to objects, more complex setups are not handled, 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 id_data.animation_data.action = action
# And actually populate the action! # And actually populate the action!
blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset, global_scale, 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 # 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. # 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! # Animation!
def _(): 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_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack'))
fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer')) fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer'))
stacks = {} stacks = {}
@ -3733,7 +3753,8 @@ def load(operator, context, filepath="",
curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel) curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel)
# And now that we have sorted all this, apply animations! # 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 _ _(); del _