diff --git a/addons/io_scene_gltf2/__init__.py b/addons/io_scene_gltf2/__init__.py index e078177e3..e71879c53 100644 --- a/addons/io_scene_gltf2/__init__.py +++ b/addons/io_scene_gltf2/__init__.py @@ -737,11 +737,11 @@ class ExportGLTF2_Base(ConvertGLTF2_Base): default=False ) - export_optimize_armature_disable_viewport: BoolProperty( - name='Disable viewport if possible', + export_optimize_disable_viewport: BoolProperty( + name='Disable viewport for other objects', description=( - "When exporting armature, disable viewport for other objects, " - "for performance. Drivers on shape keys for skined meshes prevent this optimization for now" + "When exporting animations, disable viewport for other objects, " + "for performance" ), default=False ) @@ -1145,7 +1145,7 @@ def execute(self, context): export_settings['gltf_optimize_animation'] = self.export_optimize_animation_size export_settings['gltf_optimize_animation_keep_armature'] = self.export_optimize_animation_keep_anim_armature export_settings['gltf_optimize_animation_keep_object'] = self.export_optimize_animation_keep_anim_object - export_settings['gltf_optimize_armature_disable_viewport'] = self.export_optimize_armature_disable_viewport + export_settings['gltf_optimize_disable_viewport'] = self.export_optimize_disable_viewport export_settings['gltf_export_anim_single_armature'] = self.export_anim_single_armature export_settings['gltf_export_reset_pose_bones'] = self.export_reset_pose_bones export_settings['gltf_export_reset_sk_data'] = self.export_morph_reset_sk_data @@ -1161,7 +1161,7 @@ def execute(self, context): export_settings['gltf_optimize_animation'] = False export_settings['gltf_optimize_animation_keep_armature'] = False export_settings['gltf_optimize_animation_keep_object'] = False - export_settings['gltf_optimize_armature_disable_viewport'] = False + export_settings['gltf_optimize_disable_viewport'] = False export_settings['gltf_export_anim_single_armature'] = False export_settings['gltf_export_reset_pose_bones'] = False export_settings['gltf_export_reset_sk_data'] = False @@ -1638,7 +1638,7 @@ def export_panel_animation_optimize(layout, operator): row.prop(operator, 'export_optimize_animation_keep_anim_object') row = body.row() - row.prop(operator, 'export_optimize_armature_disable_viewport') + row.prop(operator, 'export_optimize_disable_viewport') def export_panel_animation_extra(layout, operator): diff --git a/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py b/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py index 73f9ff4c5..af8ce861b 100644 --- a/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py +++ b/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py @@ -28,7 +28,7 @@ from .sampled.shapekeys.gltf2_blender_gather_sk_action_sampled import gather_action_sk_sampled from .sampled.object.gltf2_blender_gather_object_channels import gather_object_sampled_channels, gather_sampled_object_channel from .sampled.shapekeys.gltf2_blender_gather_sk_channels import gather_sampled_sk_channel -from .gltf2_blender_gather_drivers import get_sk_drivers +from .gltf2_blender_gather_drivers import get_sk_drivers, get_driver_on_shapekey from .gltf2_blender_gather_animation_utils import reset_bone_matrix, reset_sk_data, link_samplers, add_slide_data, merge_tracks_perform, bake_animation def gather_actions_animations(export_settings): @@ -270,24 +270,36 @@ def gather_action_animations( obj_uuid: int, current_use_nla = blender_object.animation_data.use_nla blender_object.animation_data.use_nla = False - # Try to disable all except armature in viewport, for performance - if export_settings['gltf_optimize_armature_disable_viewport'] \ + # Disable all except armature in viewport, for performance + if export_settings['gltf_optimize_disable_viewport'] \ and export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE": - # If the skinned mesh has driver(s), we can't disable it to bake armature. - need_to_enable_again = False - sk_drivers = get_sk_drivers(obj_uuid, export_settings) - if len(sk_drivers) == 0: - need_to_enable_again = True - # Before baking, disabling from viewport all meshes - for obj in [n.blender_object for n in export_settings['vtree'].nodes.values() if n.blender_type in - [VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]: - obj.hide_viewport = True - export_settings['vtree'].nodes[obj_uuid].blender_object.hide_viewport = False - else: - export_settings['log'].warning("Can't disable viewport because of drivers") - export_settings['gltf_optimize_armature_disable_viewport'] = False # We changed the option here, so we don't need to re-check it later, during - + # Before baking, disabling from viewport all meshes + for obj in [n.blender_object for n in export_settings['vtree'].nodes.values() if n.blender_type in + [VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]: + obj.hide_viewport = True + export_settings['vtree'].nodes[obj_uuid].blender_object.hide_viewport = False + + # We need to create custom properties on armature to store shape keys drivers on disabled meshes + # This way, we can evaluate drivers on shape keys, and bake them + drivers = get_sk_drivers(obj_uuid, export_settings) + if drivers: + # So ... Let's create some costum properties and the armature + # First, retrieve the armature object + for mesh_uuid in drivers: + _, channels = get_driver_on_shapekey(mesh_uuid, export_settings) + blender_object["gltf_" + mesh_uuid] = [0.0] * len(channels) + for idx, channel in enumerate(channels): + if channel is None: + continue + if blender_object.animation_data is None or blender_object.animation_data.drivers is None: + # There is no animation on the armature, so no need to crate driver + # But, we need to copy the current value of the shape key to the custom property + blender_object["gltf_" + mesh_uuid][idx] = blender_object.data.shape_keys.key_blocks[channel.data_path.split('"')[1]].value + else: + dr = blender_object.animation_data.drivers.from_existing(src_driver=channel) + dr.data_path = "[\"gltf_" + mesh_uuid + "\"]" + dr.array_index = idx export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, False) @@ -464,14 +476,21 @@ def gather_action_animations( obj_uuid: int, if blender_object and current_world_matrix is not None: blender_object.matrix_world = current_world_matrix - if export_settings['gltf_optimize_armature_disable_viewport'] \ + if export_settings['gltf_optimize_disable_viewport'] \ and export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE": - if need_to_enable_again is True: - # And now, restoring meshes in viewport - for node, obj in [(n, n.blender_object) for n in export_settings['vtree'].nodes.values() if n.blender_type in - [VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]: - obj.hide_viewport = node.default_hide_viewport - export_settings['vtree'].nodes[obj_uuid].blender_object.hide_viewport = export_settings['vtree'].nodes[obj_uuid].default_hide_viewport + # And now, restoring meshes in viewport + for node, obj in [(n, n.blender_object) for n in export_settings['vtree'].nodes.values() if n.blender_type in + [VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]: + obj.hide_viewport = node.default_hide_viewport + export_settings['vtree'].nodes[obj_uuid].blender_object.hide_viewport = export_settings['vtree'].nodes[obj_uuid].default_hide_viewport + # Let's remove the custom properties, and first, remove drivers + drivers = get_sk_drivers(obj_uuid, export_settings) + if drivers: + for mesh_uuid in drivers: + for armature_driver in blender_object.animation_data.drivers: + if "gltf_" + mesh_uuid in armature_driver.data_path: + blender_object.animation_data.drivers.remove(armature_driver) + del blender_object["gltf_" + mesh_uuid] export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, True) diff --git a/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_drivers.py b/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_drivers.py index 5b11d5c40..007d2d90a 100644 --- a/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_drivers.py +++ b/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_drivers.py @@ -31,58 +31,62 @@ def get_sk_drivers(blender_armature_uuid, export_settings): children_list.extend(export_settings['vtree'].nodes[bone].children) for child_uuid in children_list: + uuid, channels = get_driver_on_shapekey(child_uuid, export_settings) + if uuid is not None: + drivers.append(child_uuid) - if export_settings['vtree'].nodes[child_uuid].blender_type == "BONE": - continue + return drivers - child = export_settings['vtree'].nodes[child_uuid].blender_object +def get_driver_on_shapekey(blender_object_uuid, export_settings): + if export_settings['vtree'].nodes[blender_object_uuid].blender_type == "BONE": + return None, None - if not child.data: - continue - # child.data can be an armature - which has no shapekeys - if not hasattr(child.data, 'shape_keys'): - continue - if not child.data.shape_keys: - continue - if not child.data.shape_keys.animation_data: - continue - if not child.data.shape_keys.animation_data.drivers: - continue - if len(child.data.shape_keys.animation_data.drivers) <= 0: - continue + child = export_settings['vtree'].nodes[blender_object_uuid].blender_object - shapekeys_idx = {} - cpt_sk = 0 - for sk in get_sk_exported(child.data.shape_keys.key_blocks): - shapekeys_idx[sk.name] = cpt_sk - cpt_sk += 1 + if not child.data: + return None, None + # child.data can be an armature - which has no shapekeys + if not hasattr(child.data, 'shape_keys'): + return None, None + if not child.data.shape_keys: + return None, None + if not child.data.shape_keys.animation_data: + return None, None + if not child.data.shape_keys.animation_data.drivers: + return None, None + if len(child.data.shape_keys.animation_data.drivers) <= 0: + return None, None - # 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 child.data.shape_keys.animation_data.drivers: - # Check if driver is valid. If not, ignore this driver channel - try: - # Check if driver is valid. - # Try/Except is no more a suffisant check, starting with version Blender 3.0, - # Blender crashes when trying to resolve path on invalid driver - if not sk_c.is_valid: - continue - sk_name = child.data.shape_keys.path_resolve(get_target_object_path(sk_c.data_path)).name - except: - continue - if skip_sk(child.data.shape_keys.key_blocks, child.data.shape_keys.key_blocks[sk_name]): - continue - 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]) + shapekeys_idx = {} + cpt_sk = 0 + for sk in get_sk_exported(child.data.shape_keys.key_blocks): + shapekeys_idx[sk.name] = cpt_sk + cpt_sk += 1 - # Checks there are some driver on SK, and that there is not only invalid drivers - if len(all_sorted_channels) > 0 and not all([i is None for i in all_sorted_channels]): - drivers.append(child_uuid) + # 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 child.data.shape_keys.animation_data.drivers: + # Check if driver is valid. If not, ignore this driver channel + try: + # Check if driver is valid. + # Try/Except is no more a suffisant check, starting with version Blender 3.0, + # Blender crashes when trying to resolve path on invalid driver + if not sk_c.is_valid: + return None, None + sk_name = child.data.shape_keys.path_resolve(get_target_object_path(sk_c.data_path)).name + except: + return None, None + if skip_sk(child.data.shape_keys.key_blocks, child.data.shape_keys.key_blocks[sk_name]): + return None, None + 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 drivers + # Checks there are some driver on SK, and that there is not only invalid drivers + if len(all_sorted_channels) > 0 and not all([i is None for i in all_sorted_channels]): + return blender_object_uuid, all_sorted_channels diff --git a/addons/io_scene_gltf2/blender/exp/animation/sampled/gltf2_blender_gather_animation_sampling_cache.py b/addons/io_scene_gltf2/blender/exp/animation/sampled/gltf2_blender_gather_animation_sampling_cache.py index 0189ec245..e507bb57d 100644 --- a/addons/io_scene_gltf2/blender/exp/animation/sampled/gltf2_blender_gather_animation_sampling_cache.py +++ b/addons/io_scene_gltf2/blender/exp/animation/sampled/gltf2_blender_gather_animation_sampling_cache.py @@ -53,10 +53,9 @@ def get_cache_data(path: str, # - Action mode, where some object have multiple actions # - For this case, on first call, we will cache active action for all objects # - On next calls, we will cache only the action of current object, so we can disable viewport for others - # For armature : We already checked that we can disable viewport (in case of drivers, this is currently not possible) need_to_enable_again = False - if export_settings['gltf_optimize_armature_disable_viewport'] is True and len(obj_uuids) == 1: + if export_settings['gltf_optimize_disable_viewport'] is True and len(obj_uuids) == 1: need_to_enable_again = True # Before baking, disabling from viewport all meshes for obj in [n.blender_object for n in export_settings['vtree'].nodes.values() if n.blender_type in @@ -65,7 +64,7 @@ def get_cache_data(path: str, continue obj.hide_viewport = True export_settings['vtree'].nodes[obj_uuids[0]].blender_object.hide_viewport = False - + # Work for drivers on shapekeys is already done at start of animation export depsgraph = bpy.context.evaluated_depsgraph_get() @@ -494,7 +493,11 @@ def object_caching(data, obj_uuids, current_instance, action_name, frame, depsgr if cache_sk: initialize_data_dict(data, key1, key2, key3, key4) - data[key1][key2][key3][key4][frame] = [k.value for k in get_sk_exported(driver_object.data.shape_keys.key_blocks)] + if export_settings['gltf_optimize_disable_viewport'] is True: + # Retrieve data from custom properties instead of shape keys + data[key1][key2][key3][key4][frame] = list(blender_obj['gltf_' + dr_obj]) # This include only exported SK + else: + data[key1][key2][key3][key4][frame] = [k.value for k in get_sk_exported(driver_object.data.shape_keys.key_blocks)] cache_sk = False def light_nodetree_caching(data, action_name, frame, export_settings): diff --git a/addons/io_scene_gltf2/blender/exp/animation/sampled/shapekeys/gltf2_blender_gather_sk_keyframes.py b/addons/io_scene_gltf2/blender/exp/animation/sampled/shapekeys/gltf2_blender_gather_sk_keyframes.py index e807fa6b1..0e53adedd 100644 --- a/addons/io_scene_gltf2/blender/exp/animation/sampled/shapekeys/gltf2_blender_gather_sk_keyframes.py +++ b/addons/io_scene_gltf2/blender/exp/animation/sampled/shapekeys/gltf2_blender_gather_sk_keyframes.py @@ -17,9 +17,11 @@ import numpy as np from ......blender.com.gltf2_blender_data_path import get_sk_exported from ....gltf2_blender_gather_cache import cached +from ....gltf2_blender_gather_tree import VExportNode from ...gltf2_blender_gather_keyframes import Keyframe from ...fcurves.gltf2_blender_gather_fcurves_channels import get_channel_groups from ...fcurves.gltf2_blender_gather_fcurves_keyframes import gather_non_keyed_values +from ...gltf2_blender_gather_drivers import get_driver_on_shapekey from ..gltf2_blender_gather_animation_sampling_cache import get_cache_data @@ -38,34 +40,59 @@ def gather_sk_sampled_keyframes(obj_uuid, blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object - if export_settings['gltf_optimize_armature_disable_viewport'] is True: - # Using this option, we miss the drivers :( - # No solution exists for now. In the future, we should be able to copy a driver - if action_name in bpy.data.actions: - channel_group, _, _ = get_channel_groups(obj_uuid, bpy.data.actions[action_name], export_settings, no_sample_option=True) - elif blender_obj.data.shape_keys.animation_data and blender_obj.data.shape_keys.animation_data.action: - channel_group, _, _ = get_channel_groups(obj_uuid, blender_obj.data.shape_keys.animation_data.action, export_settings, no_sample_option=True) - else: - channel_group = {} - channels = [None] * len(get_sk_exported(blender_obj.data.shape_keys.key_blocks)) + if export_settings['gltf_optimize_disable_viewport'] is True: - # One day, if we will be able to bake drivers or evaluate it the right way, we can add here the driver fcurves + # First, check if there are drivers on shapekeys + drs, channels = get_driver_on_shapekey(obj_uuid, export_settings) - for chan in channel_group.values(): - channels = chan['properties']['value'] - break + if channels is None: - non_keyed_values = gather_non_keyed_values(obj_uuid, channels, None, False, export_settings) + # If we are here because of a shapekey animation, we need to get the fcurves + if action_name in bpy.data.actions: + channel_group, _, _ = get_channel_groups(obj_uuid, bpy.data.actions[action_name], export_settings, no_sample_option=True) + elif blender_obj.data.shape_keys.animation_data and blender_obj.data.shape_keys.animation_data.action: + channel_group, _, _ = get_channel_groups(obj_uuid, blender_obj.data.shape_keys.animation_data.action, export_settings, no_sample_option=True) + else: + channel_group = {} + channels = [None] * len(get_sk_exported(blender_obj.data.shape_keys.key_blocks)) - while frame <= end_frame: - key = Keyframe(channels, frame, None) - key.value = [c.evaluate(frame) for c in channels if c is not None] - # Complete key with non keyed values, if needed - if len([c for c in channels if c is not None]) != key.get_target_len(): - complete_key(key, non_keyed_values) + for chan in channel_group.values(): + channels = chan['properties']['value'] + break + + non_keyed_values = gather_non_keyed_values(obj_uuid, channels, None, False, export_settings) + + while frame <= end_frame: + key = Keyframe(channels, frame, None) + key.value = [c.evaluate(frame) for c in channels if c is not None] + # Complete key with non keyed values, if needed + if len([c for c in channels if c is not None]) != key.get_target_len(): + complete_key(key, non_keyed_values) + + keyframes.append(key) + frame += step + + else: + # So, drivers will be evaluated, on the custom property + + non_keyed_values = gather_non_keyed_values(obj_uuid, channels, None, False, export_settings) + + # The bake tool will store the value of the custom property + while frame <= end_frame: + key = Keyframe([None] * (len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))), frame, 'value') + key.value_total = get_cache_data( + 'sk', + obj_uuid, + None, + action_name, + frame, + step, + export_settings + ) + + keyframes.append(key) + frame += step - keyframes.append(key) - frame += step else: # Full bake, we will go frame by frame. This can take time (more than using evaluate) diff --git a/docs/blender_docs/scene_gltf2.rst b/docs/blender_docs/scene_gltf2.rst index 9a2fe693f..cb1a11bf4 100644 --- a/docs/blender_docs/scene_gltf2.rst +++ b/docs/blender_docs/scene_gltf2.rst @@ -1076,7 +1076,9 @@ Optimize Animation Size Force keeping channel for armature / bones if all keyframes are identical in a rig, force keeping the minimal animation. Force keeping channel for objects - if all keyframes are identical for object transformations, force keeping the minimal animation + if all keyframes are identical for object transformations, force keeping the minimal animation. +Disable viewport for other objects + When exporting animations, disable viewport for other objects, for performance reasons, when possible. Animation - Filter ^^^^^^^^^^^^^^^^^^ diff --git a/tests/scenes/25_drivers_disable_viewport.blend b/tests/scenes/25_drivers_disable_viewport.blend new file mode 100644 index 000000000..7f197c230 Binary files /dev/null and b/tests/scenes/25_drivers_disable_viewport.blend differ