# SPDX-License-Identifier: Apache-2.0 # Copyright 2018-2021 The glTF-Blender-IO authors. import bpy import typing from ..com.gltf2_blender_data_path import get_target_object_path, get_target_property_name, get_rotation_modes from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.io.com import gltf2_io_debug from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_samplers from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channel_target from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_sampler_keyframes from io_scene_gltf2.blender.exp import gltf2_blender_get from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins from io_scene_gltf2.blender.exp import gltf2_blender_gather_drivers from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode from . import gltf2_blender_export_keys @cached def gather_animation_channels(obj_uuid: int, blender_action: bpy.types.Action, export_settings ) -> typing.List[gltf2_io.AnimationChannel]: channels = [] blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object # First calculate range of animation for baking # This is need if user set 'Force sampling' and in case we need to bake bake_range_start = None bake_range_end = None force_range = False # If range is manually set, use it. Else, calculate it if blender_action.use_frame_range is True: bake_range_start = blender_action.frame_start bake_range_end = blender_action.frame_end force_range = True # keyframe_points is read-only, we can't restrict here else: 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 if channel is not None] if bake_range_start is None: 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 if channel is not None])) if bake_range_end is None: 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 if channel is not None])) if blender_object.type == "ARMATURE" and export_settings['gltf_force_sampling'] is True: # We have to store sampled animation data for every deformation bones # Check that there are some anim in this action if bake_range_start is None: return [] # Then bake all bones bones_to_be_animated = [] bones_uuid = export_settings["vtree"].get_all_bones(obj_uuid) bones_to_be_animated = [blender_object.pose.bones[export_settings["vtree"].nodes[b].blender_bone.name] for b in bones_uuid] list_of_animated_bone_channels = [] for channel_group in __get_channel_groups(blender_action, blender_object, export_settings): channel_group_sorted = __get_channel_group_sorted(channel_group, blender_object) list_of_animated_bone_channels.extend([(gltf2_blender_get.get_object_from_datapath(blender_object, get_target_object_path(i.data_path)).name, get_target_property_name(i.data_path)) for i in channel_group]) for bone in bones_to_be_animated: for p in ["location", "rotation_quaternion", "scale"]: channel = gather_animation_channel( obj_uuid, (), export_settings, bone.name, p, bake_range_start, bake_range_end, force_range, blender_action.name, None, (bone.name, p) in list_of_animated_bone_channels) if channel is not None: channels.append(channel) # Retrieve animation on armature object itself, if any fcurves_armature = __gather_armature_object_channel_groups(blender_action, blender_object, export_settings) for channel_group in fcurves_armature: # No need to sort on armature, that can't have SK if len(channel_group) == 0: # Only errors on channels, ignoring continue channel = gather_animation_channel(obj_uuid, channel_group, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, None, True) if channel is not None: channels.append(channel) # Retrieve channels for drivers, if needed drivers_to_manage = gltf2_blender_gather_drivers.get_sk_drivers(obj_uuid, export_settings) for obj_driver_uuid, fcurves in drivers_to_manage: channel = gather_animation_channel( obj_uuid, fcurves, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, obj_driver_uuid, True) if channel is not None: channels.append(channel) else: done_paths = [] for channel_group in __get_channel_groups(blender_action, blender_object, export_settings): channel_group_sorted = __get_channel_group_sorted(channel_group, blender_object) if len(channel_group_sorted) == 0: # Only errors on channels, ignoring continue channel = gather_animation_channel( obj_uuid, channel_group_sorted, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, None, True ) if channel is not None: channels.append(channel) # Store already done channel path target = [c for c in channel_group_sorted if c is not None][0].data_path.split('.')[-1] path = { "delta_location": "location", "delta_rotation_euler": "rotation_quaternion", "location": "location", "rotation_axis_angle": "rotation_quaternion", "rotation_euler": "rotation_quaternion", "rotation_quaternion": "rotation_quaternion", "scale": "scale", "value": "weights" }.get(target) if path is not None: done_paths.append(path) done_paths = list(set(done_paths)) if export_settings['gltf_selected'] is True and export_settings['vtree'].tree_troncated is True: start_frame = min([v[0] for v in [a.frame_range for a in bpy.data.actions]]) end_frame = max([v[1] for v in [a.frame_range for a in bpy.data.actions]]) to_be_done = ['location', 'rotation_quaternion', 'scale'] to_be_done = [c for c in to_be_done if c not in done_paths] # In case of weight action, do nothing. # If there is only weight --> TRS is already managed at first if not (len(done_paths) == 1 and 'weights' in done_paths): for p in to_be_done: channel = gather_animation_channel( obj_uuid, (), export_settings, None, p, start_frame, end_frame, force_range, blender_action.name, None, False #If Object is not animated, don't keep animation for this channel ) if channel is not None: channels.append(channel) # resetting driver caches gltf2_blender_gather_drivers.get_sk_driver_values.reset_cache() gltf2_blender_gather_drivers.get_sk_drivers.reset_cache() # resetting bone caches gltf2_blender_gather_animation_sampler_keyframes.get_bone_matrix.reset_cache() return channels def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object): # if this is shapekey animation, we need to sort in same order than shapekeys # else, no need to sort if blender_object.type == "MESH": first_channel = channels[0] object_path = get_target_object_path(first_channel.data_path) if object_path: if not blender_object.data.shape_keys: # Something is wrong. Maybe the user assigned an armature action # to a mesh object. Returning without sorting return channels # This is shapekeys, we need to sort channels 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[sk.name] = cpt_sk cpt_sk += 1 # 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: try: 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)) except: # Something is wrong. For example, an armature action linked to a mesh object continue 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]) if all([i is None for i in all_sorted_channels]): # all channel in error, and some non keyed SK return channels # This happen when an armature action is linked to a mesh object with non keyed SK return tuple(all_sorted_channels) # if not shapekeys, stay in same order, because order doesn't matter return channels # This function can be called directly from gather_animation in case of bake animation (non animated selected object) def gather_animation_channel(obj_uuid: str, channels: typing.Tuple[bpy.types.FCurve], export_settings, bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, force_range: bool, action_name: str, driver_obj_uuid, node_channel_is_animated: bool ) -> typing.Union[gltf2_io.AnimationChannel, None]: blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object if not __filter_animation_channel(channels, blender_object, export_settings): return None __target= __gather_target(obj_uuid, channels, export_settings, bake_bone, bake_channel, driver_obj_uuid) if __target.path is not None: sampler = __gather_sampler(channels, obj_uuid, export_settings, bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj_uuid, node_channel_is_animated) if sampler is None: # After check, no need to animate this node for this channel return None animation_channel = gltf2_io.AnimationChannel( extensions=__gather_extensions(channels, blender_object, export_settings, bake_bone), extras=__gather_extras(channels, blender_object, export_settings, bake_bone), sampler=sampler, target=__target ) export_user_extensions('gather_animation_channel_hook', export_settings, animation_channel, channels, blender_object, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name) return animation_channel return None def __filter_animation_channel(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object, export_settings ) -> bool: return True def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object, export_settings, bake_bone: typing.Union[str, None] ) -> typing.Any: return None def __gather_extras(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object, export_settings, bake_bone: typing.Union[str, None] ) -> typing.Any: return None def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve], obj_uuid: str, export_settings, bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, force_range: bool, action_name, driver_obj_uuid, node_channel_is_animated: bool ) -> gltf2_io.AnimationSampler: need_rotation_correction = (export_settings[gltf2_blender_export_keys.CAMERAS] and export_settings['vtree'].nodes[obj_uuid].blender_type == VExportNode.CAMERA) or \ (export_settings[gltf2_blender_export_keys.LIGHTS] and export_settings['vtree'].nodes[obj_uuid].blender_type == VExportNode.LIGHT) return gltf2_blender_gather_animation_samplers.gather_animation_sampler( channels, obj_uuid, bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj_uuid, node_channel_is_animated, need_rotation_correction, export_settings ) def __gather_target(obj_uuid: str, channels: typing.Tuple[bpy.types.FCurve], export_settings, bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], driver_obj_uuid ) -> gltf2_io.AnimationChannelTarget: return gltf2_blender_gather_animation_channel_target.gather_animation_channel_target( obj_uuid, channels, bake_bone, bake_channel, driver_obj_uuid, export_settings) def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings): targets = {} multiple_rotation_mode_detected = False delta_rotation_detection = [False, False] # Normal / Delta for fcurve in blender_action.fcurves: # In some invalid files, channel hasn't any keyframes ... this channel need to be ignored if len(fcurve.keyframe_points) == 0: continue try: target_property = get_target_property_name(fcurve.data_path) except: gltf2_io_debug.print_console("WARNING", "Invalid animation fcurve name on action {}".format(blender_action.name)) continue object_path = get_target_object_path(fcurve.data_path) # find the object affected by this action if not object_path: target = blender_object else: try: target = gltf2_blender_get.get_object_from_datapath(blender_object, object_path) if blender_object.type == "MESH" and object_path.startswith("key_blocks"): shape_key = blender_object.data.shape_keys.path_resolve(object_path) if shape_key.mute is True: continue target = blender_object.data.shape_keys except ValueError as e: # if the object is a mesh and the action target path can not be resolved, we know that this is a morph # animation. if blender_object.type == "MESH": try: shape_key = blender_object.data.shape_keys.path_resolve(object_path) if shape_key.mute is True: continue target = blender_object.data.shape_keys except: # Something is wrong, for example a bone animation is linked to an object mesh... gltf2_io_debug.print_console("WARNING", "Animation target {} not found".format(object_path)) continue else: gltf2_io_debug.print_console("WARNING", "Animation target {} not found".format(object_path)) continue # Detect that object or bone are not multiple keyed for euler and quaternion # Keep only the current rotation mode used by object rotation, delta, rotation_modes = get_rotation_modes(target_property) # Delta rotation management if delta is False: if delta_rotation_detection[1] is True: # normal rotation coming, but delta is already present multiple_rotation_mode_detected = True continue delta_rotation_detection[0] = True else: if delta_rotation_detection[0] is True: # delta rotation coming, but normal is already present multiple_rotation_mode_detected = True continue delta_rotation_detection[1] = True if rotation and target.rotation_mode not in rotation_modes: multiple_rotation_mode_detected = True continue # group channels by target object and affected property of the target target_properties = targets.get(target, {}) channels = target_properties.get(target_property, []) channels.append(fcurve) target_properties[target_property] = channels targets[target] = target_properties groups = [] for p in targets.values(): groups += list(p.values()) if multiple_rotation_mode_detected is True: gltf2_io_debug.print_console("WARNING", "Multiple rotation mode detected for {}".format(blender_object.name)) return map(tuple, groups) def __gather_armature_object_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings): targets = {} if blender_object.type != "ARMATURE": return tuple() delta_rotation_detection = [False, False] # Normal / Delta for fcurve in blender_action.fcurves: object_path = get_target_object_path(fcurve.data_path) if object_path != "": continue # In some invalid files, channel hasn't any keyframes ... this channel need to be ignored if len(fcurve.keyframe_points) == 0: continue try: target_property = get_target_property_name(fcurve.data_path) except: gltf2_io_debug.print_console("WARNING", "Invalid animation fcurve name on action {}".format(blender_action.name)) continue target = gltf2_blender_get.get_object_from_datapath(blender_object, object_path) # Detect that armature is not multiple keyed for euler and quaternion # Keep only the current rotation mode used by bone rotation, delta, rotation_modes = get_rotation_modes(target_property) # Delta rotation management if delta is False: if delta_rotation_detection[1] is True: # normal rotation coming, but delta is already present continue delta_rotation_detection[0] = True else: if delta_rotation_detection[0] is True: # delta rotation coming, but normal is already present continue delta_rotation_detection[1] = True if rotation and target.rotation_mode not in rotation_modes: continue # group channels by target object and affected property of the target target_properties = targets.get(target, {}) channels = target_properties.get(target_property, []) channels.append(fcurve) target_properties[target_property] = channels targets[target] = target_properties groups = [] for p in targets.values(): groups += list(p.values()) return map(tuple, groups)