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:
@@ -28,11 +28,13 @@ from . import exporter
|
|||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
exporter.ASE_OT_ExportOperator,
|
exporter.ASE_OT_ExportOperator,
|
||||||
|
exporter.ASE_OT_ExportCollections,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def menu_func_export(self, context):
|
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_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():
|
def register():
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from bpy.types import Object
|
||||||
|
|
||||||
from .ase import *
|
from .ase import *
|
||||||
import bpy
|
import bpy
|
||||||
import bmesh
|
import bmesh
|
||||||
@@ -16,11 +20,11 @@ class ASEBuilderOptions(object):
|
|||||||
|
|
||||||
|
|
||||||
class ASEBuilder(object):
|
class ASEBuilder(object):
|
||||||
def build(self, context, options: ASEBuilderOptions):
|
def build(self, context, options: ASEBuilderOptions, objects: Iterable[Object]):
|
||||||
ase = ASE()
|
ase = ASE()
|
||||||
|
|
||||||
main_geometry_object = None
|
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':
|
if selected_object is None or selected_object.type != 'MESH':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ class ASEBuilder(object):
|
|||||||
raise ASEBuilderError(f'Mesh \'{selected_object.name}\' must have at least one material')
|
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
|
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):
|
for vertex_index, vertex in enumerate(mesh_data.vertices):
|
||||||
geometry_object.vertices.append(vertex_transform @ vertex.co)
|
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
|
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % 32
|
||||||
geometry_object.faces.append(face)
|
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:
|
if not geometry_object.is_collision:
|
||||||
# Normals
|
# Normals
|
||||||
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
||||||
@@ -125,6 +140,13 @@ class ASEBuilder(object):
|
|||||||
|
|
||||||
# Texture Faces
|
# Texture Faces
|
||||||
for loop_triangle in mesh_data.loop_triangles:
|
for loop_triangle in mesh_data.loop_triangles:
|
||||||
|
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_faces.append((
|
||||||
geometry_object.texture_vertex_offset + loop_triangle.loops[0],
|
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[1],
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import os.path
|
||||||
|
|
||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
from bpy.props import StringProperty, EnumProperty, BoolProperty
|
from bpy.props import StringProperty, EnumProperty, BoolProperty
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator
|
||||||
@@ -41,10 +43,72 @@ class ASE_OT_ExportOperator(Operator, ExportHelper):
|
|||||||
options.scale = self.units_scale[self.units]
|
options.scale = self.units_scale[self.units]
|
||||||
options.use_raw_mesh_data = self.use_raw_mesh_data
|
options.use_raw_mesh_data = self.use_raw_mesh_data
|
||||||
try:
|
try:
|
||||||
ase = ASEBuilder().build(context, options)
|
ase = ASEBuilder().build(context, options, context.selected_objects)
|
||||||
ASEWriter().write(self.filepath, ase)
|
ASEWriter().write(self.filepath, ase)
|
||||||
self.report({'INFO'}, 'ASE exported successful')
|
self.report({'INFO'}, 'ASE exported successful')
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except ASEBuilderError as e:
|
except ASEBuilderError as e:
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, str(e))
|
||||||
return {'CANCELLED'}
|
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'}
|
||||||
|
|||||||
Reference in New Issue
Block a user