diff --git a/src/__init__.py b/src/__init__.py index eed97c3..271ead9 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -20,15 +20,13 @@ if 'bpy' in locals(): if 'exporter' in locals(): importlib.reload(exporter) import bpy -import bpy.utils.previews +from bpy.props import PointerProperty from . import ase from . import builder from . import writer from . import exporter -classes = ( - exporter.ASE_OT_ExportOperator, -) +classes = exporter.__classes__ def menu_func_export(self, context): @@ -41,8 +39,12 @@ def register(): bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + bpy.types.Scene.ase_export = PointerProperty(type=exporter.AseExportPropertyGroup) + def unregister(): + del bpy.types.Scene.ase_export + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) for cls in classes: diff --git a/src/ase.py b/src/ase.py index b769991..34b74dc 100644 --- a/src/ase.py +++ b/src/ase.py @@ -1,4 +1,7 @@ -class ASEFace(object): +from typing import List, Tuple + + +class AseFace(object): def __init__(self): self.a = 0 self.b = 0 @@ -10,46 +13,37 @@ class ASEFace(object): self.material_index = 0 -class ASEVertexNormal(object): +class AseVertexNormal(object): def __init__(self): self.vertex_index = 0 self.normal = (0.0, 0.0, 0.0) -class ASEFaceNormal(object): +class AseFaceNormal(object): def __init__(self): self.normal = (0.0, 0.0, 1.0) - self.vertex_normals = [ASEVertexNormal()] * 3 + self.vertex_normals = [AseVertexNormal()] * 3 -def is_collision_name(name): - return name.startswith('MCDCX_') - - -class ASEUVLayer(object): +class AseUVLayer(object): def __init__(self): - self.texture_vertices = [] + self.texture_vertices: List[Tuple[float, float, float]] = [] -class ASEGeometryObject(object): +class AseGeometryObject(object): def __init__(self): self.name = '' - self.vertices = [] - self.uv_layers = [] - self.faces = [] + self.vertices: List[Tuple[float, float, float]] = [] + self.uv_layers: List[AseUVLayer] = [] + self.faces: List[AseFace] = [] self.texture_vertex_faces = [] - self.face_normals = [] - self.vertex_colors = [] + self.face_normals: List[AseFaceNormal] = [] + self.vertex_colors: List[Tuple[float, float, float]] = [] self.vertex_offset = 0 self.texture_vertex_offset = 0 - @property - def is_collision(self): - return is_collision_name(self.name) - -class ASE(object): +class Ase(object): def __init__(self): - self.materials = [] - self.geometry_objects = [] - + self.materials: List[str] = [] + self.geometry_objects: List[AseGeometryObject] = [] diff --git a/src/builder.py b/src/builder.py index a255963..802729d 100644 --- a/src/builder.py +++ b/src/builder.py @@ -5,146 +5,153 @@ import math from mathutils import Matrix -class ASEBuilderError(Exception): +def is_collision_name(name: str): + return name.startswith('MCDCX_') + + +def is_collision(geometry_object: AseGeometryObject): + return is_collision_name(geometry_object.name) + + +class AseBuilderError(Exception): pass -class ASEBuilderOptions(object): +class AseBuilderOptions(object): def __init__(self): self.scale = 1.0 self.use_raw_mesh_data = False -class ASEBuilder(object): - def build(self, context, options: ASEBuilderOptions): - ase = ASE() +def build_ase(context: bpy.types.Context, options: AseBuilderOptions): + ase = Ase() - main_geometry_object = None - for selected_object in context.selected_objects: - if selected_object is None or selected_object.type != 'MESH': - continue + main_geometry_object = None + for selected_object in context.view_layer.objects.selected: + if selected_object is None or selected_object.type != 'MESH': + continue - # Evaluate the mesh after modifiers are applied - if options.use_raw_mesh_data: - mesh_object = selected_object - mesh_data = mesh_object.data - else: - depsgraph = context.evaluated_depsgraph_get() - bm = bmesh.new() - bm.from_object(selected_object, 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 + # Evaluate the mesh after modifiers are applied + if options.use_raw_mesh_data: + mesh_object = selected_object + mesh_data = mesh_object.data + else: + depsgraph = context.evaluated_depsgraph_get() + bm = bmesh.new() + bm.from_object(selected_object, 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 - if not is_collision_name(selected_object.name) and main_geometry_object is not None: - geometry_object = main_geometry_object - else: - geometry_object = ASEGeometryObject() - geometry_object.name = selected_object.name - if not geometry_object.is_collision: - main_geometry_object = geometry_object - ase.geometry_objects.append(geometry_object) + if not is_collision_name(selected_object.name) and main_geometry_object is not None: + geometry_object = main_geometry_object + else: + geometry_object = AseGeometryObject() + geometry_object.name = selected_object.name + if not is_collision(geometry_object): + main_geometry_object = geometry_object + ase.geometry_objects.append(geometry_object) - if geometry_object.is_collision: - # Test that collision meshes are manifold and convex. - bm = bmesh.new() - bm.from_mesh(mesh_object.data) - for edge in bm.edges: - if not edge.is_manifold: - del bm - raise ASEBuilderError(f'Collision mesh \'{selected_object.name}\' is not manifold') - if not edge.is_convex: - del bm - raise ASEBuilderError(f'Collision mesh \'{selected_object.name}\' is not convex') + if is_collision(geometry_object): + # Test that collision meshes are manifold and convex. + bm = bmesh.new() + bm.from_mesh(mesh_data) + for edge in bm.edges: + if not edge.is_manifold: + del bm + raise AseBuilderError(f'Collision mesh \'{selected_object.name}\' is not manifold') + if not edge.is_convex: + del bm + raise AseBuilderError(f'Collision mesh \'{selected_object.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 is_collision(geometry_object) and len(selected_object.data.materials) == 0: + 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) + vertex_transform = Matrix.Scale(options.scale, 4) @ Matrix.Rotation(math.pi, 4, 'Z') @ mesh_object.matrix_world + for vertex in 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): - if material is None: - raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{selected_object.name}\' cannot be empty') - try: - # Reuse existing material entries for duplicates - material_index = ase.materials.index(material.name) - except ValueError: - material_index = len(ase.materials) - ase.materials.append(material.name) - material_indices.append(material_index) + material_indices = [] + if not is_collision(geometry_object): + for mesh_material_index, material in enumerate(selected_object.data.materials): + if material is None: + raise AseBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{selected_object.name}\' cannot be empty') + try: + # Reuse existing material entries for duplicates + material_index = ase.materials.index(material.name) + except ValueError: + material_index = len(ase.materials) + ase.materials.append(material.name) + material_indices.append(material_index) - mesh_data.calc_loop_triangles() - mesh_data.calc_normals_split() - poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False) + mesh_data.calc_loop_triangles() + mesh_data.calc_normals_split() + poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False) - # Faces - for face_index, loop_triangle in enumerate(mesh_data.loop_triangles): - face = ASEFace() - face.a = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[0]].vertex_index - face.b = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[1]].vertex_index - face.c = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[2]].vertex_index - if not geometry_object.is_collision: - face.material_index = material_indices[loop_triangle.material_index] - # The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the - # smoothing groups and effectively makes the whole model use sharp-edge rendering. - # The fix is to constrain the smoothing group between 0 and 31 by applying a modulo of 32 to the actual - # smoothing group index. - # This may result in bad calculated normals on export in rare cases. For example, if a face with a - # smoothing group of 3 is adjacent to a face with a smoothing group of 35 (35 % 32 == 3), those faces - # will be treated as part of the same smoothing group. - face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32 - geometry_object.faces.append(face) + # Faces + for loop_triangle in mesh_data.loop_triangles: + face = AseFace() + face.a = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[0]].vertex_index + face.b = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[1]].vertex_index + face.c = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[2]].vertex_index + if not is_collision(geometry_object): + face.material_index = material_indices[loop_triangle.material_index] + # The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the + # smoothing groups and effectively makes the whole model use sharp-edge rendering. + # The fix is to constrain the smoothing group between 0 and 31 by applying a modulo of 32 to the actual + # smoothing group index. + # This may result in bad calculated normals on export in rare cases. For example, if a face with a + # smoothing group of 3 is adjacent to a face with a smoothing group of 35 (35 % 32 == 3), those faces + # will be treated as part of the same smoothing group. + face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32 + geometry_object.faces.append(face) - if not geometry_object.is_collision: - # Normals - for face_index, loop_triangle in enumerate(mesh_data.loop_triangles): - face_normal = ASEFaceNormal() - face_normal.normal = loop_triangle.normal - face_normal.vertex_normals = [] - for i in range(3): - vertex_normal = ASEVertexNormal() - vertex_normal.vertex_index = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[i]].vertex_index - vertex_normal.normal = loop_triangle.split_normals[i] - face_normal.vertex_normals.append(vertex_normal) - geometry_object.face_normals.append(face_normal) + if not is_collision(geometry_object): + # Normals + for loop_triangle in mesh_data.loop_triangles: + face_normal = AseFaceNormal() + face_normal.normal = loop_triangle.normal + face_normal.vertex_normals = [] + for i in range(3): + vertex_normal = AseVertexNormal() + vertex_normal.vertex_index = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[i]].vertex_index + vertex_normal.normal = loop_triangle.split_normals[i] + face_normal.vertex_normals.append(vertex_normal) + geometry_object.face_normals.append(face_normal) - # Texture Coordinates - for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]): - if i >= len(geometry_object.uv_layers): - geometry_object.uv_layers.append(ASEUVLayer()) - uv_layer = geometry_object.uv_layers[i] - for loop_index, loop in enumerate(mesh_data.loops): - u, v = uv_layer_data[loop_index].uv - uv_layer.texture_vertices.append((u, v, 0.0)) + # Texture Coordinates + for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]): + if i >= len(geometry_object.uv_layers): + geometry_object.uv_layers.append(AseUVLayer()) + uv_layer = geometry_object.uv_layers[i] + for loop_index, loop in enumerate(mesh_data.loops): + u, v = uv_layer_data[loop_index].uv + uv_layer.texture_vertices.append((u, v, 0.0)) - # 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] - )) + # 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] + )) - # Vertex Colors - if len(mesh_data.vertex_colors) > 0: - vertex_colors = mesh_data.vertex_colors.active.data - for color in map(lambda x: x.color, vertex_colors): - geometry_object.vertex_colors.append(tuple(color[0:3])) + # Vertex Colors + if len(mesh_data.vertex_colors) > 0: + vertex_colors = mesh_data.vertex_colors.active.data + for color in map(lambda x: x.color, vertex_colors): + geometry_object.vertex_colors.append(tuple(color[0:3])) - # Update data offsets for next iteration - geometry_object.texture_vertex_offset += len(mesh_data.loops) - geometry_object.vertex_offset = len(geometry_object.vertices) + # Update data offsets for next iteration + geometry_object.texture_vertex_offset += len(mesh_data.loops) + geometry_object.vertex_offset = len(geometry_object.vertices) - if len(ase.geometry_objects) == 0: - raise ASEBuilderError('At least one mesh object must be selected') + if len(ase.geometry_objects) == 0: + raise AseBuilderError('At least one mesh object must be selected') - if main_geometry_object is None: - raise ASEBuilderError('At least one non-collision mesh must be exported') + if main_geometry_object is None: + raise AseBuilderError('At least one non-collision mesh must be exported') - return ase + return ase diff --git a/src/exporter.py b/src/exporter.py index be85dee..ca7f9f8 100644 --- a/src/exporter.py +++ b/src/exporter.py @@ -1,10 +1,53 @@ -import bpy import bpy_extras -from bpy.props import StringProperty, FloatProperty, EnumProperty, BoolProperty +import typing +from bpy.types import UIList, Context, UILayout, AnyType, Operator +from bpy.props import StringProperty, EnumProperty, BoolProperty, PointerProperty, CollectionProperty, IntProperty from .builder import * from .writer import * +class AseExportCollectionPropertyGroup(bpy.types.PropertyGroup): + is_selected: BoolProperty() + name: StringProperty() + collection: PointerProperty(type=bpy.types.Collection) + + +class AseExportPropertyGroup(bpy.types.PropertyGroup): + collection_list: CollectionProperty(type=AseExportCollectionPropertyGroup) + collection_list_index: IntProperty() + + +class ASE_UL_CollectionList(UIList): + def draw_item(self, context: Context, layout: UILayout, data: AnyType, item: AnyType, icon: int, + active_data: AnyType, active_property: str, index: int = 0, flt_flag: int = 0): + collection: bpy.types.Collection = getattr(item, 'collection') + row = layout.row() + row.prop(item, 'is_selected', text='') + row.label(text=collection.name, icon='OUTLINER_COLLECTION') + + +class AseExportCollectionsSelectAll(Operator): + bl_idname = 'ase_export.collections_select_all' + bl_label = 'All' + + def execute(self, context: Context) -> typing.Union[typing.Set[str], typing.Set[int]]: + pg = getattr(context.scene, 'ase_export') + for collection in pg.collection_list: + collection.is_selected = True + return {'FINISHED'} + + +class AseExportCollectionsDeselectAll(Operator): + bl_idname = 'ase_export.collections_deselect_all' + bl_label = 'None' + + def execute(self, context: Context) -> typing.Union[typing.Set[str], typing.Set[int]]: + pg = getattr(context.scene, 'ase_export') + for collection in pg.collection_list: + collection.is_selected = False + return {'FINISHED'} + + class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper): bl_idname = 'io_scene_ase.ase_export' # important since its how bpy.ops.import_test.some_data is constructed bl_label = 'Export ASE' @@ -30,21 +73,63 @@ class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper 'M': 60.352, 'U': 1.0 } + should_use_sub_materials: BoolProperty( + default=True, + description='Material format', # fill this in with a more human-friendly name/description + name='Use Sub-materials' + ) def draw(self, context): + pg = getattr(context.scene, 'ase_export') layout = self.layout + rows = max(3, min(len(pg.collection_list), 10)) layout.prop(self, 'units', expand=False) layout.prop(self, 'use_raw_mesh_data') + layout.prop(self, 'should_use_sub_materials') + + # # SELECT ALL/NONE + # row = layout.row(align=True) + # row.label(text='Select') + # row.operator(AseExportCollectionsSelectAll.bl_idname, text='All', icon='CHECKBOX_HLT') + # row.operator(AseExportCollectionsDeselectAll.bl_idname, text='None', icon='CHECKBOX_DEHLT') + # + # layout.template_list('ASE_UL_CollectionList', '', pg, 'collection_list', pg, 'collection_list_index', rows=rows) + + def invoke(self, context: bpy.types.Context, event): + # TODO: build a list of collections that have meshes in them + pg = getattr(context.scene, 'ase_export') + pg.collection_list.clear() + for collection in bpy.data.collections: + has_meshes = any(map(lambda x: x.type == 'MESH', collection.objects)) + if has_meshes: + c = pg.collection_list.add() + c.collection = collection + + context.window_manager.fileselect_add(self) + + return {'RUNNING_MODAL'} def execute(self, context): - options = ASEBuilderOptions() + options = AseBuilderOptions() options.scale = self.units_scale[self.units] options.use_raw_mesh_data = self.use_raw_mesh_data try: - ase = ASEBuilder().build(context, options) - ASEWriter().write(self.filepath, ase) + ase = build_ase(context, options) + writer_options = AseWriterOptions() + writer_options.should_use_sub_materials = self.should_use_sub_materials + AseWriter().write(self.filepath, ase, writer_options) self.report({'INFO'}, 'ASE exported successful') return {'FINISHED'} - except ASEBuilderError as e: + except AseBuilderError as e: self.report({'ERROR'}, str(e)) return {'CANCELLED'} + + +__classes__ = ( + AseExportCollectionPropertyGroup, + AseExportPropertyGroup, + AseExportCollectionsSelectAll, + AseExportCollectionsDeselectAll, + ASE_UL_CollectionList, + ASE_OT_ExportOperator +) diff --git a/src/writer.py b/src/writer.py index 1407ec6..88c1ba2 100644 --- a/src/writer.py +++ b/src/writer.py @@ -1,17 +1,22 @@ -from .ase import * +from .ase import Ase -class ASEFile(object): +class AseWriterOptions(object): + def __init__(self): + self.should_use_sub_materials = True + + +class AseFile(object): def __init__(self): self.commands = [] def add_command(self, name): - command = ASECommand(name) + command = AseCommand(name) self.commands.append(command) return command -class ASECommand(object): +class AseCommand(object): def __init__(self, name): self.name = name self.data = [] @@ -39,17 +44,17 @@ class ASECommand(object): return self def push_sub_command(self, name): - command = ASECommand(name) + command = AseCommand(name) self.sub_commands.append(command) return command def push_child(self, name): - child = ASECommand(name) + child = AseCommand(name) self.children.append(child) return child -class ASEWriter(object): +class AseWriter(object): def __init__(self): self.fp = None @@ -97,47 +102,77 @@ class ASEWriter(object): else: self.fp.write('\n') - def write_file(self, file: ASEFile): + def write_file(self, file: AseFile): + self.indent = 0 for command in file.commands: self.write_command(command) @staticmethod - def build_ase_tree(ase) -> ASEFile: - root = ASEFile() + def build_ase_file(ase: Ase, options: AseWriterOptions) -> AseFile: + root = AseFile() root.add_command('3DSMAX_ASCIIEXPORT').push_datum(200) # Materials if len(ase.materials) > 0: material_list = root.add_command('MATERIAL_LIST') material_list.push_child('MATERIAL_COUNT').push_datum(len(ase.materials)) - material_node = material_list.push_child('MATERIAL') - material_node.push_child('NUMSUBMTLS').push_datum(len(ase.materials)) - for material_index, material in enumerate(ase.materials): - submaterial_node = material_node.push_child('SUBMATERIAL') - submaterial_node.push_datum(material_index) - submaterial_node.push_child('MATERIAL_NAME').push_datum(material) - diffuse_node = submaterial_node.push_child('MAP_DIFFUSE') - diffuse_node.push_child('MAP_NAME').push_datum('default') - diffuse_node.push_child('UVW_U_OFFSET').push_datum(0.0) - diffuse_node.push_child('UVW_V_OFFSET').push_datum(0.0) - diffuse_node.push_child('UVW_U_TILING').push_datum(1.0) - diffuse_node.push_child('UVW_V_TILING').push_datum(1.0) + if options.should_use_sub_materials: + material_node = material_list.push_child('MATERIAL') + material_node.push_child('NUMSUBMTLS').push_datum(len(ase.materials)) + for material_index, material in enumerate(ase.materials): + submaterial_node = material_node.push_child('SUBMATERIAL') + submaterial_node.push_datum(material_index) + submaterial_node.push_child('MATERIAL_NAME').push_datum(material) + diffuse_node = submaterial_node.push_child('MAP_DIFFUSE') + diffuse_node.push_child('MAP_NAME').push_datum('default') + diffuse_node.push_child('UVW_U_OFFSET').push_datum(0.0) + diffuse_node.push_child('UVW_V_OFFSET').push_datum(0.0) + diffuse_node.push_child('UVW_U_TILING').push_datum(1.0) + diffuse_node.push_child('UVW_V_TILING').push_datum(1.0) + else: + for material_index, material in enumerate(ase.materials): + material_node = material_list.push_child('MATERIAL').push_datum(material_index) + material_node.push_child('MATERIAL_NAME').push_datum(material) + diffuse_node = material_node.push_child('MAP_DIFFUSE') + diffuse_node.push_child('MAP_NAME').push_datum('default') + diffuse_node.push_child('UVW_U_OFFSET').push_datum(0.0) + diffuse_node.push_child('UVW_V_OFFSET').push_datum(0.0) + diffuse_node.push_child('UVW_U_TILING').push_datum(1.0) + diffuse_node.push_child('UVW_V_TILING').push_datum(1.0) for geometry_object in ase.geometry_objects: geomobject_node = root.add_command('GEOMOBJECT') geomobject_node.push_child('NODE_NAME').push_datum(geometry_object.name) + # TODO: only do this in T3D compatibility mode (or just do it always because it makes no difference?) + transform_node = geomobject_node.push_child('NODE_TM') + transform_node.push_child('NODE_NAME').push_datum(geometry_object.name) + transform_node.push_child('INHERIT_POS').push_data([0, 0, 0]) + transform_node.push_child('INHERIT_ROT').push_data([0, 0, 0]) + transform_node.push_child('INHERIT_SCL').push_data([0, 0, 0]) + transform_node.push_child('TM_ROW0').push_data([1.0, 0.0, 0.0]) + transform_node.push_child('TM_ROW1').push_data([0.0, 1.0, 0.0]) + transform_node.push_child('TM_ROW2').push_data([0.0, 0.0, 1.0]) + transform_node.push_child('TM_ROW3').push_data([0.0, 0.0, 0.0]) + transform_node.push_child('TM_POS').push_data([0.0, 0.0, 0.0]) + transform_node.push_child('TM_ROTAXIS').push_data([0.0, 0.0, 0.0]) + transform_node.push_child('TM_ROTANGLE').push_datum(0.0) + transform_node.push_child('TM_SCALE').push_datum(0.0) + transform_node.push_child('TM_SCALEAXIS').push_data([0.0, 0.0, 0.0]) + transform_node.push_child('TM_SCALEAXISANG').push_datum(0.0) + mesh_node = geomobject_node.push_child('MESH') - # Vertices mesh_node.push_child('MESH_NUMVERTEX').push_datum(len(geometry_object.vertices)) + mesh_node.push_child('MESH_NUMFACES').push_datum(len(geometry_object.faces)) + + # Vertices vertex_list_node = mesh_node.push_child('MESH_VERTEX_LIST') for vertex_index, vertex in enumerate(geometry_object.vertices): mesh_vertex = vertex_list_node.push_child('MESH_VERTEX').push_datum(vertex_index) mesh_vertex.push_data([x for x in vertex]) # Faces - mesh_node.push_child('MESH_NUMFACES').push_datum(len(geometry_object.faces)) faces_node = mesh_node.push_child('MESH_FACE_LIST') for face_index, face in enumerate(geometry_object.faces): face_node = faces_node.push_child('MESH_FACE') @@ -182,18 +217,20 @@ class ASEWriter(object): cvert_list = mesh_node.push_child('MESH_CVERTLIST') for i, vertex_color in enumerate(geometry_object.vertex_colors): cvert_list.push_child('MESH_VERTCOL').push_datum(i).push_data(vertex_color) - parent_node.push_child('MESH_NUMCVFACES').push_datum(len(geometry_object.texture_vertex_faces)) - texture_faces_node = parent_node.push_child('MESH_CFACELIST') + mesh_node.push_child('MESH_NUMCVFACES').push_datum(len(geometry_object.texture_vertex_faces)) + texture_faces_node = mesh_node.push_child('MESH_CFACELIST') for texture_face_index, texture_face in enumerate(geometry_object.texture_vertex_faces): texture_face_node = texture_faces_node.push_child('MESH_CFACE') texture_face_node.push_data([texture_face_index] + list(texture_face)) + geomobject_node.push_child('PROP_MOTIONBLUR').push_datum(0) + geomobject_node.push_child('PROP_CASTSHADOW').push_datum(1) + geomobject_node.push_child('PROP_RECVSHADOW').push_datum(1) geomobject_node.push_child('MATERIAL_REF').push_datum(0) return root - def write(self, filepath, ase): - self.indent = 0 - ase_file = self.build_ase_tree(ase) + def write(self, filepath, ase: Ase, options: AseWriterOptions): + ase_file = self.build_ase_file(ase, options) with open(filepath, 'w') as self.fp: self.write_file(ase_file)