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]:
|
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.
|
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:
|
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
|
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:
|
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]
|
psk.bones = [bone.psx_bone for bone in psx_bone_create_result.bones]
|
||||||
|
|
||||||
# Materials
|
# Materials
|
||||||
|
mesh_objects = [dfs_object.obj for dfs_object in input_objects.mesh_dfs_objects]
|
||||||
|
|
||||||
match options.material_order_mode:
|
match options.material_order_mode:
|
||||||
case 'AUTOMATIC':
|
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))
|
materials = list(get_materials_for_mesh_objects(context.evaluated_depsgraph_get(), mesh_objects))
|
||||||
case 'MANUAL':
|
case 'MANUAL':
|
||||||
# The material name list may contain materials that are not on the mesh objects.
|
# 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.
|
# 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.
|
# 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]
|
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 _:
|
case _:
|
||||||
assert False, f'Invalid material order mode: {options.material_order_mode}'
|
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)
|
material.psk.mesh_triangle_bit_flags)
|
||||||
psk.materials.append(psk_material)
|
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.
|
# Ensure at least one material exists
|
||||||
# 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.
|
|
||||||
if len(psk.materials) == 0:
|
if len(psk.materials) == 0:
|
||||||
# Add a default material if no materials are present.
|
# Add a default material if no materials are present.
|
||||||
psk_material = Psk.Material()
|
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))
|
material_indices = list(_get_material_name_indices(obj, material_names))
|
||||||
|
|
||||||
if len(material_indices) == 0:
|
if len(material_indices) == 0:
|
||||||
# Add a default material if no materials are present.
|
# If the mesh has no material slots, map to the 'None' material index
|
||||||
material_indices = [0]
|
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.
|
# Store the reference to the evaluated object and data so that we can clean them up later.
|
||||||
evaluated_mesh_object = None
|
evaluated_mesh_object = None
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def populate_material_name_list(depsgraph: Depsgraph, mesh_objects: Iterable[Obj
|
|||||||
material_list.clear()
|
material_list.clear()
|
||||||
for index, material in enumerate(materials):
|
for index, material in enumerate(materials):
|
||||||
m = material_list.add()
|
m = material_list.add()
|
||||||
m.material_name = material.name
|
m.material_name = material.name if material is not None else 'None'
|
||||||
m.index = index
|
m.index = index
|
||||||
|
|
||||||
|
|
||||||
@@ -173,19 +173,32 @@ class PSK_OT_material_list_name_move_down(Operator):
|
|||||||
return {'FINISHED'}
|
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
|
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
|
@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_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_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))
|
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:
|
def get_psk_build_options_from_property_group(scene: Scene, pg: PskExportMixin) -> PskBuildOptions:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Iterable, cast as typing_cast
|
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 mathutils import Matrix, Vector, Quaternion as BpyQuaternion
|
||||||
from psk_psa_py.shared.data import PsxBone, Quaternion, Vector3
|
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]):
|
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:
|
for mesh_object in mesh_objects:
|
||||||
evaluated_mesh_object = mesh_object.evaluated_get(depsgraph)
|
evaluated_mesh_object = mesh_object.evaluated_get(depsgraph)
|
||||||
for i, material_slot in enumerate(evaluated_mesh_object.material_slots):
|
# Check if mesh has no material slots or any empty material slots
|
||||||
material = material_slot.material
|
if len(evaluated_mesh_object.material_slots) == 0:
|
||||||
if material is None:
|
has_none_material = True
|
||||||
raise RuntimeError(f'Material slots cannot be empty. ({mesh_object.name}, index {i})')
|
else:
|
||||||
if material not in yielded_materials:
|
for material_slot in evaluated_mesh_object.material_slots:
|
||||||
yielded_materials.add(material)
|
material = material_slot.material
|
||||||
yield 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]:
|
def get_mesh_objects_for_collection(collection: Collection) -> Iterable[DfsObject]:
|
||||||
|
|||||||
Reference in New Issue
Block a user