Fixed vertex color export & added "invert normals" option
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from bpy.types import Material
|
||||
|
||||
|
||||
class ASEFace(object):
|
||||
def __init__(self):
|
||||
self.a = 0
|
||||
@@ -50,6 +55,5 @@ class ASEGeometryObject(object):
|
||||
|
||||
class ASE(object):
|
||||
def __init__(self):
|
||||
self.materials = []
|
||||
self.materials: List[Optional[Material]] = []
|
||||
self.geometry_objects = []
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Iterable, Optional, List, Tuple
|
||||
|
||||
from bpy.types import Object, Context, Material
|
||||
from bpy.types import Object, Context, Material, Mesh
|
||||
|
||||
from .ase import ASE, ASEGeometryObject, ASEFace, ASEFaceNormal, ASEVertexNormal, ASEUVLayer, is_collision_name
|
||||
import bpy
|
||||
@@ -10,14 +10,20 @@ from mathutils import Matrix, Vector
|
||||
|
||||
SMOOTHING_GROUP_MAX = 32
|
||||
|
||||
class ASEBuilderError(Exception):
|
||||
class ASEBuildError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ASEBuilderOptions(object):
|
||||
class ASEBuildOptions(object):
|
||||
def __init__(self):
|
||||
self.object_eval_state = 'EVALUATED'
|
||||
self.materials: Optional[List[Material]] = None
|
||||
self.transform = Matrix.Identity(4)
|
||||
self.should_export_vertex_colors = True
|
||||
self.vertex_color_mode = 'ACTIVE'
|
||||
self.has_vertex_colors = False
|
||||
self.vertex_color_attribute = ''
|
||||
self.should_invert_normals = False
|
||||
|
||||
|
||||
def get_object_matrix(obj: Object, asset_instance: Optional[Object] = None) -> Matrix:
|
||||
@@ -38,9 +44,7 @@ def get_mesh_objects(objects: Iterable[Object]) -> List[Tuple[Object, Optional[O
|
||||
return mesh_objects
|
||||
|
||||
|
||||
|
||||
class ASEBuilder(object):
|
||||
def build(self, context: Context, options: ASEBuilderOptions, objects: Iterable[Object]):
|
||||
def build_ase(context: Context, options: ASEBuildOptions, objects: Iterable[Object]) -> ASE:
|
||||
ase = ASE()
|
||||
|
||||
main_geometry_object = None
|
||||
@@ -54,12 +58,15 @@ class ASEBuilder(object):
|
||||
|
||||
matrix_world = get_object_matrix(obj, asset_instance)
|
||||
|
||||
# Evaluate the mesh after modifiers are applied
|
||||
# Save the active color name for vertex color export.
|
||||
active_color_name = obj.data.color_attributes.active_color_name
|
||||
|
||||
match options.object_eval_state:
|
||||
case 'ORIGINAL':
|
||||
mesh_object = obj
|
||||
mesh_data = mesh_object.data
|
||||
case 'EVALUATED':
|
||||
# Evaluate the mesh after modifiers are applied
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
bm = bmesh.new()
|
||||
bm.from_object(obj, depsgraph)
|
||||
@@ -85,13 +92,10 @@ class ASEBuilder(object):
|
||||
for edge in bm.edges:
|
||||
if not edge.is_manifold:
|
||||
del bm
|
||||
raise ASEBuilderError(f'Collision mesh \'{obj.name}\' is not manifold')
|
||||
raise ASEBuildError(f'Collision mesh \'{obj.name}\' is not manifold')
|
||||
if not edge.is_convex:
|
||||
del bm
|
||||
raise ASEBuilderError(f'Collision mesh \'{obj.name}\' is not convex')
|
||||
|
||||
if not geometry_object.is_collision and len(obj.data.materials) == 0:
|
||||
raise ASEBuilderError(f'Mesh \'{obj.name}\' must have at least one material')
|
||||
raise ASEBuildError(f'Collision mesh \'{obj.name}\' is not convex')
|
||||
|
||||
vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ matrix_world
|
||||
|
||||
@@ -102,9 +106,13 @@ class ASEBuilder(object):
|
||||
if not geometry_object.is_collision:
|
||||
for mesh_material_index, material in enumerate(obj.data.materials):
|
||||
if material is None:
|
||||
raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{obj.name}\' cannot be empty')
|
||||
raise ASEBuildError(f'Material slot {mesh_material_index + 1} for mesh \'{obj.name}\' cannot be empty')
|
||||
material_indices.append(ase.materials.index(material))
|
||||
|
||||
if len(material_indices) == 0:
|
||||
# If no materials are assigned to the mesh, just have a single empty material.
|
||||
material_indices.append(0)
|
||||
|
||||
mesh_data.calc_loop_triangles()
|
||||
|
||||
# Calculate smoothing groups.
|
||||
@@ -115,6 +123,8 @@ class ASEBuilder(object):
|
||||
_, _, 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 options.should_invert_normals:
|
||||
should_invert_normals = not should_invert_normals
|
||||
|
||||
loop_triangle_index_order = (2, 1, 0) if should_invert_normals else (0, 1, 2)
|
||||
|
||||
@@ -165,10 +175,20 @@ class ASEBuilder(object):
|
||||
)
|
||||
|
||||
# Vertex Colors
|
||||
if len(mesh_data.vertex_colors) > 0:
|
||||
if mesh_data.vertex_colors.active is not None:
|
||||
vertex_colors = mesh_data.vertex_colors.active.data
|
||||
for color in map(lambda x: x.color, vertex_colors):
|
||||
if options.should_export_vertex_colors and options.has_vertex_colors:
|
||||
color_attribute = None
|
||||
match options.vertex_color_mode:
|
||||
case 'ACTIVE':
|
||||
color_attribute = mesh_data.color_attributes[active_color_name]
|
||||
case 'EXPLICIT':
|
||||
color_attribute = mesh_data.color_attributes.get(options.vertex_color_attribute, None)
|
||||
|
||||
if color_attribute is not None:
|
||||
# Make sure that the selected color attribute is on the CORNER domain.
|
||||
if color_attribute.domain != 'CORNER':
|
||||
raise ASEBuildError(f'Color attribute \'{color_attribute.name}\' for object \'{obj.name}\' must have domain of \'CORNER\' (found \'{color_attribute.domain}\')')
|
||||
|
||||
for color in map(lambda x: x.color, color_attribute.data):
|
||||
geometry_object.vertex_colors.append(tuple(color[0:3]))
|
||||
|
||||
# Update data offsets for next iteration
|
||||
@@ -180,9 +200,9 @@ class ASEBuilder(object):
|
||||
context.window_manager.progress_end()
|
||||
|
||||
if len(ase.geometry_objects) == 0:
|
||||
raise ASEBuilderError('At least one mesh object must be selected')
|
||||
raise ASEBuildError('At least one mesh object must be selected')
|
||||
|
||||
if main_geometry_object is None:
|
||||
raise ASEBuilderError('At least one non-collision mesh must be exported')
|
||||
raise ASEBuildError('At least one non-collision mesh must be exported')
|
||||
|
||||
return ase
|
||||
|
||||
@@ -3,9 +3,11 @@ from typing import Iterable, List, Set, Union
|
||||
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty, EnumProperty
|
||||
from bpy.types import Operator, Material, PropertyGroup, UIList, Object, FileHandler, Collection
|
||||
from .builder import ASEBuilder, ASEBuilderOptions, ASEBuilderError, get_mesh_objects
|
||||
from bpy.props import StringProperty, CollectionProperty, PointerProperty, IntProperty, EnumProperty, BoolProperty
|
||||
from bpy.types import Operator, Material, PropertyGroup, UIList, Object, FileHandler
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
from .builder import ASEBuildOptions, ASEBuildError, get_mesh_objects, build_ase
|
||||
from .writer import ASEWriter
|
||||
|
||||
|
||||
@@ -13,9 +15,35 @@ class ASE_PG_material(PropertyGroup):
|
||||
material: PointerProperty(type=Material)
|
||||
|
||||
|
||||
def get_vertex_color_attributes_from_objects(objects: Iterable[Object]) -> Set[str]:
|
||||
'''
|
||||
Get the unique vertex color attributes from all the selected objects.
|
||||
:param objects: The objects to search for vertex color attributes.
|
||||
:return: A set of unique vertex color attributes.
|
||||
'''
|
||||
items = set()
|
||||
for obj in filter(lambda x: x.type == 'MESH', objects):
|
||||
for layer in filter(lambda x: x.domain == 'CORNER', obj.data.color_attributes):
|
||||
items.add(layer.name)
|
||||
return items
|
||||
|
||||
|
||||
def vertex_color_attribute_items(self, context):
|
||||
# Get the unique color attributes from all the selected objects.
|
||||
return [(x, x, '') for x in sorted(get_vertex_color_attributes_from_objects(context.selected_objects))]
|
||||
|
||||
|
||||
class ASE_PG_export(PropertyGroup):
|
||||
material_list: CollectionProperty(name='Materials', type=ASE_PG_material)
|
||||
material_list_index: IntProperty(name='Index', default=0)
|
||||
should_export_vertex_colors: BoolProperty(name='Export Vertex Colors', default=True)
|
||||
vertex_color_mode: EnumProperty(name='Vertex Color Mode', items=(
|
||||
('ACTIVE', 'Active', 'Use the active vertex color attribute'),
|
||||
('EXPLICIT', 'Explicit', 'Use the vertex color attribute specified below'),
|
||||
))
|
||||
has_vertex_colors: BoolProperty(name='Has Vertex Colors', default=False, options={'HIDDEN'})
|
||||
vertex_color_attribute: EnumProperty(name='Attribute', items=vertex_color_attribute_items)
|
||||
should_invert_normals: BoolProperty(name='Invert Normals', default=False, description='Invert the normals of the exported geometry. This should be used if the software you are exporting to uses a different winding order than Blender')
|
||||
|
||||
|
||||
def get_unique_materials(mesh_objects: Iterable[Object]) -> List[Material]:
|
||||
@@ -109,17 +137,35 @@ class ASE_OT_export(Operator, ExportHelper):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
pg = context.scene.ase_export
|
||||
|
||||
materials_header, materials_panel = layout.panel('Materials', default_closed=False)
|
||||
materials_header.label(text='Materials')
|
||||
|
||||
if materials_panel:
|
||||
row = materials_panel.row()
|
||||
row.template_list('ASE_UL_materials', '', context.scene.ase_export, 'material_list', context.scene.ase_export, 'material_list_index')
|
||||
row.template_list('ASE_UL_materials', '', pg, 'material_list', pg, 'material_list_index')
|
||||
col = row.column(align=True)
|
||||
col.operator(ASE_OT_material_list_move_up.bl_idname, icon='TRIA_UP', text='')
|
||||
col.operator(ASE_OT_material_list_move_down.bl_idname, icon='TRIA_DOWN', text='')
|
||||
|
||||
|
||||
has_vertex_colors = len(get_vertex_color_attributes_from_objects(context.selected_objects)) > 0
|
||||
vertex_colors_header, vertex_colors_panel = layout.panel_prop(pg, 'should_export_vertex_colors')
|
||||
row = vertex_colors_header.row()
|
||||
row.enabled = has_vertex_colors
|
||||
row.prop(pg, 'should_export_vertex_colors', text='Vertex Colors')
|
||||
|
||||
if vertex_colors_panel:
|
||||
vertex_colors_panel.use_property_split = True
|
||||
vertex_colors_panel.use_property_decorate = False
|
||||
if has_vertex_colors:
|
||||
vertex_colors_panel.prop(pg, 'vertex_color_mode', text='Mode')
|
||||
if pg.vertex_color_mode == 'EXPLICIT':
|
||||
vertex_colors_panel.prop(pg, 'vertex_color_attribute', icon='GROUP_VCOL')
|
||||
else:
|
||||
vertex_colors_panel.label(text='No vertex color attributes found')
|
||||
|
||||
advanced_header, advanced_panel = layout.panel('Advanced', default_closed=True)
|
||||
advanced_header.label(text='Advanced')
|
||||
|
||||
@@ -127,6 +173,7 @@ class ASE_OT_export(Operator, ExportHelper):
|
||||
advanced_panel.use_property_split = True
|
||||
advanced_panel.use_property_decorate = False
|
||||
advanced_panel.prop(self, 'object_eval_state')
|
||||
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)]
|
||||
@@ -141,16 +188,22 @@ class ASE_OT_export(Operator, ExportHelper):
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
options = ASEBuilderOptions()
|
||||
options.object_eval_state = self.object_eval_state
|
||||
pg = getattr(context.scene, 'ase_export')
|
||||
|
||||
options = ASEBuildOptions()
|
||||
options.object_eval_state = self.object_eval_state
|
||||
options.should_export_vertex_colors = pg.should_export_vertex_colors
|
||||
options.vertex_color_mode = pg.vertex_color_mode
|
||||
options.has_vertex_colors = len(get_vertex_color_attributes_from_objects(context.selected_objects)) > 0
|
||||
options.vertex_color_attribute = pg.vertex_color_attribute
|
||||
options.materials = [x.material for x in pg.material_list]
|
||||
options.should_invert_normals = pg.should_invert_normals
|
||||
try:
|
||||
ase = ASEBuilder().build(context, options, context.selected_objects)
|
||||
ase = build_ase(context, options, context.selected_objects)
|
||||
ASEWriter().write(self.filepath, ase)
|
||||
self.report({'INFO'}, 'ASE exported successfully')
|
||||
return {'FINISHED'}
|
||||
except ASEBuilderError as e:
|
||||
except ASEBuildError as e:
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
@@ -190,8 +243,9 @@ class ASE_OT_export_collection(Operator, ExportHelper):
|
||||
def execute(self, context):
|
||||
collection = bpy.data.collections.get(self.collection)
|
||||
|
||||
options = ASEBuilderOptions()
|
||||
options = ASEBuildOptions()
|
||||
options.object_eval_state = self.object_eval_state
|
||||
options.transform = Matrix.Translation(-Vector(collection.instance_offset))
|
||||
|
||||
# Iterate over all the objects in the collection.
|
||||
mesh_objects = get_mesh_objects(collection.all_objects)
|
||||
@@ -199,8 +253,8 @@ class ASE_OT_export_collection(Operator, ExportHelper):
|
||||
options.materials = get_unique_materials([x[0] for x in mesh_objects])
|
||||
|
||||
try:
|
||||
ase = ASEBuilder().build(context, options, collection.all_objects)
|
||||
except ASEBuilderError as e:
|
||||
ase = build_ase(context, options, collection.all_objects)
|
||||
except ASEBuildError as e:
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from .ase import ASE
|
||||
|
||||
|
||||
class ASEFile(object):
|
||||
def __init__(self):
|
||||
self.commands = []
|
||||
@@ -99,7 +102,7 @@ class ASEWriter(object):
|
||||
self.write_command(command)
|
||||
|
||||
@staticmethod
|
||||
def build_ase_tree(ase) -> ASEFile:
|
||||
def build_ase_tree(ase: ASE) -> ASEFile:
|
||||
root = ASEFile()
|
||||
root.add_command('3DSMAX_ASCIIEXPORT').push_datum(200)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user