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:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user