Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 55 additions & 17 deletions send2ue/core/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
"""
Expand Down Expand Up @@ -169,30 +169,60 @@ 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.

:param str asset_name: The name of the asset to export.
: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


Expand All @@ -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)

Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 33 additions & 12 deletions send2ue/core/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)


Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion send2ue/core/validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down