From 782f8585f4cc131a7043269ed5ccb14a36742e3d Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Wed, 2 Mar 2022 21:36:18 +0100 Subject: glTF exporter: Big refactoring - precompute tree before export - manage collections / instances / linked - use custom cache to avoid name collision - animations are baked from world matrix More info on https://github.com/KhronosGroup/glTF-Blender-IO --- io_scene_gltf2/__init__.py | 4 +- io_scene_gltf2/blender/com/gltf2_blender_math.py | 18 +- .../blender/exp/gltf2_blender_export_keys.py | 1 + .../blender/exp/gltf2_blender_extract.py | 8 +- io_scene_gltf2/blender/exp/gltf2_blender_gather.py | 52 ++- ...ltf2_blender_gather_animation_channel_target.py | 35 +- .../exp/gltf2_blender_gather_animation_channels.py | 131 ++++--- ...2_blender_gather_animation_sampler_keyframes.py | 196 ++++++++--- .../exp/gltf2_blender_gather_animation_samplers.py | 101 ++++-- .../blender/exp/gltf2_blender_gather_animations.py | 78 ++++- .../blender/exp/gltf2_blender_gather_cache.py | 184 ++++++---- .../blender/exp/gltf2_blender_gather_drivers.py | 18 +- .../blender/exp/gltf2_blender_gather_joints.py | 78 +++-- .../blender/exp/gltf2_blender_gather_materials.py | 10 +- .../blender/exp/gltf2_blender_gather_mesh.py | 71 ++-- .../blender/exp/gltf2_blender_gather_nodes.py | 329 +++++------------- .../blender/exp/gltf2_blender_gather_primitives.py | 49 ++- .../blender/exp/gltf2_blender_gather_skins.py | 143 +++----- .../blender/exp/gltf2_blender_gather_tree.py | 375 +++++++++++++++++++++ 19 files changed, 1244 insertions(+), 637 deletions(-) create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py (limited to 'io_scene_gltf2') diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index a672be22..1b3c0cc2 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -4,7 +4,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (3, 2, 7), + "version": (3, 2, 8), 'blender': (3, 1, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -879,6 +879,8 @@ class GLTF_PT_export_animation_export(bpy.types.Panel): row = layout.row() row.active = operator.export_force_sampling row.prop(operator, 'export_def_bones') + if operator.export_force_sampling is False and operator.export_def_bones is True: + layout.label(text="Export only deformation bones is not possible when not sampling animation") class GLTF_PT_export_animation_shapekeys(bpy.types.Panel): diff --git a/io_scene_gltf2/blender/com/gltf2_blender_math.py b/io_scene_gltf2/blender/com/gltf2_blender_math.py index 0498e8f8..d2a018a8 100755 --- a/io_scene_gltf2/blender/com/gltf2_blender_math.py +++ b/io_scene_gltf2/blender/com/gltf2_blender_math.py @@ -98,7 +98,7 @@ def swizzle_yup_value(value: typing.Any) -> typing.Any: return value -def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4)) -> typing \ +def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4), need_rotation_correction: bool = False) -> typing \ .Union[Vector, Quaternion]: """Manage transformations.""" target = get_target_property_name(data_path) @@ -116,25 +116,31 @@ def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Ma if transform_func is None: raise RuntimeError("Cannot transform values at {}".format(data_path)) - return transform_func(v, transform) + return transform_func(v, transform, need_rotation_correction) -def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector: +def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4), need_rotation_correction:bool = False) -> Vector: """Transform location.""" + correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0)) m = Matrix.Translation(location) + if need_rotation_correction: + m @= correction.to_matrix().to_4x4() m = transform @ m return m.to_translation() -def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4)) -> Quaternion: +def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4), need_rotation_correction: bool = False) -> Quaternion: """Transform rotation.""" rotation.normalize() + correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0)) m = rotation.to_matrix().to_4x4() + if need_rotation_correction: + m @= correction.to_matrix().to_4x4() m = transform @ m return m.to_quaternion() -def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector: +def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4), need_rotation_correction: bool = False) -> Vector: """Transform scale.""" m = Matrix.Identity(4) m[0][0] = scale.x @@ -145,7 +151,7 @@ def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Ve return m.to_scale() -def transform_value(value: Vector, _: Matrix = Matrix.Identity(4)) -> Vector: +def transform_value(value: Vector, _: Matrix = Matrix.Identity(4), need_rotation_correction: bool = False) -> Vector: """Transform value.""" return value diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py index 61a9f5bf..812db3f9 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py @@ -19,6 +19,7 @@ VISIBLE = 'gltf_visible' RENDERABLE = 'gltf_renderable' ACTIVE_COLLECTION = 'gltf_active_collection' SKINS = 'gltf_skins' +DEF_BONES_ONLY = 'gltf_def_bones' DISPLACEMENT = 'gltf_displacement' FORCE_SAMPLING = 'gltf_force_sampling' FRAME_RANGE = 'gltf_frame_range' diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index f5b69f13..d81bd706 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -9,10 +9,14 @@ from ...io.com.gltf2_io_debug import print_console from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins -def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vertex_groups, modifiers, export_settings): +def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings): """Extract primitives from a mesh.""" print_console('INFO', 'Extracting primitive: ' + blender_mesh.name) + blender_object = None + if uuid_for_skined_data: + blender_object = export_settings['vtree'].nodes[uuid_for_skined_data].blender_object + use_normals = export_settings[gltf2_blender_export_keys.NORMALS] if use_normals: blender_mesh.calc_normals_split() @@ -57,7 +61,7 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert armature = None if armature: - skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings) + skin = gltf2_blender_gather_skins.gather_skin(export_settings['vtree'].nodes[uuid_for_skined_data].armature, export_settings) if not skin: armature = None diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py index 31c0fa62..f515da8c 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py @@ -7,10 +7,12 @@ 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 import gltf2_blender_gather_animation_sampler_keyframes from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from ..com.gltf2_blender_extras import generate_extras from io_scene_gltf2.blender.exp import gltf2_blender_export_keys from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions +from io_scene_gltf2.blender.exp import gltf2_blender_gather_tree def gather_gltf2(export_settings): @@ -22,12 +24,18 @@ def gather_gltf2(export_settings): scenes = [] animations = [] # unfortunately animations in gltf2 are just as 'root' as scenes. active_scene = None + store_user_scene = bpy.context.scene for blender_scene in bpy.data.scenes: scenes.append(__gather_scene(blender_scene, export_settings)) if export_settings[gltf2_blender_export_keys.ANIMATIONS]: + # resetting object cache + gltf2_blender_gather_animation_sampler_keyframes.get_object_matrix.reset_cache() animations += __gather_animations(blender_scene, export_settings) if bpy.context.scene.name == blender_scene.name: active_scene = len(scenes) -1 + + # restore user scene + bpy.context.window.scene = store_user_scene return active_scene, scenes, animations @@ -40,14 +48,25 @@ def __gather_scene(blender_scene, export_settings): nodes=[] ) - for blender_object in blender_scene.objects: - if blender_object.parent is None: - node = gltf2_blender_gather_nodes.gather_node( - blender_object, - blender_object.library.name if blender_object.library else None, - blender_scene, None, export_settings) - if node is not None: - scene.nodes.append(node) + + vtree = gltf2_blender_gather_tree.VExportTree(export_settings) + vtree.construct(blender_scene) + vtree.search_missing_armature() # In case armature are no parented correctly + + export_user_extensions('vtree_before_filter_hook', export_settings, vtree) + + # Now, we can filter tree if needed + vtree.filter() + + export_user_extensions('vtree_after_filter_hook', export_settings, vtree) + + export_settings['vtree'] = vtree + + for r in [vtree.nodes[r] for r in vtree.roots]: + node = gltf2_blender_gather_nodes.gather_node( + r, export_settings) + if node is not None: + scene.nodes.append(node) export_user_extensions('gather_scene_hook', export_settings, scene, blender_scene) @@ -58,15 +77,16 @@ def __gather_animations(blender_scene, export_settings): animations = [] merged_tracks = {} - for blender_object in blender_scene.objects: + vtree = export_settings['vtree'] + for obj_uuid in vtree.get_all_objects(): + blender_object = vtree.nodes[obj_uuid].blender_object + + # Do not manage not exported objects + if vtree.nodes[obj_uuid].node is None: + continue - # 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_object.library.name if blender_object.library else None, - blender_scene, None, export_settings) - if obj_node is not None: - animations_, merged_tracks = gltf2_blender_gather_animations.gather_animations(blender_object, merged_tracks, len(animations), export_settings) - animations += animations_ + animations_, merged_tracks = gltf2_blender_gather_animations.gather_animations(obj_uuid, merged_tracks, len(animations), export_settings) + animations += animations_ if export_settings['gltf_nla_strips'] is False: # Fake an animation with all animations of the scene diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py index 928fa14a..0e542de8 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py @@ -12,18 +12,20 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions @cached -def gather_animation_channel_target(channels: typing.Tuple[bpy.types.FCurve], - blender_object: bpy.types.Object, +def gather_animation_channel_target(obj_uuid: int, + channels: typing.Tuple[bpy.types.FCurve], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], - driver_obj, + driver_obj_uuid, export_settings ) -> gltf2_io.AnimationChannelTarget: + blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object + animation_channel_target = gltf2_io.AnimationChannelTarget( extensions=__gather_extensions(channels, blender_object, export_settings, bake_bone), extras=__gather_extras(channels, blender_object, export_settings, bake_bone), - node=__gather_node(channels, blender_object, export_settings, bake_bone, driver_obj), + node=__gather_node(channels, obj_uuid, export_settings, bake_bone, driver_obj_uuid), path=__gather_path(channels, blender_object, export_settings, bake_bone, bake_channel) ) @@ -54,16 +56,16 @@ def __gather_extras(channels: typing.Tuple[bpy.types.FCurve], def __gather_node(channels: typing.Tuple[bpy.types.FCurve], - blender_object: bpy.types.Object, + obj_uuid: str, export_settings, bake_bone: typing.Union[str, None], - driver_obj + driver_obj_uuid ) -> gltf2_io.Node: - if driver_obj is not None: - return gltf2_blender_gather_nodes.gather_node(driver_obj, - driver_obj.library.name if driver_obj.library else None, - None, None, export_settings) + blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object + + if driver_obj_uuid is not None: + return export_settings['vtree'].nodes[driver_obj_uuid].node if blender_object.type == "ARMATURE": # TODO: get joint from fcurve data_path and gather_joint @@ -74,16 +76,9 @@ def __gather_node(channels: typing.Tuple[bpy.types.FCurve], blender_bone = blender_object.path_resolve(channels[0].data_path.rsplit('.', 1)[0]) if isinstance(blender_bone, bpy.types.PoseBone): - if export_settings["gltf_def_bones"] is False: - return gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings) - else: - bones, _, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_object) - if blender_bone.name in [b.name for b in bones]: - return gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings) - - return gltf2_blender_gather_nodes.gather_node(blender_object, - blender_object.library.name if blender_object.library else None, - None, None, export_settings) + return gltf2_blender_gather_joints.gather_joint_vnode(export_settings['vtree'].nodes[obj_uuid].bones[blender_bone.name], export_settings) + + return export_settings['vtree'].nodes[obj_uuid].node def __gather_path(channels: typing.Tuple[bpy.types.FCurve], diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py index 4c79092c..87ef7c13 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py @@ -15,15 +15,18 @@ 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(blender_action: bpy.types.Action, - blender_object: bpy.types.Object, +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 @@ -59,11 +62,8 @@ def gather_animation_channels(blender_action: bpy.types.Action, # Then bake all bones bones_to_be_animated = [] - if export_settings["gltf_def_bones"] is False: - bones_to_be_animated = blender_object.data.bones - else: - bones_to_be_animated, _, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_object) - bones_to_be_animated = [blender_object.pose.bones[b.name] for b in 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): @@ -72,9 +72,9 @@ def gather_animation_channels(blender_action: bpy.types.Action, for bone in bones_to_be_animated: for p in ["location", "rotation_quaternion", "scale"]: - channel = __gather_animation_channel( + channel = gather_animation_channel( + obj_uuid, (), - blender_object, export_settings, bone.name, p, @@ -95,17 +95,17 @@ def gather_animation_channels(blender_action: bpy.types.Action, if len(channel_group) == 0: # Only errors on channels, ignoring continue - channel = __gather_animation_channel(channel_group, blender_object, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, None, True) + 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(blender_object) - for obj, fcurves in drivers_to_manage: - channel = __gather_animation_channel( + 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, - blender_object, export_settings, None, None, @@ -113,31 +113,77 @@ def gather_animation_channels(blender_action: bpy.types.Action, bake_range_end, force_range, blender_action.name, - obj, - False) + 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( - channel_group_sorted, - blender_object, + 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, - None, - bake_range_start, - bake_range_end, + p, + start_frame, + end_frame, force_range, blender_action.name, None, - False) - if channel is not None: - channels.append(channel) + False #If Object is not animated, don't keep animation for this channel + ) + + if channel is not None: + channels.append(channel) + # resetting driver caches @@ -198,8 +244,9 @@ def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender # if not shapekeys, stay in same order, because order doesn't matter return channels -def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve], - blender_object: bpy.types.Object, +# 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], @@ -207,15 +254,18 @@ def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve], bake_range_end, force_range: bool, action_name: str, - driver_obj, + 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(channels, blender_object, export_settings, bake_bone, bake_channel, driver_obj) + __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, blender_object, export_settings, bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj, node_channel_is_animated) + 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 @@ -268,7 +318,7 @@ def __gather_extras(channels: typing.Tuple[bpy.types.FCurve], def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve], - blender_object: bpy.types.Object, + obj_uuid: str, export_settings, bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], @@ -276,33 +326,38 @@ def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve], bake_range_end, force_range: bool, action_name, - driver_obj, + 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, - blender_object, + obj_uuid, bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, - driver_obj, + driver_obj_uuid, node_channel_is_animated, + need_rotation_correction, export_settings ) -def __gather_target(channels: typing.Tuple[bpy.types.FCurve], - blender_object: bpy.types.Object, +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 + driver_obj_uuid ) -> gltf2_io.AnimationChannelTarget: return gltf2_blender_gather_animation_channel_target.gather_animation_channel_target( - channels, blender_object, bake_bone, bake_channel, driver_obj, export_settings) + 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): diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py index d24db395..cd836682 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py @@ -5,12 +5,13 @@ import bpy import mathutils import typing -from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, bonecache +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, bonecache, objectcache from io_scene_gltf2.blender.com import gltf2_blender_math from io_scene_gltf2.blender.exp import gltf2_blender_get from io_scene_gltf2.blender.exp.gltf2_blender_gather_drivers import get_sk_drivers, get_sk_driver_values from . import gltf2_blender_export_keys from io_scene_gltf2.io.com import gltf2_io_debug +from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode import numpy as np @@ -95,6 +96,10 @@ class Keyframe: def value(self, value: typing.List[float]): self.__value = self.__set_indexed(value) + @value.setter + def value_total(self, value: typing.List[float]): + self.__value = value + @property def in_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]: if self.__in_tangent is None: @@ -120,9 +125,75 @@ class Keyframe: self.__out_tangent = self.__set_indexed(value) +@objectcache +def get_object_matrix(blender_obj_uuid: str, + action_name: str, + bake_range_start: int, + bake_range_end: int, + current_frame: int, + step: int, + export_settings + ): + + data = {} + + # TODO : bake_range_start & bake_range_end are no more needed here + # Because we bake, we don't know exactly the frame range, + # So using min / max of all actions + + 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]]) + + frame = start_frame + while frame <= end_frame: + bpy.context.scene.frame_set(int(frame)) + + for obj_uuid in [uid for (uid, n) in export_settings['vtree'].nodes.items() if n.blender_type not in [VExportNode.BONE]]: + blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object + + # if this object is not animated, do not skip : + # We need this object too in case of bake + + # calculate local matrix + if export_settings['vtree'].nodes[obj_uuid].parent_uuid is None: + parent_mat = mathutils.Matrix.Identity(4).freeze() + else: + if export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_uuid].blender_type not in [VExportNode.BONE]: + parent_mat = export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_uuid].blender_object.matrix_world + else: + # Object animated is parented to a bone + blender_bone = export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_bone_uuid].blender_bone + armature_object = export_settings['vtree'].nodes[export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_bone_uuid].armature].blender_object + axis_basis_change = mathutils.Matrix( + ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + + parent_mat = armature_object.matrix_world @ blender_bone.matrix @ axis_basis_change + + #For object inside collection (at root), matrix world is already expressed regarding collection parent + if export_settings['vtree'].nodes[obj_uuid].parent_uuid is not None and export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_uuid].blender_type == VExportNode.COLLECTION: + parent_mat = mathutils.Matrix.Identity(4).freeze() + + mat = parent_mat.inverted_safe() @ blender_obj.matrix_world + + if obj_uuid not in data.keys(): + data[obj_uuid] = {} + + if blender_obj.animation_data and blender_obj.animation_data.action: + if blender_obj.animation_data.action.name not in data[obj_uuid].keys(): + data[obj_uuid][blender_obj.animation_data.action.name] = {} + data[obj_uuid][blender_obj.animation_data.action.name][frame] = mat + else: + # case of baking selected object. + # There is no animation, so use uuid of object as key + if obj_uuid not in data[obj_uuid].keys(): + data[obj_uuid][obj_uuid] = {} + data[obj_uuid][obj_uuid][frame] = mat + + frame += step + return data @bonecache -def get_bone_matrix(blender_object_if_armature: typing.Optional[bpy.types.Object], +def get_bone_matrix(blender_obj_uuid_if_armature: typing.Optional[str], channels: typing.Tuple[bpy.types.FCurve], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], @@ -130,9 +201,11 @@ def get_bone_matrix(blender_object_if_armature: typing.Optional[bpy.types.Object bake_range_end, action_name: str, current_frame: int, - step: int + step: int, + export_settings ): + blender_object_if_armature = export_settings['vtree'].nodes[blender_obj_uuid_if_armature].blender_object if blender_obj_uuid_if_armature is not None else None data = {} # Always using bake_range, because some bones may need to be baked, @@ -145,35 +218,40 @@ def get_bone_matrix(blender_object_if_armature: typing.Optional[bpy.types.Object frame = start_frame while frame <= end_frame: data[frame] = {} - # we need to bake in the constraints bpy.context.scene.frame_set(int(frame)) - for pbone in blender_object_if_armature.pose.bones: - if bake_bone is None: - matrix = pbone.matrix_basis.copy() + bones = export_settings['vtree'].get_all_bones(blender_obj_uuid_if_armature) + + for bone_uuid in bones: + blender_bone = export_settings['vtree'].nodes[bone_uuid].blender_bone + + if export_settings['vtree'].nodes[bone_uuid].parent_uuid is not None and export_settings['vtree'].nodes[export_settings['vtree'].nodes[bone_uuid].parent_uuid].blender_type == VExportNode.BONE: + blender_bone_parent = export_settings['vtree'].nodes[export_settings['vtree'].nodes[bone_uuid].parent_uuid].blender_bone + rest_mat = blender_bone_parent.bone.matrix_local.inverted_safe() @ blender_bone.bone.matrix_local + matrix = rest_mat.inverted_safe() @ blender_bone_parent.matrix.inverted_safe() @ blender_bone.matrix else: - if (pbone.bone.use_inherit_rotation == False or pbone.bone.inherit_scale != "FULL") and pbone.parent != None: - rest_mat = (pbone.parent.bone.matrix_local.inverted_safe() @ pbone.bone.matrix_local) - matrix = (rest_mat.inverted_safe() @ pbone.parent.matrix.inverted_safe() @ pbone.matrix) + if blender_bone.parent is None: + matrix = blender_bone.bone.matrix_local.inverted_safe() @ blender_bone.matrix else: - matrix = pbone.matrix - matrix = blender_object_if_armature.convert_space(pose_bone=pbone, matrix=matrix, from_space='POSE', to_space='LOCAL') - + # Bone has a parent, but in export, after filter, is at root of armature + matrix = blender_bone.matrix.copy() - data[frame][pbone.name] = matrix + data[frame][blender_bone.name] = matrix # If some drivers must be evaluated, do it here, to avoid to have to change frame by frame later - drivers_to_manage = get_sk_drivers(blender_object_if_armature) - for dr_obj, dr_fcurves in drivers_to_manage: - vals = get_sk_driver_values(dr_obj, frame, dr_fcurves) + drivers_to_manage = get_sk_drivers(blender_obj_uuid_if_armature, export_settings) + for dr_obj_uuid, dr_fcurves in drivers_to_manage: + vals = get_sk_driver_values(dr_obj_uuid, frame, dr_fcurves, export_settings) frame += step return data # cache for performance reasons +# This function is called 2 times, for input (timing) and output (key values) @cached -def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Object], +def gather_keyframes(blender_obj_uuid: str, + is_armature: bool, channels: typing.Tuple[bpy.types.FCurve], non_keyed_values: typing.Tuple[typing.Optional[float]], bake_bone: typing.Union[str, None], @@ -182,32 +260,40 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec bake_range_end, force_range: bool, action_name: str, - driver_obj, + driver_obj_uuid, node_channel_is_animated: bool, export_settings - ) -> typing.List[Keyframe]: + ) -> typing.Tuple[typing.List[Keyframe], bool]: """Convert the blender action groups' fcurves to keyframes for use in glTF.""" + + blender_object_if_armature = export_settings['vtree'].nodes[blender_obj_uuid].blender_object if is_armature is True is not None else None + blender_obj_uuid_if_armature = blender_obj_uuid if is_armature is True else None + if force_range is True: start_frame = bake_range_start end_frame = bake_range_end else: - if bake_bone is None and driver_obj is None: + if bake_bone is None and driver_obj_uuid is None: # Find the start and end of the whole action group # Note: channels has some None items only for SK if some SK are not animated ranges = [channel.range() for channel in channels if channel is not None] - start_frame = min([channel.range()[0] for channel in channels if channel is not None]) - end_frame = max([channel.range()[1] for channel in channels if channel is not None]) + if len(channels) != 0: + start_frame = min([channel.range()[0] for channel in channels if channel is not None]) + end_frame = max([channel.range()[1] for channel in channels if channel is not None]) + else: + start_frame = bake_range_start + end_frame = bake_range_end else: start_frame = bake_range_start end_frame = bake_range_end keyframes = [] - if needs_baking(blender_object_if_armature, channels, export_settings): + baking_is_needed = needs_baking(blender_object_if_armature, channels, export_settings) + if baking_is_needed: # Bake the animation, by evaluating the animation for all frames - # TODO: maybe baking can also be done with FCurve.convert_to_samples - if blender_object_if_armature is not None and driver_obj is None: + if blender_object_if_armature is not None and driver_obj_uuid is None: if bake_bone is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) @@ -224,7 +310,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec if isinstance(pose_bone_if_armature, bpy.types.PoseBone): mat = get_bone_matrix( - blender_object_if_armature, + blender_obj_uuid_if_armature, channels, bake_bone, bake_channel, @@ -232,7 +318,8 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec bake_range_end, action_name, frame, - step + step, + export_settings ) trans, rot, scale = mat.decompose() @@ -248,12 +335,36 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec "scale": scale }[target_property] else: - if driver_obj is None: - # Note: channels has some None items only for SK if some SK are not animated - key.value = [c.evaluate(frame) for c in channels if c is not None] - complete_key(key, non_keyed_values) + if driver_obj_uuid is None: + # If channel is TRS, we bake from world matrix, else this is SK + if len(channels) != 0: + target = [c for c in channels if c is not None][0].data_path.split('.')[-1] + else: + target = bake_channel + if target == "value": #SK + # Note: channels has some None items only for SK if some SK are not animated + key.value = [c.evaluate(frame) for c in channels if c is not None] + complete_key(key, non_keyed_values) + else: + + mat = get_object_matrix(blender_obj_uuid, + action_name, + bake_range_start, + bake_range_end, + frame, + step, + export_settings) + + trans, rot, sca = mat.decompose() + key.value_total = { + "location": trans, + "rotation_axis_angle": [rot.to_axis_angle()[1], rot.to_axis_angle()[0][0], rot.to_axis_angle()[0][1], rot.to_axis_angle()[0][2]], + "rotation_euler": rot.to_euler(), + "rotation_quaternion": rot, + "scale": sca + }[target] else: - key.value = get_sk_driver_values(driver_obj, frame, channels) + key.value = get_sk_driver_values(driver_obj_uuid, frame, channels, export_settings) complete_key(key, non_keyed_values) keyframes.append(key) frame += step @@ -307,7 +418,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec keyframes.append(key) if not export_settings[gltf2_blender_export_keys.OPTIMIZE_ANIMS]: - return keyframes + return (keyframes, baking_is_needed) # For armature only # Check if all values are the same @@ -319,17 +430,20 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec if node_channel_is_animated is True: # fcurve on this bone for this property # Keep animation, but keep only 2 keyframes if data are not changing - return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes + return ([keyframes[0], keyframes[-1]], baking_is_needed) if cst is True and len(keyframes) >= 2 else (keyframes, baking_is_needed) else: # bone is not animated (no fcurve) # Not keeping if not changing property - return None if cst is True else keyframes + return (None, baking_is_needed) if cst is True else (keyframes, baking_is_needed) else: # For objects, if all values are the same, we keep only first and last cst = fcurve_is_constant(keyframes) - return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes - + if node_channel_is_animated is True: + return ([keyframes[0], keyframes[-1]], baking_is_needed) if cst is True and len(keyframes) >= 2 else (keyframes, baking_is_needed) + else: + # baked object (selected but not animated) + return (None, baking_is_needed) if cst is True else (keyframes, baking_is_needed) - return keyframes + return (keyframes, baking_is_needed) def fcurve_is_constant(keyframes): @@ -374,6 +488,10 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: return True + # If tree is troncated, sampling is forced + if export_settings['vtree'].tree_troncated is True: + return True + # Sampling due to unsupported interpolation interpolation = [c for c in channels if c is not None][0].keyframe_points[0].interpolation if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]: diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py index b3cc9d30..143fccea 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py @@ -3,6 +3,7 @@ import typing +from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode import bpy import mathutils @@ -21,20 +22,23 @@ from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extension @cached def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve], - blender_object: bpy.types.Object, + obj_uuid: str, 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, + driver_obj_uuid, node_channel_is_animated: bool, + need_rotation_correction, export_settings ) -> gltf2_io.AnimationSampler: - blender_object_if_armature = blender_object if blender_object.type == "ARMATURE" else None - if blender_object_if_armature is not None and driver_obj is None: + blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object + is_armature = True if blender_object.type == "ARMATURE" else False + blender_object_if_armature = blender_object if is_armature is True else None + if is_armature is True and driver_obj_uuid is None: if bake_bone is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) @@ -45,15 +49,15 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve], non_keyed_values = __gather_non_keyed_values(channels, blender_object, blender_object_if_armature, pose_bone_if_armature, bake_channel, - driver_obj, + driver_obj_uuid, export_settings) if blender_object.parent is not None: matrix_parent_inverse = blender_object.matrix_parent_inverse.copy().freeze() else: matrix_parent_inverse = mathutils.Matrix.Identity(4).freeze() - input = __gather_input(channels, blender_object_if_armature, non_keyed_values, - bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj, node_channel_is_animated, export_settings) + input = __gather_input(channels, obj_uuid, is_armature, non_keyed_values, + bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj_uuid, node_channel_is_animated, export_settings) if input is None: # After check, no need to animate this node for this channel @@ -66,7 +70,8 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve], interpolation=__gather_interpolation(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), output=__gather_output(channels, matrix_parent_inverse, - blender_object_if_armature, + obj_uuid, + is_armature, non_keyed_values, bake_bone, bake_channel, @@ -74,8 +79,9 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve], bake_range_end, force_range, action_name, - driver_obj, + driver_obj_uuid, node_channel_is_animated, + need_rotation_correction, export_settings) ) @@ -97,12 +103,13 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve], blender_object_if_armature: typing.Optional[bpy.types.Object], pose_bone_if_armature: typing.Optional[bpy.types.PoseBone], bake_channel: typing.Union[str, None], - driver_obj, + driver_obj_uuid, export_settings ) -> typing.Tuple[typing.Optional[float]]: non_keyed_values = [] + driver_obj = export_settings['vtree'].nodes[driver_obj_uuid].blender_object if driver_obj_uuid is not None else None obj = blender_object if driver_obj is None else driver_obj # Note: channels has some None items only for SK if some SK are not animated @@ -217,10 +224,10 @@ def __gather_extras(channels: typing.Tuple[bpy.types.FCurve], ) -> typing.Any: return None - @cached def __gather_input(channels: typing.Tuple[bpy.types.FCurve], - blender_object_if_armature: typing.Optional[bpy.types.Object], + blender_obj_uuid: str, + is_armature: bool, non_keyed_values: typing.Tuple[typing.Optional[float]], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], @@ -228,12 +235,13 @@ def __gather_input(channels: typing.Tuple[bpy.types.FCurve], bake_range_end, force_range: bool, action_name, - driver_obj, + driver_obj_uuid, node_channel_is_animated: bool, export_settings ) -> gltf2_io.Accessor: """Gather the key time codes.""" - keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_object_if_armature, + keyframes, is_baked = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_obj_uuid, + is_armature, channels, non_keyed_values, bake_bone, @@ -242,7 +250,7 @@ def __gather_input(channels: typing.Tuple[bpy.types.FCurve], bake_range_end, force_range, action_name, - driver_obj, + driver_obj_uuid, node_channel_is_animated, export_settings) if keyframes is None: @@ -277,14 +285,15 @@ def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve], # TODO: check if the bone was animated with CONSTANT return 'LINEAR' else: - max_keyframes = max([len(ch.keyframe_points) for ch in channels if ch is not None]) - # If only single keyframe revert to STEP - if max_keyframes < 2: - return 'STEP' + if len(channels) != 0: # channels can be empty when baking object (non animated selected object) + max_keyframes = max([len(ch.keyframe_points) for ch in channels if ch is not None]) + # If only single keyframe revert to STEP + if max_keyframes < 2: + return 'STEP' - # If all keyframes are CONSTANT, we can use STEP. - if all(all(k.interpolation == 'CONSTANT' for k in c.keyframe_points) for c in channels if c is not None): - return 'STEP' + # If all keyframes are CONSTANT, we can use STEP. + if all(all(k.interpolation == 'CONSTANT' for k in c.keyframe_points) for c in channels if c is not None): + return 'STEP' # Otherwise, sampled keyframes use LINEAR interpolation. return 'LINEAR' @@ -304,7 +313,8 @@ def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve], @cached def __gather_output(channels: typing.Tuple[bpy.types.FCurve], parent_inverse, - blender_object_if_armature: typing.Optional[bpy.types.Object], + blender_obj_uuid: str, + is_armature: bool, non_keyed_values: typing.Tuple[typing.Optional[float]], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], @@ -314,10 +324,12 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], action_name, driver_obj, node_channel_is_animated: bool, + need_rotation_correction: bool, export_settings ) -> gltf2_io.Accessor: """Gather the data of the keyframes.""" - keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_object_if_armature, + keyframes, is_baked = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_obj_uuid, + is_armature, channels, non_keyed_values, bake_bone, @@ -329,10 +341,19 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], driver_obj, node_channel_is_animated, export_settings) + + if is_baked is True: + parent_inverse = mathutils.Matrix.Identity(4).freeze() + + blender_object_if_armature = export_settings['vtree'].nodes[blender_obj_uuid].blender_object if is_armature is True else None + if bake_bone is not None: target_datapath = "pose.bones['" + bake_bone + "']." + bake_channel else: - target_datapath = [c for c in channels if c is not None][0].data_path + if len(channels) != 0: # channels can be empty when baking object (non animated selected object) + target_datapath = [c for c in channels if c is not None][0].data_path + else: + target_datapath = bake_channel is_yup = export_settings[gltf2_blender_export_keys.YUP] @@ -355,6 +376,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], bone = blender_object_if_armature.pose.bones[bake_bone] if isinstance(bone, bpy.types.PoseBone): if bone.parent is None: + # bone at root of armature axis_basis_change = mathutils.Matrix.Identity(4) if export_settings[gltf2_blender_export_keys.YUP]: axis_basis_change = mathutils.Matrix( @@ -364,10 +386,25 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], (0.0, 0.0, 0.0, 1.0))) correction_matrix_local = axis_basis_change @ bone.bone.matrix_local else: - correction_matrix_local = ( - bone.parent.bone.matrix_local.inverted_safe() @ - bone.bone.matrix_local - ) + # Bone is not at root of armature + # There are 2 cases : + parent_uuid = export_settings['vtree'].nodes[export_settings['vtree'].nodes[blender_obj_uuid].bones[bone.name]].parent_uuid + if parent_uuid is not None and export_settings['vtree'].nodes[parent_uuid].blender_type == VExportNode.BONE: + # export bone is not at root of armature neither + correction_matrix_local = ( + bone.parent.bone.matrix_local.inverted_safe() @ + bone.bone.matrix_local + ) + else: + # exported bone (after filter) is at root of armature + axis_basis_change = mathutils.Matrix.Identity(4) + if export_settings[gltf2_blender_export_keys.YUP]: + axis_basis_change = mathutils.Matrix( + ((1.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.0), + (0.0, -1.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 1.0))) + correction_matrix_local = axis_basis_change transform = correction_matrix_local else: @@ -378,14 +415,14 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], values = [] for keyframe in keyframes: # Transform the data and build gltf control points - value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform) + value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction) if is_yup and not is_armature_animation: value = gltf2_blender_math.swizzle_yup(value, target_datapath) keyframe_value = gltf2_blender_math.mathutils_to_gltf(value) if keyframe.in_tangent is not None: # we can directly transform the tangent as it currently is represented by a control point - in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform) + in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform, need_rotation_correction) if is_yup and blender_object_if_armature is None: in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath) # the tangent in glTF is relative to the keyframe value @@ -397,7 +434,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], if keyframe.out_tangent is not None: # we can directly transform the tangent as it currently is represented by a control point - out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform) + out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform, need_rotation_correction) if is_yup and blender_object_if_armature is None: out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath) # the tangent in glTF is relative to the keyframe value 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 39f09d52..828d1955 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py @@ -11,7 +11,36 @@ from ..com.gltf2_blender_extras import generate_extras from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions -def gather_animations(blender_object: bpy.types.Object, +def __gather_channels_baked(obj_uuid, export_settings): + channels = [] + + # If no animation in file, no need to bake + if len(bpy.data.actions) == 0: + return None + + 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]]) + + for p in ["location", "rotation_quaternion", "scale"]: + channel = gltf2_blender_gather_animation_channels.gather_animation_channel( + obj_uuid, + (), + export_settings, + None, + p, + start_frame, + end_frame, + False, + obj_uuid, # Use obj uuid as action name for caching + None, + False #If Object is not animated, don't keep animation for this channel + ) + if channel is not None: + channels.append(channel) + + return channels if len(channels) > 0 else None + +def gather_animations( obj_uuid: int, tracks: typing.Dict[str, typing.List[int]], offset: int, export_settings) -> typing.Tuple[typing.List[gltf2_io.Animation], typing.Dict[str, typing.List[int]]]: @@ -24,11 +53,29 @@ def gather_animations(blender_object: bpy.types.Object, """ animations = [] + blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object + # Collect all 'actions' affecting this object. There is a direct mapping between blender actions and glTF animations 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 + if len([a for a in blender_actions if a[2] == "OBJECT"]) == 0: + # No TRS animation are found for this object. + # But we need to bake, in case we export selection + if export_settings['gltf_selected'] is True and blender_object.type != "ARMATURE": + channels = __gather_channels_baked(obj_uuid, export_settings) + if channels is not None: + animation = gltf2_io.Animation( + channels=channels, + extensions=None, # as other animations + extras=None, # Because there is no animation to get extras from + name=blender_object.name, # Use object name as animation name + samplers=[] + ) + + __link_samplers(animation, export_settings) + if animation is not None: + animations.append(animation) + current_action = None if blender_object.animation_data and blender_object.animation_data.action: current_action = blender_object.animation_data.action @@ -63,7 +110,7 @@ def gather_animations(blender_object: bpy.types.Object, # No need to set active shapekeys animations, this is needed for bone baking - animation = __gather_animation(blender_action, blender_object, export_settings) + animation = __gather_animation(obj_uuid, blender_action, export_settings) if animation is not None: animations.append(animation) @@ -91,21 +138,24 @@ def gather_animations(blender_object: bpy.types.Object, return animations, tracks -def __gather_animation(blender_action: bpy.types.Action, - blender_object: bpy.types.Object, - export_settings +def __gather_animation( obj_uuid: int, + blender_action: bpy.types.Action, + export_settings ) -> typing.Optional[gltf2_io.Animation]: + + blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object + if not __filter_animation(blender_action, blender_object, export_settings): return None name = __gather_name(blender_action, blender_object, export_settings) try: animation = gltf2_io.Animation( - channels=__gather_channels(blender_action, blender_object, export_settings), + channels=__gather_channels(obj_uuid, blender_action, export_settings), extensions=__gather_extensions(blender_action, blender_object, export_settings), extras=__gather_extras(blender_action, blender_object, export_settings), name=name, - samplers=__gather_samplers(blender_action, blender_object, export_settings) + samplers=__gather_samplers(obj_uuid, blender_action, export_settings) ) except RuntimeError as error: print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(name, error)) @@ -134,12 +184,12 @@ def __filter_animation(blender_action: bpy.types.Action, return True -def __gather_channels(blender_action: bpy.types.Action, - blender_object: bpy.types.Object, +def __gather_channels(obj_uuid: int, + blender_action: bpy.types.Action, export_settings ) -> typing.List[gltf2_io.AnimationChannel]: return gltf2_blender_gather_animation_channels.gather_animation_channels( - blender_action, blender_object, export_settings) + obj_uuid, blender_action, export_settings) def __gather_extensions(blender_action: bpy.types.Action, @@ -166,8 +216,8 @@ def __gather_name(blender_action: bpy.types.Action, return blender_action.name -def __gather_samplers(blender_action: bpy.types.Action, - blender_object: bpy.types.Object, +def __gather_samplers(obj_uuid: str, + blender_action: bpy.types.Action, export_settings ) -> typing.List[gltf2_io.AnimationSampler]: # We need to gather the samplers after gathering all channels --> populate this list in __link_samplers diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py index 7e49ac02..4f95431c 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py @@ -6,83 +6,134 @@ import bpy from io_scene_gltf2.blender.exp import gltf2_blender_get -def cached(func): +def cached_by_key(key): + """ + Decorates functions whose result should be cached. Use it like: + @cached_by_key(key=...) + def func(..., export_settings): + ... + The decorated function, func, must always take an "export_settings" arg + (the cache is stored here). + The key argument to the decorator is a function that computes the key to + cache on. It is passed all the arguments to func. """ - Decorate the cache gather functions results. + def inner(func): + @functools.wraps(func) + def wrapper_cached(*args, **kwargs): + if kwargs.get("export_settings"): + export_settings = kwargs["export_settings"] + else: + export_settings = args[-1] + + cache_key = key(*args, **kwargs) + + # invalidate cache if export settings have changed + if not hasattr(func, "__export_settings") or export_settings != func.__export_settings: + func.__cache = {} + func.__export_settings = export_settings + # use or fill cache + if cache_key in func.__cache: + return func.__cache[cache_key] + else: + result = func(*args, **kwargs) + func.__cache[cache_key] = result + return result + + return wrapper_cached + + return inner - The gather function is only executed if its result isn't in the cache yet - :param func: the function to be decorated. It will have a static __cache member afterwards - :return: + +def default_key(*args, **kwargs): + """ + Default cache key for @cached functions. + Cache on all arguments (except export_settings). """ + assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function" + cache_key_args = args + # make a shallow copy of the keyword arguments so that 'export_settings' can be removed + cache_key_kwargs = dict(kwargs) + if kwargs.get("export_settings"): + del cache_key_kwargs["export_settings"] + else: + cache_key_args = args[:-1] + + cache_key = () + for i in cache_key_args: + cache_key += (i,) + for i in cache_key_kwargs.values(): + cache_key += (i,) + + return cache_key + + +def cached(func): + return cached_by_key(key=default_key)(func) + +def objectcache(func): + + def reset_cache_objectcache(): + func.__objectcache = {} + + func.reset_cache = reset_cache_objectcache + @functools.wraps(func) - def wrapper_cached(*args, **kwargs): - assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function" + def wrapper_objectcache(*args, **kwargs): cache_key_args = args - # make a shallow copy of the keyword arguments so that 'export_settings' can be removed - cache_key_kwargs = dict(kwargs) - if kwargs.get("export_settings"): - export_settings = kwargs["export_settings"] - # 'export_settings' should not be cached - del cache_key_kwargs["export_settings"] - else: - export_settings = args[-1] - cache_key_args = args[:-1] + cache_key_args = args[:-1] - __by_name = [bpy.types.Object, bpy.types.Scene, bpy.types.Material, bpy.types.Action, bpy.types.Mesh, bpy.types.PoseBone] + if not hasattr(func, "__objectcache"): + func.reset_cache() - # we make a tuple from the function arguments so that they can be used as a key to the cache - cache_key = () - for i in cache_key_args: - if type(i) in __by_name: - cache_key += (i.name,) - else: - cache_key += (i,) - for i in cache_key_kwargs.values(): - if type(i) in __by_name: - cache_key += (i.name,) - else: - cache_key += (i,) - - # invalidate cache if export settings have changed - if not hasattr(func, "__export_settings") or export_settings != func.__export_settings: - func.__cache = {} - func.__export_settings = export_settings - # use or fill cache - if cache_key in func.__cache: - return func.__cache[cache_key] - else: + # object is not cached yet + if cache_key_args[0] not in func.__objectcache.keys(): result = func(*args) - func.__cache[cache_key] = result - return result - return wrapper_cached + func.__objectcache = result + return result[cache_key_args[0]][cache_key_args[1]][cache_key_args[4]] + # object is in cache, but not this action + # We need to keep other actions + elif cache_key_args[1] not in func.__objectcache[cache_key_args[0]].keys(): + result = func(*args) + func.__objectcache[cache_key_args[0]][cache_key_args[1]] = result[cache_key_args[0]][cache_key_args[1]] + return result[cache_key_args[0]][cache_key_args[1]][cache_key_args[4]] + # all is already cached + else: + return func.__objectcache[cache_key_args[0]][cache_key_args[1]][cache_key_args[4]] + return wrapper_objectcache def bonecache(func): def reset_cache_bonecache(): func.__current_action_name = None - func.__current_armature_name = None + func.__current_armature_uuid = None func.__bonecache = {} func.reset_cache = reset_cache_bonecache @functools.wraps(func) def wrapper_bonecache(*args, **kwargs): - if args[2] is None: - pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(args[0], - args[1][0].data_path) + + armature = args[-1]['vtree'].nodes[args[0]].blender_object + + cache_key_args = args + cache_key_args = args[:-1] + + if cache_key_args[2] is None: + pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(armature, + cache_key_args[1][0].data_path) else: - pose_bone_if_armature = args[0].pose.bones[args[2]] + pose_bone_if_armature = armature.pose.bones[cache_key_args[2]] if not hasattr(func, "__current_action_name"): func.reset_cache() - if args[6] != func.__current_action_name or args[0] != func.__current_armature_name: + if cache_key_args[6] != func.__current_action_name or cache_key_args[0] != func.__current_armature_uuid: result = func(*args) func.__bonecache = result - func.__current_action_name = args[6] - func.__current_armature_name = args[0] - return result[args[7]][pose_bone_if_armature.name] + func.__current_action_name = cache_key_args[6] + func.__current_armature_uuid = cache_key_args[0] + return result[cache_key_args[7]][pose_bone_if_armature.name] else: - return func.__bonecache[args[7]][pose_bone_if_armature.name] + return func.__bonecache[cache_key_args[7]][pose_bone_if_armature.name] return wrapper_bonecache # TODO: replace "cached" with "unique" in all cases where the caching is functional and not only for performance reasons @@ -92,23 +143,27 @@ unique = cached def skdriverdiscovercache(func): def reset_cache_skdriverdiscovercache(): - func.__current_armature_name = None + func.__current_armature_uuid = None func.__skdriverdiscover = {} func.reset_cache = reset_cache_skdriverdiscovercache @functools.wraps(func) def wrapper_skdriverdiscover(*args, **kwargs): - if not hasattr(func, "__current_armature_name") or func.__current_armature_name is None: + + cache_key_args = args + cache_key_args = args[:-1] + + if not hasattr(func, "__current_armature_uuid") or func.__current_armature_uuid is None: func.reset_cache() - if args[0] != func.__current_armature_name: + if cache_key_args[0] != func.__current_armature_uuid: result = func(*args) - func.__skdriverdiscover[args[0]] = result - func.__current_armature_name = args[0] + func.__skdriverdiscover[cache_key_args[0]] = result + func.__current_armature_uuid = cache_key_args[0] return result else: - return func.__skdriverdiscover[args[0]] + return func.__skdriverdiscover[cache_key_args[0]] return wrapper_skdriverdiscover def skdrivervalues(func): @@ -123,12 +178,17 @@ def skdrivervalues(func): if not hasattr(func, "__skdrivervalues") or func.__skdrivervalues is None: func.reset_cache() - if args[0].name not in func.__skdrivervalues.keys(): - func.__skdrivervalues[args[0].name] = {} - if args[1] not in func.__skdrivervalues[args[0].name]: + armature = args[-1]['vtree'].nodes[args[0]].blender_object + + cache_key_args = args + cache_key_args = args[:-1] + + if armature.name not in func.__skdrivervalues.keys(): + func.__skdrivervalues[armature.name] = {} + if cache_key_args[1] not in func.__skdrivervalues[armature.name]: vals = func(*args) - func.__skdrivervalues[args[0].name][args[1]] = vals + func.__skdrivervalues[armature.name][cache_key_args[1]] = vals return vals else: - return func.__skdrivervalues[args[0].name][args[1]] + return func.__skdrivervalues[armature.name][cache_key_args[1]] return wrapper_skdrivervalues diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_drivers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_drivers.py index 1f82c2b3..4e77f60e 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_drivers.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_drivers.py @@ -5,13 +5,20 @@ from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import skdriverdiscovercache, skdrivervalues from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_object_path - @skdriverdiscovercache -def get_sk_drivers(blender_armature): +def get_sk_drivers(blender_armature_uuid, export_settings): + + blender_armature = export_settings['vtree'].nodes[blender_armature_uuid].blender_object drivers = [] - for child in blender_armature.children: + for child_uuid in export_settings['vtree'].nodes[blender_armature_uuid].children: + + if export_settings['vtree'].nodes[child_uuid].blender_type == "BONE": + continue + + child = export_settings['vtree'].nodes[child_uuid].blender_object + if not child.data: continue # child.data can be an armature - which has no shapekeys @@ -63,13 +70,14 @@ def get_sk_drivers(blender_armature): all_sorted_channels.append(existing_idx[i]) if len(all_sorted_channels) > 0: - drivers.append((child, tuple(all_sorted_channels))) + drivers.append((child_uuid, tuple(all_sorted_channels))) return tuple(drivers) @skdrivervalues -def get_sk_driver_values(blender_object, frame, fcurves): +def get_sk_driver_values(blender_object_uuid, frame, fcurves, export_settings): sk_values = [] + blender_object = export_settings['vtree'].nodes[blender_object_uuid].blender_object for f in [f for f in fcurves if f is not None]: sk_values.append(blender_object.data.shape_keys.path_resolve(get_target_object_path(f.data_path)).value) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py index ffc1231b..f4fd6c51 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2018-2021 The glTF-Blender-IO authors. -import mathutils +from mathutils import Matrix, Quaternion, Vector from . import gltf2_blender_export_keys from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached @@ -9,9 +9,40 @@ from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions from ..com.gltf2_blender_extras import generate_extras +from io_scene_gltf2.blender.exp import gltf2_blender_gather_tree + + + +# TODO these 3 functions move to shared file +def __convert_swizzle_location(loc, export_settings): + """Convert a location from Blender coordinate system to glTF coordinate system.""" + if export_settings[gltf2_blender_export_keys.YUP]: + return Vector((loc[0], loc[2], -loc[1])) + else: + return Vector((loc[0], loc[1], loc[2])) + + +def __convert_swizzle_rotation(rot, export_settings): + """ + Convert a quaternion rotation from Blender coordinate system to glTF coordinate system. + + 'w' is still at first position. + """ + if export_settings[gltf2_blender_export_keys.YUP]: + return Quaternion((rot[0], rot[1], rot[3], -rot[2])) + else: + return Quaternion((rot[0], rot[1], rot[2], rot[3])) + + +def __convert_swizzle_scale(scale, export_settings): + """Convert a scale from Blender coordinate system to glTF coordinate system.""" + if export_settings[gltf2_blender_export_keys.YUP]: + return Vector((scale[0], scale[2], scale[1])) + else: + return Vector((scale[0], scale[1], scale[2])) @cached -def gather_joint(blender_object, blender_bone, export_settings): +def gather_joint_vnode(vnode, export_settings): """ Generate a glTF2 node from a blender bone, as joints in glTF2 are simply nodes. @@ -19,28 +50,19 @@ def gather_joint(blender_object, blender_bone, export_settings): :param export_settings: the settings for this export :return: a glTF2 node (acting as a joint) """ - axis_basis_change = mathutils.Matrix.Identity(4) - if export_settings[gltf2_blender_export_keys.YUP]: - axis_basis_change = mathutils.Matrix( - ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + vtree = export_settings['vtree'] + blender_object = vtree.nodes[vnode].blender_object + blender_bone = vtree.nodes[vnode].blender_bone - # extract bone transform - if blender_bone.parent is None: - correction_matrix_local = axis_basis_change @ blender_bone.bone.matrix_local - else: - correction_matrix_local = ( - blender_bone.parent.bone.matrix_local.inverted_safe() @ - blender_bone.bone.matrix_local - ) - - if (blender_bone.bone.use_inherit_rotation == False or blender_bone.bone.inherit_scale != "FULL") and blender_bone.parent != None: - rest_mat = (blender_bone.parent.bone.matrix_local.inverted_safe() @ blender_bone.bone.matrix_local) - matrix_basis = (rest_mat.inverted_safe() @ blender_bone.parent.matrix.inverted_safe() @ blender_bone.matrix) - else: - matrix_basis = blender_bone.matrix - matrix_basis = blender_object.convert_space(pose_bone=blender_bone, matrix=matrix_basis, from_space='POSE', to_space='LOCAL') - trans, rot, sca = (correction_matrix_local @ matrix_basis).decompose() + mat = vtree.nodes[vtree.nodes[vnode].parent_uuid].matrix_world.inverted_safe() @ vtree.nodes[vnode].matrix_world + + trans, rot, sca = mat.decompose() + + trans = __convert_swizzle_location(trans, export_settings) + rot = __convert_swizzle_rotation(rot, export_settings) + sca = __convert_swizzle_scale(sca, export_settings) + translation, rotation, scale = (None, None, None) if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0: translation = [trans[0], trans[1], trans[2]] @@ -52,14 +74,8 @@ def gather_joint(blender_object, blender_bone, export_settings): # traverse into children children = [] - if export_settings["gltf_def_bones"] is False: - for bone in blender_bone.children: - children.append(gather_joint(blender_object, bone, export_settings)) - else: - _, children_, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_bone.id_data) - if blender_bone.name in children_.keys(): - for bone in children_[blender_bone.name]: - children.append(gather_joint(blender_object, blender_bone.id_data.pose.bones[bone], export_settings)) + for bone_uuid in [c for c in vtree.nodes[vnode].children if vtree.nodes[c].blender_type == gltf2_blender_gather_tree.VExportNode.BONE]: + children.append(gather_joint_vnode(bone_uuid, export_settings)) # finally add to the joints array containing all the joints in the hierarchy node = gltf2_io.Node( @@ -79,6 +95,8 @@ def gather_joint(blender_object, blender_bone, export_settings): export_user_extensions('gather_joint_hook', export_settings, node, blender_bone) + vtree.nodes[vnode].node = node + return node def __gather_extras(blender_bone, export_settings): diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py index 2d83b0dd..845024e5 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py @@ -3,7 +3,7 @@ import bpy -from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.io.com.gltf2_io_extensions import Extension from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info, gltf2_blender_export_keys @@ -16,8 +16,14 @@ from io_scene_gltf2.blender.exp import gltf2_blender_get from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions from io_scene_gltf2.io.com.gltf2_io_debug import print_console - @cached +def get_material_cache_key(blender_material, export_settings): + # Use id of material + # Do not use bpy.types that can be unhashable + # Do not use material name, that can be not unique (when linked) + return ((id(blender_material),)) + +@cached_by_key(key=get_material_cache_key) def gather_material(blender_material, export_settings): """ Gather the material used by the blender primitive. diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py index ac90e4cd..c8987127 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py @@ -4,7 +4,7 @@ import bpy from typing import Optional, Dict, List, Any, Tuple from .gltf2_blender_export_keys import MORPH -from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives from ..com.gltf2_blender_extras import generate_extras @@ -13,30 +13,64 @@ from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extension @cached +def get_mesh_cache_key(blender_mesh, + blender_object, + vertex_groups, + modifiers, + skip_filter, + materials, + original_mesh, + export_settings): + # Use id of original mesh + # Do not use bpy.types that can be unhashable + # Do not use mesh name, that can be not unique (when linked) + + # If materials are not exported, no need to cache by material + if export_settings['gltf_materials'] is None: + mats = None + else: + mats = tuple(id(m) if m is not None else None for m in materials) + + # TODO check what is really needed for modifiers + + mesh_to_id_cache = blender_mesh if original_mesh is None else original_mesh + return ( + (id(mesh_to_id_cache),), + (modifiers,), + (skip_filter,), #TODO to check if still needed + mats + ) + +@cached_by_key(key=get_mesh_cache_key) def gather_mesh(blender_mesh: bpy.types.Mesh, - library: Optional[str], - blender_object: Optional[bpy.types.Object], + uuid_for_skined_data, vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], skip_filter: bool, - material_names: Tuple[str], + materials: Tuple[bpy.types.Material], + original_mesh: bpy.types.Mesh, export_settings ) -> Optional[gltf2_io.Mesh]: - if not skip_filter and not __filter_mesh(blender_mesh, library, vertex_groups, modifiers, export_settings): + if not skip_filter and not __filter_mesh(blender_mesh, vertex_groups, modifiers, export_settings): return None mesh = gltf2_io.Mesh( - extensions=__gather_extensions(blender_mesh, library, vertex_groups, modifiers, export_settings), - extras=__gather_extras(blender_mesh, library, vertex_groups, modifiers, export_settings), - name=__gather_name(blender_mesh, library, vertex_groups, modifiers, export_settings), - weights=__gather_weights(blender_mesh, library, vertex_groups, modifiers, export_settings), - primitives=__gather_primitives(blender_mesh, library, blender_object, vertex_groups, modifiers, material_names, export_settings), + extensions=__gather_extensions(blender_mesh, vertex_groups, modifiers, export_settings), + extras=__gather_extras(blender_mesh, vertex_groups, modifiers, export_settings), + name=__gather_name(blender_mesh, vertex_groups, modifiers, export_settings), + weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings), + primitives=__gather_primitives(blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, materials, export_settings), ) if len(mesh.primitives) == 0: print_console("WARNING", "Mesh '{}' has no primitives and will be omitted.".format(mesh.name)) return None + blender_object = None + if uuid_for_skined_data: + blender_object = export_settings['vtree'].nodes[uuid_for_skined_data].blender_object + + export_user_extensions('gather_mesh_hook', export_settings, mesh, @@ -45,13 +79,12 @@ def gather_mesh(blender_mesh: bpy.types.Mesh, vertex_groups, modifiers, skip_filter, - material_names) + materials) return mesh def __filter_mesh(blender_mesh: bpy.types.Mesh, - library: Optional[str], vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], export_settings @@ -63,7 +96,6 @@ def __filter_mesh(blender_mesh: bpy.types.Mesh, def __gather_extensions(blender_mesh: bpy.types.Mesh, - library: Optional[str], vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], export_settings @@ -72,7 +104,6 @@ def __gather_extensions(blender_mesh: bpy.types.Mesh, def __gather_extras(blender_mesh: bpy.types.Mesh, - library: Optional[str], vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], export_settings @@ -100,7 +131,6 @@ def __gather_extras(blender_mesh: bpy.types.Mesh, def __gather_name(blender_mesh: bpy.types.Mesh, - library: Optional[str], vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], export_settings @@ -109,24 +139,21 @@ def __gather_name(blender_mesh: bpy.types.Mesh, def __gather_primitives(blender_mesh: bpy.types.Mesh, - library: Optional[str], - blender_object: Optional[bpy.types.Object], + uuid_for_skined_data, vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], - material_names: Tuple[str], + materials: Tuple[bpy.types.Material], export_settings ) -> List[gltf2_io.MeshPrimitive]: return gltf2_blender_gather_primitives.gather_primitives(blender_mesh, - library, - blender_object, + uuid_for_skined_data, vertex_groups, modifiers, - material_names, + materials, export_settings) def __gather_weights(blender_mesh: bpy.types.Mesh, - library: Optional[str], vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], export_settings diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py index a69f5d58..25784960 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py @@ -13,135 +13,47 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras from io_scene_gltf2.blender.exp import gltf2_blender_gather_mesh from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints from io_scene_gltf2.blender.exp import gltf2_blender_gather_lights +from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode from ..com.gltf2_blender_extras import generate_extras from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.io.com import gltf2_io_extensions from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions from io_scene_gltf2.io.com.gltf2_io_debug import print_console +from io_scene_gltf2.blender.exp import gltf2_blender_gather_tree -def gather_node(blender_object, library, blender_scene, dupli_object_parent, export_settings): - # custom cache to avoid cache miss when called from animation - # with blender_scene=None - - # invalidate cache if export settings have changed - if not hasattr(gather_node, "__export_settings") or export_settings != gather_node.__export_settings: - gather_node.__cache = {} - gather_node.__export_settings = export_settings - - if blender_scene is None and (blender_object.name, library) in gather_node.__cache: - return gather_node.__cache[(blender_object.name, library)] - - node = __gather_node(blender_object, library, blender_scene, dupli_object_parent, export_settings) - gather_node.__cache[(blender_object.name, library)] = node - return node - -@cached -def __gather_node(blender_object, library, blender_scene, dupli_object_parent, export_settings): - children, only_bone_children = __gather_children(blender_object, blender_scene, export_settings) - - camera = None - mesh = None - skin = None - weights = None - - # If blender_scene is None, we are coming from animation export - # Check to know if object is exported is already done, so we don't check - # again if object is instanced in scene : this check was already done when exporting object itself - if not __filter_node(blender_object, blender_scene, export_settings): - if children: - # This node should be filtered out, but has un-filtered children present. - # So, export this node, excluding its camera, mesh, skin, and weights. - # The transformations and animations on this node will have visible effects on children. - - # Armature always have children node(s) (that are bone(s)) - # We have to check if children are only bones or not for armatures - if blender_object.type == "ARMATURE" and only_bone_children is True: - return None - - pass - else: - # This node is filtered out, and has no un-filtered children or descendants. - return None - else: - # This node is being fully exported. - camera = __gather_camera(blender_object, export_settings) - mesh = __gather_mesh(blender_object, library, export_settings) - skin = __gather_skin(blender_object, export_settings) - weights = __gather_weights(blender_object, export_settings) +def gather_node(vnode, export_settings): + blender_object = vnode.blender_object + skin = __gather_skin(vnode, blender_object, export_settings) node = gltf2_io.Node( - camera=camera, - children=children, + camera=__gather_camera(blender_object, export_settings), + children=__gather_children(vnode, blender_object, export_settings), extensions=__gather_extensions(blender_object, export_settings), extras=__gather_extras(blender_object, export_settings), matrix=__gather_matrix(blender_object, export_settings), - mesh=mesh, + mesh=__gather_mesh(vnode, blender_object, export_settings), name=__gather_name(blender_object, export_settings), rotation=None, scale=None, skin=skin, translation=None, - weights=weights + weights=__gather_weights(blender_object, export_settings) ) # If node mesh is skined, transforms should be ignored at import, so no need to set them here if node.skin is None: - node.translation, node.rotation, node.scale = __gather_trans_rot_scale(blender_object, export_settings) + node.translation, node.rotation, node.scale = __gather_trans_rot_scale(vnode, export_settings) - if export_settings[gltf2_blender_export_keys.YUP]: - # Checking node.extensions is making sure that the type of lamp is managed, and will be exported - if blender_object.type == 'LIGHT' and export_settings[gltf2_blender_export_keys.LIGHTS] and node.extensions: - correction_node = __get_correction_node(blender_object, export_settings) - correction_node.extensions = {"KHR_lights_punctual": node.extensions["KHR_lights_punctual"]} - del node.extensions["KHR_lights_punctual"] - node.children.append(correction_node) - if blender_object.type == 'CAMERA' and export_settings[gltf2_blender_export_keys.CAMERAS]: - correction_node = __get_correction_node(blender_object, export_settings) - correction_node.camera = node.camera - node.children.append(correction_node) - node.camera = None export_user_extensions('gather_node_hook', export_settings, node, blender_object) - return node - + vnode.node = node -def __filter_node(blender_object, blender_scene, export_settings): - if blender_object.users == 0: - return False - if blender_scene is not None: - instanced = any([blender_object.name in layer.objects for layer in blender_scene.view_layers]) - if instanced is False: - # Check if object is from a linked collection - if any([blender_object.name in coll.objects for coll in bpy.data.collections if coll.library is not None]): - pass - else: - # Not instanced, not linked -> We don't keep this object - return False - if export_settings[gltf2_blender_export_keys.SELECTED] and blender_object.select_get() is False: - return False + if node.skin is not None: + vnode.skin = skin - if export_settings[gltf2_blender_export_keys.VISIBLE] and blender_object.visible_get() is False: - return False - - # render_get() doesn't exist, so unfortunately this won't take into account the Collection settings - if export_settings[gltf2_blender_export_keys.RENDERABLE] and blender_object.hide_render is True: - return False - - if export_settings[gltf2_blender_export_keys.ACTIVE_COLLECTION]: - found = any(x == blender_object for x in bpy.context.collection.all_objects) - - if not found: - return False - - if blender_object.type == 'LIGHT': - return export_settings[gltf2_blender_export_keys.LIGHTS] - - if blender_object.type == 'CAMERA': - return export_settings[gltf2_blender_export_keys.CAMERAS] - - return True + return node def __gather_camera(blender_object, export_settings): @@ -151,54 +63,35 @@ def __gather_camera(blender_object, export_settings): return gltf2_blender_gather_cameras.gather_camera(blender_object.data, export_settings) -def __gather_children(blender_object, blender_scene, export_settings): +def __gather_children(vnode, blender_object, export_settings): children = [] - only_bone_children = True # True by default, will be set to False if needed - # standard children - for child_object in blender_object.children: - if child_object.parent_bone: - # this is handled further down, - # as the object should be a child of the specific bone, - # not the Armature object - continue - - node = gather_node(child_object, - child_object.library.name if child_object.library else None, - blender_scene, None, export_settings) + + vtree = export_settings['vtree'] + + # Standard Children / Collection + for c in [vtree.nodes[c] for c in vnode.children if vtree.nodes[c].blender_type != gltf2_blender_gather_tree.VExportNode.BONE]: + node = gather_node(c, export_settings) if node is not None: children.append(node) - only_bone_children = False - # blender dupli objects - if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection: - for dupli_object in blender_object.instance_collection.objects: - if dupli_object.parent is not None: - continue - if dupli_object.type == "ARMATURE": - continue # There is probably a proxy (no more existing) - node = gather_node(dupli_object, - dupli_object.library.name if dupli_object.library else None, - blender_scene, blender_object.name, export_settings) - if node is not None: - children.append(node) - only_bone_children = False - - # blender bones - if blender_object.type == "ARMATURE": + + + # Armature --> Retrieve Blender bones + if vnode.blender_type == gltf2_blender_gather_tree.VExportNode.ARMATURE: root_joints = [] - if export_settings["gltf_def_bones"] is False: - bones = blender_object.pose.bones - else: - bones, _, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_object) - bones = [blender_object.pose.bones[b.name] for b in bones] - for blender_bone in bones: - if not blender_bone.parent: - joint = gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings) - children.append(joint) - root_joints.append(joint) - # handle objects directly parented to bones - direct_bone_children = [child for child in blender_object.children if child.parent_bone] - if len(direct_bone_children) != 0: - only_bone_children = False + + all_armature_children = vnode.children + root_bones_uuid = [c for c in all_armature_children if export_settings['vtree'].nodes[c].blender_type == VExportNode.BONE] + for bone_uuid in root_bones_uuid: + joint = gltf2_blender_gather_joints.gather_joint_vnode(bone_uuid, export_settings) + children.append(joint) + root_joints.append(joint) + + # Object parented to bones + direct_bone_children = [] + for n in [vtree.nodes[i] for i in vtree.get_all_bones(vnode.uuid)]: + direct_bone_children.extend([c for c in n.children if vtree.nodes[c].blender_type != gltf2_blender_gather_tree.VExportNode.BONE]) + + def find_parent_joint(joints, name): for joint in joints: if joint.name == name: @@ -207,44 +100,40 @@ def __gather_children(blender_object, blender_scene, export_settings): if parent_joint: return parent_joint return None - for child in direct_bone_children: + + for child in direct_bone_children: # List of object that are parented to bones # find parent joint - parent_joint = find_parent_joint(root_joints, child.parent_bone) + parent_joint = find_parent_joint(root_joints, vtree.nodes[child].blender_object.parent_bone) if not parent_joint: continue - child_node = gather_node(child, None, blender_scene, None, export_settings) + child_node = gather_node(vtree.nodes[child], export_settings) if child_node is None: continue blender_bone = blender_object.pose.bones[parent_joint.name] - # fix rotation - if export_settings[gltf2_blender_export_keys.YUP]: - rot = child_node.rotation - if rot is None: - rot = [0, 0, 0, 1] - - rot_quat = Quaternion(rot) - axis_basis_change = Matrix( - ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, -1.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) - mat = child.matrix_parent_inverse @ child.matrix_basis - mat = mat @ axis_basis_change - - _, rot_quat, _ = mat.decompose() - child_node.rotation = [rot_quat[1], rot_quat[2], rot_quat[3], rot_quat[0]] - - # fix translation (in blender bone's tail is the origin for children) - trans, _, _ = child.matrix_local.decompose() - if trans is None: - trans = [0, 0, 0] - # bones go down their local y axis - if blender_bone.matrix.to_scale()[1] >= 1e-6: - bone_tail = [0, blender_bone.length / blender_bone.matrix.to_scale()[1], 0] - else: - bone_tail = [0,0,0] # If scale is 0, tail == head - child_node.translation = [trans[idx] + bone_tail[idx] for idx in range(3)] + + mat = vtree.nodes[vtree.nodes[child].parent_bone_uuid].matrix_world.inverted_safe() @ vtree.nodes[child].matrix_world + loc, rot_quat, scale = mat.decompose() + + trans = __convert_swizzle_location(loc, export_settings) + rot = __convert_swizzle_rotation(rot_quat, export_settings) + sca = __convert_swizzle_scale(scale, export_settings) + + + translation, rotation, scale = (None, None, None) + if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0: + translation = [trans[0], trans[1], trans[2]] + if rot[0] != 1.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 0.0: + rotation = [rot[1], rot[2], rot[3], rot[0]] + if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0: + scale = [sca[0], sca[1], sca[2]] + + child_node.translation = translation + child_node.rotation = rotation + child_node.scale = scale parent_joint.children.append(child_node) - return children, only_bone_children + return children def __gather_extensions(blender_object, export_settings): @@ -283,13 +172,17 @@ def __gather_matrix(blender_object, export_settings): return [] -def __gather_mesh(blender_object, library, export_settings): +def __gather_mesh(vnode, blender_object, export_settings): if blender_object.type in ['CURVE', 'SURFACE', 'FONT']: - return __gather_mesh_from_nonmesh(blender_object, library, export_settings) + return __gather_mesh_from_nonmesh(blender_object, export_settings) if blender_object.type != "MESH": return None + # For duplis instancer, when show is off -> export as empty + if vnode.force_as_empty is True: + return None + # Be sure that object is valid (no NaN for example) blender_object.data.validate() @@ -301,6 +194,8 @@ def __gather_mesh(blender_object, library, export_settings): if len(modifiers) == 0: modifiers = None + # TODO for objects without any modifiers, we can keep original mesh_data + # It will instance mesh in glTF if export_settings[gltf2_blender_export_keys.APPLY]: armature_modifiers = {} if export_settings[gltf2_blender_export_keys.SKINS]: @@ -335,24 +230,23 @@ def __gather_mesh(blender_object, library, export_settings): modifiers = None materials = tuple(ms.material for ms in blender_object.material_slots) - material_names = tuple(None if mat is None else mat.name for mat in materials) # retrieve armature # Because mesh data will be transforms to skeleton space, # we can't instantiate multiple object at different location, skined by same armature - blender_object_for_skined_data = None + uuid_for_skined_data = None if export_settings[gltf2_blender_export_keys.SKINS]: for idx, modifier in enumerate(blender_object.modifiers): if modifier.type == 'ARMATURE': - blender_object_for_skined_data = blender_object + uuid_for_skined_data = vnode.uuid result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh, - library, - blender_object_for_skined_data, + uuid_for_skined_data, vertex_groups, modifiers, skip_filter, - material_names, + materials, + None, export_settings) if export_settings[gltf2_blender_export_keys.APPLY]: @@ -361,7 +255,7 @@ def __gather_mesh(blender_object, library, export_settings): return result -def __gather_mesh_from_nonmesh(blender_object, library, export_settings): +def __gather_mesh_from_nonmesh(blender_object, export_settings): """Handles curves, surfaces, text, etc.""" needs_to_mesh_clear = False try: @@ -387,18 +281,18 @@ def __gather_mesh_from_nonmesh(blender_object, library, export_settings): needs_to_mesh_clear = True skip_filter = True - material_names = tuple([ms.material.name for ms in blender_object.material_slots if ms.material is not None]) + materials = tuple([ms.material for ms in blender_object.material_slots if ms.material is not None]) vertex_groups = None modifiers = None blender_object_for_skined_data = None result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh, - library, blender_object_for_skined_data, vertex_groups, modifiers, skip_filter, - material_names, + materials, + blender_object.data, export_settings) finally: @@ -411,33 +305,15 @@ def __gather_mesh_from_nonmesh(blender_object, library, export_settings): def __gather_name(blender_object, export_settings): return blender_object.name - -def __gather_trans_rot_scale(blender_object, export_settings): - if blender_object.matrix_parent_inverse == Matrix.Identity(4): - trans = blender_object.location - - if blender_object.rotation_mode in ['QUATERNION', 'AXIS_ANGLE']: - rot = blender_object.rotation_quaternion - else: - rot = blender_object.rotation_euler.to_quaternion() - - sca = blender_object.scale +def __gather_trans_rot_scale(vnode, export_settings): + if vnode.parent_uuid is None: + # No parent, so matrix is world matrix + trans, rot, sca = vnode.matrix_world.decompose() else: - # matrix_local = matrix_parent_inverse*location*rotation*scale - # Decomposing matrix_local gives less accuracy, but is needed if matrix_parent_inverse is not the identity. + # calculate local matrix + trans, rot, sca = (export_settings['vtree'].nodes[vnode.parent_uuid].matrix_world.inverted_safe() @ vnode.matrix_world).decompose() - if blender_object.matrix_local[3][3] != 0.0: - trans, rot, sca = blender_object.matrix_local.decompose() - else: - # Some really weird cases, scale is null (if parent is null when evaluation is done) - print_console('WARNING', 'Some nodes are 0 scaled during evaluation. Result can be wrong') - trans = blender_object.location - if blender_object.rotation_mode in ['QUATERNION', 'AXIS_ANGLE']: - rot = blender_object.rotation_quaternion - else: - rot = blender_object.rotation_euler.to_quaternion() - sca = blender_object.scale # make sure the rotation is normalized rot.normalize() @@ -446,9 +322,9 @@ def __gather_trans_rot_scale(blender_object, export_settings): rot = __convert_swizzle_rotation(rot, export_settings) sca = __convert_swizzle_scale(sca, export_settings) - if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection: + if vnode.blender_object.instance_type == 'COLLECTION' and vnode.blender_object.instance_collection: offset = -__convert_swizzle_location( - blender_object.instance_collection.instance_offset, export_settings) + vnode.blender_object.instance_collection.instance_offset, export_settings) s = Matrix.Diagonal(sca).to_4x4() r = rot.to_matrix().to_4x4() @@ -473,8 +349,7 @@ def __gather_trans_rot_scale(blender_object, export_settings): scale = [sca[0], sca[1], sca[2]] return translation, rotation, scale - -def __gather_skin(blender_object, export_settings): +def __gather_skin(vnode, blender_object, export_settings): modifiers = {m.type: m for m in blender_object.modifiers} if "ARMATURE" not in modifiers or modifiers["ARMATURE"].object is None: return None @@ -501,34 +376,12 @@ def __gather_skin(blender_object, export_settings): return None # Skins and meshes must be in the same glTF node, which is different from how blender handles armatures - return gltf2_blender_gather_skins.gather_skin(modifiers["ARMATURE"].object, export_settings) + return gltf2_blender_gather_skins.gather_skin(vnode.armature, export_settings) def __gather_weights(blender_object, export_settings): return None - -def __get_correction_node(blender_object, export_settings): - correction_quaternion = __convert_swizzle_rotation( - Quaternion((1.0, 0.0, 0.0), math.radians(-90.0)), export_settings) - correction_quaternion = [correction_quaternion[1], correction_quaternion[2], - correction_quaternion[3], correction_quaternion[0]] - return gltf2_io.Node( - camera=None, - children=[], - extensions=None, - extras=None, - matrix=None, - mesh=None, - name=blender_object.name + '_Orientation', - rotation=correction_quaternion, - scale=None, - skin=None, - translation=None, - weights=None - ) - - def __convert_swizzle_location(loc, export_settings): """Convert a location from Blender coordinate system to glTF coordinate system.""" if export_settings[gltf2_blender_export_keys.YUP]: diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py index 82ff7f66..9e5ce648 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py @@ -7,7 +7,7 @@ import numpy as np from .gltf2_blender_export_keys import NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH -from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key from io_scene_gltf2.blender.exp import gltf2_blender_extract from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes @@ -20,13 +20,34 @@ from io_scene_gltf2.io.com.gltf2_io_debug import print_console @cached +def get_primitive_cache_key( + blender_mesh, + blender_object, + vertex_groups, + modifiers, + materials, + export_settings): + + # Use id of mesh + # Do not use bpy.types that can be unhashable + # Do not use mesh name, that can be not unique (when linked) + + # TODO check what is really needed for modifiers + + return ( + (id(blender_mesh),), + (modifiers,), + tuple(id(m) if m is not None else None for m in materials) + ) + + +@cached_by_key(key=get_primitive_cache_key) def gather_primitives( blender_mesh: bpy.types.Mesh, - library: Optional[str], - blender_object: Optional[bpy.types.Object], + uuid_for_skined_data, vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], - material_names: Tuple[str], + materials: Tuple[bpy.types.Material], export_settings ) -> List[gltf2_io.MeshPrimitive]: """ @@ -36,7 +57,7 @@ def gather_primitives( """ primitives = [] - blender_primitives = __gather_cache_primitives(blender_mesh, library, blender_object, + blender_primitives = __gather_cache_primitives(blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, export_settings) for internal_primitive in blender_primitives: @@ -45,14 +66,13 @@ def gather_primitives( if export_settings['gltf_materials'] == "EXPORT" and material_idx is not None: blender_material = None - if material_names: - i = material_idx if material_idx < len(material_names) else -1 - material_name = material_names[i] - if material_name is not None: - blender_material = bpy.data.materials[material_name] - if blender_material is not None: + mat = None + if materials: + i = material_idx if material_idx < len(materials) else -1 + mat = materials[i] + if mat is not None: material = gltf2_blender_gather_materials.gather_material( - blender_material, + mat, export_settings, ) @@ -72,8 +92,7 @@ def gather_primitives( @cached def __gather_cache_primitives( blender_mesh: bpy.types.Mesh, - library: Optional[str], - blender_object: Optional[bpy.types.Object], + uuid_for_skined_data, vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], export_settings @@ -84,7 +103,7 @@ def __gather_cache_primitives( primitives = [] blender_primitives = gltf2_blender_extract.extract_primitives( - None, blender_mesh, library, blender_object, vertex_groups, modifiers, export_settings) + blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, export_settings) for internal_primitive in blender_primitives: primitive = { diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py index e5534c5a..136d654d 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py @@ -10,10 +10,12 @@ from io_scene_gltf2.io.com import gltf2_io_constants from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions +from io_scene_gltf2.blender.exp import gltf2_blender_gather_tree +from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode @cached -def gather_skin(blender_object, export_settings): +def gather_skin(armature_uuid, export_settings): """ Gather armatures, bones etc into a glTF2 skin object. @@ -21,78 +23,70 @@ def gather_skin(blender_object, export_settings): :param export_settings: :return: a glTF2 skin object """ - if not __filter_skin(blender_object, export_settings): + + blender_armature_object = export_settings['vtree'].nodes[armature_uuid].blender_object + + if not __filter_skin(blender_armature_object, export_settings): return None skin = gltf2_io.Skin( - extensions=__gather_extensions(blender_object, export_settings), - extras=__gather_extras(blender_object, export_settings), - inverse_bind_matrices=__gather_inverse_bind_matrices(blender_object, export_settings), - joints=__gather_joints(blender_object, export_settings), - name=__gather_name(blender_object, export_settings), - skeleton=__gather_skeleton(blender_object, export_settings) + extensions=__gather_extensions(blender_armature_object, export_settings), + extras=__gather_extras(blender_armature_object, export_settings), + inverse_bind_matrices=__gather_inverse_bind_matrices(armature_uuid, export_settings), + joints=__gather_joints(armature_uuid, export_settings), + name=__gather_name(blender_armature_object, export_settings), + skeleton=__gather_skeleton(blender_armature_object, export_settings) ) - export_user_extensions('gather_skin_hook', export_settings, skin, blender_object) + # If armature is not exported, joints will be empty. + # Do not construct skin in that case + if len(skin.joints) == 0: + return None + + export_user_extensions('gather_skin_hook', export_settings, skin, blender_armature_object) return skin -def __filter_skin(blender_object, export_settings): +def __filter_skin(blender_armature_object, export_settings): if not export_settings[gltf2_blender_export_keys.SKINS]: return False - if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0: + if blender_armature_object.type != 'ARMATURE' or len(blender_armature_object.pose.bones) == 0: return False return True -def __gather_extensions(blender_object, export_settings): +def __gather_extensions(blender_armature_object, export_settings): return None -def __gather_extras(blender_object, export_settings): +def __gather_extras(blender_armature_object, export_settings): return None -def __gather_inverse_bind_matrices(blender_object, export_settings): +def __gather_inverse_bind_matrices(armature_uuid, export_settings): + + blender_armature_object = export_settings['vtree'].nodes[armature_uuid].blender_object + axis_basis_change = mathutils.Matrix.Identity(4) if export_settings[gltf2_blender_export_keys.YUP]: axis_basis_change = mathutils.Matrix( ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) - if export_settings['gltf_def_bones'] is False: - # build the hierarchy of nodes out of the bones - root_bones = [] - for blender_bone in blender_object.pose.bones: - if not blender_bone.parent: - root_bones.append(blender_bone) - else: - _, children_, root_bones = get_bone_tree(None, blender_object) - - matrices = [] - - # traverse the matrices in the same order as the joints and compute the inverse bind matrix + bones_uuid = export_settings['vtree'].get_all_bones(armature_uuid) def __collect_matrices(bone): inverse_bind_matrix = ( axis_basis_change @ ( - blender_object.matrix_world @ + blender_armature_object.matrix_world @ bone.bone.matrix_local ) ).inverted_safe() matrices.append(inverse_bind_matrix) - if export_settings['gltf_def_bones'] is False: - for child in bone.children: - __collect_matrices(child) - else: - if bone.name in children_.keys(): - for child in children_[bone.name]: - __collect_matrices(blender_object.pose.bones[child]) - - # start with the "root" bones and recurse into children, in the same ordering as the how joints are gathered - for root_bone in root_bones: - __collect_matrices(root_bone) + matrices = [] + for b in bones_uuid: + __collect_matrices(blender_armature_object.pose.bones[export_settings['vtree'].nodes[b].blender_bone.name]) # flatten the matrices inverse_matrices = [] @@ -113,67 +107,26 @@ def __gather_inverse_bind_matrices(blender_object, export_settings): ) -def __gather_joints(blender_object, export_settings): - root_joints = [] - if export_settings['gltf_def_bones'] is False: - # build the hierarchy of nodes out of the bones - for blender_bone in blender_object.pose.bones: - if not blender_bone.parent: - root_joints.append(gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings)) - else: - _, children_, root_joints = get_bone_tree(None, blender_object) - root_joints = [gltf2_blender_gather_joints.gather_joint(blender_object, i, export_settings) for i in root_joints] - - # joints is a flat list containing all nodes belonging to the skin - joints = [] - - def __collect_joints(node): - joints.append(node) - if export_settings['gltf_def_bones'] is False: - for child in node.children: - __collect_joints(child) - else: - if node.name in children_.keys(): - for child in children_[node.name]: - __collect_joints(gltf2_blender_gather_joints.gather_joint(blender_object, blender_object.pose.bones[child], export_settings)) - - for joint in root_joints: - __collect_joints(joint) +def __gather_joints(armature_uuid, export_settings): + + blender_armature_object = export_settings['vtree'].nodes[armature_uuid].blender_object + + all_armature_children = export_settings['vtree'].nodes[armature_uuid].children + root_bones_uuid = [c for c in all_armature_children if export_settings['vtree'].nodes[c].blender_type == VExportNode.BONE] + # Create bone nodes + for root_bone_uuid in root_bones_uuid: + gltf2_blender_gather_joints.gather_joint_vnode(root_bone_uuid, export_settings) + + bones_uuid = export_settings['vtree'].get_all_bones(armature_uuid) + joints = [export_settings['vtree'].nodes[b].node for b in bones_uuid] return joints -def __gather_name(blender_object, export_settings): - return blender_object.name +def __gather_name(blender_armature_object, export_settings): + return blender_armature_object.name -def __gather_skeleton(blender_object, export_settings): +def __gather_skeleton(blender_armature_object, export_settings): # In the future support the result of https://github.com/KhronosGroup/glTF/pull/1195 - return None # gltf2_blender_gather_nodes.gather_node(blender_object, blender_scene, export_settings) - -@cached -def get_bone_tree(blender_dummy, blender_object): - - bones = [] - children = {} - root_bones = [] - - def get_parent(bone): - bones.append(bone.name) - if bone.parent is not None: - if bone.parent.name not in children.keys(): - children[bone.parent.name] = [] - children[bone.parent.name].append(bone.name) - get_parent(bone.parent) - else: - root_bones.append(bone.name) - - for bone in [b for b in blender_object.data.bones if b.use_deform is True]: - get_parent(bone) - - # remove duplicates - for k, v in children.items(): - children[k] = list(set(v)) - list_ = list(set(bones)) - root_ = list(set(root_bones)) - return [blender_object.data.bones[b] for b in list_], children, [blender_object.pose.bones[b] for b in root_] + return None diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py new file mode 100644 index 00000000..7a5ce2be --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py @@ -0,0 +1,375 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2021 The glTF-Blender-IO authors. + +import bpy +import uuid + +from . import gltf2_blender_export_keys +from mathutils import Quaternion, Matrix + +class VExportNode: + + OBJECT = 1 + ARMATURE = 2 + BONE = 3 + LIGHT = 4 + CAMERA = 5 + COLLECTION = 6 + + # Parent type, to be set on child regarding its parent + NO_PARENT = 54 + PARENT_OBJECT = 50 + PARENT_BONE = 51 + PARENT_BONE_RELATIVE = 52 + PARENT_ROOT_BONE = 53 + PARENT_BONE_BONE = 55 + + + def __init__(self): + self.children = [] + self.blender_type = None + self.world_matrix = None + self.parent_type = None + + self.blender_object = None + self.blender_bone = None + + self.force_as_empty = False # Used for instancer display + + # Only for bone/bone and object parented to bone + self.parent_bone_uuid = None + + # Only for bones + self.use_deform = None + + # Only for armature + self.bones = {} + + # For deformed object + self.armature = None # for deformed object and for bone + self.skin = None + + # glTF + self.node = None + + def add_child(self, uuid): + self.children.append(uuid) + + def set_world_matrix(self, matrix): + self.world_matrix = matrix + + def set_blender_data(self, blender_object, blender_bone): + self.blender_object = blender_object + self.blender_bone = blender_bone + + def recursive_display(self, tree, mode): + if mode == "simple": + for c in self.children: + print(self.blender_object.name, "/", self.blender_bone.name if self.blender_bone else "", "-->", tree.nodes[c].blender_object.name, "/", tree.nodes[c].blender_bone.name if tree.nodes[c].blender_bone else "" ) + tree.nodes[c].recursive_display(tree, mode) + +class VExportTree: + def __init__(self, export_settings): + self.nodes = {} + self.roots = [] + + self.export_settings = export_settings + + self.tree_troncated = False + + def add_node(self, node): + self.nodes[node.uuid] = node + + def add_children(self, uuid_parent, uuid_child): + self.nodes[uuid_parent].add_child(uuid_child) + + def construct(self, blender_scene): + bpy.context.window.scene = blender_scene + depsgraph = bpy.context.evaluated_depsgraph_get() + + for blender_object in [obj.original for obj in depsgraph.scene_eval.objects if obj.parent is None]: + self.recursive_node_traverse(blender_object, None, None, Matrix.Identity(4)) + + def recursive_node_traverse(self, blender_object, blender_bone, parent_uuid, parent_coll_matrix_world, armature_uuid=None, dupli_world_matrix=None): + node = VExportNode() + node.uuid = str(uuid.uuid4()) + node.parent_uuid = parent_uuid + node.set_blender_data(blender_object, blender_bone) + + # add to parent if needed + if parent_uuid is not None: + self.add_children(parent_uuid, node.uuid) + else: + self.roots.append(node.uuid) + + # Set blender type + if blender_bone is not None: + node.blender_type = VExportNode.BONE + self.nodes[armature_uuid].bones[blender_bone.name] = node.uuid + node.use_deform = blender_bone.id_data.data.bones[blender_bone.name].use_deform + elif blender_object.type == "ARMATURE": + node.blender_type = VExportNode.ARMATURE + elif blender_object.type == "CAMERA": + node.blender_type = VExportNode.CAMERA + elif blender_object.type == "LIGHT": + node.blender_type = VExportNode.LIGHT + elif blender_object.instance_type == "COLLECTION": + node.blender_type = VExportNode.COLLECTION + else: + node.blender_type = VExportNode.OBJECT + + # For meshes with armature modifier (parent is armature), keep armature uuid + if node.blender_type == VExportNode.OBJECT: + modifiers = {m.type: m for m in blender_object.modifiers} + if "ARMATURE" in modifiers and modifiers["ARMATURE"].object is not None: + if parent_uuid is None or not self.nodes[parent_uuid].blender_type == VExportNode.ARMATURE: + # correct workflow is to parent skinned mesh to armature, but ... + # all users don't use correct workflow + print("WARNING: Armature must be the parent of skinned mesh") + print("Armature is selected by its name, but may be false in case of instances") + # Search an armature by name, and use the first found + # This will be done after all objects are setup + node.armature_needed = modifiers["ARMATURE"].object.name + else: + node.armature = parent_uuid + + # For bones, store uuid of armature + if blender_bone is not None: + node.armature = armature_uuid + + # for bone/bone parenting, store parent, this will help armature tree management + if parent_uuid is not None and self.nodes[parent_uuid].blender_type == VExportNode.BONE and node.blender_type == VExportNode.BONE: + node.parent_bone_uuid = parent_uuid + + + # Objects parented to bone + if parent_uuid is not None and self.nodes[parent_uuid].blender_type == VExportNode.BONE and node.blender_type != VExportNode.BONE: + node.parent_bone_uuid = parent_uuid + + # World Matrix + # Store World Matrix for objects + if dupli_world_matrix is not None: + node.matrix_world = dupli_world_matrix + elif node.blender_type in [VExportNode.OBJECT, VExportNode.COLLECTION, VExportNode.ARMATURE, VExportNode.CAMERA, VExportNode.LIGHT]: + # Matrix World of object is expressed based on collection instance objects are + # So real world matrix is collection world_matrix @ "world_matrix" of object + node.matrix_world = parent_coll_matrix_world @ blender_object.matrix_world.copy() + if node.blender_type == VExportNode.CAMERA and self.export_settings[gltf2_blender_export_keys.CAMERAS]: + correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0)) + node.matrix_world @= correction.to_matrix().to_4x4() + elif node.blender_type == VExportNode.LIGHT and self.export_settings[gltf2_blender_export_keys.LIGHTS]: + correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0)) + node.matrix_world @= correction.to_matrix().to_4x4() + elif node.blender_type == VExportNode.BONE: + node.matrix_world = self.nodes[node.armature].matrix_world @ blender_bone.matrix + axis_basis_change = Matrix( + ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + node.matrix_world = node.matrix_world @ axis_basis_change + + # Force empty ? + # For duplis, if instancer is not display, we should create an empty + if blender_object.is_instancer is True and blender_object.show_instancer_for_render is False: + node.force_as_empty = True + + # Storing this node + self.add_node(node) + + ###### Manage children ###### + + # standard children + if blender_bone is None and blender_object.is_instancer is False: + for child_object in blender_object.children: + if child_object.parent_bone: + # Object parented to bones + # Will be manage later + continue + else: + # Classic parenting + self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world) + + # Collections + if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection: + for dupli_object in blender_object.instance_collection.objects: + if dupli_object.parent is not None: + continue + self.recursive_node_traverse(dupli_object, None, node.uuid, node.matrix_world) + + # Armature : children are bones with no parent + if blender_object.type == "ARMATURE" and blender_bone is None: + for b in [b for b in blender_object.pose.bones if b.parent is None]: + self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, node.uuid) + + # Bones + if blender_object.type == "ARMATURE" and blender_bone is not None: + for b in blender_bone.children: + self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, armature_uuid) + + # Object parented to bone + if blender_bone is not None: + for child_object in [c for c in blender_object.children if c.parent_bone is not None and c.parent_bone == blender_bone.name]: + self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world) + + # Duplis + if blender_object.is_instancer is True and blender_object.instance_type != 'COLLECTION': + depsgraph = bpy.context.evaluated_depsgraph_get() + for (dupl, mat) in [(dup.object.original, dup.matrix_world.copy()) for dup in depsgraph.object_instances if dup.parent and id(dup.parent.original) == id(blender_object)]: + self.recursive_node_traverse(dupl, None, node.uuid, parent_coll_matrix_world, dupli_world_matrix=mat) + + def get_all_objects(self): + return [n.uuid for n in self.nodes.values() if n.blender_type != VExportNode.BONE] + + def get_all_bones(self, uuid): #For armatue Only + if self.nodes[uuid].blender_type == VExportNode.ARMATURE: + def recursive_get_all_bones(uuid): + total = [] + if self.nodes[uuid].blender_type == VExportNode.BONE: + total.append(uuid) + for child_uuid in self.nodes[uuid].children: + total.extend(recursive_get_all_bones(child_uuid)) + + return total + + tot = [] + for c_uuid in self.nodes[uuid].children: + tot.extend(recursive_get_all_bones(c_uuid)) + return tot + else: + return [] + + def display(self, mode): + if mode == "simple": + for n in self.roots: + print("Root", self.nodes[n].blender_object.name, "/", self.nodes[n].blender_bone.name if self.nodes[n].blender_bone else "" ) + self.nodes[n].recursive_display(self, mode) + + + def filter_tag(self): + roots = self.roots.copy() + for r in roots: + self.recursive_filter_tag(r, None) + + def filter_perform(self): + roots = self.roots.copy() + for r in roots: + self.recursive_filter(r, None) # Root, so no parent + + def filter(self): + self.filter_tag() + self.filter_perform() + + + def recursive_filter_tag(self, uuid, parent_keep_tag): + # parent_keep_tag is for collection instance + # some properties (selection, visibility, renderability) + # are defined at collection level, and we need to use these values + # for all objects of the collection instance. + # But some properties (camera, lamp ...) are not defined at collection level + if parent_keep_tag is None: + self.nodes[uuid].keep_tag = self.node_filter_not_inheritable_is_kept(uuid) and self.node_filter_inheritable_is_kept(uuid) + elif parent_keep_tag is True: + self.nodes[uuid].keep_tag = self.node_filter_not_inheritable_is_kept(uuid) + elif parent_keep_tag is False: + self.nodes[uuid].keep_tag = False + else: + print("This should not happen!") + + for child in self.nodes[uuid].children: + if self.nodes[uuid].blender_type == VExportNode.COLLECTION: + self.recursive_filter_tag(child, self.nodes[uuid].keep_tag) + else: + self.recursive_filter_tag(child, parent_keep_tag) + + def recursive_filter(self, uuid, parent_kept_uuid): + children = self.nodes[uuid].children.copy() + + new_parent_kept_uuid = None + if self.nodes[uuid].keep_tag is False: + new_parent_kept_uuid = parent_kept_uuid + # Need to modify tree + if self.nodes[uuid].parent_uuid is not None: + self.nodes[self.nodes[uuid].parent_uuid].children.remove(uuid) + else: + # Remove from root + self.roots.remove(uuid) + else: + new_parent_kept_uuid = uuid + + # If parent_uuid is not parent_kept_uuid, we need to modify children list of parent_kept_uuid + if parent_kept_uuid != self.nodes[uuid].parent_uuid and parent_kept_uuid is not None: + self.tree_troncated = True + self.nodes[parent_kept_uuid].children.append(uuid) + + # If parent_kept_uuid is None, and parent_uuid was not, add to root list + if self.nodes[uuid].parent_uuid is not None and parent_kept_uuid is None: + self.tree_troncated = True + self.roots.append(uuid) + + # Modify parent uuid + self.nodes[uuid].parent_uuid = parent_kept_uuid + + for child in children: + self.recursive_filter(child, new_parent_kept_uuid) + + + def node_filter_not_inheritable_is_kept(self, uuid): + # Export Camera or not + if self.nodes[uuid].blender_type == VExportNode.CAMERA: + if self.export_settings[gltf2_blender_export_keys.CAMERAS] is False: + return False + + # Export Lamp or not + if self.nodes[uuid].blender_type == VExportNode.LIGHT: + if self.export_settings[gltf2_blender_export_keys.LIGHTS] is False: + return False + + # Export deform bones only + if self.nodes[uuid].blender_type == VExportNode.BONE: + if self.export_settings['gltf_def_bones'] is True and self.nodes[uuid].use_deform is False: + # Check if bone has some objected parented to bone. We need to keep it in that case, even if this is not a def bone + if len([c for c in self.nodes[uuid].children if self.nodes[c].blender_type != VExportNode.BONE]) != 0: + return True + return False + + return True + + def node_filter_inheritable_is_kept(self, uuid): + + if self.export_settings[gltf2_blender_export_keys.SELECTED] and self.nodes[uuid].blender_object.select_get() is False: + return False + + if self.export_settings[gltf2_blender_export_keys.VISIBLE]: + # The eye in outliner (object) + if self.nodes[uuid].blender_object.visible_get() is False: + return False + + # The screen in outliner (object) + if self.nodes[uuid].blender_object.hide_viewport is True: + return False + + # The screen in outliner (collections) + if all([c.hide_viewport for c in self.nodes[uuid].blender_object.users_collection]): + return False + + # The camera in outliner (object) + if self.export_settings[gltf2_blender_export_keys.RENDERABLE]: + if self.nodes[uuid].blender_object.hide_render is True: + return False + + # The camera in outliner (collections) + if all([c.hide_render for c in self.nodes[uuid].blender_object.users_collection]): + return False + + if self.export_settings[gltf2_blender_export_keys.ACTIVE_COLLECTION]: + found = any(x == self.nodes[uuid].blender_object for x in bpy.context.collection.all_objects) + if not found: + return False + + return True + + def search_missing_armature(self): + for n in [n for n in self.nodes.values() if hasattr(n, "armature_needed") is True]: + candidates = [i for i in self.nodes.values() if i.blender_type == VExportNode.ARMATURE and i.blender_object.name == n.armature_needed] + if len(candidates) > 0: + n.armature = candidates[0].uuid + del n.armature_needed + \ No newline at end of file -- cgit v1.2.3