Added the ability to change the forward/up axis of the export.

Useful when you want to change the handedness or orientation of an
export without changing anything in the scene.

Also fixed some issues with the normal contextual export operator such
as not respecting the selection status of the objects and doing
double-exports on objects.
This commit is contained in:
Colin Basnett
2024-12-16 17:02:33 -08:00
parent 0bf35f6157
commit 324481d8d2
4 changed files with 113 additions and 24 deletions

View File

@@ -4,6 +4,7 @@ if 'bpy' in locals():
if 'builder' in locals(): importlib.reload(builder)
if 'writer' in locals(): importlib.reload(writer)
if 'exporter' in locals(): importlib.reload(exporter)
if 'dfs' in locals(): importlib.reload(dfs)
import bpy
import bpy.utils.previews
@@ -11,6 +12,7 @@ from . import ase
from . import builder
from . import writer
from . import exporter
from . import dfs
classes = exporter.classes

View File

@@ -28,6 +28,36 @@ class ASEBuildOptions(object):
self.should_invert_normals = False
self.should_export_visible_only = True
self.scale = 1.0
self.forward_axis = 'X'
self.up_axis = 'Z'
def get_vector_from_axis_identifier(axis_identifier: str) -> Vector:
match axis_identifier:
case 'X':
return Vector((1.0, 0.0, 0.0))
case 'Y':
return Vector((0.0, 1.0, 0.0))
case 'Z':
return Vector((0.0, 0.0, 1.0))
case '-X':
return Vector((-1.0, 0.0, 0.0))
case '-Y':
return Vector((0.0, -1.0, 0.0))
case '-Z':
return Vector((0.0, 0.0, -1.0))
def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix:
forward = get_vector_from_axis_identifier(forward_axis)
up = get_vector_from_axis_identifier(up_axis)
left = up.cross(forward)
return Matrix((
(forward.x, forward.y, forward.z, 0.0),
(left.x, left.y, left.z, 0.0),
(up.x, up.y, up.z, 0.0),
(0.0, 0.0, 0.0, 1.0)
))
def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[DfsObject]) -> ASE:
@@ -46,6 +76,8 @@ def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[
mesh_data = cast(Mesh, dfs_object.obj.data)
max_uv_layers = max(max_uv_layers, len(mesh_data.uv_layers))
coordinate_system_transform = get_coordinate_system_transform(options.forward_axis, options.up_axis)
for object_index, dfs_object in enumerate(dfs_objects):
obj = dfs_object.obj
matrix_world = dfs_object.matrix_world
@@ -92,7 +124,9 @@ def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[
vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ Matrix.Scale(options.scale, 4) @ matrix_world
for vertex_index, vertex in enumerate(mesh_data.vertices):
geometry_object.vertices.append(vertex_transform @ vertex.co)
vertex = vertex_transform @ vertex.co
vertex = coordinate_system_transform @ vertex
geometry_object.vertices.append(vertex)
material_indices = []
if not geometry_object.is_collision:

View File

@@ -7,7 +7,7 @@ instances. This is useful for exporters that need to traverse the object hierarc
from typing import Optional, Set, Iterable, List
from bpy.types import Collection, Object, ViewLayer, LayerCollection
from bpy.types import Collection, Object, ViewLayer, LayerCollection, Context
from mathutils import Matrix
@@ -133,22 +133,12 @@ def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]:
@param view_layer: The view layer to inspect.
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
'''
def layer_collection_objects_recursive(layer_collection: LayerCollection):
def layer_collection_objects_recursive(layer_collection: LayerCollection, visited: Set[Object]=None):
if visited is None:
visited = set()
for child in layer_collection.children:
yield from layer_collection_objects_recursive(child)
yield from layer_collection_objects_recursive(child, visited=visited)
# Iterate only the top-level objects in this collection first.
yield from _dfs_collection_objects_recursive(layer_collection.collection)
yield from _dfs_collection_objects_recursive(layer_collection.collection, visited=visited)
yield from layer_collection_objects_recursive(view_layer.layer_collection)
def _is_dfs_object_visible(obj: Object, instance_objects: List[Object]) -> bool:
'''
Check if a DFS object is visible.
@param obj: The object.
@param instance_objects: The instance objects.
@return: True if the object is visible, False otherwise.
'''
if instance_objects:
return instance_objects[-1].visible_get()
return obj.visible_get()

View File

@@ -275,6 +275,36 @@ class ASE_OT_populate_material_order_list(Operator):
return {'FINISHED'}
empty_set = set()
axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z')
forward_items = (
('X', 'X Forward', ''),
('Y', 'Y Forward', ''),
('Z', 'Z Forward', ''),
('-X', '-X Forward', ''),
('-Y', '-Y Forward', ''),
('-Z', '-Z Forward', ''),
)
up_items = (
('X', 'X Up', ''),
('Y', 'Y Up', ''),
('Z', 'Z Up', ''),
('-X', '-X Up', ''),
('-Y', '-Y Up', ''),
('-Z', '-Z Up', ''),
)
def forward_axis_update(self, _context: Context):
if self.forward_axis == self.up_axis:
self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z')
def up_axis_update(self, _context: Context):
if self.up_axis == self.forward_axis:
self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X')
class ASE_OT_export(Operator, ExportHelper):
bl_idname = 'io_scene_ase.ase_export'
bl_label = 'Export ASE'
@@ -283,6 +313,8 @@ class ASE_OT_export(Operator, ExportHelper):
bl_description = 'Export selected objects to ASE'
filename_ext = '.ase'
filter_glob: StringProperty(default="*.ase", options={'HIDDEN'}, maxlen=255)
# TODO: why are these not part of the ASE_PG_export property group?
object_eval_state: EnumProperty(
items=object_eval_state_items,
name='Data',
@@ -290,11 +322,13 @@ class ASE_OT_export(Operator, ExportHelper):
)
should_export_visible_only: BoolProperty(name='Visible Only', default=False, description='Export only visible objects')
scale: FloatProperty(name='Scale', default=1.0, min=0.0001, soft_max=1000.0, description='Scale factor to apply to the exported geometry')
forward_axis: EnumProperty(name='Forward', items=forward_items, default='X', update=forward_axis_update)
up_axis: EnumProperty(name='Up', items=up_items, default='Z', update=up_axis_update)
@classmethod
def poll(cls, context):
if not any(x.type == 'MESH' for x in context.selected_objects):
cls.poll_message_set('At least one mesh must be selected')
if not any(x.type == 'MESH' or (x.type == 'EMPTY' and x.instance_collection is not None) for x in context.selected_objects):
cls.poll_message_set('At least one mesh or instanced collection must be selected')
return False
return True
@@ -305,8 +339,6 @@ class ASE_OT_export(Operator, ExportHelper):
flow = layout.grid_flow()
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(self, 'should_export_visible_only')
flow.prop(self, 'scale')
materials_header, materials_panel = layout.panel('Materials', default_closed=False)
materials_header.label(text='Materials')
@@ -334,6 +366,16 @@ class ASE_OT_export(Operator, ExportHelper):
else:
vertex_colors_panel.label(text='No vertex color attributes found')
transform_header, transform_panel = layout.panel('Transform', default_closed=True)
transform_header.label(text='Transform')
if transform_panel:
transform_panel.use_property_split = True
transform_panel.use_property_decorate = False
transform_panel.prop(self, 'scale')
transform_panel.prop(self, 'forward_axis')
transform_panel.prop(self, 'up_axis')
advanced_header, advanced_panel = layout.panel('Advanced', default_closed=True)
advanced_header.label(text='Advanced')
@@ -344,7 +386,13 @@ class ASE_OT_export(Operator, ExportHelper):
advanced_panel.prop(pg, 'should_invert_normals')
def invoke(self, context: 'Context', event: 'Event' ) -> Union[Set[str], Set[int]]:
mesh_objects = [x[0] for x in get_mesh_objects(context.selected_objects)]
from .dfs import dfs_view_layer_objects
mesh_objects = list(map(lambda x: x.obj, filter(lambda x: x.is_selected and x.obj.type == 'MESH', dfs_view_layer_objects(context.view_layer))))
if len(mesh_objects) == 0:
self.report({'ERROR'}, 'No mesh objects selected')
return {'CANCELLED'}
pg = getattr(context.scene, 'ase_export')
@@ -373,10 +421,12 @@ class ASE_OT_export(Operator, ExportHelper):
options.should_invert_normals = pg.should_invert_normals
options.should_export_visible_only = self.should_export_visible_only
options.scale = self.scale
options.forward_axis = self.forward_axis
options.up_axis = self.up_axis
from .dfs import dfs_view_layer_objects
dfs_objects = list(filter(lambda x: x.obj.type == 'MESH', dfs_view_layer_objects(context.view_layer)))
dfs_objects = list(filter(lambda x: x.is_selected and x.obj.type == 'MESH', dfs_view_layer_objects(context.view_layer)))
try:
ase = build_ase(context, options, dfs_objects)
@@ -425,6 +475,8 @@ class ASE_OT_export_collection(Operator, ExportHelper):
export_space: EnumProperty(name='Export Space', items=export_space_items, default='INSTANCE')
should_export_visible_only: BoolProperty(name='Visible Only', default=False, description='Export only visible objects')
scale: FloatProperty(name='Scale', default=1.0, min=0.0001, soft_max=1000.0, description='Scale factor to apply to the exported geometry')
forward_axis: EnumProperty(name='Forward', items=forward_items, default='X', update=forward_axis_update)
up_axis: EnumProperty(name='Up', items=up_items, default='Z', update=up_axis_update)
def draw(self, context):
layout = self.layout
@@ -433,7 +485,6 @@ class ASE_OT_export_collection(Operator, ExportHelper):
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(self, 'should_export_visible_only')
flow.prop(self, 'scale')
materials_header, materials_panel = layout.panel('Materials', default_closed=True)
materials_header.label(text='Materials')
@@ -450,6 +501,16 @@ class ASE_OT_export_collection(Operator, ExportHelper):
col.separator()
col.operator(ASE_OT_populate_material_order_list.bl_idname, icon='FILE_REFRESH', text='')
transform_header, transform_panel = layout.panel('Transform', default_closed=True)
transform_header.label(text='Transform')
if transform_panel:
transform_panel.use_property_split = True
transform_panel.use_property_decorate = False
transform_panel.prop(self, 'scale')
transform_panel.prop(self, 'forward_axis')
transform_panel.prop(self, 'up_axis')
advanced_header, advanced_panel = layout.panel('Advanced', default_closed=True)
advanced_header.label(text='Advanced')
@@ -465,6 +526,8 @@ class ASE_OT_export_collection(Operator, ExportHelper):
options = ASEBuildOptions()
options.object_eval_state = self.object_eval_state
options.scale = self.scale
options.forward_axis = self.forward_axis
options.up_axis = self.up_axis
match self.export_space:
case 'WORLD':