From 3fe4a6af53556f7c06c633a3024b234dedd01336 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 28 May 2024 21:57:15 -0700 Subject: [PATCH] A number of changes and additions to the exporter: * The "Scale"/"Units" option has been removed from the exporter. If users want to work in proper units, they should override their Scene unit scaling. * Asset instances can now be exported. * Added operator to batch export top-level collections. --- io_scene_ase/builder.py | 61 +++++++++++++++++++++++++++------------- io_scene_ase/exporter.py | 32 +++++++++++++-------- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/io_scene_ase/builder.py b/io_scene_ase/builder.py index b44ffe4..7baee9c 100644 --- a/io_scene_ase/builder.py +++ b/io_scene_ase/builder.py @@ -1,6 +1,6 @@ -from typing import Iterable +from typing import Iterable, Optional, List, Tuple -from bpy.types import Object, Context +from bpy.types import Object, Context, Material from .ase import * import bpy @@ -16,9 +16,28 @@ class ASEBuilderError(Exception): class ASEBuilderOptions(object): def __init__(self): - self.scale = 1.0 self.use_raw_mesh_data = False - self.materials = [] + self.materials: Optional[List[Material]] = None + + +def get_object_matrix(obj: Object, asset_instance: Optional[Object] = None) -> Matrix: + if asset_instance is not None: + return asset_instance.matrix_world @ Matrix().Translation(asset_instance.instance_collection.instance_offset) @ obj.matrix_local + return obj.matrix_world + + +def get_mesh_objects(objects: Iterable[Object]) -> List[Tuple[Object, Optional[Object]]]: + mesh_objects = [] + for obj in objects: + if obj.type == 'MESH': + mesh_objects.append((obj, None)) + elif obj.instance_collection: + # TODO: This probably needs to recurse. + for instance_object in obj.instance_collection.all_objects: + if instance_object.type == 'MESH': + mesh_objects.append((instance_object, obj)) + return mesh_objects + class ASEBuilder(object): @@ -26,33 +45,35 @@ class ASEBuilder(object): ase = ASE() main_geometry_object = None + mesh_objects = get_mesh_objects(objects) - mesh_objects = [obj for obj in objects if obj.type == 'MESH'] context.window_manager.progress_begin(0, len(mesh_objects)) - for material in options.materials: - ase.materials.append(material) + ase.materials = options.materials + + for object_index, (obj, asset_instance) in enumerate(mesh_objects): + + matrix_world = get_object_matrix(obj, asset_instance) - for object_index, selected_object in enumerate(mesh_objects): # Evaluate the mesh after modifiers are applied if options.use_raw_mesh_data: - mesh_object = selected_object + mesh_object = obj mesh_data = mesh_object.data else: depsgraph = context.evaluated_depsgraph_get() bm = bmesh.new() - bm.from_object(selected_object, depsgraph) + bm.from_object(obj, depsgraph) mesh_data = bpy.data.meshes.new('') bm.to_mesh(mesh_data) del bm mesh_object = bpy.data.objects.new('', mesh_data) - mesh_object.matrix_world = selected_object.matrix_world + mesh_object.matrix_world = matrix_world - if not is_collision_name(selected_object.name) and main_geometry_object is not None: + if not is_collision_name(obj.name) and main_geometry_object is not None: geometry_object = main_geometry_object else: geometry_object = ASEGeometryObject() - geometry_object.name = selected_object.name + geometry_object.name = obj.name if not geometry_object.is_collision: main_geometry_object = geometry_object ase.geometry_objects.append(geometry_object) @@ -64,24 +85,24 @@ class ASEBuilder(object): for edge in bm.edges: if not edge.is_manifold: del bm - raise ASEBuilderError(f'Collision mesh \'{selected_object.name}\' is not manifold') + raise ASEBuilderError(f'Collision mesh \'{obj.name}\' is not manifold') if not edge.is_convex: del bm - raise ASEBuilderError(f'Collision mesh \'{selected_object.name}\' is not convex') + raise ASEBuilderError(f'Collision mesh \'{obj.name}\' is not convex') - if not geometry_object.is_collision and len(selected_object.data.materials) == 0: - raise ASEBuilderError(f'Mesh \'{selected_object.name}\' must have at least one material') + if not geometry_object.is_collision and len(obj.data.materials) == 0: + raise ASEBuilderError(f'Mesh \'{obj.name}\' must have at least one material') - vertex_transform = Matrix.Scale(options.scale, 4) @ Matrix.Rotation(math.pi, 4, 'Z') @ mesh_object.matrix_world + vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ matrix_world for vertex_index, vertex in enumerate(mesh_data.vertices): geometry_object.vertices.append(vertex_transform @ vertex.co) material_indices = [] if not geometry_object.is_collision: - for mesh_material_index, material in enumerate(selected_object.data.materials): + for mesh_material_index, material in enumerate(obj.data.materials): if material is None: - raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{selected_object.name}\' cannot be empty') + raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{obj.name}\' cannot be empty') material_indices.append(ase.materials.index(material)) mesh_data.calc_loop_triangles() diff --git a/io_scene_ase/exporter.py b/io_scene_ase/exporter.py index 9f2bf09..987bd1f 100644 --- a/io_scene_ase/exporter.py +++ b/io_scene_ase/exporter.py @@ -17,18 +17,20 @@ class ASE_PG_export(PropertyGroup): material_list_index: IntProperty(name='Index', default=0) -def populate_material_list(mesh_objects, material_list): - material_list.clear() - - materials = [] +def get_unique_materials(mesh_objects: Iterable[Object]) -> List[Material]: + materials = set() for mesh_object in mesh_objects: for i, material_slot in enumerate(mesh_object.material_slots): material = material_slot.material if material is None: raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')') - if material not in materials: - materials.append(material) + materials.add(material) + return list(materials) + +def populate_material_list(mesh_objects: Iterable[Object], material_list): + materials = get_unique_materials(mesh_objects) + material_list.clear() for index, material in enumerate(materials): m = material_list.add() m.material = material @@ -107,7 +109,8 @@ class ASE_OT_export(Operator, ExportHelper): advanced_panel.prop(self, 'use_raw_mesh_data') def invoke(self, context: 'Context', event: 'Event' ) -> typing.Union[typing.Set[str], typing.Set[int]]: - mesh_objects = [x for x in context.selected_objects if x.type == 'MESH'] + mesh_objects = [x[0] for x in get_mesh_objects(context.selected_objects)] + pg = getattr(context.scene, 'ase_export') populate_material_list(mesh_objects, pg.material_list) @@ -117,7 +120,6 @@ class ASE_OT_export(Operator, ExportHelper): def execute(self, context): options = ASEBuilderOptions() - options.scale = 1.0 options.use_raw_mesh_data = self.use_raw_mesh_data pg = getattr(context.scene, 'ase_export') options.materials = [x.material for x in pg.material_list] @@ -153,21 +155,27 @@ class ASE_OT_export_collections(Operator, ExportHelper): def execute(self, context): options = ASEBuilderOptions() - options.scale = 1.0 options.use_raw_mesh_data = self.use_raw_mesh_data # Iterate over all the visible collections in the scene. layer_collections = context.view_layer.layer_collection.children - collections = [x.collection for x in layer_collections if not x.hide_viewport] + collections = [x.collection for x in layer_collections if not x.hide_viewport and not x.exclude] context.window_manager.progress_begin(0, len(layer_collections)) for i, collection in enumerate(collections): # Iterate over all the objects in the collection. + mesh_objects = get_mesh_objects(collection.all_objects) + # Get all the materials used by the objects in the collection. + options.materials = get_unique_materials([x[0] for x in mesh_objects]) + + print(collection, options.materials) + try: - ase = ASEBuilder().build(context, options, collection.objects) + ase = ASEBuilder().build(context, options, collection.all_objects) dirname = os.path.dirname(self.filepath) - ASEWriter().write(os.path.join(dirname, collection.name + '.ase'), ase) + filepath = os.path.join(dirname, collection.name + '.ase') + ASEWriter().write(filepath, ase) except ASEBuilderError as e: self.report({'ERROR'}, str(e)) return {'CANCELLED'}