From e565cbc89fe1bd2135b789c256aef381da7b7920 Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 18:51:41 +0100 Subject: [PATCH 01/13] v4.3.9 --- ReleaseLogs/Version_4.3.2.md | 3 ++- ReleaseLogs/Version_4.3.9.md | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 ReleaseLogs/Version_4.3.9.md diff --git a/ReleaseLogs/Version_4.3.2.md b/ReleaseLogs/Version_4.3.2.md index 0fa87b4d..02ebb62b 100644 --- a/ReleaseLogs/Version_4.3.2.md +++ b/ReleaseLogs/Version_4.3.2.md @@ -3,4 +3,5 @@ Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/ ### Version 4.3.2 -- New: Addon renamed from "Blender For Unreal Engine" To "Unreal Engine Assets Exporter". This was needed to follow [Blender Extensions Terms of Service](https://extensions.blender.org/terms-of-service/) (Branding) +- New: Addon renamed from "Blender For Unreal Engine" To "Unreal Engine Assets Exporter". + This was needed to follow [Blender Extensions Terms of Service](https://extensions.blender.org/terms-of-service/) (Branding) diff --git a/ReleaseLogs/Version_4.3.9.md b/ReleaseLogs/Version_4.3.9.md new file mode 100644 index 00000000..4d42902c --- /dev/null +++ b/ReleaseLogs/Version_4.3.9.md @@ -0,0 +1,6 @@ +# Unreal Engine Assets Exporter - Release Log +Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Release-Logs + +### Version 4.3.9 + +- Fixed: Animation UI not visible on linked Armatures. \ No newline at end of file From 4e0ba2071f2bb363ad4b41dc5c15f967a921da95 Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 18:51:47 +0100 Subject: [PATCH 02/13] - Fixed: Animation UI not visible on linked Armatures. --- blender-for-unrealengine/addon_generate_config.json | 2 +- .../bfu_anim_action/bfu_anim_action_ui.py | 5 +---- .../bfu_anim_action_adv/bfu_anim_action_adv_ui.py | 2 -- blender-for-unrealengine/bfu_anim_base/bfu_anim_base_ui.py | 2 -- blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_ui.py | 2 -- .../bfu_anim_nla_adv/bfu_anim_nla_adv_ui.py | 2 -- 6 files changed, 2 insertions(+), 13 deletions(-) diff --git a/blender-for-unrealengine/addon_generate_config.json b/blender-for-unrealengine/addon_generate_config.json index e67ebe10..581c3e9f 100644 --- a/blender-for-unrealengine/addon_generate_config.json +++ b/blender-for-unrealengine/addon_generate_config.json @@ -2,7 +2,7 @@ "schema_version": [1,0,0], "blender_manifest": { "id": "unrealengine_assets_exporter", - "version": [4,3,8], + "version": [4,3,9], "name": "Unreal Engine Assets Exporter", "tagline": "Allows to batch export and import in Unreal Engine", "maintainer": "Loux Xavier (BleuRaven) xavierloux.loux@gmail.com", diff --git a/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_ui.py b/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_ui.py index 621f63a3..300c85e6 100644 --- a/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_ui.py +++ b/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_ui.py @@ -35,17 +35,14 @@ def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): is_camera = bfu_camera.bfu_camera_utils.is_camera(obj) is_alembic_animation = bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj) - # Hide filters if obj is None: return - if bfu_utils.GetExportAsProxy(obj): - return if obj.bfu_export_type != "export_recursive": return if True not in [is_skeletal_mesh, is_camera, is_alembic_animation]: return - + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "ANIM"): scene.bfu_animation_action_properties_expanded.draw(layout) if scene.bfu_animation_action_properties_expanded.is_expend(): diff --git a/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_ui.py b/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_ui.py index e3e716d6..a9031240 100644 --- a/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_ui.py +++ b/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_ui.py @@ -32,8 +32,6 @@ def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): # Hide filters if obj is None: return - if bfu_utils.GetExportAsProxy(obj): - return if obj.bfu_export_type != "export_recursive": return if bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj): diff --git a/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_ui.py b/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_ui.py index 2eaac84d..830e1c05 100644 --- a/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_ui.py +++ b/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_ui.py @@ -35,8 +35,6 @@ def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): # Hide filters if obj is None: return - if bfu_utils.GetExportAsProxy(obj): - return if obj.bfu_export_type != "export_recursive": return diff --git a/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_ui.py b/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_ui.py index cb861e34..537cb90c 100644 --- a/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_ui.py +++ b/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_ui.py @@ -35,8 +35,6 @@ def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): # Hide filters if obj is None: return - if bfu_utils.GetExportAsProxy(obj): - return if obj.bfu_export_type != "export_recursive": return diff --git a/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_ui.py b/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_ui.py index 3b7dba6b..9f3e04a7 100644 --- a/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_ui.py +++ b/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_ui.py @@ -34,8 +34,6 @@ def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): # Hide filters if obj is None: return - if bfu_utils.GetExportAsProxy(obj): - return if obj.bfu_export_type != "export_recursive": return if bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj): From 7ba066ec0988b88f6d7fef8ef7b7935c68331c6d Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 19:25:56 +0100 Subject: [PATCH 03/13] Update BBAM lib to avoid toml import --- blender-for-unrealengine/bbam/__init__.py | 2 +- blender-for-unrealengine/bbam/config.py | 2 + .../bbam/manifest_generate.py | 72 ++++++++++++------- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/blender-for-unrealengine/bbam/__init__.py b/blender-for-unrealengine/bbam/__init__.py index 7994211d..366caa40 100644 --- a/blender-for-unrealengine/bbam/__init__.py +++ b/blender-for-unrealengine/bbam/__init__.py @@ -89,7 +89,7 @@ def install_from_blender_with_build_data(addon_path, addon_manifest_data): for target_build_name in addon_manifest_data["builds"]: # Create temporary addon folder temp_addon_path = addon_file_management.create_temp_addon_folder( - addon_path, addon_manifest_data, target_build_name + addon_path, addon_manifest_data, target_build_name, config.show_debug ) # Zip the addon folder for installation zip_file = addon_file_management.zip_addon_folder( diff --git a/blender-for-unrealengine/bbam/config.py b/blender-for-unrealengine/bbam/config.py index 04235c04..c63e500b 100644 --- a/blender-for-unrealengine/bbam/config.py +++ b/blender-for-unrealengine/bbam/config.py @@ -36,3 +36,5 @@ # Folder where the generated build files will be stored build_output_folder = "generated_builds" + +show_debug = False \ No newline at end of file diff --git a/blender-for-unrealengine/bbam/manifest_generate.py b/blender-for-unrealengine/bbam/manifest_generate.py index 4d9b8e9a..4f89be31 100644 --- a/blender-for-unrealengine/bbam/manifest_generate.py +++ b/blender-for-unrealengine/bbam/manifest_generate.py @@ -24,32 +24,11 @@ # ---------------------------------------------- import os -import toml -from toml.encoder import TomlEncoder from . import config from . import utils from . import blender_utils -class MultiLineTomlEncoder(TomlEncoder): - """ - Custom TOML encoder that formats lists to display on multiple lines in the output. - """ - def dump_list(self, v): - """ - Formats a list to display each item on a new line in the TOML output. - - Parameters: - v (list): The list to format for multi-line display. - - Returns: - str: A formatted multi-line string representation of the list. - """ - output = "[\n" - for item in v: - output += f" {toml.encoder._dump_str(item)},\n" # Indent each item - output += "]" - return output def generate_new_manifest(addon_generate_config_data, target_build_name): """ @@ -81,15 +60,53 @@ def generate_new_manifest(addon_generate_config_data, target_build_name): data["website"] = manifest_data["website_url"] data["type"] = manifest_data["type"] data["tags"] = manifest_data["tags"] - data["permissions"] = manifest_data["permissions"] data["blender_version_min"] = utils.get_str_version(build_data["blender_version_min"]) data["license"] = manifest_data["license"] data["copyright"] = manifest_data["copyright"] + data["permissions"] = manifest_data["permissions"] return data +def dump_list(v): + """ + Formats a list to display each item on a new line in the TOML output. + + Parameters: + v (list): The list to format for multi-line display. + + Returns: + str: A formatted multi-line string representation of the list. + """ + output = "[\n" + for item in v: + output += f" \"{str(item)}\",\n" # Indent each item and escape as a string + output += "]" + return output + +def dict_to_toml(data): + """ + Convert a dictionary to a TOML-formatted string. + + Parameters: + data (dict): The dictionary to format. + + Returns: + str: A TOML-formatted string representation of the dictionary. + """ + toml_string = "" + for key, value in data.items(): + if isinstance(value, dict): + toml_string += f"[{key}]\n" + dict_to_toml(value) # Recursive call for nested dictionaries + elif isinstance(value, list): + toml_string += f"{key} = {dump_list(value)}\n" + elif isinstance(value, str): + toml_string += f"{key} = \"{value}\"\n" # Add quotes for strings + else: + toml_string += f"{key} = {value}\n" + return toml_string + def save_addon_manifest(addon_path, data, show_debug=False): """ - Saves the addon manifest as a TOML file using the MultiLineTomlEncoder for custom formatting. + Saves the addon manifest as a TOML file manually. Parameters: addon_path (str): Path to the addon's root folder. @@ -98,9 +115,12 @@ def save_addon_manifest(addon_path, data, show_debug=False): """ addon_manifest_path = os.path.join(addon_path, config.blender_manifest) - # Save the manifest as a TOML file with custom encoder for multi-line list formatting + # Generate TOML content manually + toml_content = dict_to_toml(data) + + # Save to file with open(addon_manifest_path, "w") as file: - toml.dump(data, file, encoder=MultiLineTomlEncoder()) + file.write(toml_content) if show_debug: - print(f"Addon manifest saved successfully at: {addon_manifest_path}") + print(f"Addon manifest saved successfully at: {addon_manifest_path}") \ No newline at end of file From 6bc0429fcdfffbd4d19c34ca9a795d6f5518c9d2 Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 20:27:26 +0100 Subject: [PATCH 04/13] Better function search using ast --- .../fbxio/generator/edit_fbx_utils.py | 47 ++++++++++--------- .../fbxio/generator/edit_files.py | 8 ++++ .../fbxio/generator/generator.py | 7 +-- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/blender-for-unrealengine/fbxio/generator/edit_fbx_utils.py b/blender-for-unrealengine/fbxio/generator/edit_fbx_utils.py index f0674026..3754fc53 100644 --- a/blender-for-unrealengine/fbxio/generator/edit_fbx_utils.py +++ b/blender-for-unrealengine/fbxio/generator/edit_fbx_utils.py @@ -1,30 +1,35 @@ from . import edit_files - -has_valid_parent_function = ''' - def has_valid_parent(self, objects): - par = self.parent - if par in objects: - if self._tag == 'OB': - par_type = self.bdata.parent_type - if par_type in {'OBJECT', 'BONE'}: - return True - else: - print("Sorry, “{}” parenting type is not supported".format(par_type)) - return False - return True - return False -''' +import ast + +def get_has_valid_parent_func(file_path): + content = edit_files.get_file_content(file_path) + tree = ast.parse(content) + lines = None + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == "ObjectWrapper": + for class_node in node.body: + if isinstance(class_node, ast.FunctionDef) and class_node.name == "has_valid_parent": + # Extraire le code source de la fonction + start_line = class_node.lineno - 1 + end_line = class_node.end_lineno + function_lines = content.splitlines()[start_line:end_line] + return "\n".join(function_lines) + edit_files.print_edit_error(f"Function 'has_valid_parent' inside 'ObjectWrapper' not found in {file_path}") def update_fbx_utils(file_path, version): add_re_import(file_path) edit_files.add_quaternion_import(file_path) add_support_for_custom_kind(file_path) - new_func = add_is_leg_bone_func(has_valid_parent_function, file_path) - new_func = add_is_rightside_bone_func(new_func, file_path) - new_func = add_is_reverse_direction_bone_func(new_func, file_path) - new_func = add_is_basic_bone_func(new_func, file_path) - new_func = add_aling_matrix_funcs(new_func, file_path) - add_disable_free_scale_animation(file_path) + + lines = get_has_valid_parent_func(file_path) + + if lines: + new_func = add_is_leg_bone_func(lines, file_path) + new_func = add_is_rightside_bone_func(new_func, file_path) + new_func = add_is_reverse_direction_bone_func(new_func, file_path) + new_func = add_is_basic_bone_func(new_func, file_path) + new_func = add_aling_matrix_funcs(new_func, file_path) + add_disable_free_scale_animation(file_path) def add_re_import(file_path): diff --git a/blender-for-unrealengine/fbxio/generator/edit_files.py b/blender-for-unrealengine/fbxio/generator/edit_files.py index 4f42ae12..f880d5fd 100644 --- a/blender-for-unrealengine/fbxio/generator/edit_files.py +++ b/blender-for-unrealengine/fbxio/generator/edit_files.py @@ -1,6 +1,14 @@ +import ast + def print_edit_error(text): print(f"\033[91m{text}\033[0m") +def get_file_content(file_path): + # Lire le contenu du fichier Python + with open(file_path, 'r+', encoding='utf-8') as f: + content = f.read() + return content + def add_header_to_file(file_path): header = ( "# --------------------------------------------- \n" diff --git a/blender-for-unrealengine/fbxio/generator/generator.py b/blender-for-unrealengine/fbxio/generator/generator.py index 800b2682..b424483f 100644 --- a/blender-for-unrealengine/fbxio/generator/generator.py +++ b/blender-for-unrealengine/fbxio/generator/generator.py @@ -79,6 +79,7 @@ def run_generate(self): # Create the destination folder in the parent directory self.update_fbx_addon_version() version_as_module = self.get_str_version() + print("Start Generate ", version_as_module) dest_folder = os.path.join(parent_directory, io_scene_fbx_prefix+version_as_module) if not os.path.exists(dest_folder): os.makedirs(dest_folder) @@ -112,8 +113,6 @@ def update_fbx_addon_version(self): self.fbx_addon_version = tuple(map(int, elements)) return - - def copy_export_files(self, dest_folder): addon_folder = self.get_addon_folder() new_files = [] @@ -132,7 +131,9 @@ def copy_export_files(self, dest_folder): else: print(f"File does not exist: {source_file}") - print(f"Copied specified FBX exporter files for Blender {self.version} to {dest_folder}") + print(f"Copied specified FBX exporter files.") + print(f"Source: {source_file}") + print(f"Target: {dest_folder}") return new_files From d25491f9927dfe02919c78e3d6cdfb8f3b2f7486 Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 20:27:44 +0100 Subject: [PATCH 05/13] Update bbam lib --- blender-for-unrealengine/bbam/blender_utils.py | 17 ++++++++++++----- blender-for-unrealengine/bbam/utils.py | 3 +++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/blender-for-unrealengine/bbam/blender_utils.py b/blender-for-unrealengine/bbam/blender_utils.py index 59f85338..b96cb329 100644 --- a/blender-for-unrealengine/bbam/blender_utils.py +++ b/blender-for-unrealengine/bbam/blender_utils.py @@ -24,6 +24,7 @@ # ---------------------------------------------- import os +from . import utils def uninstall_addon_from_blender(bpy, pkg_id, module): """ @@ -56,11 +57,17 @@ def install_zip_addon_from_blender(bpy, zip_file, module): if bpy.app.version >= (4, 2, 0): # For Blender version 4.2.0 and above, install as an extension print("Installing as extension...", zip_file) - bpy.ops.extensions.package_install_files(repo="user_default", filepath=zip_file, enable_on_install=True) - print("Extension installation complete.") + try: + bpy.ops.extensions.package_install_files(repo="user_default", filepath=zip_file, enable_on_install=True) + print("Extension installation complete.") + except Exception as e: + utils.print_red("An error occurred during installation:", str(e)) else: # For earlier versions, install and enable as an addon print("Installing as add-on...", zip_file) - bpy.ops.preferences.addon_install(overwrite=True, filepath=zip_file) - bpy.ops.preferences.addon_enable(module=module) - print("Add-on installation complete.") + try: + bpy.ops.preferences.addon_install(overwrite=True, filepath=zip_file) + bpy.ops.preferences.addon_enable(module=module) + print("Add-on installation complete.") + except Exception as e: + utils.print_red("An error occurred during installation:", str(e)) \ No newline at end of file diff --git a/blender-for-unrealengine/bbam/utils.py b/blender-for-unrealengine/bbam/utils.py index cbb1a226..8f4e85e9 100644 --- a/blender-for-unrealengine/bbam/utils.py +++ b/blender-for-unrealengine/bbam/utils.py @@ -23,6 +23,9 @@ # XavierLoux.com # ---------------------------------------------- +def print_red(*values): + print("\033[91m", *values, "\033[0m") + def get_str_version(data): """ Converts a list of version components into a version string. From 087d49b09be1c6b5b5136cc5d4c19cbc4f64eb1c Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 20:29:21 +0100 Subject: [PATCH 06/13] Update FBX files for Blender 4.3 --- .../fbxio/io_scene_fbx_2_83/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_2_93/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_3_1/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_3_2/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_3_3/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_3_4/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_3_5/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_3_6/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_4_0/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_4_1/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_4_2/fbx_utils.py | 2 +- .../fbxio/io_scene_fbx_4_3/export_fbx_bin.py | 17 ++++++++++------- .../fbxio/io_scene_fbx_4_3/fbx_utils.py | 12 ++++++------ .../io_scene_fbx_4_3/fbx_utils_threading.py | 2 +- 14 files changed, 28 insertions(+), 25 deletions(-) diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_2_83/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_2_83/fbx_utils.py index d5b94ec9..fd16dd62 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_2_83/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_2_83/fbx_utils.py @@ -1084,7 +1084,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1140,6 +1139,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_2_93/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_2_93/fbx_utils.py index d5b94ec9..fd16dd62 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_2_93/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_2_93/fbx_utils.py @@ -1084,7 +1084,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1140,6 +1139,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_3_1/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_3_1/fbx_utils.py index 4e6f2707..dc4bef2e 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_3_1/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_3_1/fbx_utils.py @@ -1085,7 +1085,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1141,6 +1140,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_3_2/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_3_2/fbx_utils.py index 05afbc63..bf268813 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_3_2/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_3_2/fbx_utils.py @@ -1070,7 +1070,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1126,6 +1125,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_3_3/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_3_3/fbx_utils.py index eddb9009..79744b91 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_3_3/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_3_3/fbx_utils.py @@ -1075,7 +1075,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1131,6 +1130,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_3_4/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_3_4/fbx_utils.py index 839bf08d..b00a615e 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_3_4/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_3_4/fbx_utils.py @@ -1075,7 +1075,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1131,6 +1130,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_3_5/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_3_5/fbx_utils.py index e005bdd6..895fb2d2 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_3_5/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_3_5/fbx_utils.py @@ -1075,7 +1075,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1131,6 +1130,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_3_6/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_3_6/fbx_utils.py index 4207fa71..0426fd16 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_3_6/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_3_6/fbx_utils.py @@ -1400,7 +1400,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1456,6 +1455,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_4_0/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_4_0/fbx_utils.py index a639fa81..6e837780 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_4_0/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_4_0/fbx_utils.py @@ -1737,7 +1737,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1793,6 +1792,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_4_1/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_4_1/fbx_utils.py index c5a9da41..2157701c 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_4_1/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_4_1/fbx_utils.py @@ -1750,7 +1750,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1806,6 +1805,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_4_2/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_4_2/fbx_utils.py index 68b247d6..a257cc23 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_4_2/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_4_2/fbx_utils.py @@ -1750,7 +1750,6 @@ def has_valid_parent(self, objects): return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1806,6 +1805,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/export_fbx_bin.py b/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/export_fbx_bin.py index 1b5d77e3..5453146f 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/export_fbx_bin.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/export_fbx_bin.py @@ -1755,7 +1755,7 @@ def fbx_data_video_elements(root, vid, scene_data): with open(filepath, 'br') as f: elem_data_single_bytes(fbx_vid, b"Content", f.read()) except Exception as e: - print("WARNING: embedding file {} failed ({})".format(filepath, e)) + print("WARNING: embedding file {:s} failed ({:s})".format(filepath, str(e))) elem_data_single_bytes(fbx_vid, b"Content", b"") msetts.embedded_set.add(filepath) # Looks like we'd rather not write any 'Content' element in this case (see T44442). @@ -2731,7 +2731,7 @@ def fbx_data_from_scene(scene, depsgraph, settings): eval_mats = tuple(slot.material.original if slot.material else None for slot in ob_to_convert.material_slots) if orig_mats != eval_mats: - # Override the default behaviour of getting materials from ob_obj.bdata.material_slots. + # Override the default behavior of getting materials from `ob_obj.bdata.material_slots`. ob_obj.override_materials = eval_mats elif do_convert: tmp_me = bpy.data.meshes.new_from_object(ob, preserve_all_data_layers=True, depsgraph=depsgraph) @@ -3086,7 +3086,7 @@ def sk_cos(shape_key): idx = _objs_indices[ob_obj] = _objs_indices.get(ob_obj, -1) + 1 # XXX If a mesh has multiple material slots with the same material, they are combined into one slot. # Even if duplicate materials were exported without combining them into one slot, keeping duplicate - # materials separated does not appear to be common behaviour of external software when importing FBX. + # materials separated does not appear to be common behavior of external software when importing FBX. mesh_material_indices.setdefault(me, {})[ma] = idx del _objs_indices @@ -3160,7 +3160,7 @@ def fbx_header_elements(root, scene_data, time=None): app_vendor = "Blender Foundation" app_name = "Blender (Blender for UnrealEngine specialized FBX IO)" app_ver = bpy.app.version_string - addon_ver = (5, 12, 4) # Blender-For-UnrealEngine edited version. + addon_ver = (5, 12, 5) # Blender-For-UnrealEngine edited version. # ##### Start of FBXHeaderExtension element. header_ext = elem_empty(root, b"FBXHeaderExtension") @@ -3575,7 +3575,7 @@ def save_single(operator, scene, depsgraph, filepath="", import bpy_extras.io_utils print('\nFBX export starting... %r' % filepath) - start_time = time.process_time() + start_time = time.time() # Generate some data about exported scene... scene_data = fbx_data_from_scene(scene, depsgraph, settings) @@ -3618,7 +3618,7 @@ def save_single(operator, scene, depsgraph, filepath="", if not media_settings.embed_textures: bpy_extras.io_utils.path_reference_copy(media_settings.copy_set) - print('export finished in %.4f sec.' % (time.process_time() - start_time)) + print('export finished in %.4f sec.' % (time.time() - start_time)) return {'FINISHED'} @@ -3718,9 +3718,12 @@ def save(operator, context, if animation_only: ctx_objects = tuple(obj for obj in ctx_objects if not obj.type in BLENDER_OBJECT_TYPES_MESHLIKE) + # Sort exported objects by their names. + ctx_objects = sorted(ctx_objects, key=lambda ob: ob.name) + # Ensure no Objects are in Edit mode. # Copy to a tuple for safety, to avoid the risk of modifying ctx_objects while iterating. - for obj in tuple(ctx_objects): + for obj in ctx_objects: if not ensure_object_not_in_edit_mode(context, obj): operator.report({'ERROR'}, "%s could not be set out of Edit Mode, so cannot be exported" % obj.name) return {'CANCELLED'} diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/fbx_utils.py b/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/fbx_utils.py index 68b247d6..a4c15c8e 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/fbx_utils.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/fbx_utils.py @@ -361,7 +361,7 @@ def _mat4_vec3_array_multiply(mat4, vec3_array, dtype=None, return_4d=False): # column_vector_multiplication in mathutils_Vector.c uses double precision math for Matrix @ Vector by casting the # matrix's values to double precision and then casts back to single precision when returning the result, so at least - # double precision math is always be used to match standard Blender behaviour. + # double precision math is always be used to match standard Blender behavior. math_precision = np.result_type(np.double, vec3_array) to_multiply = None @@ -537,7 +537,7 @@ def fast_first_axis_unique(ar, return_unique=True, return_index=False, return_in Float type caveats: All elements of -0.0 in the input array will be replaced with 0.0 to ensure that both values are collapsed into one. - NaN values can have lots of different byte representations (e.g. signalling/quiet and custom payloads). Only the + NaN values can have lots of different byte representations (e.g. signaling/quiet and custom payloads). Only the duplicates of each unique byte representation will be collapsed into one.""" # At least something should always be returned. assert(return_unique or return_index or return_inverse or return_counts) @@ -731,7 +731,7 @@ class AttributeDescription: domain: str # Some attributes are required to exist if certain conditions are met. If a required attribute does not exist when # attempting to get it, an AssertionError is raised. - is_required_check: Callable[[bpy.types.AttributeGroup], bool] = None + is_required_check: Callable[[bpy.types.AttributeGroupMesh], bool] = None # NumPy dtype that matches the internal C data of this attribute. dtype: np.dtype = field(init=False) # The default attribute name to use with foreach_get and foreach_set. @@ -848,7 +848,7 @@ def _key_to_uuid(uuids, key): uuid += inc if 0 > uuid >= 2**63: # Note that this is more that unlikely, but does not harm anyway... - raise ValueError("Unable to generate an UUID for key {}".format(key)) + raise ValueError("Unable to generate an UUID for key {!r}".format(key)) return UUID(uuid) @@ -1746,11 +1746,10 @@ def has_valid_parent(self, objects): if par_type in {'OBJECT', 'BONE'}: return True else: - print("Sorry, “{}” parenting type is not supported".format(par_type)) + print("Sorry, \"{:s}\" parenting type is not supported".format(par_type)) return False return True return False - LEG_NAME_PATTERN = re.compile(r'[^a-zA-Z]?(thigh|calf)([^a-zA-Z]|$)', re.IGNORECASE) def is_leg_bone(self): @@ -1806,6 +1805,7 @@ def get_parent_bone_align_matrix_inv(self, scene_data): return bone_aligns[self.armature.name][self.bdata.parent.name][1] return None + def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... diff --git a/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/fbx_utils_threading.py b/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/fbx_utils_threading.py index 8e8f80fa..35949787 100644 --- a/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/fbx_utils_threading.py +++ b/blender-for-unrealengine/fbxio/io_scene_fbx_4_3/fbx_utils_threading.py @@ -108,7 +108,7 @@ def new_cpu_bound_cm(cls, consumer_function, other_cpu_bound_threads_in_use=1, h Any task that fails with an exception will cause all task consumer threads to stop. The maximum number of threads used matches the number of cpus available up to a maximum of `hard_max_threads`. - `hard_max_threads`'s default of 32 matches ThreadPoolExecutor's default behaviour. + `hard_max_threads`'s default of 32 matches ThreadPoolExecutor's default behavior. The maximum number of threads used is decreased by `other_cpu_bound_threads_in_use`. Defaulting to `1`, assuming that the calling thread will also be doing CPU-bound work. From 8840b2b12f8bd2d696e8e68234f7e72d42412b50 Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 22:00:18 +0100 Subject: [PATCH 07/13] Update bbam for build generate in Blender 4.1 and older --- .../bbam/addon_file_management.py | 2 +- .../bbam/bl_info_generate.py | 122 ++++++++++++++---- 2 files changed, 97 insertions(+), 27 deletions(-) diff --git a/blender-for-unrealengine/bbam/addon_file_management.py b/blender-for-unrealengine/bbam/addon_file_management.py index bd2add4a..d7c1a39d 100644 --- a/blender-for-unrealengine/bbam/addon_file_management.py +++ b/blender-for-unrealengine/bbam/addon_file_management.py @@ -172,7 +172,7 @@ def zip_addon_folder(src, addon_path, addon_manifest_data, target_build_name, bl print("Start creating simple ZIP file with root folder using shutil") # Specify the root folder name inside the ZIP file - root_folder_name = "my_addon_root_folder" + root_folder_name = build_data["module"] # Create a temporary directory with tempfile.TemporaryDirectory() as temp_dir: diff --git a/blender-for-unrealengine/bbam/bl_info_generate.py b/blender-for-unrealengine/bbam/bl_info_generate.py index da3dfde5..0dc2207b 100644 --- a/blender-for-unrealengine/bbam/bl_info_generate.py +++ b/blender-for-unrealengine/bbam/bl_info_generate.py @@ -24,8 +24,9 @@ # ---------------------------------------------- import os -import re +import ast from . import config +from . import utils def generate_new_bl_info(addon_generate_config_data, target_build_name): """ @@ -63,6 +64,18 @@ def generate_new_bl_info(addon_generate_config_data, target_build_name): return data +def format_bl_info_lines(data): + # Format the new `bl_info` dictionary with line breaks and indentation + new_bl_info_lines = ["bl_info = {"] + items = list(data.items()) + for i, (key, value) in enumerate(items): + if i < len(items) - 1: + new_bl_info_lines.append(f" '{key}': {repr(value)},") + else: + new_bl_info_lines.append(f" '{key}': {repr(value)}") + new_bl_info_lines.append("}\n") # Close `bl_info` and add an extra line break for readability + return new_bl_info_lines + def update_file_bl_info(addon_path, data, show_debug=False): """ Updates the `bl_info` dictionary in the addon's __init__.py file with new data. @@ -74,28 +87,85 @@ def update_file_bl_info(addon_path, data, show_debug=False): """ addon_init_file_path = os.path.join(addon_path, "__init__.py") - # Format the new `bl_info` dictionary with line breaks and indentation - new_bl_info_lines = ["bl_info = {\n"] - for key, value in data.items(): - new_bl_info_lines.append(f" '{key}': {repr(value)},\n") - new_bl_info_lines.append("}\n\n") # Close `bl_info` and add an extra line break for readability - - # Read the existing lines of the __init__.py file - with open(addon_init_file_path, 'r') as file: - lines = file.readlines() - - # Write the updated lines, replacing the old `bl_info` if it exists - with open(addon_init_file_path, "w") as file: - in_bl_info = False - for line in lines: - # Detect the start of `bl_info` - if line.strip().startswith("bl_info = {") and not in_bl_info: - in_bl_info = True - file.writelines(new_bl_info_lines) # Write the new `bl_info` dictionary - elif in_bl_info and line.strip() == "}": - in_bl_info = False # End of `bl_info`, but skip this closing brace line - elif not in_bl_info: - file.write(line) # Write all other lines unchanged - - if show_debug: - print(f"Addon bl_info successfully updated at: {addon_init_file_path}") \ No newline at end of file + result = replace_file_bl_info(addon_init_file_path, data) + if result is False: + result = add_new_bl_info(addon_init_file_path, data) + + if result is False: + utils.print_red(f"Failed to replace or add bl_info! File: {addon_init_file_path}") + + if search_file_bl_info(addon_init_file_path): + if show_debug: + print(f"Addon bl_info successfully updated at: {addon_init_file_path}") + return + else: + utils.print_red(f"Failed to found bl_info after update!: {addon_init_file_path}") + + + +def search_file_bl_info(file_path): + with open(file_path, "r") as file: + content = file.read() + tree = ast.parse(content) + + # Locate existing `bl_info` definition + for node in tree.body: + if isinstance(node, ast.Assign) and any(target.id == "bl_info" for target in node.targets): + return True + return False + +def replace_file_bl_info(file_path, data): + with open(file_path, "r") as file: + content = file.read() + tree = ast.parse(content) + + # Locate existing `bl_info` definition + start_bl_info = None + end_bl_info = None + for index, node in enumerate(tree.body): + if isinstance(node, ast.Assign) and any(target.id == "bl_info" for target in node.targets): + start_bl_info = node.lineno - 1 # Start line of `bl_info` + end_bl_info = node.end_lineno # End line of `bl_info` + break + + if start_bl_info is not None and end_bl_info is not None: + lines = content.splitlines() + # Remove the existing `bl_info` block + del lines[start_bl_info:end_bl_info] + # Insert the new `bl_info` block at the same position + new_bl_info_lines = format_bl_info_lines(data) + lines[start_bl_info:start_bl_info] = new_bl_info_lines + + # Write the updated content back to the file + with open(file_path, "w") as file: + file.write("\n".join(lines)) + return True + return False + +def add_new_bl_info(file_path, data): + with open(file_path, "r") as file: + content = file.read() + tree = ast.parse(content) + + # Find the line number of the `register` function + index_register = None + for index, node in enumerate(tree.body): + if isinstance(node, ast.FunctionDef) and node.name == "register": + index_register = node.lineno - 1 # `lineno` starts at 1, so we subtract 1 for zero-based index + break + + lines = content.splitlines() + new_bl_info_lines = format_bl_info_lines(data) + + if index_register is not None: + # Insert `bl_info` lines before the `register` function + for i, line in enumerate(new_bl_info_lines): + lines.insert(index_register + i, line) + else: + # If `register` is not found, append `bl_info` at the end + lines.extend(new_bl_info_lines) + + # Write the updated content back to the file + with open(file_path, "w") as file: + file.write("\n".join(lines)) + return True \ No newline at end of file From a2cb35e220401c453e41efc7e995f1398ca52f4e Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 22:02:33 +0100 Subject: [PATCH 08/13] - Fixed: Generated Builds for Blender 4.1 and older is wrong. --- ReleaseLogs/Version_4.3.9.md | 3 ++- blender-for-unrealengine/__init__.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ReleaseLogs/Version_4.3.9.md b/ReleaseLogs/Version_4.3.9.md index 4d42902c..a20d87c4 100644 --- a/ReleaseLogs/Version_4.3.9.md +++ b/ReleaseLogs/Version_4.3.9.md @@ -3,4 +3,5 @@ Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/ ### Version 4.3.9 -- Fixed: Animation UI not visible on linked Armatures. \ No newline at end of file +- Fixed: Animation UI not visible on linked Armatures. +- Fixed: Generated Builds for Blender 4.1 and older is wrong. \ No newline at end of file diff --git a/blender-for-unrealengine/__init__.py b/blender-for-unrealengine/__init__.py index 6bb0b904..7819c285 100644 --- a/blender-for-unrealengine/__init__.py +++ b/blender-for-unrealengine/__init__.py @@ -183,8 +183,6 @@ if "bfu_cached_asset_list" in locals(): importlib.reload(bfu_cached_asset_list) -bl_info = {} - class BFUCachedAction(bpy.types.PropertyGroup): """ Represents a cached action for Blender File Utils (BFU). From 127a30a94b94d9133eebbd06bdf9b13491e5f285 Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 22:16:03 +0100 Subject: [PATCH 09/13] Wrong import in asset and sequencer import script. --- .../bfu_import_module/asset_import_script.py | 20 +++++++++++++++++-- .../sequencer_import_script.py | 20 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/blender-for-unrealengine/bfu_import_module/asset_import_script.py b/blender-for-unrealengine/bfu_import_module/asset_import_script.py index dcfd6d3b..a970200c 100644 --- a/blender-for-unrealengine/bfu_import_module/asset_import_script.py +++ b/blender-for-unrealengine/bfu_import_module/asset_import_script.py @@ -7,15 +7,31 @@ import importlib import importlib.util import os -from . import import_module_utils +import sys +import json +def JsonLoad(json_file): + # Changed in Python 3.9: The keyword argument encoding has been removed. + if sys.version_info >= (3, 9): + return json.load(json_file) + else: + return json.load(json_file, encoding="utf8") + + +def JsonLoadFile(json_file_path): + if sys.version_info[0] < 3: + with open(json_file_path, "r") as json_file: + return JsonLoad(json_file) + else: + with open(json_file_path, "r", encoding="utf8") as json_file: + return JsonLoad(json_file) def RunImportScriptWithJsonData(): # Prepare process import json_data_file = 'ImportAssetData.json' dir_path = os.path.dirname(os.path.realpath(__file__)) import_file_path = os.path.join(dir_path, json_data_file) - assets_data = import_module_utils.JsonLoadFile(import_file_path) + assets_data = JsonLoadFile(import_file_path) file_path = os.path.join(assets_data["info"]["addon_path"],'run_unreal_import_script.py') spec = importlib.util.spec_from_file_location("__import_assets__", file_path) diff --git a/blender-for-unrealengine/bfu_import_module/sequencer_import_script.py b/blender-for-unrealengine/bfu_import_module/sequencer_import_script.py index 03a66105..ea49d428 100644 --- a/blender-for-unrealengine/bfu_import_module/sequencer_import_script.py +++ b/blender-for-unrealengine/bfu_import_module/sequencer_import_script.py @@ -7,15 +7,31 @@ import importlib import importlib.util import os -from . import import_module_utils +import sys +import json +def JsonLoad(json_file): + # Changed in Python 3.9: The keyword argument encoding has been removed. + if sys.version_info >= (3, 9): + return json.load(json_file) + else: + return json.load(json_file, encoding="utf8") + + +def JsonLoadFile(json_file_path): + if sys.version_info[0] < 3: + with open(json_file_path, "r") as json_file: + return JsonLoad(json_file) + else: + with open(json_file_path, "r", encoding="utf8") as json_file: + return JsonLoad(json_file) def RunImportScriptWithJsonData(): # Prepare process import json_data_file = 'ImportSequencerData.json' dir_path = os.path.dirname(os.path.realpath(__file__)) import_file_path = os.path.join(dir_path, json_data_file) - sequence_data = import_module_utils.JsonLoadFile(import_file_path) + sequence_data = JsonLoadFile(import_file_path) file_path = os.path.join(sequence_data["info"]["addon_path"],'run_unreal_import_script.py') spec = importlib.util.spec_from_file_location("__import_sequencer__", file_path) From b7dbffcf9cf5c6c444127d2a70fcdfb567775ea2 Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 22:21:24 +0100 Subject: [PATCH 10/13] Fix missing var in import_skeletal_lod() --- .../bfu_import_module/import_module_post_treatment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blender-for-unrealengine/bfu_import_module/import_module_post_treatment.py b/blender-for-unrealengine/bfu_import_module/import_module_post_treatment.py index 8798a244..04773c47 100644 --- a/blender-for-unrealengine/bfu_import_module/import_module_post_treatment.py +++ b/blender-for-unrealengine/bfu_import_module/import_module_post_treatment.py @@ -51,7 +51,7 @@ def import_static_lod(asset, asset_options, asset_data, asset_additional_data, l slot_replaced = unreal.EditorStaticMeshLibrary.set_lod_from_static_mesh(asset, lod_number, lodAsset, 0, True) unreal.EditorAssetLibrary.delete_asset(lodTask.imported_object_paths[0]) -def import_skeletal_lod(asset, asset_data, asset_additional_data, lod_name, lod_number): +def import_skeletal_lod(asset, asset_options, asset_data, asset_additional_data, lod_name, lod_number): if "LevelOfDetail" in asset_additional_data: if lod_name in asset_additional_data["LevelOfDetail"]: # Unreal python no longer support Skeletal mesh LODS import. @@ -66,6 +66,7 @@ def set_static_mesh_lods(asset, asset_options, asset_data, asset_additional_data import_static_lod(asset, asset_options, asset_data, asset_additional_data, "lod_4", 4) import_static_lod(asset, asset_options, asset_data, asset_additional_data, "lod_5", 5) + def set_skeletal_mesh_lods(asset, asset_options, asset_data, asset_additional_data): # Import the SkeletalMesh lod(s) import_skeletal_lod(asset, asset_options, asset_data, asset_additional_data, "lod_1", 1) From a1e93ea9246e29bb9606b82714d06556d43a0765 Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Thu, 14 Nov 2024 22:45:53 +0100 Subject: [PATCH 11/13] Update asset exemple about unit scale --- docs/Examples/AssetsExample_BlenderSize.blend | Bin 1069888 -> 1066616 bytes docs/Examples/AssetsExample_UnrealSize.blend | Bin 923160 -> 1063916 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/Examples/AssetsExample_BlenderSize.blend b/docs/Examples/AssetsExample_BlenderSize.blend index 5323762847a84e783ffc3a26467716ac1c1f509a..327b3636df3d401e8c60a768e32ebd9a12ad32aa 100644 GIT binary patch literal 1066616 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