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.
This commit is contained in:
Colin Basnett
2024-05-28 21:57:15 -07:00
parent 972ea5deda
commit 3fe4a6af53
2 changed files with 61 additions and 32 deletions

View File

@@ -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()

View File

@@ -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'}