From 92e1cdb829053ac57c2790324439539043a62b91 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 29 Jul 2023 11:33:25 +0100 Subject: [PATCH 1/9] - Initial experimentation for fully recursive collection instance exporting. --- send2ue/core/export.py | 14 ++++++++++++++ send2ue/core/utilities.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/send2ue/core/export.py b/send2ue/core/export.py index 4987c132..b8d5881d 100644 --- a/send2ue/core/export.py +++ b/send2ue/core/export.py @@ -193,6 +193,20 @@ def get_asset_sockets(asset_name, properties): 'relative_rotation': relative_rotation, 'relative_scale': child.matrix_local.to_scale()[:] } + elif child.type == 'EMPTY' and child.is_instancer: + # demote collection instances to sockets + name = 'MeshAttach_' + child.instance_collection.name + '#' + child.name + relative_location = utilities.convert_blender_to_unreal_location( + child.location + ) + relative_rotation = utilities.convert_blender_rotation_to_unreal_rotation( + child.rotation_euler + ) + socket_data[name] = { + 'relative_location': relative_location, + 'relative_rotation': relative_rotation, + 'relative_scale': child.matrix_local.to_scale()[:] + } return socket_data diff --git a/send2ue/core/utilities.py b/send2ue/core/utilities.py index 9ca93be5..1cfa3e9f 100644 --- a/send2ue/core/utilities.py +++ b/send2ue/core/utilities.py @@ -356,6 +356,30 @@ def get_mesh_object_for_groom_name(groom_name): return scene_object.data.surface +def get_from_collection_recursive(object_type, collection): + """ + Internal method for get_from_collection() + """ + collection_objects = [] + + # get all the objects in the collection + for collection_object in collection.all_objects: + if collection_object.is_instancer: + # recurse into collection instances + collection_objects += get_from_collection_recursive(object_type, collection_object.instance_collection) + else: + # if the object is the correct type + if collection_object.type == object_type: + # if the object is visible + if collection_object.visible_get(): + # ensure the object doesn't end with one of the post fix tokens + if not any(collection_object.name.startswith(f'{token.value}_') for token in PreFixToken): + # add it to the group of objects + collection_objects.append(collection_object) + + return collection_objects + + def get_from_collection(object_type): """ This function fetches the objects inside each collection according to type and returns @@ -369,16 +393,7 @@ def get_from_collection(object_type): # get the collection with the given name export_collection = bpy.data.collections.get(ToolInfo.EXPORT_COLLECTION.value) if export_collection: - # get all the objects in the collection - for collection_object in export_collection.all_objects: - # if the object is the correct type - if collection_object.type == object_type: - # if the object is visible - if collection_object.visible_get(): - # ensure the object doesn't end with one of the post fix tokens - if not any(collection_object.name.startswith(f'{token.value}_') for token in PreFixToken): - # add it to the group of objects - collection_objects.append(collection_object) + collection_objects = get_from_collection_recursive(object_type, export_collection) return sorted(collection_objects, key=lambda obj: obj.name) From 36f3aa62ad57a7cf8d5cc5b992653a3a10c50767 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sat, 29 Jul 2023 13:04:15 +0100 Subject: [PATCH 2/9] - Fix for 'use immediate parent name' option. --- send2ue/core/utilities.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/send2ue/core/utilities.py b/send2ue/core/utilities.py index 1cfa3e9f..87868d2f 100644 --- a/send2ue/core/utilities.py +++ b/send2ue/core/utilities.py @@ -449,6 +449,12 @@ def get_parent_collection(scene_object, collection): if scene_object in collection.objects.values(): return collection + # fallback in case scene_object is not contained within specified collection + for collection in bpy.data.collections: + for object in collection.objects: + if object == scene_object: + return collection + def get_skeleton_asset_path(rig_object, properties, get_path_function=get_import_path, *args, **kwargs): """ From 1e760f028a09a3f1c32a4e96e007dd0b8530e419 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Mon, 31 Jul 2023 09:09:02 +0100 Subject: [PATCH 3/9] - Socket resolution fixes for nested collections. Takes into consideration collection offsets. --- send2ue/core/export.py | 76 +++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/send2ue/core/export.py b/send2ue/core/export.py index b8d5881d..62e9297b 100644 --- a/send2ue/core/export.py +++ b/send2ue/core/export.py @@ -6,7 +6,7 @@ import bpy from . import utilities, validations, settings, ingest, extension, io from ..constants import BlenderTypes, UnrealTypes, FileTypes, PreFixToken, ToolInfo, ExtensionTasks - +import mathutils def get_file_path(asset_name, properties, asset_type, lod=False, file_extension='fbx'): """ @@ -169,7 +169,24 @@ def export_file(properties, lod=0, file_type=FileTypes.FBX): export_alembic_file(file_path, export_settings) -def get_asset_sockets(asset_name, properties): +def get_asset_sockets_for_collection(collection_object, properties, parent_mtx, scan_depth=0, unique_id=''): + socket_data = {} + if collection_object: + for object in collection_object.objects: + local_mtx = parent_mtx @ mathutils.Matrix.Translation(-collection_object.instance_offset) @ object.matrix_local + if object.is_instancer: + # collection object is an instance of another collection + socket_data |= get_asset_sockets_for_collection(object.instance_collection, properties, local_mtx, scan_depth+1, unique_id + '_' + object.name) + elif scan_depth > 0: + # demote collection instances to sockets + name = 'MeshAttach_' + collection_object.name + '#' + unique_id + socket_data[name] = local_mtx + else: + socket_data |= get_asset_sockets_for_mesh(object, properties, local_mtx, scan_depth+1, unique_id) + return socket_data + + +def get_asset_sockets_for_mesh(mesh_object, properties, parent_mtx, scan_depth=0, unique_id=''): """ Gets the socket under the given asset. @@ -177,36 +194,33 @@ def get_asset_sockets(asset_name, properties): :param object properties: The property group that contains variables that maintain the addon's correct state. """ socket_data = {} - mesh_object = bpy.data.objects.get(asset_name) if mesh_object: + local_mtx = parent_mtx @ mesh_object.matrix_local + if mesh_object.type == 'EMPTY' and mesh_object.name.startswith(f'{PreFixToken.SOCKET.value}_'): + name = utilities.get_asset_name(mesh_object.name.replace(f'{PreFixToken.SOCKET.value}_', ''), properties) + socket_data[name] = local_mtx + elif mesh_object.type == 'EMPTY' and mesh_object.is_instancer: + if unique_id: + local_id = unique_id + '_' + mesh_object.name + else: + local_id = mesh_object.name + socket_data = get_asset_sockets_for_collection(mesh_object.instance_collection, properties, local_mtx, scan_depth+1, local_id) + + # recurse into child meshes for child in mesh_object.children: - if child.type == 'EMPTY' and child.name.startswith(f'{PreFixToken.SOCKET.value}_'): - name = utilities.get_asset_name(child.name.replace(f'{PreFixToken.SOCKET.value}_', ''), properties) - relative_location = utilities.convert_blender_to_unreal_location( - child.matrix_local.translation - ) - relative_rotation = utilities.convert_blender_rotation_to_unreal_rotation( - child.rotation_euler - ) - socket_data[name] = { - 'relative_location': relative_location, - 'relative_rotation': relative_rotation, - 'relative_scale': child.matrix_local.to_scale()[:] - } - elif child.type == 'EMPTY' and child.is_instancer: - # demote collection instances to sockets - name = 'MeshAttach_' + child.instance_collection.name + '#' + child.name - relative_location = utilities.convert_blender_to_unreal_location( - child.location - ) - relative_rotation = utilities.convert_blender_rotation_to_unreal_rotation( - child.rotation_euler - ) - socket_data[name] = { - 'relative_location': relative_location, - 'relative_rotation': relative_rotation, - 'relative_scale': child.matrix_local.to_scale()[:] - } + socket_data |= get_asset_sockets_for_mesh(child, properties, local_mtx, scan_depth, unique_id) + return socket_data + + +def get_asset_sockets(mesh_object, properties): + socket_data = get_asset_sockets_for_mesh(mesh_object, properties, mathutils.Matrix.Identity(4)) + for socket in socket_data: + decomposed = socket_data[socket].decompose() + decomposed = socket_data[socket] = { + 'relative_location': utilities.convert_blender_to_unreal_location(decomposed[0]), + 'relative_rotation': utilities.convert_blender_rotation_to_unreal_rotation(decomposed[1].to_euler()), + 'relative_scale': decomposed[2][:] + } return socket_data @@ -447,7 +461,7 @@ def create_mesh_data(mesh_objects, rig_objects, properties): 'asset_path': f'{import_path}{asset_name}', 'skeleton_asset_path': properties.unreal_skeleton_asset_path, 'lods': export_lods(asset_id, asset_name, properties), - 'sockets': get_asset_sockets(mesh_object.name, properties), + 'sockets': get_asset_sockets(mesh_object, properties), 'skip': False } previous_asset_names.append(asset_name) From a9df639466fe64d6cd8018a3b7a1562c7407f819 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Mon, 31 Jul 2023 15:13:32 +0100 Subject: [PATCH 4/9] - Couple of fixes for instance collection offsets not applying to meshes or sockets. --- send2ue/core/export.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/send2ue/core/export.py b/send2ue/core/export.py index 62e9297b..80adec60 100644 --- a/send2ue/core/export.py +++ b/send2ue/core/export.py @@ -198,8 +198,9 @@ def get_asset_sockets_for_mesh(mesh_object, properties, parent_mtx, scan_depth=0 local_mtx = parent_mtx @ mesh_object.matrix_local if mesh_object.type == 'EMPTY' and mesh_object.name.startswith(f'{PreFixToken.SOCKET.value}_'): name = utilities.get_asset_name(mesh_object.name.replace(f'{PreFixToken.SOCKET.value}_', ''), properties) - socket_data[name] = local_mtx + socket_data[name] = mesh_object.matrix_local elif mesh_object.type == 'EMPTY' and mesh_object.is_instancer: + local_mtx = parent_mtx @ mathutils.Matrix.Translation(-mesh_object.instance_collection.instance_offset) @ mesh_object.matrix_local if unique_id: local_id = unique_id + '_' + mesh_object.name else: @@ -241,6 +242,10 @@ def export_mesh(asset_id, mesh_object, properties, lod=0): if lod == 0: extension.run_extension_tasks(ExtensionTasks.PRE_MESH_EXPORT.value) + # apply instance offset + if len(mesh_object.users_collection) == 1: + mesh_object.delta_location -= mesh_object.users_collection[0].instance_offset + # select the scene object mesh_object.select_set(True) @@ -263,6 +268,10 @@ def export_mesh(asset_id, mesh_object, properties, lod=0): if lod == 0: extension.run_extension_tasks(ExtensionTasks.POST_MESH_EXPORT.value) + # unapply instance offset + if len(mesh_object.users_collection) == 1: + mesh_object.delta_location += mesh_object.users_collection[0].instance_offset + @utilities.track_progress(message='Exporting animation "{attribute}"...', attribute='file_path') def export_animation(asset_id, rig_object, action_name, properties): From 869a745fb49b6ba7af5e1031ef6e59196ddf77a4 Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Sun, 6 Aug 2023 15:02:59 +0100 Subject: [PATCH 5/9] - Fix for socket transform accumulation, was not correctly taking into consideration scaled collection instances. --- send2ue/core/export.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/send2ue/core/export.py b/send2ue/core/export.py index 80adec60..1a834f73 100644 --- a/send2ue/core/export.py +++ b/send2ue/core/export.py @@ -200,7 +200,6 @@ def get_asset_sockets_for_mesh(mesh_object, properties, parent_mtx, scan_depth=0 name = utilities.get_asset_name(mesh_object.name.replace(f'{PreFixToken.SOCKET.value}_', ''), properties) socket_data[name] = mesh_object.matrix_local elif mesh_object.type == 'EMPTY' and mesh_object.is_instancer: - local_mtx = parent_mtx @ mathutils.Matrix.Translation(-mesh_object.instance_collection.instance_offset) @ mesh_object.matrix_local if unique_id: local_id = unique_id + '_' + mesh_object.name else: @@ -214,7 +213,7 @@ def get_asset_sockets_for_mesh(mesh_object, properties, parent_mtx, scan_depth=0 def get_asset_sockets(mesh_object, properties): - socket_data = get_asset_sockets_for_mesh(mesh_object, properties, mathutils.Matrix.Identity(4)) + socket_data = get_asset_sockets_for_mesh(mesh_object, properties, mesh_object.matrix_world.inverted()) for socket in socket_data: decomposed = socket_data[socket].decompose() decomposed = socket_data[socket] = { From 02d0f451e1972e367cd2e31c80d804c6c1290b3b Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Mon, 7 Aug 2023 15:46:39 +0100 Subject: [PATCH 6/9] - Extra error checking in validate_materials to fix very vague error message during OOB. --- send2ue/core/validations.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/send2ue/core/validations.py b/send2ue/core/validations.py index f9fc2e70..2c3c3bb2 100644 --- a/send2ue/core/validations.py +++ b/send2ue/core/validations.py @@ -207,8 +207,11 @@ def validate_materials(self): if len(mesh_object.material_slots) > 0: # for each polygon check for its material index for polygon in mesh_object.data.polygons: - material = mesh_object.material_slots[polygon.material_index].name + if polygon.material_index >= len(mesh_object.material_slots): + utilities.report_error('Material index out of bounds!', f'Object "{mesh_object.name}" at polygon #{polygon.index} references invalid material index #{polygon.material_index}.') + continue + material = mesh_object.material_slots[polygon.material_index].name # remove used material names from the list of unused material names if material in material_slots: material_slots.remove(material) From cde4af168f73bd76be650bd1ba89cbff8acf4fbb Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Mon, 7 Aug 2023 15:52:33 +0100 Subject: [PATCH 7/9] - Minor: correct return value to match function signature. --- send2ue/core/validations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/send2ue/core/validations.py b/send2ue/core/validations.py index 2c3c3bb2..8943bed7 100644 --- a/send2ue/core/validations.py +++ b/send2ue/core/validations.py @@ -209,7 +209,7 @@ def validate_materials(self): for polygon in mesh_object.data.polygons: if polygon.material_index >= len(mesh_object.material_slots): utilities.report_error('Material index out of bounds!', f'Object "{mesh_object.name}" at polygon #{polygon.index} references invalid material index #{polygon.material_index}.') - continue + return False material = mesh_object.material_slots[polygon.material_index].name # remove used material names from the list of unused material names From caddcd4bc56a56a792eaa3ba1010df96570ed26a Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Wed, 9 Aug 2023 09:35:03 +0100 Subject: [PATCH 8/9] - Implement collision objects within collection instances. --- send2ue/core/utilities.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/send2ue/core/utilities.py b/send2ue/core/utilities.py index 87868d2f..33ccd9a1 100644 --- a/send2ue/core/utilities.py +++ b/send2ue/core/utilities.py @@ -356,7 +356,7 @@ def get_mesh_object_for_groom_name(groom_name): return scene_object.data.surface -def get_from_collection_recursive(object_type, collection): +def get_from_collection_recursive(object_type, collection, exclude_prefix_tokens): """ Internal method for get_from_collection() """ @@ -366,21 +366,21 @@ def get_from_collection_recursive(object_type, collection): for collection_object in collection.all_objects: if collection_object.is_instancer: # recurse into collection instances - collection_objects += get_from_collection_recursive(object_type, collection_object.instance_collection) + collection_objects += get_from_collection_recursive(object_type, collection_object.instance_collection, exclude_prefix_tokens) else: # if the object is the correct type if collection_object.type == object_type: # if the object is visible if collection_object.visible_get(): # ensure the object doesn't end with one of the post fix tokens - if not any(collection_object.name.startswith(f'{token.value}_') for token in PreFixToken): + if not exclude_prefix_tokens or not any(collection_object.name.startswith(f'{token.value}_') for token in PreFixToken): # add it to the group of objects collection_objects.append(collection_object) return collection_objects -def get_from_collection(object_type): +def get_from_collection(object_type, exclude_prefix_tokens=True): """ This function fetches the objects inside each collection according to type and returns an alphabetically sorted list of object references. @@ -393,7 +393,7 @@ def get_from_collection(object_type): # get the collection with the given name export_collection = bpy.data.collections.get(ToolInfo.EXPORT_COLLECTION.value) if export_collection: - collection_objects = get_from_collection_recursive(object_type, export_collection) + collection_objects = get_from_collection_recursive(object_type, export_collection, exclude_prefix_tokens) return sorted(collection_objects, key=lambda obj: obj.name) @@ -653,7 +653,7 @@ def get_asset_collisions(asset_name, properties): collision_meshes = [] export_collection = bpy.data.collections.get(ToolInfo.EXPORT_COLLECTION.value) if export_collection: - for mesh_object in export_collection.all_objects: + for mesh_object in get_from_collection(BlenderTypes.MESH, False): if is_collision_of(asset_name, mesh_object.name, properties): collision_meshes.append(mesh_object) return collision_meshes From d84d311977c5e4709544caaeb9b69ff68f7b8a0e Mon Sep 17 00:00:00 2001 From: Adam Parkinson Date: Wed, 9 Aug 2023 12:42:16 +0100 Subject: [PATCH 9/9] - More transform fixes for subtle differences in hierarchies. --- send2ue/core/export.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/send2ue/core/export.py b/send2ue/core/export.py index 1a834f73..4abf7322 100644 --- a/send2ue/core/export.py +++ b/send2ue/core/export.py @@ -172,8 +172,10 @@ def export_file(properties, lod=0, file_type=FileTypes.FBX): def get_asset_sockets_for_collection(collection_object, properties, parent_mtx, scan_depth=0, unique_id=''): socket_data = {} if collection_object: - for object in collection_object.objects: - local_mtx = parent_mtx @ mathutils.Matrix.Translation(-collection_object.instance_offset) @ object.matrix_local + # only iterate children which are rooted directly to the collection, children-of-children handled by inner recursion + root_objects = [x for x in collection_object.objects if not x.parent] + for object in root_objects: + local_mtx = parent_mtx @ mathutils.Matrix.Translation(-collection_object.instance_offset) @ object.matrix_world if object.is_instancer: # collection object is an instance of another collection socket_data |= get_asset_sockets_for_collection(object.instance_collection, properties, local_mtx, scan_depth+1, unique_id + '_' + object.name) @@ -195,10 +197,10 @@ def get_asset_sockets_for_mesh(mesh_object, properties, parent_mtx, scan_depth=0 """ socket_data = {} if mesh_object: - local_mtx = parent_mtx @ mesh_object.matrix_local + local_mtx = parent_mtx @ mesh_object.matrix_world if mesh_object.type == 'EMPTY' and mesh_object.name.startswith(f'{PreFixToken.SOCKET.value}_'): name = utilities.get_asset_name(mesh_object.name.replace(f'{PreFixToken.SOCKET.value}_', ''), properties) - socket_data[name] = mesh_object.matrix_local + socket_data[name] = local_mtx elif mesh_object.type == 'EMPTY' and mesh_object.is_instancer: if unique_id: local_id = unique_id + '_' + mesh_object.name @@ -208,7 +210,7 @@ def get_asset_sockets_for_mesh(mesh_object, properties, parent_mtx, scan_depth=0 # recurse into child meshes for child in mesh_object.children: - socket_data |= get_asset_sockets_for_mesh(child, properties, local_mtx, scan_depth, unique_id) + socket_data |= get_asset_sockets_for_mesh(child, properties, parent_mtx, scan_depth, unique_id) return socket_data