From 06766ff5d5932a7acb05a45d6cd045e5374e6b38 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sat, 12 Oct 2019 13:21:42 +0200 Subject: glTF exporter: fix exporting shapekeys animation when some sk are not animated --- io_scene_gltf2/__init__.py | 3 +- ...ltf2_blender_gather_animation_channel_target.py | 3 +- .../exp/gltf2_blender_gather_animation_channels.py | 29 +++-- ...2_blender_gather_animation_sampler_keyframes.py | 65 +++++----- .../exp/gltf2_blender_gather_animation_samplers.py | 131 +++++++++++++-------- 5 files changed, 144 insertions(+), 87 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 0548f2ec..36fda5ce 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', '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), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -834,3 +834,4 @@ def unregister(): # remove from the export / import menu bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py index 2c458742..04028d20 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py @@ -78,7 +78,8 @@ def __gather_path(channels: typing.Tuple[bpy.types.FCurve], bake_channel: typing.Union[str, None] ) -> str: 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: target = bake_channel path = { diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py index da963eab..edee0971 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py @@ -37,16 +37,17 @@ def gather_animation_channels(blender_action: bpy.types.Action, bake_range_start = None bake_range_end = None 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: - 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: - 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: - 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: - 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: - 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: @@ -55,7 +56,7 @@ def gather_animation_channels(blender_action: bpy.types.Action, # Check that there are some anim in this action if bake_range_start is None: return [] - + # Then bake all bones for bone in blender_object.data.bones: for p in ["location", "rotation_quaternion", "scale"]: @@ -96,7 +97,21 @@ def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender shapekeys_idx[sk.name] = cpt_sk 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 return channels diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py index 8a4f011a..8b6f0db6 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py @@ -29,9 +29,15 @@ class Keyframe: self.seconds = frame / bpy.context.scene.render.fps self.frame = frame 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: - self.target = channels[0].data_path.split('.')[-1] - self.__indices = [c.array_index for c in channels] + self.target = [c for c in channels if c is not None][0].data_path.split('.')[-1] + 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: self.target = bake_channel self.__indices = [] @@ -53,7 +59,7 @@ class Keyframe: "rotation_euler": 3, "rotation_quaternion": 4, "scale": 3, - "value": 1 + "value": self.__length_morph }.get(self.target) if length is None: @@ -62,17 +68,18 @@ class Keyframe: return length 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 # contain a complete Vector/ Quaternion --> use the array_index value of the keyframe to set components in such # structures + # For SK, must contains all SK values result = [0.0] * self.get_target_len() for i, v in zip(self.__indices, value): result[i] = v - result = gltf2_blender_math.list_to_mathutils(result, self.target) - return result + if self.target == "value": + return result + else: + result = gltf2_blender_math.list_to_mathutils(result, self.target) + return result def get_indices(self): return self.__indices @@ -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.""" if bake_bone is None: # 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]) - end_frame = max([channel.range()[1] 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 if channel is not None]) else: start_frame = bake_range_start end_frame = bake_range_end @@ -218,25 +226,27 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec "scale": scale }[target_property] 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) keyframes.append(key) frame += step else: # 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 frames = sorted(set(frames)) for i, frame in enumerate(frames): key = Keyframe(channels, frame, bake_channel) # 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 - 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) # 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 if frame == frames[0]: # start in-tangent should become all zero @@ -248,7 +258,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec key.in_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1] ) / (frame - frames[i - 1])) - for c in channels + for c in channels if c is not None ] # Construct the out tangent if frame == frames[-1]: @@ -261,7 +271,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec key.out_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1] ) / (frames[i + 1] - frame)) - for c in channels + for c in channels if c is not None ] keyframes.append(key) @@ -273,12 +283,9 @@ def complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[f """ Complete keyframe with non keyed values """ - - if key.target == "value": - return # No array_index for i in range(0, key.get_target_len()): 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]) 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): return lst[1:] == lst[:-1] + # Note: channels has some None items only for SK if some SK are not animated + # Sampling is forced if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: return True # 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"]: gltf2_io_debug.print_console("WARNING", "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], ) 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 gltf2_io_debug.print_console("WARNING", "Baking animation because there are keyframes with different " @@ -314,24 +323,24 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], ) 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", "Baking animation because the number of keyframes is not " "equal for all channel tracks") 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 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 gltf2_io_debug.print_console("WARNING", "Baking animation because of differently located keyframes in one channel") return True 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 len(animation_target.constraints) != 0: # Constraints such as IK act on the bone -> can not be represented in glTF atm diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py index 16346729..5d5d310c 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py @@ -82,45 +82,38 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve], non_keyed_values = [] - if bake_channel is None: - target = channels[0].data_path.split('.')[-1] - else: - target = bake_channel - if target == "value": - return () - - 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) + # Note: channels has some None items only for SK if some SK are not animated + if None not in channels: + # classic case for object TRS or bone TRS + # Or if all morph target are animated + + if bake_channel is None: + target = channels[0].data_path.split('.')[-1] 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({ "delta_location" : blender_object.delta_location, "delta_rotation_euler" : blender_object.delta_rotation_euler, @@ -130,18 +123,55 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve], "rotation_quaternion" : blender_object.rotation_quaternion, "scale" : blender_object.scale }[target][i]) + elif i in indices: + non_keyed_values.append(None) 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 + if blender_object_if_armature is 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]) + 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], blender_object_if_armature: typing.Optional[bpy.types.Object], @@ -201,17 +231,18 @@ def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None] ) -> 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, channels, export_settings): if bake_bone is not None: return 'LINEAR' 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 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 return { @@ -246,7 +277,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], if bake_bone is not None: target_datapath = "pose.bones['" + bake_bone + "']." + bake_channel 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] -- cgit v1.2.3