diff --git a/send2ue/core/export.py b/send2ue/core/export.py index 4987c132..4abf7322 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,26 @@ 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: + # 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) + 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,22 +196,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_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] = 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()[:] - } + socket_data |= get_asset_sockets_for_mesh(child, properties, parent_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, mesh_object.matrix_world.inverted()) + 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 @@ -213,6 +243,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) @@ -235,6 +269,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): @@ -433,7 +471,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) diff --git a/send2ue/core/utilities.py b/send2ue/core/utilities.py index 9ca93be5..33ccd9a1 100644 --- a/send2ue/core/utilities.py +++ b/send2ue/core/utilities.py @@ -356,7 +356,31 @@ def get_mesh_object_for_groom_name(groom_name): return scene_object.data.surface -def get_from_collection(object_type): +def get_from_collection_recursive(object_type, collection, exclude_prefix_tokens): + """ + 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, 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 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, exclude_prefix_tokens=True): """ This function fetches the objects inside each collection according to type and returns an alphabetically sorted list of object references. @@ -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, exclude_prefix_tokens) return sorted(collection_objects, key=lambda obj: obj.name) @@ -434,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): """ @@ -632,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 diff --git a/send2ue/core/validations.py b/send2ue/core/validations.py index f9fc2e70..8943bed7 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}.') + return False + 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)