glTF exporter: manage use_nla option to avoid exporting merged animations
[blender-addons.git] / io_scene_gltf2 / blender / exp / gltf2_blender_gather_animations.py
blob2b8a0682bb21a3f91e72d9faa21e05dca2b5cf05
1 # SPDX-License-Identifier: Apache-2.0
2 # Copyright 2018-2021 The glTF-Blender-IO authors.
4 import bpy
5 import typing
7 from io_scene_gltf2.io.com import gltf2_io
8 from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channels
9 from io_scene_gltf2.io.com.gltf2_io_debug import print_console
10 from ..com.gltf2_blender_extras import generate_extras
11 from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
12 from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode
13 from ..com.gltf2_blender_data_path import is_bone_anim_channel
16 def __gather_channels_baked(obj_uuid, export_settings):
17 channels = []
19 # If no animation in file, no need to bake
20 if len(bpy.data.actions) == 0:
21 return None
23 start_frame = min([v[0] for v in [a.frame_range for a in bpy.data.actions]])
24 end_frame = max([v[1] for v in [a.frame_range for a in bpy.data.actions]])
26 for p in ["location", "rotation_quaternion", "scale"]:
27 channel = gltf2_blender_gather_animation_channels.gather_animation_channel(
28 obj_uuid,
29 (),
30 export_settings,
31 None,
33 start_frame,
34 end_frame,
35 False,
36 obj_uuid, # Use obj uuid as action name for caching
37 None,
38 False #If Object is not animated, don't keep animation for this channel
40 if channel is not None:
41 channels.append(channel)
43 return channels if len(channels) > 0 else None
45 def gather_animations( obj_uuid: int,
46 tracks: typing.Dict[str, typing.List[int]],
47 offset: int,
48 export_settings) -> typing.Tuple[typing.List[gltf2_io.Animation], typing.Dict[str, typing.List[int]]]:
49 """
50 Gather all animations which contribute to the objects property, and corresponding track names
52 :param blender_object: The blender object which is animated
53 :param export_settings:
54 :return: A list of glTF2 animations and tracks
55 """
56 animations = []
58 blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
60 # Collect all 'actions' affecting this object. There is a direct mapping between blender actions and glTF animations
61 blender_actions = __get_blender_actions(blender_object, export_settings)
63 if len([a for a in blender_actions if a[2] == "OBJECT"]) == 0:
64 # No TRS animation are found for this object.
65 # But we need to bake, in case we export selection
66 if export_settings['gltf_selected'] is True and blender_object.type != "ARMATURE":
67 channels = __gather_channels_baked(obj_uuid, export_settings)
68 if channels is not None:
69 animation = gltf2_io.Animation(
70 channels=channels,
71 extensions=None, # as other animations
72 extras=None, # Because there is no animation to get extras from
73 name=blender_object.name, # Use object name as animation name
74 samplers=[]
77 __link_samplers(animation, export_settings)
78 if animation is not None:
79 animations.append(animation)
80 elif export_settings['gltf_selected'] is True and blender_object.type == "ARMATURE":
81 # We need to bake all bones. Because some bone can have some constraints linking to
82 # some other armature bones, for example
83 #TODO
84 pass
87 current_action = None
88 if blender_object.animation_data and blender_object.animation_data.action:
89 current_action = blender_object.animation_data.action
90 # Remove any solo (starred) NLA track. Restored after export
91 solo_track = None
92 if blender_object.animation_data:
93 for track in blender_object.animation_data.nla_tracks:
94 if track.is_solo:
95 solo_track = track
96 track.is_solo = False
97 break
99 # Remove any tweak mode. Restore after export
100 if blender_object.animation_data:
101 restore_tweak_mode = blender_object.animation_data.use_tweak_mode
103 # Remove use of NLA. Restore after export
104 if blender_object.animation_data:
105 current_use_nla = blender_object.animation_data.use_nla
106 blender_object.animation_data.use_nla = False
108 # Export all collected actions.
109 for blender_action, track_name, on_type in blender_actions:
111 # Set action as active, to be able to bake if needed
112 if on_type == "OBJECT": # Not for shapekeys!
113 if blender_object.animation_data.action is None \
114 or (blender_object.animation_data.action.name != blender_action.name):
115 if blender_object.animation_data.is_property_readonly('action'):
116 blender_object.animation_data.use_tweak_mode = False
117 try:
118 blender_object.animation_data.action = blender_action
119 except:
120 error = "Action is readonly. Please check NLA editor"
121 print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(blender_action.name, error))
122 continue
124 # No need to set active shapekeys animations, this is needed for bone baking
126 animation = __gather_animation(obj_uuid, blender_action, export_settings)
127 if animation is not None:
128 animations.append(animation)
130 # Store data for merging animation later
131 if track_name is not None: # Do not take into account animation not in NLA
132 # Do not take into account default NLA track names
133 if not (track_name.startswith("NlaTrack") or track_name.startswith("[Action Stash]")):
134 if track_name not in tracks.keys():
135 tracks[track_name] = []
136 tracks[track_name].append(offset + len(animations)-1) # Store index of animation in animations
138 # Restore action status
139 # TODO: do this in a finally
140 if blender_object.animation_data:
141 if blender_object.animation_data.action is not None:
142 if current_action is None:
143 # remove last exported action
144 blender_object.animation_data.action = None
145 elif blender_object.animation_data.action.name != current_action.name:
146 # Restore action that was active at start of exporting
147 blender_object.animation_data.action = current_action
148 if solo_track is not None:
149 solo_track.is_solo = True
150 blender_object.animation_data.use_tweak_mode = restore_tweak_mode
151 blender_object.animation_data.use_nla = current_use_nla
153 return animations, tracks
156 def __gather_animation( obj_uuid: int,
157 blender_action: bpy.types.Action,
158 export_settings
159 ) -> typing.Optional[gltf2_io.Animation]:
161 blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
163 if not __filter_animation(blender_action, blender_object, export_settings):
164 return None
166 name = __gather_name(blender_action, blender_object, export_settings)
167 try:
168 animation = gltf2_io.Animation(
169 channels=__gather_channels(obj_uuid, blender_action, export_settings),
170 extensions=__gather_extensions(blender_action, blender_object, export_settings),
171 extras=__gather_extras(blender_action, blender_object, export_settings),
172 name=name,
173 samplers=__gather_samplers(obj_uuid, blender_action, export_settings)
175 except RuntimeError as error:
176 print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(name, error))
177 return None
179 export_user_extensions('pre_gather_animation_hook', export_settings, animation, blender_action, blender_object)
181 if not animation.channels:
182 return None
184 # To allow reuse of samplers in one animation,
185 __link_samplers(animation, export_settings)
187 export_user_extensions('gather_animation_hook', export_settings, animation, blender_action, blender_object)
189 return animation
192 def __filter_animation(blender_action: bpy.types.Action,
193 blender_object: bpy.types.Object,
194 export_settings
195 ) -> bool:
196 if blender_action.users == 0:
197 return False
199 return True
202 def __gather_channels(obj_uuid: int,
203 blender_action: bpy.types.Action,
204 export_settings
205 ) -> typing.List[gltf2_io.AnimationChannel]:
206 return gltf2_blender_gather_animation_channels.gather_animation_channels(
207 obj_uuid, blender_action, export_settings)
210 def __gather_extensions(blender_action: bpy.types.Action,
211 blender_object: bpy.types.Object,
212 export_settings
213 ) -> typing.Any:
214 return None
217 def __gather_extras(blender_action: bpy.types.Action,
218 blender_object: bpy.types.Object,
219 export_settings
220 ) -> typing.Any:
222 if export_settings['gltf_extras']:
223 return generate_extras(blender_action)
224 return None
227 def __gather_name(blender_action: bpy.types.Action,
228 blender_object: bpy.types.Object,
229 export_settings
230 ) -> typing.Optional[str]:
231 return blender_action.name
234 def __gather_samplers(obj_uuid: str,
235 blender_action: bpy.types.Action,
236 export_settings
237 ) -> typing.List[gltf2_io.AnimationSampler]:
238 # We need to gather the samplers after gathering all channels --> populate this list in __link_samplers
239 return []
242 def __link_samplers(animation: gltf2_io.Animation, export_settings):
244 Move animation samplers to their own list and store their indices at their previous locations.
246 After gathering, samplers are stored in the channels properties of the animation and need to be moved
247 to their own list while storing an index into this list at the position where they previously were.
248 This behaviour is similar to that of the glTFExporter that traverses all nodes
249 :param animation:
250 :param export_settings:
251 :return:
253 # TODO: move this to some util module and update gltf2 exporter also
254 T = typing.TypeVar('T')
256 def __append_unique_and_get_index(l: typing.List[T], item: T):
257 if item in l:
258 return l.index(item)
259 else:
260 index = len(l)
261 l.append(item)
262 return index
264 for i, channel in enumerate(animation.channels):
265 animation.channels[i].sampler = __append_unique_and_get_index(animation.samplers, channel.sampler)
268 def __get_blender_actions(blender_object: bpy.types.Object,
269 export_settings
270 ) -> typing.List[typing.Tuple[bpy.types.Action, str, str]]:
271 blender_actions = []
272 blender_tracks = {}
273 action_on_type = {}
275 export_user_extensions('pre_gather_actions_hook', export_settings, blender_object)
277 if blender_object.animation_data is not None:
278 # Collect active action.
279 if blender_object.animation_data.action is not None:
280 blender_actions.append(blender_object.animation_data.action)
281 blender_tracks[blender_object.animation_data.action.name] = None
282 action_on_type[blender_object.animation_data.action.name] = "OBJECT"
284 # Collect associated strips from NLA tracks.
285 if export_settings['gltf_nla_strips'] is True:
286 for track in blender_object.animation_data.nla_tracks:
287 # Multi-strip tracks do not export correctly yet (they need to be baked),
288 # so skip them for now and only write single-strip tracks.
289 non_muted_strips = [strip for strip in track.strips if strip.action is not None and strip.mute is False]
290 if track.strips is None or len(non_muted_strips) != 1:
291 continue
292 for strip in non_muted_strips:
293 blender_actions.append(strip.action)
294 blender_tracks[strip.action.name] = track.name # Always set after possible active action -> None will be overwrite
295 action_on_type[strip.action.name] = "OBJECT"
297 if blender_object.type == "MESH" \
298 and blender_object.data is not None \
299 and blender_object.data.shape_keys is not None \
300 and blender_object.data.shape_keys.animation_data is not None:
302 if blender_object.data.shape_keys.animation_data.action is not None:
303 blender_actions.append(blender_object.data.shape_keys.animation_data.action)
304 blender_tracks[blender_object.data.shape_keys.animation_data.action.name] = None
305 action_on_type[blender_object.data.shape_keys.animation_data.action.name] = "SHAPEKEY"
307 if export_settings['gltf_nla_strips'] is True:
308 for track in blender_object.data.shape_keys.animation_data.nla_tracks:
309 # Multi-strip tracks do not export correctly yet (they need to be baked),
310 # so skip them for now and only write single-strip tracks.
311 non_muted_strips = [strip for strip in track.strips if strip.action is not None and strip.mute is False]
312 if track.strips is None or len(non_muted_strips) != 1:
313 continue
314 for strip in non_muted_strips:
315 blender_actions.append(strip.action)
316 blender_tracks[strip.action.name] = track.name # Always set after possible active action -> None will be overwrite
317 action_on_type[strip.action.name] = "SHAPEKEY"
319 # If there are only 1 armature, include all animations, even if not in NLA
320 if blender_object.type == "ARMATURE":
321 if len(export_settings['vtree'].get_all_node_of_type(VExportNode.ARMATURE)) == 1:
322 # Keep all actions on objects (no Shapekey animation)
323 for act in [a for a in bpy.data.actions if a.id_root == "OBJECT"]:
324 # We need to check this is an armature action
325 # Checking that at least 1 bone is animated
326 if not __is_armature_action(act):
327 continue
328 # Check if this action is already taken into account
329 if act.name in blender_tracks.keys():
330 continue
331 blender_actions.append(act)
332 blender_tracks[act.name] = None
333 action_on_type[act.name] = "OBJECT"
335 export_user_extensions('gather_actions_hook', export_settings, blender_object, blender_actions, blender_tracks, action_on_type)
337 # Remove duplicate actions.
338 blender_actions = list(set(blender_actions))
339 # sort animations alphabetically (case insensitive) so they have a defined order and match Blender's Action list
340 blender_actions.sort(key = lambda a: a.name.lower())
342 return [(blender_action, blender_tracks[blender_action.name], action_on_type[blender_action.name]) for blender_action in blender_actions]
345 def __is_armature_action(blender_action) -> bool:
346 for fcurve in blender_action.fcurves:
347 if is_bone_anim_channel(fcurve.data_path):
348 return True
349 return False