diff --git a/io_scene_ase/__init__.py b/io_scene_ase/__init__.py index 6418df4..40733c9 100644 --- a/io_scene_ase/__init__.py +++ b/io_scene_ase/__init__.py @@ -28,11 +28,13 @@ from . import exporter classes = ( exporter.ASE_OT_ExportOperator, + exporter.ASE_OT_ExportCollections, ) def menu_func_export(self, context): self.layout.operator(exporter.ASE_OT_ExportOperator.bl_idname, text='ASCII Scene Export (.ase)') + self.layout.operator(exporter.ASE_OT_ExportCollections.bl_idname, text='ASCII Scene Export Collections (.ase)') def register(): diff --git a/io_scene_ase/builder.py b/io_scene_ase/builder.py index 46b9cda..9cd6a7b 100644 --- a/io_scene_ase/builder.py +++ b/io_scene_ase/builder.py @@ -1,3 +1,7 @@ +from typing import Iterable + +from bpy.types import Object + from .ase import * import bpy import bmesh @@ -16,11 +20,11 @@ class ASEBuilderOptions(object): class ASEBuilder(object): - def build(self, context, options: ASEBuilderOptions): + def build(self, context, options: ASEBuilderOptions, objects: Iterable[Object]): ase = ASE() main_geometry_object = None - for selected_object in context.selected_objects: + for selected_object in objects: if selected_object is None or selected_object.type != 'MESH': continue @@ -63,6 +67,7 @@ class ASEBuilder(object): raise ASEBuilderError(f'Mesh \'{selected_object.name}\' must have at least one material') vertex_transform = Matrix.Scale(options.scale, 4) @ Matrix.Rotation(math.pi, 4, 'Z') @ mesh_object.matrix_world + for vertex_index, vertex in enumerate(mesh_data.vertices): geometry_object.vertices.append(vertex_transform @ vertex.co) @@ -101,6 +106,16 @@ class ASEBuilder(object): face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32 geometry_object.faces.append(face) + # Figure out how many scaling axes are negative. + # This is important for calculating the normals of the mesh. + _, _, scale = vertex_transform.decompose() + negative_scaling_axes = sum([1 for x in scale if x < 0]) + should_invert_normals = negative_scaling_axes % 2 == 1 + + if should_invert_normals: + for face in geometry_object.faces: + face.a, face.c = face.c, face.a + if not geometry_object.is_collision: # Normals for face_index, loop_triangle in enumerate(mesh_data.loop_triangles): @@ -125,11 +140,18 @@ class ASEBuilder(object): # Texture Faces for loop_triangle in mesh_data.loop_triangles: - geometry_object.texture_vertex_faces.append(( - geometry_object.texture_vertex_offset + loop_triangle.loops[0], - geometry_object.texture_vertex_offset + loop_triangle.loops[1], - geometry_object.texture_vertex_offset + loop_triangle.loops[2] - )) + if should_invert_normals: + geometry_object.texture_vertex_faces.append(( + geometry_object.texture_vertex_offset + loop_triangle.loops[2], + geometry_object.texture_vertex_offset + loop_triangle.loops[1], + geometry_object.texture_vertex_offset + loop_triangle.loops[0] + )) + else: + geometry_object.texture_vertex_faces.append(( + geometry_object.texture_vertex_offset + loop_triangle.loops[0], + geometry_object.texture_vertex_offset + loop_triangle.loops[1], + geometry_object.texture_vertex_offset + loop_triangle.loops[2] + )) # Vertex Colors if len(mesh_data.vertex_colors) > 0: diff --git a/io_scene_ase/exporter.py b/io_scene_ase/exporter.py index 2ce5396..59144d1 100644 --- a/io_scene_ase/exporter.py +++ b/io_scene_ase/exporter.py @@ -1,3 +1,5 @@ +import os.path + from bpy_extras.io_utils import ExportHelper from bpy.props import StringProperty, EnumProperty, BoolProperty from bpy.types import Operator @@ -41,10 +43,72 @@ class ASE_OT_ExportOperator(Operator, ExportHelper): options.scale = self.units_scale[self.units] options.use_raw_mesh_data = self.use_raw_mesh_data try: - ase = ASEBuilder().build(context, options) + ase = ASEBuilder().build(context, options, context.selected_objects) ASEWriter().write(self.filepath, ase) self.report({'INFO'}, 'ASE exported successful') return {'FINISHED'} except ASEBuilderError as e: self.report({'ERROR'}, str(e)) return {'CANCELLED'} + + +class ASE_OT_ExportCollections(Operator, ExportHelper): + bl_idname = 'io_scene_ase.ase_export_collections' # important since its how bpy.ops.import_test.some_data is constructed + bl_label = 'Export Collections to ASE' + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + filename_ext = '.ase' + filter_glob: StringProperty( + default="*.ase", + options={'HIDDEN'}, + maxlen=255, # Max internal buffer length, longer would be hilighted. + ) + units: EnumProperty( + default='U', + items=(('M', 'Meters', ''), + ('U', 'Unreal', '')), + name='Units' + ) + use_raw_mesh_data: BoolProperty( + default=False, + description='No modifiers will be evaluated as part of the exported mesh', + name='Raw Mesh Data') + units_scale = { + 'M': 60.352, + 'U': 1.0 + } + + def draw(self, context): + layout = self.layout + layout.prop(self, 'units', expand=False) + layout.prop(self, 'use_raw_mesh_data') + + def execute(self, context): + options = ASEBuilderOptions() + options.scale = self.units_scale[self.units] + 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] + + context.window_manager.progress_begin(0, len(layer_collections)) + + for i, collection in enumerate(collections): + print(type(collection), collection, collection.hide_viewport) + # Iterate over all the objects in the collection. + try: + ase = ASEBuilder().build(context, options, collection.objects) + dirname = os.path.dirname(self.filepath) + ASEWriter().write(os.path.join(dirname, collection.name + '.ase'), ase) + except ASEBuilderError as e: + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} + + context.window_manager.progress_update(i) + + context.window_manager.progress_end() + + self.report({'INFO'}, f'{len(collections)} collections exported successfully') + + return {'FINISHED'}