Added logic to detect negative object scaling values and accomodate WYSIWYG normals on export

Also added "export collections" operator that exports each viible collection as an ASE
This commit is contained in:
Colin Basnett
2024-05-26 13:47:26 -07:00
parent ecfd9897b1
commit bd90088aed
3 changed files with 96 additions and 8 deletions

View File

@@ -28,11 +28,13 @@ from . import exporter
classes = (
exporter.ASE_OT_ExportOperator,
exporter.ASE_OT_ExportCollections,
)
def menu_func_export(self, context):
self.layout.operator(exporter.ASE_OT_ExportOperator.bl_idname, text='ASCII Scene Export (.ase)')
self.layout.operator(exporter.ASE_OT_ExportCollections.bl_idname, text='ASCII Scene Export Collections (.ase)')
def register():

View File

@@ -1,3 +1,7 @@
from typing import Iterable
from bpy.types import Object
from .ase import *
import bpy
import bmesh
@@ -16,11 +20,11 @@ class ASEBuilderOptions(object):
class ASEBuilder(object):
def build(self, context, options: ASEBuilderOptions):
def build(self, context, options: ASEBuilderOptions, objects: Iterable[Object]):
ase = ASE()
main_geometry_object = None
for selected_object in context.selected_objects:
for selected_object in objects:
if selected_object is None or selected_object.type != 'MESH':
continue
@@ -63,6 +67,7 @@ class ASEBuilder(object):
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
for vertex_index, vertex in enumerate(mesh_data.vertices):
geometry_object.vertices.append(vertex_transform @ vertex.co)
@@ -101,6 +106,16 @@ class ASEBuilder(object):
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32
geometry_object.faces.append(face)
# Figure out how many scaling axes are negative.
# This is important for calculating the normals of the mesh.
_, _, scale = vertex_transform.decompose()
negative_scaling_axes = sum([1 for x in scale if x < 0])
should_invert_normals = negative_scaling_axes % 2 == 1
if should_invert_normals:
for face in geometry_object.faces:
face.a, face.c = face.c, face.a
if not geometry_object.is_collision:
# Normals
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
@@ -125,11 +140,18 @@ class ASEBuilder(object):
# Texture Faces
for loop_triangle in mesh_data.loop_triangles:
geometry_object.texture_vertex_faces.append((
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[2]
))
if should_invert_normals:
geometry_object.texture_vertex_faces.append((
geometry_object.texture_vertex_offset + loop_triangle.loops[2],
geometry_object.texture_vertex_offset + loop_triangle.loops[1],
geometry_object.texture_vertex_offset + loop_triangle.loops[0]
))
else:
geometry_object.texture_vertex_faces.append((
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[2]
))
# Vertex Colors
if len(mesh_data.vertex_colors) > 0:

View File

@@ -1,3 +1,5 @@
import os.path
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, EnumProperty, BoolProperty
from bpy.types import Operator
@@ -41,10 +43,72 @@ class ASE_OT_ExportOperator(Operator, ExportHelper):
options.scale = self.units_scale[self.units]
options.use_raw_mesh_data = self.use_raw_mesh_data
try:
ase = ASEBuilder().build(context, options)
ase = ASEBuilder().build(context, options, context.selected_objects)
ASEWriter().write(self.filepath, ase)
self.report({'INFO'}, 'ASE exported successful')
return {'FINISHED'}
except ASEBuilderError as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class ASE_OT_ExportCollections(Operator, ExportHelper):
bl_idname = 'io_scene_ase.ase_export_collections' # important since its how bpy.ops.import_test.some_data is constructed
bl_label = 'Export Collections to ASE'
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
filename_ext = '.ase'
filter_glob: StringProperty(
default="*.ase",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be hilighted.
)
units: EnumProperty(
default='U',
items=(('M', 'Meters', ''),
('U', 'Unreal', '')),
name='Units'
)
use_raw_mesh_data: BoolProperty(
default=False,
description='No modifiers will be evaluated as part of the exported mesh',
name='Raw Mesh Data')
units_scale = {
'M': 60.352,
'U': 1.0
}
def draw(self, context):
layout = self.layout
layout.prop(self, 'units', expand=False)
layout.prop(self, 'use_raw_mesh_data')
def execute(self, context):
options = ASEBuilderOptions()
options.scale = self.units_scale[self.units]
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]
context.window_manager.progress_begin(0, len(layer_collections))
for i, collection in enumerate(collections):
print(type(collection), collection, collection.hide_viewport)
# Iterate over all the objects in the collection.
try:
ase = ASEBuilder().build(context, options, collection.objects)
dirname = os.path.dirname(self.filepath)
ASEWriter().write(os.path.join(dirname, collection.name + '.ase'), ase)
except ASEBuilderError as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
context.window_manager.progress_update(i)
context.window_manager.progress_end()
self.report({'INFO'}, f'{len(collections)} collections exported successfully')
return {'FINISHED'}