Fixed a bug where secondary UV layers would not be exported correctly if not all objects shared the same number of UV layers
This commit is contained in:
@@ -62,52 +62,37 @@ def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z')
|
|||||||
|
|
||||||
def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[DfsObject]) -> ASE:
|
def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[DfsObject]) -> ASE:
|
||||||
ase = ASE()
|
ase = ASE()
|
||||||
|
ase.materials = options.materials
|
||||||
main_geometry_object = None
|
|
||||||
|
|
||||||
dfs_objects = list(dfs_objects)
|
dfs_objects = list(dfs_objects)
|
||||||
|
dfs_objects_processed = 0
|
||||||
|
|
||||||
context.window_manager.progress_begin(0, len(dfs_objects))
|
context.window_manager.progress_begin(0, len(dfs_objects))
|
||||||
|
|
||||||
ase.materials = options.materials
|
class GeometryObjectInfo:
|
||||||
|
def __init__(self, name: str):
|
||||||
|
self.name = name
|
||||||
|
self.dfs_objects = []
|
||||||
|
|
||||||
max_uv_layers = 0
|
main_geometry_object_info = GeometryObjectInfo('io_scene_ase')
|
||||||
for dfs_object in dfs_objects:
|
geometry_object_infos: List[GeometryObjectInfo] = [
|
||||||
mesh_data = cast(Mesh, dfs_object.obj.data)
|
main_geometry_object_info,
|
||||||
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):
|
for object_index, dfs_object in enumerate(dfs_objects):
|
||||||
obj = dfs_object.obj
|
if is_collision_name(dfs_object.obj.name):
|
||||||
matrix_world = dfs_object.matrix_world
|
geometry_object_info = GeometryObjectInfo(dfs_object.obj.name)
|
||||||
|
geometry_object_info.dfs_objects.append(dfs_object)
|
||||||
# Save the active color name for vertex color export.
|
geometry_object_infos.append(geometry_object_info)
|
||||||
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)
|
|
||||||
mesh_data = bpy.data.meshes.new('')
|
|
||||||
bm.to_mesh(mesh_data)
|
|
||||||
del bm
|
|
||||||
mesh_object = bpy.data.objects.new('', mesh_data)
|
|
||||||
mesh_object.matrix_world = matrix_world
|
|
||||||
|
|
||||||
if not is_collision_name(obj.name) and main_geometry_object is not None:
|
|
||||||
geometry_object = main_geometry_object
|
|
||||||
else:
|
else:
|
||||||
geometry_object = ASEGeometryObject()
|
main_geometry_object_info.dfs_objects.append(dfs_object)
|
||||||
geometry_object.name = obj.name
|
|
||||||
if not geometry_object.is_collision:
|
# Sort the DFS objects into collision and non-collision objects.
|
||||||
main_geometry_object = geometry_object
|
coordinate_system_transform = get_coordinate_system_transform(options.forward_axis, options.up_axis)
|
||||||
ase.geometry_objects.append(geometry_object)
|
|
||||||
|
for geometry_object_info in geometry_object_infos:
|
||||||
|
geometry_object = ASEGeometryObject()
|
||||||
|
geometry_object.name = geometry_object_info.name
|
||||||
|
|
||||||
if geometry_object.is_collision:
|
if geometry_object.is_collision:
|
||||||
# Test that collision meshes are manifold and convex.
|
# Test that collision meshes are manifold and convex.
|
||||||
@@ -121,139 +106,166 @@ def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[
|
|||||||
del bm
|
del bm
|
||||||
raise ASEBuildError(f'Collision mesh \'{obj.name}\' is not convex')
|
raise ASEBuildError(f'Collision mesh \'{obj.name}\' is not convex')
|
||||||
|
|
||||||
vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ Matrix.Scale(options.scale, 4) @ matrix_world
|
max_uv_layers = 0
|
||||||
|
for dfs_object in geometry_object_info.dfs_objects:
|
||||||
|
mesh_data = cast(Mesh, dfs_object.obj.data)
|
||||||
|
max_uv_layers = max(max_uv_layers, len(mesh_data.uv_layers))
|
||||||
|
|
||||||
for vertex_index, vertex in enumerate(mesh_data.vertices):
|
geometry_object.uv_layers = [ASEUVLayer() for _ in range(max_uv_layers)]
|
||||||
vertex = vertex_transform @ vertex.co
|
print('max_uv_layers', max_uv_layers)
|
||||||
vertex = coordinate_system_transform @ vertex
|
|
||||||
geometry_object.vertices.append(vertex)
|
|
||||||
|
|
||||||
material_indices = []
|
for dfs_object in geometry_object_info.dfs_objects:
|
||||||
if not geometry_object.is_collision:
|
obj = dfs_object.obj
|
||||||
for mesh_material_index, material in enumerate(obj.data.materials):
|
matrix_world = dfs_object.matrix_world
|
||||||
if material is None:
|
|
||||||
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:
|
# Save the active color name for vertex color export.
|
||||||
# If no materials are assigned to the mesh, just have a single empty material.
|
active_color_name = obj.data.color_attributes.active_color_name
|
||||||
material_indices.append(0)
|
|
||||||
|
|
||||||
mesh_data.calc_loop_triangles()
|
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)
|
||||||
|
mesh_data = bpy.data.meshes.new('')
|
||||||
|
bm.to_mesh(mesh_data)
|
||||||
|
del bm
|
||||||
|
mesh_object = bpy.data.objects.new('', mesh_data)
|
||||||
|
mesh_object.matrix_world = matrix_world
|
||||||
|
|
||||||
# Calculate smoothing groups.
|
vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ Matrix.Scale(options.scale, 4) @ matrix_world
|
||||||
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
|
|
||||||
|
|
||||||
# Figure out how many scaling axes are negative.
|
for vertex_index, vertex in enumerate(mesh_data.vertices):
|
||||||
# This is important for calculating the normals of the mesh.
|
vertex = vertex_transform @ vertex.co
|
||||||
_, _, scale = vertex_transform.decompose()
|
vertex = coordinate_system_transform @ vertex
|
||||||
negative_scaling_axes = sum([1 for x in scale if x < 0])
|
geometry_object.vertices.append(vertex)
|
||||||
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)
|
material_indices = []
|
||||||
|
|
||||||
# Gather the list of unique material indices in the loop triangles.
|
|
||||||
face_material_indices = {loop_triangle.material_index for loop_triangle in mesh_data.loop_triangles}
|
|
||||||
|
|
||||||
# Make sure that each material index is within the bounds of the material indices list.
|
|
||||||
for material_index in face_material_indices:
|
|
||||||
if material_index >= len(material_indices):
|
|
||||||
raise ASEBuildError(f'Material index {material_index} for mesh \'{obj.name}\' is out of bounds.\n'
|
|
||||||
f'This means that one or more faces are assigned to a material slot that does '
|
|
||||||
f'not exist.\n'
|
|
||||||
f'The referenced material indices in the faces are: {sorted(list(face_material_indices))}.\n'
|
|
||||||
f'Either add enough materials to the object or assign faces to existing material slots.'
|
|
||||||
)
|
|
||||||
|
|
||||||
del face_material_indices
|
|
||||||
|
|
||||||
# Faces
|
|
||||||
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
|
||||||
face = ASEFace()
|
|
||||||
face.a, face.b, face.c = map(lambda j: geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[j]].vertex_index, loop_triangle_index_order)
|
|
||||||
if not geometry_object.is_collision:
|
if not geometry_object.is_collision:
|
||||||
face.material_index = material_indices[loop_triangle.material_index]
|
for mesh_material_index, material in enumerate(obj.data.materials):
|
||||||
# The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the
|
if material is None:
|
||||||
# smoothing groups and effectively makes the whole model use sharp-edge rendering.
|
raise ASEBuildError(f'Material slot {mesh_material_index + 1} for mesh \'{obj.name}\' cannot be empty')
|
||||||
# The fix is to constrain the smoothing group between 0 and 31 by applying a modulo of 32 to the actual
|
material_indices.append(ase.materials.index(material))
|
||||||
# smoothing group index.
|
|
||||||
# This may result in bad calculated normals on export in rare cases. For example, if a face with a
|
|
||||||
# smoothing group of 3 is adjacent to a face with a smoothing group of 35 (35 % 32 == 3), those faces
|
|
||||||
# will be treated as part of the same smoothing group.
|
|
||||||
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % SMOOTHING_GROUP_MAX
|
|
||||||
geometry_object.faces.append(face)
|
|
||||||
|
|
||||||
if not geometry_object.is_collision:
|
if len(material_indices) == 0:
|
||||||
# Normals
|
# 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.
|
||||||
|
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
|
||||||
|
|
||||||
|
# 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 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)
|
||||||
|
|
||||||
|
# Gather the list of unique material indices in the loop triangles.
|
||||||
|
face_material_indices = {loop_triangle.material_index for loop_triangle in mesh_data.loop_triangles}
|
||||||
|
|
||||||
|
# Make sure that each material index is within the bounds of the material indices list.
|
||||||
|
for material_index in face_material_indices:
|
||||||
|
if material_index >= len(material_indices):
|
||||||
|
raise ASEBuildError(f'Material index {material_index} for mesh \'{obj.name}\' is out of bounds.\n'
|
||||||
|
f'This means that one or more faces are assigned to a material slot that does '
|
||||||
|
f'not exist.\n'
|
||||||
|
f'The referenced material indices in the faces are: {sorted(list(face_material_indices))}.\n'
|
||||||
|
f'Either add enough materials to the object or assign faces to existing material slots.'
|
||||||
|
)
|
||||||
|
|
||||||
|
del face_material_indices
|
||||||
|
|
||||||
|
# Faces
|
||||||
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
||||||
face_normal = ASEFaceNormal()
|
face = ASEFace()
|
||||||
face_normal.normal = loop_triangle.normal
|
face.a, face.b, face.c = map(lambda j: geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[j]].vertex_index, loop_triangle_index_order)
|
||||||
face_normal.vertex_normals = []
|
if not geometry_object.is_collision:
|
||||||
for i in loop_triangle_index_order:
|
face.material_index = material_indices[loop_triangle.material_index]
|
||||||
vertex_normal = ASEVertexNormal()
|
# The UT2K4 importer only accepts 32 smoothing groups. Anything past this completely mangles the
|
||||||
vertex_normal.vertex_index = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[i]].vertex_index
|
# smoothing groups and effectively makes the whole model use sharp-edge rendering.
|
||||||
vertex_normal.normal = loop_triangle.split_normals[i]
|
# The fix is to constrain the smoothing group between 0 and 31 by applying a modulo of 32 to the actual
|
||||||
if should_invert_normals:
|
# smoothing group index.
|
||||||
vertex_normal.normal = (-Vector(vertex_normal.normal)).to_tuple()
|
# This may result in bad calculated normals on export in rare cases. For example, if a face with a
|
||||||
face_normal.vertex_normals.append(vertex_normal)
|
# smoothing group of 3 is adjacent to a face with a smoothing group of 35 (35 % 32 == 3), those faces
|
||||||
geometry_object.face_normals.append(face_normal)
|
# will be treated as part of the same smoothing group.
|
||||||
|
face.smoothing = (poly_groups[loop_triangle.polygon_index] - 1) % SMOOTHING_GROUP_MAX
|
||||||
|
geometry_object.faces.append(face)
|
||||||
|
|
||||||
# Texture Coordinates
|
if not geometry_object.is_collision:
|
||||||
for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]):
|
# Normals
|
||||||
if i >= len(geometry_object.uv_layers):
|
for face_index, loop_triangle in enumerate(mesh_data.loop_triangles):
|
||||||
geometry_object.uv_layers.append(ASEUVLayer())
|
face_normal = ASEFaceNormal()
|
||||||
uv_layer = geometry_object.uv_layers[i]
|
face_normal.normal = loop_triangle.normal
|
||||||
for loop_index, loop in enumerate(mesh_data.loops):
|
face_normal.vertex_normals = []
|
||||||
u, v = uv_layer_data[loop_index].uv
|
for i in loop_triangle_index_order:
|
||||||
uv_layer.texture_vertices.append((u, v, 0.0))
|
vertex_normal = ASEVertexNormal()
|
||||||
|
vertex_normal.vertex_index = geometry_object.vertex_offset + mesh_data.loops[loop_triangle.loops[i]].vertex_index
|
||||||
|
vertex_normal.normal = loop_triangle.split_normals[i]
|
||||||
|
if should_invert_normals:
|
||||||
|
vertex_normal.normal = (-Vector(vertex_normal.normal)).to_tuple()
|
||||||
|
face_normal.vertex_normals.append(vertex_normal)
|
||||||
|
geometry_object.face_normals.append(face_normal)
|
||||||
|
|
||||||
# Add zeroed texture vertices for any missing UV layers.
|
# Texture Coordinates
|
||||||
for i in range(len(geometry_object.uv_layers), max_uv_layers):
|
for i, uv_layer_data in enumerate([x.data for x in mesh_data.uv_layers]):
|
||||||
uv_layer = ASEUVLayer()
|
uv_layer = geometry_object.uv_layers[i]
|
||||||
for _ in mesh_data.loops:
|
for loop_index, loop in enumerate(mesh_data.loops):
|
||||||
uv_layer.texture_vertices.append((0.0, 0.0, 0.0))
|
u, v = uv_layer_data[loop_index].uv
|
||||||
geometry_object.uv_layers.append(uv_layer)
|
uv_layer.texture_vertices.append((u, v, 0.0))
|
||||||
|
|
||||||
# Texture Faces
|
# Add zeroed texture vertices for any missing UV layers.
|
||||||
for loop_triangle in mesh_data.loop_triangles:
|
for i in range(len(mesh_data.uv_layers), max_uv_layers):
|
||||||
geometry_object.texture_vertex_faces.append(
|
uv_layer = geometry_object.uv_layers[i]
|
||||||
tuple(map(lambda l: geometry_object.texture_vertex_offset + loop_triangle.loops[l], loop_triangle_index_order))
|
for _ in mesh_data.loops:
|
||||||
)
|
uv_layer.texture_vertices.append((0.0, 0.0, 0.0))
|
||||||
|
|
||||||
# Vertex Colors
|
# Texture Faces
|
||||||
if options.should_export_vertex_colors and options.has_vertex_colors:
|
for loop_triangle in mesh_data.loop_triangles:
|
||||||
match options.vertex_color_mode:
|
geometry_object.texture_vertex_faces.append(
|
||||||
case 'ACTIVE':
|
tuple(map(lambda l: geometry_object.texture_vertex_offset + loop_triangle.loops[l], loop_triangle_index_order))
|
||||||
color_attribute_name = active_color_name
|
)
|
||||||
case 'EXPLICIT':
|
|
||||||
color_attribute_name = options.vertex_color_attribute
|
|
||||||
case _:
|
|
||||||
raise ASEBuildError('Invalid vertex color mode')
|
|
||||||
|
|
||||||
color_attribute = mesh_data.color_attributes.get(color_attribute_name, None)
|
# Vertex Colors
|
||||||
|
if options.should_export_vertex_colors and options.has_vertex_colors:
|
||||||
|
match options.vertex_color_mode:
|
||||||
|
case 'ACTIVE':
|
||||||
|
color_attribute_name = active_color_name
|
||||||
|
case 'EXPLICIT':
|
||||||
|
color_attribute_name = options.vertex_color_attribute
|
||||||
|
case _:
|
||||||
|
raise ASEBuildError('Invalid vertex color mode')
|
||||||
|
|
||||||
if color_attribute is not None:
|
color_attribute = mesh_data.color_attributes.get(color_attribute_name, 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):
|
if color_attribute is not None:
|
||||||
geometry_object.vertex_colors.append(tuple(color[0:3]))
|
# 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}\')')
|
||||||
|
|
||||||
# Update data offsets for next iteration
|
for color in map(lambda x: x.color, color_attribute.data):
|
||||||
geometry_object.texture_vertex_offset += len(mesh_data.loops)
|
geometry_object.vertex_colors.append(tuple(color[0:3]))
|
||||||
geometry_object.vertex_offset = len(geometry_object.vertices)
|
|
||||||
|
|
||||||
context.window_manager.progress_update(object_index)
|
# Update data offsets for next iteration
|
||||||
|
geometry_object.texture_vertex_offset += len(mesh_data.loops)
|
||||||
|
geometry_object.vertex_offset = len(geometry_object.vertices)
|
||||||
|
|
||||||
|
dfs_objects_processed += 1
|
||||||
|
context.window_manager.progress_update(dfs_objects_processed)
|
||||||
|
|
||||||
|
ase.geometry_objects.append(geometry_object)
|
||||||
|
|
||||||
context.window_manager.progress_end()
|
context.window_manager.progress_end()
|
||||||
|
|
||||||
if len(ase.geometry_objects) == 0:
|
if len(ase.geometry_objects) == 0:
|
||||||
raise ASEBuildError('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 ASEBuildError('At least one non-collision mesh must be exported')
|
|
||||||
|
|
||||||
return ase
|
return ase
|
||||||
|
|||||||
@@ -231,10 +231,11 @@ class ASE_UL_materials(UIList):
|
|||||||
row.prop(item.material, 'name', text='', emboss=False, icon_value=layout.icon(item.material))
|
row.prop(item.material, 'name', text='', emboss=False, icon_value=layout.icon(item.material))
|
||||||
|
|
||||||
|
|
||||||
class ASE_UL_strings(UIList):
|
class ASE_UL_material_names(UIList):
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.prop(item, 'string', text='', emboss=False)
|
material = bpy.data.materials.get(item.string, None)
|
||||||
|
row.prop(item, 'string', text='', emboss=False, icon_value=layout.icon(material) if material is not None else 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -491,7 +492,7 @@ class ASE_OT_export_collection(Operator, ExportHelper):
|
|||||||
|
|
||||||
if materials_panel:
|
if materials_panel:
|
||||||
row = materials_panel.row()
|
row = materials_panel.row()
|
||||||
row.template_list('ASE_UL_strings', '', self, 'material_order', self, 'material_order_index')
|
row.template_list('ASE_UL_material_names', '', self, 'material_order', self, 'material_order_index')
|
||||||
col = row.column(align=True)
|
col = row.column(align=True)
|
||||||
col.operator(ASE_OT_material_order_add.bl_idname, icon='ADD', text='')
|
col.operator(ASE_OT_material_order_add.bl_idname, icon='ADD', text='')
|
||||||
col.operator(ASE_OT_material_order_remove.bl_idname, icon='REMOVE', text='')
|
col.operator(ASE_OT_material_order_remove.bl_idname, icon='REMOVE', text='')
|
||||||
@@ -588,7 +589,7 @@ classes = (
|
|||||||
ASE_PG_material,
|
ASE_PG_material,
|
||||||
ASE_PG_string,
|
ASE_PG_string,
|
||||||
ASE_UL_materials,
|
ASE_UL_materials,
|
||||||
ASE_UL_strings,
|
ASE_UL_material_names,
|
||||||
ASE_PG_export,
|
ASE_PG_export,
|
||||||
ASE_OT_export,
|
ASE_OT_export,
|
||||||
ASE_OT_export_collection,
|
ASE_OT_export_collection,
|
||||||
|
|||||||
Reference in New Issue
Block a user