1 Commits

Author SHA1 Message Date
Colin Basnett
0febd57503 Added nodes etc. for ASE2T3D compatibilty (use submtls should be off!) 2022-09-06 15:43:57 -07:00
5 changed files with 306 additions and 181 deletions

View File

@@ -20,15 +20,13 @@ if 'bpy' in locals():
if 'exporter' in locals(): importlib.reload(exporter) if 'exporter' in locals(): importlib.reload(exporter)
import bpy import bpy
import bpy.utils.previews from bpy.props import PointerProperty
from . import ase from . import ase
from . import builder from . import builder
from . import writer from . import writer
from . import exporter from . import exporter
classes = ( classes = exporter.__classes__
exporter.ASE_OT_ExportOperator,
)
def menu_func_export(self, context): def menu_func_export(self, context):
@@ -41,8 +39,12 @@ def register():
bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
bpy.types.Scene.ase_export = PointerProperty(type=exporter.AseExportPropertyGroup)
def unregister(): def unregister():
del bpy.types.Scene.ase_export
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
for cls in classes: for cls in classes:

View File

@@ -1,4 +1,7 @@
class ASEFace(object): from typing import List, Tuple
class AseFace(object):
def __init__(self): def __init__(self):
self.a = 0 self.a = 0
self.b = 0 self.b = 0
@@ -10,46 +13,37 @@ class ASEFace(object):
self.material_index = 0 self.material_index = 0
class ASEVertexNormal(object): class AseVertexNormal(object):
def __init__(self): def __init__(self):
self.vertex_index = 0 self.vertex_index = 0
self.normal = (0.0, 0.0, 0.0) self.normal = (0.0, 0.0, 0.0)
class ASEFaceNormal(object): class AseFaceNormal(object):
def __init__(self): def __init__(self):
self.normal = (0.0, 0.0, 1.0) self.normal = (0.0, 0.0, 1.0)
self.vertex_normals = [ASEVertexNormal()] * 3 self.vertex_normals = [AseVertexNormal()] * 3
def is_collision_name(name): class AseUVLayer(object):
return name.startswith('MCDCX_')
class ASEUVLayer(object):
def __init__(self): def __init__(self):
self.texture_vertices = [] self.texture_vertices: List[Tuple[float, float, float]] = []
class ASEGeometryObject(object): class AseGeometryObject(object):
def __init__(self): def __init__(self):
self.name = '' self.name = ''
self.vertices = [] self.vertices: List[Tuple[float, float, float]] = []
self.uv_layers = [] self.uv_layers: List[AseUVLayer] = []
self.faces = [] self.faces: List[AseFace] = []
self.texture_vertex_faces = [] self.texture_vertex_faces = []
self.face_normals = [] self.face_normals: List[AseFaceNormal] = []
self.vertex_colors = [] self.vertex_colors: List[Tuple[float, float, float]] = []
self.vertex_offset = 0 self.vertex_offset = 0
self.texture_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): def __init__(self):
self.materials = [] self.materials: List[str] = []
self.geometry_objects = [] self.geometry_objects: List[AseGeometryObject] = []

View File

@@ -5,146 +5,153 @@ import math
from mathutils import Matrix 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 pass
class ASEBuilderOptions(object): class AseBuilderOptions(object):
def __init__(self): def __init__(self):
self.scale = 1.0 self.scale = 1.0
self.use_raw_mesh_data = False self.use_raw_mesh_data = False
class ASEBuilder(object): def build_ase(context: bpy.types.Context, options: AseBuilderOptions):
def build(self, context, options: ASEBuilderOptions): ase = Ase()
ase = ASE()
main_geometry_object = None main_geometry_object = None
for selected_object in context.selected_objects: for selected_object in context.view_layer.objects.selected:
if selected_object is None or selected_object.type != 'MESH': if selected_object is None or selected_object.type != 'MESH':
continue continue
# Evaluate the mesh after modifiers are applied # Evaluate the mesh after modifiers are applied
if options.use_raw_mesh_data: if options.use_raw_mesh_data:
mesh_object = selected_object mesh_object = selected_object
mesh_data = mesh_object.data mesh_data = mesh_object.data
else: else:
depsgraph = context.evaluated_depsgraph_get() depsgraph = context.evaluated_depsgraph_get()
bm = bmesh.new() bm = bmesh.new()
bm.from_object(selected_object, depsgraph) bm.from_object(selected_object, depsgraph)
mesh_data = bpy.data.meshes.new('') mesh_data = bpy.data.meshes.new('')
bm.to_mesh(mesh_data) bm.to_mesh(mesh_data)
del bm del bm
mesh_object = bpy.data.objects.new('', mesh_data) mesh_object = bpy.data.objects.new('', mesh_data)
mesh_object.matrix_world = selected_object.matrix_world mesh_object.matrix_world = selected_object.matrix_world
if not is_collision_name(selected_object.name) and main_geometry_object is not None: if not is_collision_name(selected_object.name) and main_geometry_object is not None:
geometry_object = main_geometry_object geometry_object = main_geometry_object
else: else:
geometry_object = ASEGeometryObject() geometry_object = AseGeometryObject()
geometry_object.name = selected_object.name geometry_object.name = selected_object.name
if not geometry_object.is_collision: if not is_collision(geometry_object):
main_geometry_object = geometry_object main_geometry_object = geometry_object
ase.geometry_objects.append(geometry_object) ase.geometry_objects.append(geometry_object)
if geometry_object.is_collision: if is_collision(geometry_object):
# Test that collision meshes are manifold and convex. # Test that collision meshes are manifold and convex.
bm = bmesh.new() bm = bmesh.new()
bm.from_mesh(mesh_object.data) bm.from_mesh(mesh_data)
for edge in bm.edges: for edge in bm.edges:
if not edge.is_manifold: if not edge.is_manifold:
del bm del bm
raise ASEBuilderError(f'Collision mesh \'{selected_object.name}\' is not manifold') raise AseBuilderError(f'Collision mesh \'{selected_object.name}\' is not manifold')
if not edge.is_convex: if not edge.is_convex:
del bm del bm
raise ASEBuilderError(f'Collision mesh \'{selected_object.name}\' is not convex') raise AseBuilderError(f'Collision mesh \'{selected_object.name}\' is not convex')
if not geometry_object.is_collision and len(selected_object.data.materials) == 0: 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') 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 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): for vertex in mesh_data.vertices:
geometry_object.vertices.append(vertex_transform @ vertex.co) geometry_object.vertices.append(vertex_transform @ vertex.co)
material_indices = [] material_indices = []
if not geometry_object.is_collision: if not is_collision(geometry_object):
for mesh_material_index, material in enumerate(selected_object.data.materials): for mesh_material_index, material in enumerate(selected_object.data.materials):
if material is None: 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 \'{selected_object.name}\' cannot be empty')
try: try:
# Reuse existing material entries for duplicates # Reuse existing material entries for duplicates
material_index = ase.materials.index(material.name) material_index = ase.materials.index(material.name)
except ValueError: except ValueError:
material_index = len(ase.materials) material_index = len(ase.materials)
ase.materials.append(material.name) ase.materials.append(material.name)
material_indices.append(material_index) material_indices.append(material_index)
mesh_data.calc_loop_triangles() mesh_data.calc_loop_triangles()
mesh_data.calc_normals_split() mesh_data.calc_normals_split()
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False) poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
# Faces # Faces
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles): for loop_triangle in mesh_data.loop_triangles:
face = ASEFace() face = AseFace()
face.a = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[0]].vertex_index 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.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 face.c = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[2]].vertex_index
if not geometry_object.is_collision: if not is_collision(geometry_object):
face.material_index = material_indices[loop_triangle.material_index] face.material_index = material_indices[loop_triangle.material_index]
# The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the # 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. # 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 # The fix is to constrain the smoothing group between 0 and 31 by applying a modulo of 32 to the actual
# smoothing group index. # smoothing group index.
# This may result in bad calculated normals on export in rare cases. For example, if a face with a # 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 # 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. # will be treated as part of the same smoothing group.
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32 face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32
geometry_object.faces.append(face) geometry_object.faces.append(face)
if not geometry_object.is_collision: if not is_collision(geometry_object):
# Normals # Normals
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles): for loop_triangle in mesh_data.loop_triangles:
face_normal = ASEFaceNormal() face_normal = AseFaceNormal()
face_normal.normal = loop_triangle.normal face_normal.normal = loop_triangle.normal
face_normal.vertex_normals = [] face_normal.vertex_normals = []
for i in range(3): for i in range(3):
vertex_normal = ASEVertexNormal() vertex_normal = AseVertexNormal()
vertex_normal.vertex_index = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[i]].vertex_index 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] vertex_normal.normal = loop_triangle.split_normals[i]
face_normal.vertex_normals.append(vertex_normal) face_normal.vertex_normals.append(vertex_normal)
geometry_object.face_normals.append(face_normal) geometry_object.face_normals.append(face_normal)
# Texture Coordinates # Texture Coordinates
for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]): for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]):
if i >= len(geometry_object.uv_layers): if i >= len(geometry_object.uv_layers):
geometry_object.uv_layers.append(ASEUVLayer()) geometry_object.uv_layers.append(AseUVLayer())
uv_layer = geometry_object.uv_layers[i] uv_layer = geometry_object.uv_layers[i]
for loop_index, loop in enumerate(mesh_data.loops): for loop_index, loop in enumerate(mesh_data.loops):
u, v = uv_layer_data[loop_index].uv u, v = uv_layer_data[loop_index].uv
uv_layer.texture_vertices.append((u, v, 0.0)) uv_layer.texture_vertices.append((u, v, 0.0))
# Texture Faces # Texture Faces
for loop_triangle in mesh_data.loop_triangles: for loop_triangle in mesh_data.loop_triangles:
geometry_object.texture_vertex_faces.append(( geometry_object.texture_vertex_faces.append((
geometry_object.texture_vertex_offset + loop_triangle.loops[0], 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[1],
geometry_object.texture_vertex_offset + loop_triangle.loops[2] geometry_object.texture_vertex_offset + loop_triangle.loops[2]
)) ))
# Vertex Colors # Vertex Colors
if len(mesh_data.vertex_colors) > 0: if len(mesh_data.vertex_colors) > 0:
vertex_colors = mesh_data.vertex_colors.active.data vertex_colors = mesh_data.vertex_colors.active.data
for color in map(lambda x: x.color, vertex_colors): for color in map(lambda x: x.color, vertex_colors):
geometry_object.vertex_colors.append(tuple(color[0:3])) geometry_object.vertex_colors.append(tuple(color[0:3]))
# Update data offsets for next iteration # Update data offsets for next iteration
geometry_object.texture_vertex_offset += len(mesh_data.loops) geometry_object.texture_vertex_offset += len(mesh_data.loops)
geometry_object.vertex_offset = len(geometry_object.vertices) geometry_object.vertex_offset = len(geometry_object.vertices)
if len(ase.geometry_objects) == 0: if len(ase.geometry_objects) == 0:
raise ASEBuilderError('At least one mesh object must be selected') raise AseBuilderError('At least one mesh object must be selected')
if main_geometry_object is None: if main_geometry_object is None:
raise ASEBuilderError('At least one non-collision mesh must be exported') raise AseBuilderError('At least one non-collision mesh must be exported')
return ase return ase

View File

@@ -1,10 +1,53 @@
import bpy
import bpy_extras 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 .builder import *
from .writer 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): 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_idname = 'io_scene_ase.ase_export' # important since its how bpy.ops.import_test.some_data is constructed
bl_label = 'Export ASE' bl_label = 'Export ASE'
@@ -30,21 +73,63 @@ class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper
'M': 60.352, 'M': 60.352,
'U': 1.0 '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): def draw(self, context):
pg = getattr(context.scene, 'ase_export')
layout = self.layout layout = self.layout
rows = max(3, min(len(pg.collection_list), 10))
layout.prop(self, 'units', expand=False) layout.prop(self, 'units', expand=False)
layout.prop(self, 'use_raw_mesh_data') 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): def execute(self, context):
options = ASEBuilderOptions() options = AseBuilderOptions()
options.scale = self.units_scale[self.units] options.scale = self.units_scale[self.units]
options.use_raw_mesh_data = self.use_raw_mesh_data options.use_raw_mesh_data = self.use_raw_mesh_data
try: try:
ase = ASEBuilder().build(context, options) ase = build_ase(context, options)
ASEWriter().write(self.filepath, ase) 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') self.report({'INFO'}, 'ASE exported successful')
return {'FINISHED'} return {'FINISHED'}
except ASEBuilderError as e: except AseBuilderError as e:
self.report({'ERROR'}, str(e)) self.report({'ERROR'}, str(e))
return {'CANCELLED'} return {'CANCELLED'}
__classes__ = (
AseExportCollectionPropertyGroup,
AseExportPropertyGroup,
AseExportCollectionsSelectAll,
AseExportCollectionsDeselectAll,
ASE_UL_CollectionList,
ASE_OT_ExportOperator
)

View File

@@ -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): def __init__(self):
self.commands = [] self.commands = []
def add_command(self, name): def add_command(self, name):
command = ASECommand(name) command = AseCommand(name)
self.commands.append(command) self.commands.append(command)
return command return command
class ASECommand(object): class AseCommand(object):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
self.data = [] self.data = []
@@ -39,17 +44,17 @@ class ASECommand(object):
return self return self
def push_sub_command(self, name): def push_sub_command(self, name):
command = ASECommand(name) command = AseCommand(name)
self.sub_commands.append(command) self.sub_commands.append(command)
return command return command
def push_child(self, name): def push_child(self, name):
child = ASECommand(name) child = AseCommand(name)
self.children.append(child) self.children.append(child)
return child return child
class ASEWriter(object): class AseWriter(object):
def __init__(self): def __init__(self):
self.fp = None self.fp = None
@@ -97,47 +102,77 @@ class ASEWriter(object):
else: else:
self.fp.write('\n') self.fp.write('\n')
def write_file(self, file: ASEFile): def write_file(self, file: AseFile):
self.indent = 0
for command in file.commands: for command in file.commands:
self.write_command(command) self.write_command(command)
@staticmethod @staticmethod
def build_ase_tree(ase) -> ASEFile: def build_ase_file(ase: Ase, options: AseWriterOptions) -> AseFile:
root = ASEFile() root = AseFile()
root.add_command('3DSMAX_ASCIIEXPORT').push_datum(200) root.add_command('3DSMAX_ASCIIEXPORT').push_datum(200)
# Materials # Materials
if len(ase.materials) > 0: if len(ase.materials) > 0:
material_list = root.add_command('MATERIAL_LIST') material_list = root.add_command('MATERIAL_LIST')
material_list.push_child('MATERIAL_COUNT').push_datum(len(ase.materials)) material_list.push_child('MATERIAL_COUNT').push_datum(len(ase.materials))
material_node = material_list.push_child('MATERIAL') if options.should_use_sub_materials:
material_node.push_child('NUMSUBMTLS').push_datum(len(ase.materials)) material_node = material_list.push_child('MATERIAL')
for material_index, material in enumerate(ase.materials): material_node.push_child('NUMSUBMTLS').push_datum(len(ase.materials))
submaterial_node = material_node.push_child('SUBMATERIAL') for material_index, material in enumerate(ase.materials):
submaterial_node.push_datum(material_index) submaterial_node = material_node.push_child('SUBMATERIAL')
submaterial_node.push_child('MATERIAL_NAME').push_datum(material) submaterial_node.push_datum(material_index)
diffuse_node = submaterial_node.push_child('MAP_DIFFUSE') submaterial_node.push_child('MATERIAL_NAME').push_datum(material)
diffuse_node.push_child('MAP_NAME').push_datum('default') diffuse_node = submaterial_node.push_child('MAP_DIFFUSE')
diffuse_node.push_child('UVW_U_OFFSET').push_datum(0.0) diffuse_node.push_child('MAP_NAME').push_datum('default')
diffuse_node.push_child('UVW_V_OFFSET').push_datum(0.0) diffuse_node.push_child('UVW_U_OFFSET').push_datum(0.0)
diffuse_node.push_child('UVW_U_TILING').push_datum(1.0) diffuse_node.push_child('UVW_V_OFFSET').push_datum(0.0)
diffuse_node.push_child('UVW_V_TILING').push_datum(1.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: for geometry_object in ase.geometry_objects:
geomobject_node = root.add_command('GEOMOBJECT') geomobject_node = root.add_command('GEOMOBJECT')
geomobject_node.push_child('NODE_NAME').push_datum(geometry_object.name) 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') 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_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') vertex_list_node = mesh_node.push_child('MESH_VERTEX_LIST')
for vertex_index, vertex in enumerate(geometry_object.vertices): for vertex_index, vertex in enumerate(geometry_object.vertices):
mesh_vertex = vertex_list_node.push_child('MESH_VERTEX').push_datum(vertex_index) mesh_vertex = vertex_list_node.push_child('MESH_VERTEX').push_datum(vertex_index)
mesh_vertex.push_data([x for x in vertex]) mesh_vertex.push_data([x for x in vertex])
# Faces # Faces
mesh_node.push_child('MESH_NUMFACES').push_datum(len(geometry_object.faces))
faces_node = mesh_node.push_child('MESH_FACE_LIST') faces_node = mesh_node.push_child('MESH_FACE_LIST')
for face_index, face in enumerate(geometry_object.faces): for face_index, face in enumerate(geometry_object.faces):
face_node = faces_node.push_child('MESH_FACE') face_node = faces_node.push_child('MESH_FACE')
@@ -182,18 +217,20 @@ class ASEWriter(object):
cvert_list = mesh_node.push_child('MESH_CVERTLIST') cvert_list = mesh_node.push_child('MESH_CVERTLIST')
for i, vertex_color in enumerate(geometry_object.vertex_colors): for i, vertex_color in enumerate(geometry_object.vertex_colors):
cvert_list.push_child('MESH_VERTCOL').push_datum(i).push_data(vertex_color) 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)) mesh_node.push_child('MESH_NUMCVFACES').push_datum(len(geometry_object.texture_vertex_faces))
texture_faces_node = parent_node.push_child('MESH_CFACELIST') texture_faces_node = mesh_node.push_child('MESH_CFACELIST')
for texture_face_index, texture_face in enumerate(geometry_object.texture_vertex_faces): 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 = texture_faces_node.push_child('MESH_CFACE')
texture_face_node.push_data([texture_face_index] + list(texture_face)) 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) geomobject_node.push_child('MATERIAL_REF').push_datum(0)
return root return root
def write(self, filepath, ase): def write(self, filepath, ase: Ase, options: AseWriterOptions):
self.indent = 0 ase_file = self.build_ase_file(ase, options)
ase_file = self.build_ase_tree(ase)
with open(filepath, 'w') as self.fp: with open(filepath, 'w') as self.fp:
self.write_file(ase_file) self.write_file(ase_file)