glTF exporter: export all actions of a single armature is now under an export option
[blender-addons.git] / io_scene_gltf2 / blender / exp / gltf2_blender_gather_animations.py
blob2d579d5b97daae52b7fa58095a7b167f050e1cf6
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 # (Only when force sampling is ON)
67 # If force sampling is OFF, can lead to inconsistent export anyway
68 if export_settings['gltf_selected'] is True and blender_object.type != "ARMATURE" and export_settings['gltf_force_sampling'] is True:
69 channels = __gather_channels_baked(obj_uuid, export_settings)
70 if channels is not None:
71 animation = gltf2_io.Animation(
72 channels=channels,
73 extensions=None, # as other animations
74 extras=None, # Because there is no animation to get extras from
75 name=blender_object.name, # Use object name as animation name
76 samplers=[]
79 __link_samplers(animation, export_settings)
80 if animation is not None:
81 animations.append(animation)
82 elif export_settings['gltf_selected'] is True and blender_object.type == "ARMATURE":
83 # We need to bake all bones. Because some bone can have some constraints linking to
84 # some other armature bones, for example
85 #TODO
86 pass
89 current_action = None
90 if blender_object.animation_data and blender_object.animation_data.action:
91 current_action = blender_object.animation_data.action
92 # Remove any solo (starred) NLA track. Restored after export
93 solo_track = None
94 if blender_object.animation_data:
95 for track in blender_object.animation_data.nla_tracks:
96 if track.is_solo:
97 solo_track = track
98 track.is_solo = False
99 break
101 # Remove any tweak mode. Restore after export
102 if blender_object.animation_data:
103 restore_tweak_mode = blender_object.animation_data.use_tweak_mode
105 # Remove use of NLA. Restore after export
106 if blender_object.animation_data:
107 current_use_nla = blender_object.animation_data.use_nla
108 blender_object.animation_data.use_nla = False
110 # Export all collected actions.
111 for blender_action, track_name, on_type in blender_actions:
113 # Set action as active, to be able to bake if needed
114 if on_type == "OBJECT": # Not for shapekeys!
115 if blender_object.animation_data.action is None \
116 or (blender_object.animation_data.action.name != blender_action.name):
117 if blender_object.animation_data.is_property_readonly('action'):
118 blender_object.animation_data.use_tweak_mode = False
119 try:
120 blender_object.animation_data.action = blender_action
121 except:
122 error = "Action is readonly. Please check NLA editor"
123 print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(blender_action.name, error))
124 continue
126 # No need to set active shapekeys animations, this is needed for bone baking
128 animation = __gather_animation(obj_uuid, blender_action, export_settings)
129 if animation is not None:
130 animations.append(animation)
132 # Store data for merging animation later
133 if track_name is not None: # Do not take into account animation not in NLA
134 # Do not take into account default NLA track names
135 if not (track_name.startswith("NlaTrack") or track_name.startswith("[Action Stash]")):
136 if track_name not in tracks.keys():
137 tracks[track_name] = []
138 tracks[track_name].append(offset + len(animations)-1) # Store index of animation in animations
140 # Restore action status
141 # TODO: do this in a finally
142 if blender_object.animation_data:
143 if blender_object.animation_data.action is not None:
144 if current_action is None:
145 # remove last exported action
146 blender_object.animation_data.action = None
147 elif blender_object.animation_data.action.name != current_action.name:
148 # Restore action that was active at start of exporting
149 blender_object.animation_data.action = current_action
150 if solo_track is not None:
151 solo_track.is_solo = True
152 blender_object.animation_data.use_tweak_mode = restore_tweak_mode
153 blender_object.animation_data.use_nla = current_use_nla
155 return animations, tracks
158 def __gather_animation( obj_uuid: int,
159 blender_action: bpy.types.Action,
160 export_settings
161 ) -> typing.Optional[gltf2_io.Animation]:
163 blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
165 if not __filter_animation(blender_action, blender_object, export_settings):
166 return None
168 name = __gather_name(blender_action, blender_object, export_settings)
169 try:
170 animation = gltf2_io.Animation(
171 channels=__gather_channels(obj_uuid, blender_action, export_settings),
172 extensions=__gather_extensions(blender_action, blender_object, export_settings),
173 extras=__gather_extras(blender_action, blender_object, export_settings),
174 name=name,
175 samplers=__gather_samplers(obj_uuid, blender_action, export_settings)
177 except RuntimeError as error:
178 print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(name, error))
179 return None
181 export_user_extensions('pre_gather_animation_hook', export_settings, animation, blender_action, blender_object)
183 if not animation.channels:
184 return None
186 # To allow reuse of samplers in one animation,
187 __link_samplers(animation, export_settings)
189 export_user_extensions('gather_animation_hook', export_settings, animation, blender_action, blender_object)
191 return animation
194 def __filter_animation(blender_action: bpy.types.Action,
195 blender_object: bpy.types.Object,
196 export_settings
197 ) -> bool:
198 if blender_action.users == 0:
199 return False
201 return True
204 def __gather_channels(obj_uuid: int,
205 blender_action: bpy.types.Action,
206 export_settings
207 ) -> typing.List[gltf2_io.AnimationChannel]:
208 return gltf2_blender_gather_animation_channels.gather_animation_channels(
209 obj_uuid, blender_action, export_settings)
212 def __gather_extensions(blender_action: bpy.types.Action,
213 blender_object: bpy.types.Object,
214 export_settings
215 ) -> typing.Any:
216 return None
219 def __gather_extras(blender_action: bpy.types.Action,
220 blender_object: bpy.types.Object,
221 export_settings
222 ) -> typing.Any:
224 if export_settings['gltf_extras']:
225 return generate_extras(blender_action)
226 return None
229 def __gather_name(blender_action: bpy.types.Action,
230 blender_object: bpy.types.Object,
231 export_settings
232 ) -> typing.Optional[str]:
233 return blender_action.name
236 def __gather_samplers(obj_uuid: str,
237 blender_action: bpy.types.Action,
238 export_settings
239 ) -> typing.List[gltf2_io.AnimationSampler]:
240 # We need to gather the samplers after gathering all channels --> populate this list in __link_samplers
241 return []
244 def __link_samplers(animation: gltf2_io.Animation, export_settings):
246 Move animation samplers to their own list and store their indices at their previous locations.
248 After gathering, samplers are stored in the channels properties of the animation and need to be moved
249 to their own list while storing an index into this list at the position where they previously were.
250 This behaviour is similar to that of the glTFExporter that traverses all nodes
251 :param animation:
252 :param export_settings:
253 :return:
255 # TODO: move this to some util module and update gltf2 exporter also
256 T = typing.TypeVar('T')
258 def __append_unique_and_get_index(l: typing.List[T], item: T):
259 if item in l:
260 return l.index(item)
261 else:
262 index = len(l)
263 l.append(item)
264 return index
266 for i, channel in enumerate(animation.channels):
267 animation.channels[i].sampler = __append_unique_and_get_index(animation.samplers, channel.sampler)
270 def __get_blender_actions(blender_object: bpy.types.Object,
271 export_settings
272 ) -> typing.List[typing.Tuple[bpy.types.Action, str, str]]:
273 blender_actions = []
274 blender_tracks = {}
275 action_on_type = {}
277 export_user_extensions('pre_gather_actions_hook', export_settings, blender_object)
279 if blender_object.animation_data is not None:
280 # Collect active action.
281 if blender_object.animation_data.action is not None:
282 blender_actions.append(blender_object.animation_data.action)
283 blender_tracks[blender_object.animation_data.action.name] = None
284 action_on_type[blender_object.animation_data.action.name] = "OBJECT"
286 # Collect associated strips from NLA tracks.
287 if export_settings['gltf_nla_strips'] is True:
288 for track in blender_object.animation_data.nla_tracks:
289 # Multi-strip tracks do not export correctly yet (they need to be baked),
290 # so skip them for now and only write single-strip tracks.
291 non_muted_strips = [strip for strip in track.strips if strip.action is not None and strip.mute is False]
292 if track.strips is None or len(non_muted_strips) != 1:
293 continue
294 for strip in non_muted_strips:
295 blender_actions.append(strip.action)
296 blender_tracks[strip.action.name] = track.name # Always set after possible active action -> None will be overwrite
297 action_on_type[strip.action.name] = "OBJECT"
299 if blender_object.type == "MESH" \
300 and blender_object.data is not None \
301 and blender_object.data.shape_keys is not None \
302 and blender_object.data.shape_keys.animation_data is not None:
304 if blender_object.data.shape_keys.animation_data.action is not None:
305 blender_actions.append(blender_object.data.shape_keys.animation_data.action)
306 blender_tracks[blender_object.data.shape_keys.animation_data.action.name] = None
307 action_on_type[blender_object.data.shape_keys.animation_data.action.name] = "SHAPEKEY"
309 if export_settings['gltf_nla_strips'] is True:
310 for track in blender_object.data.shape_keys.animation_data.nla_tracks:
311 # Multi-strip tracks do not export correctly yet (they need to be baked),
312 # so skip them for now and only write single-strip tracks.
313 non_muted_strips = [strip for strip in track.strips if strip.action is not None and strip.mute is False]
314 if track.strips is None or len(non_muted_strips) != 1:
315 continue
316 for strip in non_muted_strips:
317 blender_actions.append(strip.action)
318 blender_tracks[strip.action.name] = track.name # Always set after possible active action -> None will be overwrite
319 action_on_type[strip.action.name] = "SHAPEKEY"
321 # If there are only 1 armature, include all animations, even if not in NLA
322 if export_settings['gltf_export_anim_single_armature'] is True:
323 if blender_object.type == "ARMATURE":
324 if len(export_settings['vtree'].get_all_node_of_type(VExportNode.ARMATURE)) == 1:
325 # Keep all actions on objects (no Shapekey animation)
326 for act in [a for a in bpy.data.actions if a.id_root == "OBJECT"]:
327 # We need to check this is an armature action
328 # Checking that at least 1 bone is animated
329 if not __is_armature_action(act):
330 continue
331 # Check if this action is already taken into account
332 if act.name in blender_tracks.keys():
333 continue
334 blender_actions.append(act)
335 blender_tracks[act.name] = None
336 action_on_type[act.name] = "OBJECT"
338 export_user_extensions('gather_actions_hook', export_settings, blender_object, blender_actions, blender_tracks, action_on_type)
340 # Remove duplicate actions.
341 blender_actions = list(set(blender_actions))
342 # sort animations alphabetically (case insensitive) so they have a defined order and match Blender's Action list
343 blender_actions.sort(key = lambda a: a.name.lower())
345 return [(blender_action, blender_tracks[blender_action.name], action_on_type[blender_action.name]) for blender_action in blender_actions]
348 def __is_armature_action(blender_action) -> bool:
349 for fcurve in blender_action.fcurves:
350 if is_bone_anim_channel(fcurve.data_path):
351 return True
352 return False