FBX IO: Only import the first animation curve per channel #104866

Merged
Thomas Barlow merged 3 commits from Mysteryem/blender-addons:fbx_anim_import_discard_extra_curves_pr into main 2023-09-08 00:33:39 +02:00
2 changed files with 25 additions and 50 deletions

View File

@ -5,7 +5,7 @@
bl_info = { bl_info = {
"name": "FBX format", "name": "FBX format",
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem", "author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
"version": (5, 7, 1), "version": (5, 7, 2),
"blender": (3, 6, 0), "blender": (3, 6, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions", "description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",

View File

@ -626,39 +626,6 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
yield from sca yield from sca
def blen_read_animation_channel_curves(curves):
"""Read one or (very rarely) more animation curves, that affect a single channel of a single property, from FBX
data.
When there are multiple curves, they will be combined into a single sorted animation curve with later curves taking
precedence when the curves contain duplicate times.
It is expected that there will almost never be more than a single curve to read because FBX's default animation
system only uses the first curve assigned to a channel.
Returns an array of sorted, unique FBX keyframe times and an array of values for each of those keyframe times."""
if len(curves) > 1:
times_and_values_tuples = list(map(blen_read_single_animation_curve, curves))
# The FBX animation system's default implementation only uses the first curve assigned to a channel.
# Additional curves per channel are allowed by the FBX specification, but the handling of these curves is
# considered the responsibility of the application that created them. Note that each curve node is expected to
# have a unique set of channels, so these additional curves with the same channel would have to belong to
# separate curve nodes. See the FBX SDK documentation for FbxAnimCurveNode.
# Combine the curves together to produce a single array of sorted keyframe times and a single array of values.
# The arrays are concatenated in reverse so that if there are duplicate times in the read curves, then only the
# value of the last occurrence is kept.
all_times = np.concatenate([t[0] for t in reversed(times_and_values_tuples)])
all_values = np.concatenate([t[1] for t in reversed(times_and_values_tuples)])
# Get the unique, sorted times and the index in all_times of the first occurrence of each unique value.
sorted_unique_times, unique_indices_in_all_times = np.unique(all_times, return_index=True)
values_of_sorted_unique_times = all_values[unique_indices_in_all_times]
return sorted_unique_times, values_of_sorted_unique_times
else:
return blen_read_single_animation_curve(curves[0])
def _combine_curve_keyframe_times(times_and_values_tuples, initial_values): def _combine_curve_keyframe_times(times_and_values_tuples, initial_values):
"""Combine multiple parsed animation curves, that affect different channels, such that every animation curve """Combine multiple parsed animation curves, that affect different channels, such that every animation curve
contains the keyframes from every other curve, interpolating the values for the newly inserted keyframes in each contains the keyframes from every other curve, interpolating the values for the newly inserted keyframes in each
@ -779,8 +746,8 @@ def _convert_fbx_time_to_blender_time(key_times, blen_start_offset, fbx_start_of
return key_times return key_times
def blen_read_single_animation_curve(fbx_curve): def blen_read_animation_curve(fbx_curve):
"""Read a single animation curve from FBX data. """Read an animation curve from FBX data.
The parsed keyframe times are guaranteed to be strictly increasing.""" The parsed keyframe times are guaranteed to be strictly increasing."""
key_times = parray_as_ndarray(elem_prop_first(elem_find_first(fbx_curve, b'KeyTime'))) key_times = parray_as_ndarray(elem_prop_first(elem_find_first(fbx_curve, b'KeyTime')))
@ -851,11 +818,19 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
""" """
from bpy.types import Object, PoseBone, ShapeKey, Material, Camera from bpy.types import Object, PoseBone, ShapeKey, Material, Camera
fbx_curves: dict[bytes, dict[int, list[FBXElem]]] = {} fbx_curves: dict[bytes, dict[int, FBXElem]] = {}
for curves, fbxprop in cnodes.values(): for curves, fbxprop in cnodes.values():
channels_dict = fbx_curves.setdefault(fbxprop, {}) channels_dict = fbx_curves.setdefault(fbxprop, {})
for (fbx_acdata, _blen_data), channel in curves.values(): for (fbx_acdata, _blen_data), channel in curves.values():
channels_dict.setdefault(channel, []).append(fbx_acdata) if channel in channels_dict:
# Ignore extra curves when one has already been found for this channel because FBX's default animation
# system implementation only uses the first curve assigned to a channel.
# Additional curves per channel are allowed by the FBX specification, but the handling of these curves
# is considered the responsibility of the application that created them. Note that each curve node is
# expected to have a unique set of channels, so these additional curves with the same channel would have
# to belong to separate curve nodes. See the FBX SDK documentation for FbxAnimCurveNode.
continue
channels_dict[channel] = fbx_acdata
# Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0). # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
if len(fbx_curves) == 0: if len(fbx_curves) == 0:
@ -894,23 +869,23 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
for prop, nbr_channels, grpname in props for channel in range(nbr_channels)] for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
if isinstance(item, Material): if isinstance(item, Material):
for fbxprop, channel_to_curves in fbx_curves.items(): for fbxprop, channel_to_curve in fbx_curves.items():
assert(fbxprop == b'DiffuseColor') assert(fbxprop == b'DiffuseColor')
for channel, curves in channel_to_curves.items(): for channel, curve in channel_to_curve.items():
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_channel_curves(curves) 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)
elif isinstance(item, ShapeKey): elif isinstance(item, ShapeKey):
deform_values = shape_key_deforms.setdefault(item, []) deform_values = shape_key_deforms.setdefault(item, [])
for fbxprop, channel_to_curves in fbx_curves.items(): for fbxprop, channel_to_curve in fbx_curves.items():
assert(fbxprop == b'DeformPercent') assert(fbxprop == b'DeformPercent')
for channel, curves in channel_to_curves.items(): for channel, curve in channel_to_curve.items():
assert(channel == 0) assert(channel == 0)
blen_curve = blen_curves[channel] blen_curve = blen_curves[channel]
fbx_key_times, values = blen_read_animation_channel_curves(curves) 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)
@ -921,15 +896,15 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
deform_values.append(values.max()) deform_values.append(values.max())
elif isinstance(item, Camera): elif isinstance(item, Camera):
for fbxprop, channel_to_curves in fbx_curves.items(): for fbxprop, channel_to_curve in fbx_curves.items():
is_focus_distance = fbxprop == b'FocusDistance' is_focus_distance = fbxprop == b'FocusDistance'
assert(fbxprop == b'FocalLength' or is_focus_distance) assert(fbxprop == b'FocalLength' or is_focus_distance)
for channel, curves in channel_to_curves.items(): for channel, curve in channel_to_curve.items():
assert(channel == 0) assert(channel == 0)
# The indices are determined by the creation of the `props` list above. # The indices are determined by the creation of the `props` list above.
blen_curve = blen_curves[1 if is_focus_distance else 0] blen_curve = blen_curves[1 if is_focus_distance else 0]
fbx_key_times, values = blen_read_animation_channel_curves(curves) fbx_key_times, values = blen_read_animation_curve(curve)
if is_focus_distance: if is_focus_distance:
# Remap the imported values from FBX to Blender. # Remap the imported values from FBX to Blender.
values = values / 1000.0 values = values / 1000.0
@ -950,13 +925,13 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
times_and_values_tuples = [] times_and_values_tuples = []
initial_values = [] initial_values = []
channel_keys = [] channel_keys = []
for fbxprop, channel_to_curves in fbx_curves.items(): for fbxprop, channel_to_curve in fbx_curves.items():
if fbxprop not in transform_prop_to_attr: if fbxprop not in transform_prop_to_attr:
# Currently, we only care about transformation curves. # Currently, we only care about transformation curves.
continue continue
for channel, curves in channel_to_curves.items(): for channel, curve in channel_to_curve.items():
assert(channel in {0, 1, 2}) assert(channel in {0, 1, 2})
fbx_key_times, values = blen_read_animation_channel_curves(curves) fbx_key_times, values = blen_read_animation_curve(curve)
channel_keys.append((fbxprop, channel)) channel_keys.append((fbxprop, channel))