diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 20f960eafc37db0102aa0da2aed1b60e31042dc5..3460c27cfa3ff47fa612a05746e2daed7225c4e2 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": (0, 9, 61), + "version": (0, 9, 62), 'blender': (2, 81, 6), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -249,6 +249,12 @@ class ExportGLTF2_Base: default=False ) + export_nla_strips: BoolProperty( + name='NLA Strips', + description='Export NLA Strip animations', + default=True + ) + export_current_frame: BoolProperty( name='Use Current Frame', description='Export the scene in the current animation frame', @@ -382,6 +388,7 @@ class ExportGLTF2_Base: if self.export_animations: export_settings['gltf_frame_range'] = self.export_frame_range export_settings['gltf_force_sampling'] = self.export_force_sampling + export_settings['gltf_nla_strips'] = self.export_nla_strips else: export_settings['gltf_frame_range'] = False export_settings['gltf_move_keyframes'] = False @@ -631,6 +638,7 @@ class GLTF_PT_export_animation_export(bpy.types.Panel): layout.prop(operator, 'export_frame_range') layout.prop(operator, 'export_frame_step') layout.prop(operator, 'export_force_sampling') + layout.prop(operator, 'export_nla_strips') class GLTF_PT_export_animation_shapekeys(bpy.types.Panel): diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py index 4d28a4bd75ef27875384135db6b9ee4ff2281e5e..92fa9ce2c3ee891345cd3899957ec66cf0c9c919 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py @@ -15,6 +15,7 @@ import bpy from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.com.gltf2_io_debug import print_console from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes from io_scene_gltf2.blender.exp import gltf2_blender_gather_animations from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached @@ -60,12 +61,65 @@ def __gather_scene(blender_scene, export_settings): def __gather_animations(blender_scene, export_settings): animations = [] + merged_tracks = {} + for blender_object in blender_scene.objects: # First check if this object is exported or not. Do not export animation of not exported object obj_node = gltf2_blender_gather_nodes.gather_node(blender_object, blender_scene, export_settings) if obj_node is not None: - animations += gltf2_blender_gather_animations.gather_animations(blender_object, export_settings) - return animations + animations_, merged_tracks = gltf2_blender_gather_animations.gather_animations(blender_object, merged_tracks, len(animations), export_settings) + animations += animations_ + + if export_settings['gltf_nla_strips'] is False: + # Fake an animation witha all animations of the scene + merged_tracks = {} + merged_tracks['Animation'] = [] + for idx, animation in enumerate(animations): + merged_tracks['Animation'].append(idx) + + + to_delete_idx = [] + for merged_anim_track in merged_tracks.keys(): + if len(merged_tracks[merged_anim_track]) < 2: + continue + + base_animation_idx = None + offset_sampler = 0 + + for idx, anim_idx in enumerate(merged_tracks[merged_anim_track]): + if idx == 0: + base_animation_idx = anim_idx + animations[anim_idx].name = merged_anim_track + already_animated = [] + for channel in animations[anim_idx].channels: + already_animated.append((channel.target.node, channel.target.path)) + continue + + to_delete_idx.append(anim_idx) + + offset_sampler = len(animations[base_animation_idx].samplers) + for sampler in animations[anim_idx].samplers: + animations[base_animation_idx].samplers.append(sampler) + + for channel in animations[anim_idx].channels: + if (channel.target.node, channel.target.path) in already_animated: + print_console("WARNING", "Some strips have same channel animation ({}), on node {} !".format(channel.target.path, channel.target.node.name)) + continue + animations[base_animation_idx].channels.append(channel) + animations[base_animation_idx].channels[-1].sampler = animations[base_animation_idx].channels[-1].sampler + offset_sampler + already_animated.append((channel.target.node, channel.target.path)) + + new_animations = [] + if len(to_delete_idx) != 0: + for idx, animation in enumerate(animations): + if idx in to_delete_idx: + continue + new_animations.append(animation) + else: + new_animations = animations + + + return new_animations def __gather_extras(blender_object, export_settings): diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py index 41a25f57862a2c904adc7e4d5e340123cc3768f4..de2059135201dbc7edd51c878ac903afbbf77a07 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py @@ -20,18 +20,21 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channels from io_scene_gltf2.io.com.gltf2_io_debug import print_console -def gather_animations(blender_object: bpy.types.Object, export_settings) -> typing.List[gltf2_io.Animation]: +def gather_animations(blender_object: bpy.types.Object, + tracks: typing.Dict[str, typing.List[int]], + offset: int, + export_settings) -> typing.Tuple[typing.List[gltf2_io.Animation], typing.Dict[str, typing.List[int]]]: """ - Gather all animations which contribute to the objects property. + Gather all animations which contribute to the objects property, and corresponding track names :param blender_object: The blender object which is animated :param export_settings: - :return: A list of glTF2 animations + :return: A list of glTF2 animations and tracks """ animations = [] # Collect all 'actions' affecting this object. There is a direct mapping between blender actions and glTF animations - blender_actions = __get_blender_actions(blender_object) + blender_actions = __get_blender_actions(blender_object, export_settings) # save the current active action of the object, if any # We will restore it after export @@ -40,7 +43,7 @@ def gather_animations(blender_object: bpy.types.Object, export_settings) -> typi current_action = blender_object.animation_data.action # Export all collected actions. - for blender_action in blender_actions: + for blender_action, track_name in blender_actions: # Set action as active, to be able to bake if needed if blender_object.animation_data: # Not for shapekeys! @@ -62,12 +65,20 @@ def gather_animations(blender_object: bpy.types.Object, export_settings) -> typi if animation is not None: animations.append(animation) + # Store data for merging animation later + if track_name is not None: # Do not take into account animation not in NLA + # Do not take into account default NLA track names + if not (track_name.startswith("NlaTrack") or track_name.startswith("[Action Stash]")): + if track_name not in tracks.keys(): + tracks[track_name] = [] + tracks[track_name].append(offset + len(animations)-1) # Store index of animation in animations + # Restore current action if blender_object.animation_data: if blender_object.animation_data.action is not None and current_action is not None and blender_object.animation_data.action.name != current_action.name: blender_object.animation_data.action = current_action - return animations + return animations, tracks def __gather_animation(blender_action: bpy.types.Action, @@ -172,23 +183,28 @@ def __link_samplers(animation: gltf2_io.Animation, export_settings): animation.channels[i].sampler = __append_unique_and_get_index(animation.samplers, channel.sampler) -def __get_blender_actions(blender_object: bpy.types.Object - ) -> typing.List[bpy.types.Action]: +def __get_blender_actions(blender_object: bpy.types.Object, + export_settings + ) -> typing.List[typing.Tuple[bpy.types.Action, str]]: blender_actions = [] + blender_tracks = {} if blender_object.animation_data is not None: # Collect active action. if blender_object.animation_data.action is not None: blender_actions.append(blender_object.animation_data.action) + blender_tracks[blender_object.animation_data.action.name] = None # Collect associated strips from NLA tracks. - for track in blender_object.animation_data.nla_tracks: - # Multi-strip tracks do not export correctly yet (they need to be baked), - # so skip them for now and only write single-strip tracks. - if track.strips is None or len(track.strips) != 1: - continue - for strip in [strip for strip in track.strips if strip.action is not None]: - blender_actions.append(strip.action) + if export_settings['gltf_nla_strips'] is True: + for track in blender_object.animation_data.nla_tracks: + # Multi-strip tracks do not export correctly yet (they need to be baked), + # so skip them for now and only write single-strip tracks. + if track.strips is None or len(track.strips) != 1: + continue + for strip in [strip for strip in track.strips if strip.action is not None]: + blender_actions.append(strip.action) + blender_tracks[strip.action.name] = track.name # Always set after possible active action -> None will be overwrite if blender_object.type == "MESH" \ and blender_object.data is not None \ @@ -197,17 +213,20 @@ def __get_blender_actions(blender_object: bpy.types.Object if blender_object.data.shape_keys.animation_data.action is not None: blender_actions.append(blender_object.data.shape_keys.animation_data.action) - - for track in blender_object.data.shape_keys.animation_data.nla_tracks: - # Multi-strip tracks do not export correctly yet (they need to be baked), - # so skip them for now and only write single-strip tracks. - if track.strips is None or len(track.strips) != 1: - continue - for strip in track.strips: - blender_actions.append(strip.action) + blender_tracks[blender_object.data.shape_keys.animation_data.action.name] = None + + if export_settings['gltf_nla_strips'] is True: + for track in blender_object.data.shape_keys.animation_data.nla_tracks: + # Multi-strip tracks do not export correctly yet (they need to be baked), + # so skip them for now and only write single-strip tracks. + if track.strips is None or len(track.strips) != 1: + continue + for strip in track.strips: + blender_actions.append(strip.action) + blender_tracks[strip.action.name] = track.name # Always set after possible active action -> None will be overwrite # Remove duplicate actions. blender_actions = list(set(blender_actions)) - return blender_actions + return [(blender_action, blender_tracks[blender_action.name]) for blender_action in blender_actions]