Empty material slots and meshes with no materials are now handled correctly.
* Meshes without materials will be treated as though they have a single empty material slot. * Empty material slots are now treated as a "None" material on export instead of being disallowed. * The "None" material slot is appended to the end of the material list unless otherwise specified through manual material ordering.
This commit is contained in:
@@ -73,16 +73,14 @@ def _get_mesh_export_space_matrix(node: ObjectNode | None, export_space: str) ->
|
||||
def _get_material_name_indices(obj: Object, material_names: list[str]) -> Iterable[int]:
|
||||
"""
|
||||
Returns the index of the material in the list of material names.
|
||||
If the material is not found, the index 0 is returned.
|
||||
If the material is not found or the slot is empty, the index of 'None' is returned.
|
||||
"""
|
||||
for material_slot in obj.material_slots:
|
||||
if material_slot.material is None:
|
||||
try:
|
||||
material_name = material_slot.material.name if material_slot.material is not None else 'None'
|
||||
yield material_names.index(material_name)
|
||||
except ValueError:
|
||||
yield 0
|
||||
else:
|
||||
try:
|
||||
yield material_names.index(material_slot.material.name)
|
||||
except ValueError:
|
||||
yield 0
|
||||
|
||||
|
||||
def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuildOptions) -> PskBuildResult:
|
||||
@@ -109,15 +107,34 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
||||
psk.bones = [bone.psx_bone for bone in psx_bone_create_result.bones]
|
||||
|
||||
# Materials
|
||||
mesh_objects = [dfs_object.obj for dfs_object in input_objects.mesh_dfs_objects]
|
||||
|
||||
match options.material_order_mode:
|
||||
case 'AUTOMATIC':
|
||||
mesh_objects = [dfs_object.obj for dfs_object in input_objects.mesh_dfs_objects]
|
||||
materials = list(get_materials_for_mesh_objects(context.evaluated_depsgraph_get(), mesh_objects))
|
||||
case 'MANUAL':
|
||||
# The material name list may contain materials that are not on the mesh objects.
|
||||
# Therefore, we can take the material_name_list as gospel and simply use it as a lookup table.
|
||||
# If a look-up fails, replace it with an empty material.
|
||||
materials = [bpy.data.materials.get(x, None) for x in options.material_name_list]
|
||||
|
||||
# Check if any mesh needs a None material (has no slots or empty slots)
|
||||
needs_none_material = False
|
||||
for mesh_object in mesh_objects:
|
||||
evaluated_mesh_object = mesh_object.evaluated_get(context.evaluated_depsgraph_get())
|
||||
if len(evaluated_mesh_object.material_slots) == 0:
|
||||
needs_none_material = True
|
||||
break
|
||||
for material_slot in evaluated_mesh_object.material_slots:
|
||||
if material_slot.material is None:
|
||||
needs_none_material = True
|
||||
break
|
||||
if needs_none_material:
|
||||
break
|
||||
|
||||
# Append None at the end if needed and not already present
|
||||
if needs_none_material and None not in materials:
|
||||
materials.append(None)
|
||||
case _:
|
||||
assert False, f'Invalid material order mode: {options.material_order_mode}'
|
||||
|
||||
@@ -130,11 +147,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
||||
material.psk.mesh_triangle_bit_flags)
|
||||
psk.materials.append(psk_material)
|
||||
|
||||
# TODO: This wasn't left in a good state. We should detect if we need to add a "default" material.
|
||||
# This can be done by checking if there is an empty material slot on any of the mesh objects, or if there are
|
||||
# no material slots on any of the mesh objects.
|
||||
# If so, it should be added to the end of the list of materials, and its index should mapped to a None value in the
|
||||
# material indices list.
|
||||
# Ensure at least one material exists
|
||||
if len(psk.materials) == 0:
|
||||
# Add a default material if no materials are present.
|
||||
psk_material = Psk.Material()
|
||||
@@ -177,8 +190,12 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
||||
material_indices = list(_get_material_name_indices(obj, material_names))
|
||||
|
||||
if len(material_indices) == 0:
|
||||
# Add a default material if no materials are present.
|
||||
material_indices = [0]
|
||||
# If the mesh has no material slots, map to the 'None' material index
|
||||
try:
|
||||
none_material_index = material_names.index('None')
|
||||
except ValueError:
|
||||
none_material_index = 0
|
||||
material_indices = [none_material_index]
|
||||
|
||||
# Store the reference to the evaluated object and data so that we can clean them up later.
|
||||
evaluated_mesh_object = None
|
||||
|
||||
@@ -29,7 +29,7 @@ def populate_material_name_list(depsgraph: Depsgraph, mesh_objects: Iterable[Obj
|
||||
material_list.clear()
|
||||
for index, material in enumerate(materials):
|
||||
m = material_list.add()
|
||||
m.material_name = material.name
|
||||
m.material_name = material.name if material is not None else 'None'
|
||||
m.index = index
|
||||
|
||||
|
||||
@@ -173,19 +173,32 @@ class PSK_OT_material_list_name_move_down(Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_sorted_materials_by_names(materials: Iterable[Material], material_names: list[str]) -> list[Material]:
|
||||
def get_sorted_materials_by_names(materials: Iterable[Material | None], material_names: list[str]) -> list[Material | None]:
|
||||
"""
|
||||
Sorts the materials by the order of the material names list. Any materials not in the list will be appended to the
|
||||
end of the list in the order they are found.
|
||||
end of the list in the order they are found. None materials (representing empty material slots) are always
|
||||
appended at the very end.
|
||||
|
||||
@param materials: A list of materials to sort
|
||||
@param materials: A list of materials to sort (can include None)
|
||||
@param material_names: A list of material names to sort by
|
||||
@return: A sorted list of materials
|
||||
@return: A sorted list of materials (with None at the end if present)
|
||||
"""
|
||||
materials = list(materials)
|
||||
has_none = None in materials
|
||||
materials = [m for m in materials if m is not None]
|
||||
|
||||
materials_in_collection = [m for m in materials if m.name in material_names]
|
||||
materials_not_in_collection = [m for m in materials if m.name not in material_names]
|
||||
materials_in_collection = sorted(materials_in_collection, key=lambda x: material_names.index(x.name))
|
||||
return materials_in_collection + materials_not_in_collection
|
||||
|
||||
result: list[Material | None] = []
|
||||
result.extend(materials_in_collection)
|
||||
result.extend(materials_not_in_collection)
|
||||
|
||||
if has_none:
|
||||
result.append(None)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_psk_build_options_from_property_group(scene: Scene, pg: PskExportMixin) -> PskBuildOptions:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import bpy
|
||||
from collections import Counter
|
||||
from typing import Iterable, cast as typing_cast
|
||||
from bpy.types import Armature, AnimData, Collection, Context, Object, ArmatureModifier, SpaceProperties, PropertyGroup
|
||||
from bpy.types import Armature, AnimData, Collection, Context, Object, ArmatureModifier, SpaceProperties, PropertyGroup, Material
|
||||
from mathutils import Matrix, Vector, Quaternion as BpyQuaternion
|
||||
from psk_psa_py.shared.data import PsxBone, Quaternion, Vector3
|
||||
|
||||
@@ -619,16 +619,29 @@ class PskInputObjects(object):
|
||||
|
||||
|
||||
def get_materials_for_mesh_objects(depsgraph: Depsgraph, mesh_objects: Iterable[Object]):
|
||||
yielded_materials = set()
|
||||
'''
|
||||
Yields unique materials used by the given mesh objects.
|
||||
If any mesh has no material slots or any empty material slots, None is yielded at the end.
|
||||
'''
|
||||
yielded_materials: Set[Material] = set()
|
||||
has_none_material = False
|
||||
for mesh_object in mesh_objects:
|
||||
evaluated_mesh_object = mesh_object.evaluated_get(depsgraph)
|
||||
for i, material_slot in enumerate(evaluated_mesh_object.material_slots):
|
||||
material = material_slot.material
|
||||
if material is None:
|
||||
raise RuntimeError(f'Material slots cannot be empty. ({mesh_object.name}, index {i})')
|
||||
if material not in yielded_materials:
|
||||
yielded_materials.add(material)
|
||||
yield material
|
||||
# Check if mesh has no material slots or any empty material slots
|
||||
if len(evaluated_mesh_object.material_slots) == 0:
|
||||
has_none_material = True
|
||||
else:
|
||||
for material_slot in evaluated_mesh_object.material_slots:
|
||||
material = material_slot.material
|
||||
if material is None:
|
||||
has_none_material = True
|
||||
else:
|
||||
if material not in yielded_materials:
|
||||
yielded_materials.add(material)
|
||||
yield material
|
||||
# Yield None at the end if any mesh had no material slots or empty material slots
|
||||
if has_none_material:
|
||||
yield None
|
||||
|
||||
|
||||
def get_mesh_objects_for_collection(collection: Collection) -> Iterable[DfsObject]:
|
||||
|
||||
Reference in New Issue
Block a user