diff --git a/core/properties.py b/core/properties.py index cc91976..f7656af 100644 --- a/core/properties.py +++ b/core/properties.py @@ -41,6 +41,12 @@ def register() -> None: default=False ))) + register_property((bpy.types.Scene, "material_search_filter", bpy.props.StringProperty( + name="Search Materials", + description="Filter materials by name", + default="" + ))) + register_property((bpy.types.Material, "include_in_atlas", bpy.props.BoolProperty( name=t("TextureAtlas.include_in_atlas"), description=t("TextureAtlas.include_in_atlas_desc"), diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py index d1a9dd1..4b925c1 100644 --- a/functions/atlas_materials.py +++ b/functions/atlas_materials.py @@ -24,19 +24,24 @@ class MaterialImageList: self.h: int = 0 self.fit = None -def scale_images_to_largest(images:list[Image]) -> set: - print([image.name for image in images]) - x: int=0 - y: int=0 - for image in images: - x = max(x,image.size[0]) - y = max(y,image.size[1]) - print(x,y) +def scale_images_to_largest(images: list[Image]) -> set: + x: int = 0 + y: int = 0 - for image in images: + # Filter out None or invalid images + valid_images = [img for img in images if img and img.has_data] + + if not valid_images: + return 0, 0 + + for image in valid_images: + x = max(x, image.size[0]) + y = max(y, image.size[1]) + + for image in valid_images: image.scale(width=int(x), height=int(y)) - return x,y + return x, y def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> list[Image]: list_of_images: list[Image] = [] @@ -57,7 +62,8 @@ def get_material_images_from_scene(context: Context) -> list[MaterialImageList]: for obj in context.scene.objects: if obj.type == 'MESH': for mat_slot in obj.material_slots: - if mat_slot.material and mat_slot.material.include_in_atlas: + # Only process materials that are selected for atlas + if mat_slot.material and mat_slot.material.include_in_atlas is True: new_mat_image_item = MaterialImageList() try: new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo] @@ -114,6 +120,7 @@ def get_material_images_from_scene(context: Context) -> list[MaterialImageList]: return material_image_list + def prep_images_in_scene(context: Context) -> list[MaterialImageList]: preped_images: list[MaterialImageList] = get_material_images_from_scene(context) for MaterialImageClass in preped_images: @@ -141,14 +148,14 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): def execute(self, context: Context) -> set: try: - # Only get materials marked for atlas creation - mat_images: list[MaterialImageList] = [m for m in prep_images_in_scene(context) if m.material.include_in_atlas] + # Get only materials that are explicitly marked for inclusion + selected_materials = [m for m in prep_images_in_scene(context) if m.material and m.material.include_in_atlas is True] - if not mat_images: + if not selected_materials: self.report({'WARNING'}, t("TextureAtlas.no_materials_selected")) return {'CANCELLED'} - packer: BinPacker = BinPacker(mat_images) + packer: BinPacker = BinPacker(selected_materials) mat_images = packer.fit() size: list[int] = [max([matimg.fit.w + matimg.albedo.size[0] for matimg in mat_images]), @@ -274,13 +281,13 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): atlased_mat.material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"]) atlased_mat.material.node_tree.links.new(normal_map_node.inputs["Color"], normal_node.outputs["Color"]) - # Only update materials for meshes that had materials included in the atlas + # Only update selected materials for meshes for obj in context.scene.objects: if obj.type == 'MESH': - if any(mat_slot.material and mat_slot.material.include_in_atlas for mat_slot in obj.material_slots): - mesh: Mesh = obj.data - mesh.materials.clear() - mesh.materials.append(atlased_mat.material) + mesh: Mesh = obj.data + for i, mat_slot in enumerate(obj.material_slots): + if mat_slot.material and mat_slot.material.include_in_atlas is True: + mesh.materials[i] = atlased_mat.material self.report({'INFO'}, t("TextureAtlas.atlas_completed")) return {"FINISHED"} diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py index c4f5def..7d5a666 100644 --- a/ui/atlas_materials.py +++ b/ui/atlas_materials.py @@ -1,11 +1,56 @@ from bpy.types import UIList, Panel, UILayout, Object, Context,Material, Operator import bpy +from math import sqrt from ..core.register import register_wrap from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.common import SceneMatClass, MaterialListBool, get_selected_armature from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials from ..functions.translations import t +@register_wrap +class AvatarToolKit_OT_SelectAllMaterials(Operator): + bl_idname = 'avatar_toolkit.select_all_materials' + bl_label = "Select All" + bl_description = "Select all materials for atlas" + + def execute(self, context): + for item in context.scene.materials: + item.mat.include_in_atlas = True + return {'FINISHED'} + +@register_wrap +class AvatarToolKit_OT_SelectNoneMaterials(Operator): + bl_idname = 'avatar_toolkit.select_none_materials' + bl_label = "Select None" + bl_description = "Deselect all materials" + + def execute(self, context): + for item in context.scene.materials: + item.mat.include_in_atlas = False + return {'FINISHED'} + +@register_wrap +class AvatarToolKit_OT_ExpandAllMaterials(Operator): + bl_idname = 'avatar_toolkit.expand_all_materials' + bl_label = "Expand All" + bl_description = "Expand all material settings" + + def execute(self, context): + for item in context.scene.materials: + item.mat.material_expanded = True + return {'FINISHED'} + +@register_wrap +class AvatarToolKit_OT_CollapseAllMaterials(Operator): + bl_idname = 'avatar_toolkit.collapse_all_materials' + bl_label = "Collapse All" + bl_description = "Collapse all material settings" + + def execute(self, context): + for item in context.scene.materials: + item.mat.material_expanded = False + return {'FINISHED'} + @register_wrap class AvatarToolKit_OT_ExpandSectionMaterials(Operator): bl_idname = 'avatar_toolkit.expand_section_materials' @@ -40,26 +85,70 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' + def draw_header(self, context): + layout = self.layout + row = layout.row(align=True) + + row.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT') + row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT') + row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN') + row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT') + row.prop(context.scene, "material_search_filter", text="", icon='VIEWZOOM') + + box = layout.box() + row = box.row() + row.label(text=f"Estimated Atlas Size: {self.calculate_atlas_size(context)}px") + def draw_item(self, context: Context, layout: UILayout, data: Object, item: SceneMatClass, icon, active_data, active_propname, index): if context.scene.texture_atlas_Has_Mat_List_Shown: - box = layout.box() - row = box.row() + if context.scene.material_search_filter and context.scene.material_search_filter.lower() not in item.mat.name.lower(): + return + + row = layout.row() - # Draw material entry + # Add a clear checkbox for material selection + row.prop(item.mat, "include_in_atlas", text="", icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT') + + # Material name and expansion toggle row.prop(item.mat, "material_expanded", text=item.mat.name, icon='DOWNARROW_HLT' if item.mat.material_expanded else 'RIGHTARROW', emboss=False) - row.prop(item.mat, "include_in_atlas", text="") + # Show texture settings if expanded if item.mat.material_expanded and item.mat.include_in_atlas: + box = layout.box() col = box.column(align=True) - col.prop(item.mat, "texture_atlas_albedo") - col.prop(item.mat, "texture_atlas_normal") - col.prop(item.mat, "texture_atlas_emission") - col.prop(item.mat, "texture_atlas_ambient_occlusion") - col.prop(item.mat, "texture_atlas_height") - col.prop(item.mat, "texture_atlas_roughness") + self.draw_texture_row(col, item.mat, "texture_atlas_albedo", "IMAGE_RGB") + self.draw_texture_row(col, item.mat, "texture_atlas_normal", "NORMALS_FACE") + self.draw_texture_row(col, item.mat, "texture_atlas_emission", "LIGHT") + self.draw_texture_row(col, item.mat, "texture_atlas_ambient_occlusion", "SHADING_SOLID") + self.draw_texture_row(col, item.mat, "texture_atlas_height", "IMAGE_ZDEPTH") + self.draw_texture_row(col, item.mat, "texture_atlas_roughness", "MATERIAL") + + col.separator(factor=0.5) + + def draw_texture_row(self, layout, material, prop_name, icon): + row = layout.row() + row.prop(material, prop_name, icon=icon) + if getattr(material, prop_name): + row.label(text="", icon='CHECKMARK') + else: + row.label(text="", icon='X') + + def is_material_ready(self, material): + return bool(material.texture_atlas_albedo or + material.texture_atlas_normal or + material.texture_atlas_emission) + + def calculate_atlas_size(self, context): + total_size = 0 + for mat in context.scene.materials: + if mat.mat.include_in_atlas: + if mat.mat.texture_atlas_albedo: + img = bpy.data.images[mat.mat.texture_atlas_albedo] + total_size += img.size[0] * img.size[1] + return f"{int(sqrt(total_size))}x{int(sqrt(total_size))}" @register_wrap class AvatarToolKit_PT_TextureAtlasPanel(Panel): @@ -106,4 +195,3 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): icon='NODE_TEXTURE') else: layout.label(text=t("Tools.select_armature"), icon='ERROR') -