Skip to content
Snippets Groups Projects
Commit 701d600b authored by Julien Duroure's avatar Julien Duroure
Browse files

glTF exporter: fix exporting shapekeys animation when some sk are not animated

parent 50938f89
No related branches found
No related tags found
No related merge requests found
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
bl_info = { bl_info = {
'name': 'glTF 2.0 format', 'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (1, 0, 3), "version": (1, 0, 4),
'blender': (2, 81, 6), 'blender': (2, 81, 6),
'location': 'File > Import-Export', 'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0', 'description': 'Import-Export as glTF 2.0',
...@@ -834,3 +834,4 @@ def unregister(): ...@@ -834,3 +834,4 @@ def unregister():
# remove from the export / import menu # remove from the export / import menu
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
...@@ -78,7 +78,8 @@ def __gather_path(channels: typing.Tuple[bpy.types.FCurve], ...@@ -78,7 +78,8 @@ def __gather_path(channels: typing.Tuple[bpy.types.FCurve],
bake_channel: typing.Union[str, None] bake_channel: typing.Union[str, None]
) -> str: ) -> str:
if bake_channel is None: if bake_channel is None:
target = channels[0].data_path.split('.')[-1] # Note: channels has some None items only for SK if some SK are not animated
target = [c for c in channels if c is not None][0].data_path.split('.')[-1]
else: else:
target = bake_channel target = bake_channel
path = { path = {
......
...@@ -37,16 +37,17 @@ def gather_animation_channels(blender_action: bpy.types.Action, ...@@ -37,16 +37,17 @@ def gather_animation_channels(blender_action: bpy.types.Action,
bake_range_start = None bake_range_start = None
bake_range_end = None bake_range_end = None
groups = __get_channel_groups(blender_action, blender_object, export_settings) groups = __get_channel_groups(blender_action, blender_object, export_settings)
# Note: channels has some None items only for SK if some SK are not animated
for chans in groups: for chans in groups:
ranges = [channel.range() for channel in chans] ranges = [channel.range() for channel in chans if channel is not None]
if bake_range_start is None: if bake_range_start is None:
bake_range_start = min([channel.range()[0] for channel in chans]) bake_range_start = min([channel.range()[0] for channel in chans if channel is not None])
else: else:
bake_range_start = min(bake_range_start, min([channel.range()[0] for channel in chans])) bake_range_start = min(bake_range_start, min([channel.range()[0] for channel in chans if channel is not None]))
if bake_range_end is None: if bake_range_end is None:
bake_range_end = max([channel.range()[1] for channel in chans]) bake_range_end = max([channel.range()[1] for channel in chans if channel is not None])
else: else:
bake_range_end = max(bake_range_end, max([channel.range()[1] for channel in chans])) bake_range_end = max(bake_range_end, max([channel.range()[1] for channel in chans if channel is not None]))
if blender_object.type == "ARMATURE" and export_settings['gltf_force_sampling'] is True: if blender_object.type == "ARMATURE" and export_settings['gltf_force_sampling'] is True:
...@@ -55,7 +56,7 @@ def gather_animation_channels(blender_action: bpy.types.Action, ...@@ -55,7 +56,7 @@ def gather_animation_channels(blender_action: bpy.types.Action,
# Check that there are some anim in this action # Check that there are some anim in this action
if bake_range_start is None: if bake_range_start is None:
return [] return []
# Then bake all bones # Then bake all bones
for bone in blender_object.data.bones: for bone in blender_object.data.bones:
for p in ["location", "rotation_quaternion", "scale"]: for p in ["location", "rotation_quaternion", "scale"]:
...@@ -96,7 +97,21 @@ def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender ...@@ -96,7 +97,21 @@ def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender
shapekeys_idx[sk.name] = cpt_sk shapekeys_idx[sk.name] = cpt_sk
cpt_sk += 1 cpt_sk += 1
return tuple(sorted(channels, key=lambda x: shapekeys_idx[blender_object.data.shape_keys.path_resolve(get_target_object_path(x.data_path)).name])) # Note: channels will have some None items only for SK if some SK are not animated
idx_channel_mapping = []
all_sorted_channels = []
for sk_c in channels:
sk_name = blender_object.data.shape_keys.path_resolve(get_target_object_path(sk_c.data_path)).name
idx = shapekeys_idx[sk_name]
idx_channel_mapping.append((shapekeys_idx[sk_name], sk_c))
existing_idx = dict(idx_channel_mapping)
for i in range(0, cpt_sk):
if i not in existing_idx.keys():
all_sorted_channels.append(None)
else:
all_sorted_channels.append(existing_idx[i])
return tuple(all_sorted_channels)
# if not shapekeys, stay in same order, because order doesn't matter # if not shapekeys, stay in same order, because order doesn't matter
return channels return channels
......
...@@ -29,9 +29,15 @@ class Keyframe: ...@@ -29,9 +29,15 @@ class Keyframe:
self.seconds = frame / bpy.context.scene.render.fps self.seconds = frame / bpy.context.scene.render.fps
self.frame = frame self.frame = frame
self.fps = bpy.context.scene.render.fps self.fps = bpy.context.scene.render.fps
self.__length_morph = 0
# Note: channels has some None items only for SK if some SK are not animated
if bake_channel is None: if bake_channel is None:
self.target = channels[0].data_path.split('.')[-1] self.target = [c for c in channels if c is not None][0].data_path.split('.')[-1]
self.__indices = [c.array_index for c in channels] if self.target != "value":
self.__indices = [c.array_index for c in channels]
else:
self.__indices = [i for i, c in enumerate(channels) if c is not None]
self.__length_morph = len(channels)
else: else:
self.target = bake_channel self.target = bake_channel
self.__indices = [] self.__indices = []
...@@ -53,7 +59,7 @@ class Keyframe: ...@@ -53,7 +59,7 @@ class Keyframe:
"rotation_euler": 3, "rotation_euler": 3,
"rotation_quaternion": 4, "rotation_quaternion": 4,
"scale": 3, "scale": 3,
"value": 1 "value": self.__length_morph
}.get(self.target) }.get(self.target)
if length is None: if length is None:
...@@ -62,17 +68,18 @@ class Keyframe: ...@@ -62,17 +68,18 @@ class Keyframe:
return length return length
def __set_indexed(self, value): def __set_indexed(self, value):
# 'value' targets don't use keyframe.array_index
if self.target == "value":
return value
# Sometimes blender animations only reference a subset of components of a data target. Keyframe should always # Sometimes blender animations only reference a subset of components of a data target. Keyframe should always
# contain a complete Vector/ Quaternion --> use the array_index value of the keyframe to set components in such # contain a complete Vector/ Quaternion --> use the array_index value of the keyframe to set components in such
# structures # structures
# For SK, must contains all SK values
result = [0.0] * self.get_target_len() result = [0.0] * self.get_target_len()
for i, v in zip(self.__indices, value): for i, v in zip(self.__indices, value):
result[i] = v result[i] = v
result = gltf2_blender_math.list_to_mathutils(result, self.target) if self.target == "value":
return result return result
else:
result = gltf2_blender_math.list_to_mathutils(result, self.target)
return result
def get_indices(self): def get_indices(self):
return self.__indices return self.__indices
...@@ -164,10 +171,11 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec ...@@ -164,10 +171,11 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
"""Convert the blender action groups' fcurves to keyframes for use in glTF.""" """Convert the blender action groups' fcurves to keyframes for use in glTF."""
if bake_bone is None: if bake_bone is None:
# Find the start and end of the whole action group # Find the start and end of the whole action group
ranges = [channel.range() for channel in channels] # Note: channels has some None items only for SK if some SK are not animated
ranges = [channel.range() for channel in channels if channel is not None]
start_frame = min([channel.range()[0] for channel in channels]) start_frame = min([channel.range()[0] for channel in channels if channel is not None])
end_frame = max([channel.range()[1] for channel in channels]) end_frame = max([channel.range()[1] for channel in channels if channel is not None])
else: else:
start_frame = bake_range_start start_frame = bake_range_start
end_frame = bake_range_end end_frame = bake_range_end
...@@ -218,25 +226,27 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec ...@@ -218,25 +226,27 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
"scale": scale "scale": scale
}[target_property] }[target_property]
else: else:
key.value = [c.evaluate(frame) for c in channels] # Note: channels has some None items only for SK if some SK are not animated
key.value = [c.evaluate(frame) for c in channels if c is not None]
complete_key(key, non_keyed_values) complete_key(key, non_keyed_values)
keyframes.append(key) keyframes.append(key)
frame += step frame += step
else: else:
# Just use the keyframes as they are specified in blender # Just use the keyframes as they are specified in blender
frames = [keyframe.co[0] for keyframe in channels[0].keyframe_points] # Note: channels has some None items only for SK if some SK are not animated
frames = [keyframe.co[0] for keyframe in [c for c in channels if c is not None][0].keyframe_points]
# some weird files have duplicate frame at same time, removed them # some weird files have duplicate frame at same time, removed them
frames = sorted(set(frames)) frames = sorted(set(frames))
for i, frame in enumerate(frames): for i, frame in enumerate(frames):
key = Keyframe(channels, frame, bake_channel) key = Keyframe(channels, frame, bake_channel)
# key.value = [c.keyframe_points[i].co[0] for c in action_group.channels] # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels]
key.value = [c.evaluate(frame) for c in channels] key.value = [c.evaluate(frame) for c in channels if c is not None]
# Complete key with non keyed values, if needed # Complete key with non keyed values, if needed
if len(channels) != key.get_target_len(): if len([c for c in channels if c is not None]) != key.get_target_len():
complete_key(key, non_keyed_values) complete_key(key, non_keyed_values)
# compute tangents for cubic spline interpolation # compute tangents for cubic spline interpolation
if channels[0].keyframe_points[0].interpolation == "BEZIER": if [c for c in channels if c is not None][0].keyframe_points[0].interpolation == "BEZIER":
# Construct the in tangent # Construct the in tangent
if frame == frames[0]: if frame == frames[0]:
# start in-tangent should become all zero # start in-tangent should become all zero
...@@ -248,7 +258,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec ...@@ -248,7 +258,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
key.in_tangent = [ key.in_tangent = [
c.keyframe_points[i].co[1] + ((c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1] c.keyframe_points[i].co[1] + ((c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1]
) / (frame - frames[i - 1])) ) / (frame - frames[i - 1]))
for c in channels for c in channels if c is not None
] ]
# Construct the out tangent # Construct the out tangent
if frame == frames[-1]: if frame == frames[-1]:
...@@ -261,7 +271,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec ...@@ -261,7 +271,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
key.out_tangent = [ key.out_tangent = [
c.keyframe_points[i].co[1] + ((c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1] c.keyframe_points[i].co[1] + ((c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1]
) / (frames[i + 1] - frame)) ) / (frames[i + 1] - frame))
for c in channels for c in channels if c is not None
] ]
keyframes.append(key) keyframes.append(key)
...@@ -273,12 +283,9 @@ def complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[f ...@@ -273,12 +283,9 @@ def complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[f
""" """
Complete keyframe with non keyed values Complete keyframe with non keyed values
""" """
if key.target == "value":
return # No array_index
for i in range(0, key.get_target_len()): for i in range(0, key.get_target_len()):
if i in key.get_indices(): if i in key.get_indices():
continue # this is a keyed array_index continue # this is a keyed array_index or a SK animated
key.set_value_index(i, non_keyed_values[i]) key.set_value_index(i, non_keyed_values[i])
def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object],
...@@ -293,12 +300,14 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], ...@@ -293,12 +300,14 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object],
def all_equal(lst): def all_equal(lst):
return lst[1:] == lst[:-1] return lst[1:] == lst[:-1]
# Note: channels has some None items only for SK if some SK are not animated
# Sampling is forced # Sampling is forced
if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]:
return True return True
# Sampling due to unsupported interpolation # Sampling due to unsupported interpolation
interpolation = channels[0].keyframe_points[0].interpolation interpolation = [c for c in channels if c is not None][0].keyframe_points[0].interpolation
if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]: if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]:
gltf2_io_debug.print_console("WARNING", gltf2_io_debug.print_console("WARNING",
"Baking animation because of an unsupported interpolation method: {}".format( "Baking animation because of an unsupported interpolation method: {}".format(
...@@ -306,7 +315,7 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], ...@@ -306,7 +315,7 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object],
) )
return True return True
if any(any(k.interpolation != interpolation for k in c.keyframe_points) for c in channels): if any(any(k.interpolation != interpolation for k in c.keyframe_points) for c in channels if c is not None):
# There are different interpolation methods in one action group # There are different interpolation methods in one action group
gltf2_io_debug.print_console("WARNING", gltf2_io_debug.print_console("WARNING",
"Baking animation because there are keyframes with different " "Baking animation because there are keyframes with different "
...@@ -314,24 +323,24 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], ...@@ -314,24 +323,24 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object],
) )
return True return True
if not all_equal([len(c.keyframe_points) for c in channels]): if not all_equal([len(c.keyframe_points) for c in channels if c is not None]):
gltf2_io_debug.print_console("WARNING", gltf2_io_debug.print_console("WARNING",
"Baking animation because the number of keyframes is not " "Baking animation because the number of keyframes is not "
"equal for all channel tracks") "equal for all channel tracks")
return True return True
if len(channels[0].keyframe_points) <= 1: if len([c for c in channels if c is not None][0].keyframe_points) <= 1:
# we need to bake to 'STEP', as at least two keyframes are required to interpolate # we need to bake to 'STEP', as at least two keyframes are required to interpolate
return True return True
if not all_equal(list(zip([[k.co[0] for k in c.keyframe_points] for c in channels]))): if not all_equal(list(zip([[k.co[0] for k in c.keyframe_points] for c in channels if c is not None]))):
# The channels have differently located keyframes # The channels have differently located keyframes
gltf2_io_debug.print_console("WARNING", gltf2_io_debug.print_console("WARNING",
"Baking animation because of differently located keyframes in one channel") "Baking animation because of differently located keyframes in one channel")
return True return True
if blender_object_if_armature is not None: if blender_object_if_armature is not None:
animation_target = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) animation_target = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, [c for c in channels if c is not None][0].data_path)
if isinstance(animation_target, bpy.types.PoseBone): if isinstance(animation_target, bpy.types.PoseBone):
if len(animation_target.constraints) != 0: if len(animation_target.constraints) != 0:
# Constraints such as IK act on the bone -> can not be represented in glTF atm # Constraints such as IK act on the bone -> can not be represented in glTF atm
......
...@@ -82,45 +82,38 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve], ...@@ -82,45 +82,38 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve],
non_keyed_values = [] non_keyed_values = []
if bake_channel is None: # Note: channels has some None items only for SK if some SK are not animated
target = channels[0].data_path.split('.')[-1] if None not in channels:
else: # classic case for object TRS or bone TRS
target = bake_channel # Or if all morph target are animated
if target == "value":
return () if bake_channel is None:
target = channels[0].data_path.split('.')[-1]
indices = [c.array_index for c in channels]
indices.sort()
length = {
"delta_location": 3,
"delta_rotation_euler": 3,
"location": 3,
"rotation_axis_angle": 4,
"rotation_euler": 3,
"rotation_quaternion": 4,
"scale": 3,
"value": 1
}.get(target)
if length is None:
# This is not a known target
return ()
for i in range(0, length):
if bake_channel is not None:
non_keyed_values.append({
"delta_location" : blender_object.delta_location,
"delta_rotation_euler" : blender_object.delta_rotation_euler,
"location" : blender_object.location,
"rotation_axis_angle" : blender_object.rotation_axis_angle,
"rotation_euler" : blender_object.rotation_euler,
"rotation_quaternion" : blender_object.rotation_quaternion,
"scale" : blender_object.scale
}[target][i])
elif i in indices:
non_keyed_values.append(None)
else: else:
if blender_object_if_armature is None: target = bake_channel
if target == "value":
# All morph targets are animated
return tuple([None] * len(channels))
indices = [c.array_index for c in channels]
indices.sort()
length = {
"delta_location": 3,
"delta_rotation_euler": 3,
"location": 3,
"rotation_axis_angle": 4,
"rotation_euler": 3,
"rotation_quaternion": 4,
"scale": 3,
"value": len(channels)
}.get(target)
if length is None:
# This is not a known target
return ()
for i in range(0, length):
if bake_channel is not None:
non_keyed_values.append({ non_keyed_values.append({
"delta_location" : blender_object.delta_location, "delta_location" : blender_object.delta_location,
"delta_rotation_euler" : blender_object.delta_rotation_euler, "delta_rotation_euler" : blender_object.delta_rotation_euler,
...@@ -130,18 +123,55 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve], ...@@ -130,18 +123,55 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve],
"rotation_quaternion" : blender_object.rotation_quaternion, "rotation_quaternion" : blender_object.rotation_quaternion,
"scale" : blender_object.scale "scale" : blender_object.scale
}[target][i]) }[target][i])
elif i in indices:
non_keyed_values.append(None)
else: else:
# TODO, this is not working if the action is not active (NLA case for example) if blender_object_if_armature is None:
trans, rot, scale = pose_bone_if_armature.matrix_basis.decompose() non_keyed_values.append({
non_keyed_values.append({ "delta_location" : blender_object.delta_location,
"location": trans, "delta_rotation_euler" : blender_object.delta_rotation_euler,
"rotation_axis_angle": rot, "location" : blender_object.location,
"rotation_euler": rot, "rotation_axis_angle" : blender_object.rotation_axis_angle,
"rotation_quaternion": rot, "rotation_euler" : blender_object.rotation_euler,
"scale": scale "rotation_quaternion" : blender_object.rotation_quaternion,
"scale" : blender_object.scale
}[target][i]) }[target][i])
else:
# TODO, this is not working if the action is not active (NLA case for example)
trans, rot, scale = pose_bone_if_armature.matrix_basis.decompose()
non_keyed_values.append({
"location": trans,
"rotation_axis_angle": rot,
"rotation_euler": rot,
"rotation_quaternion": rot,
"scale": scale
}[target][i])
return tuple(non_keyed_values)
else:
# We are in case of morph target, where all targets are not animated
# So channels has some None items
first_channel = [c for c in channels if c is not None][0]
object_path = get_target_object_path(first_channel.data_path)
if object_path:
shapekeys_idx = {}
cpt_sk = 0
for sk in blender_object.data.shape_keys.key_blocks:
if sk == sk.relative_key:
continue
if sk.mute is True:
continue
shapekeys_idx[cpt_sk] = sk.name
cpt_sk += 1
for idx_c, channel in enumerate(channels):
if channel is None:
non_keyed_values.append(blender_object.data.shape_keys.key_blocks[shapekeys_idx[idx_c]].value)
else:
non_keyed_values.append(None)
return tuple(non_keyed_values) return tuple(non_keyed_values)
def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve], def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
blender_object_if_armature: typing.Optional[bpy.types.Object], blender_object_if_armature: typing.Optional[bpy.types.Object],
...@@ -201,17 +231,18 @@ def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve], ...@@ -201,17 +231,18 @@ def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve],
bake_bone: typing.Union[str, None], bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None] bake_channel: typing.Union[str, None]
) -> str: ) -> str:
# Note: channels has some None items only for SK if some SK are not animated
if gltf2_blender_gather_animation_sampler_keyframes.needs_baking(blender_object_if_armature, if gltf2_blender_gather_animation_sampler_keyframes.needs_baking(blender_object_if_armature,
channels, channels,
export_settings): export_settings):
if bake_bone is not None: if bake_bone is not None:
return 'LINEAR' return 'LINEAR'
else: else:
max_keyframes = max([len(ch.keyframe_points) for ch in channels]) max_keyframes = max([len(ch.keyframe_points) for ch in channels if ch is not None])
# If only single keyframe revert to STEP # If only single keyframe revert to STEP
return 'STEP' if max_keyframes < 2 else 'LINEAR' return 'STEP' if max_keyframes < 2 else 'LINEAR'
blender_keyframe = channels[0].keyframe_points[0] blender_keyframe = [c for c in channels if c is not None][0].keyframe_points[0]
# Select the interpolation method. Any unsupported method will fallback to STEP # Select the interpolation method. Any unsupported method will fallback to STEP
return { return {
...@@ -246,7 +277,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], ...@@ -246,7 +277,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
if bake_bone is not None: if bake_bone is not None:
target_datapath = "pose.bones['" + bake_bone + "']." + bake_channel target_datapath = "pose.bones['" + bake_bone + "']." + bake_channel
else: else:
target_datapath = channels[0].data_path target_datapath = [c for c in channels if c is not None][0].data_path
is_yup = export_settings[gltf2_blender_export_keys.YUP] is_yup = export_settings[gltf2_blender_export_keys.YUP]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment