Skip to content

Commit

Permalink
Added the ability to change the forward/up axis of the export.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cmbasnett committed Dec 17, 2024
1 parent 0bf35f6 commit 324481d
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 24 deletions.
2 changes: 2 additions & 0 deletions io_scene_ase/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
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
from . import ase
from . import builder
from . import writer
from . import exporter
from . import dfs

classes = exporter.classes

Expand Down
36 changes: 35 additions & 1 deletion io_scene_ase/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,36 @@ def __init__(self):
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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
22 changes: 6 additions & 16 deletions io_scene_ase/dfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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


Expand Down Expand Up @@ -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()
77 changes: 70 additions & 7 deletions io_scene_ase/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,36 @@ def execute(self, context):
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'
Expand All @@ -283,18 +313,22 @@ 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',
default='EVALUATED'
)
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

Expand All @@ -305,8 +339,6 @@ def draw(self, context):
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')
Expand Down Expand Up @@ -334,6 +366,16 @@ def draw(self, context):
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')

Expand All @@ -344,7 +386,13 @@ def draw(self, context):
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')

Expand Down Expand Up @@ -373,10 +421,12 @@ def execute(self, context):
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)
Expand Down Expand Up @@ -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
Expand All @@ -433,7 +485,6 @@ def draw(self, context):
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')
Expand All @@ -450,6 +501,16 @@ def draw(self, context):
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')

Expand All @@ -465,6 +526,8 @@ def execute(self, context):
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':
Expand Down

0 comments on commit 324481d

Please sign in to comment.