diff --git a/core/properties.py b/core/properties.py index 911a395..cc91976 100644 --- a/core/properties.py +++ b/core/properties.py @@ -29,6 +29,24 @@ def register() -> None: description=t("MergeArmatures.selected_armature.label") ))) + register_property((bpy.types.Object, "material_group_expanded", bpy.props.BoolProperty( + name="Expand Material Group", + description="Show/hide materials for this mesh", + default=False + ))) + + register_property((bpy.types.Material, "material_expanded", bpy.props.BoolProperty( + name="Expand Material", + description="Show/hide material properties", + default=False + ))) + + register_property((bpy.types.Material, "include_in_atlas", bpy.props.BoolProperty( + name=t("TextureAtlas.include_in_atlas"), + description=t("TextureAtlas.include_in_atlas_desc"), + default=True + ))) + register_property((bpy.types.Scene, "merge_armature_apply_transforms", bpy.props.BoolProperty( default=False, name=t("MergeArmature.merge_armatures.apply_transforms.label"), diff --git a/core/updater.py b/core/updater.py index 1cf1529..96bc017 100644 --- a/core/updater.py +++ b/core/updater.py @@ -7,6 +7,7 @@ import shutil import pathlib import zipfile import time +from urllib import request, error from threading import Thread from bpy.app.handlers import persistent from ..functions.translations import t @@ -140,9 +141,9 @@ def get_github_releases() -> bool: try: ssl._create_default_https_context = ssl._create_unverified_context - with urllib.request.urlopen(f'https://api.github.com/repos/{GITHUB_REPO}/releases') as url: + with request.urlopen(f'https://api.github.com/repos/{GITHUB_REPO}/releases') as url: data = json.loads(url.read().decode()) - except urllib.error.URLError: + except error.URLError: print('URL ERROR') return False @@ -214,7 +215,7 @@ def download_file(update_url: str) -> None: try: ssl._create_default_https_context = ssl._create_unverified_context urllib.request.urlretrieve(update_url, update_zip_file) - except urllib.error.URLError: + except error.URLError: finish_update(error=t('download_file.cantConnect')) return diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py index 819d7b5..d1a9dd1 100644 --- a/functions/atlas_materials.py +++ b/functions/atlas_materials.py @@ -10,6 +10,20 @@ from ..core.common import SceneMatClass, MaterialListBool from ..core.packer.rectangle_packer import MaterialImageList, BinPacker from ..functions.translations import t +class MaterialImageList: + def __init__(self): + self.albedo: Image = None + self.normal: Image = None + self.emission: Image = None + self.ambient_occlusion: Image = None + self.height: Image = None + self.roughness: Image = None + self.material: Material = None + self.parent_mesh: Object = None + self.w: int = 0 + 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 @@ -38,65 +52,68 @@ def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> list[Image] def get_material_images_from_scene(context: Context) -> list[MaterialImageList]: - mat: SceneMatClass = None material_image_list: list[MaterialImageList] = [] - for mat in context.scene.materials: - new_mat_image_item: MaterialImageList = MaterialImageList() - try: - new_mat_image_item.albedo = bpy.data.images[mat.mat.texture_atlas_albedo] - except Exception as e: - name: str = mat.mat.name+"_albedo_replacement" - if name in bpy.data.images: - bpy.data.images.remove(image=bpy.data.images[name],do_unlink=True) - new_mat_image_item.albedo = bpy.data.images.new(name=name,width=32,height=32, alpha=True) - new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32) - try: - new_mat_image_item.normal = bpy.data.images[mat.mat.texture_atlas_normal] - except Exception: - name: str = mat.mat.name+"_normal_replacement" - if name in bpy.data.images: - bpy.data.images.remove(image=bpy.data.images[name],do_unlink=True) - new_mat_image_item.normal = bpy.data.images.new(name=name,width=32,height=32, alpha=True) - new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32) - try: - new_mat_image_item.emission = bpy.data.images[mat.mat.texture_atlas_emission] - except Exception: - name: str = mat.mat.name+"_emission_replacement" - if name in bpy.data.images: - bpy.data.images.remove(image=bpy.data.images[name],do_unlink=True) - new_mat_image_item.emission = bpy.data.images.new(name=name,width=32,height=32, alpha=True) - new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32) - - try: - new_mat_image_item.ambient_occlusion = bpy.data.images[mat.mat.texture_atlas_ambient_occlusion] - except Exception: - name: str = mat.mat.name+"_ambient_occlusion_replacement" - if name in bpy.data.images: - bpy.data.images.remove(image=bpy.data.images[name],do_unlink=True) - new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name,width=32,height=32, alpha=True) - new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32) - try: - new_mat_image_item.height = bpy.data.images[mat.mat.texture_atlas_height] - except Exception: - name: str = mat.mat.name+"_height_replacement" - if name in bpy.data.images: - bpy.data.images.remove(image=bpy.data.images[name],do_unlink=True) - new_mat_image_item.height = bpy.data.images.new(name=name,width=32,height=32, alpha=True) - new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32) - - try: - new_mat_image_item.roughness = bpy.data.images[mat.mat.texture_atlas_roughness] - except Exception: - name: str = mat.mat.name+"_roughness_replacement" - if name in bpy.data.images: - bpy.data.images.remove(image=bpy.data.images[name],do_unlink=True) - new_mat_image_item.roughness = bpy.data.images.new(name=name,width=32,height=32, alpha=True) - new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32) - new_mat_image_item.material = mat.mat - material_image_list.append(new_mat_image_item) + + 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: + new_mat_image_item = MaterialImageList() + try: + new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo] + except Exception: + name = mat_slot.material.name + "_albedo_replacement" + if name in bpy.data.images: + bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) + new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True) + new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32) + try: + new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal] + except Exception: + name = mat_slot.material.name + "_normal_replacement" + if name in bpy.data.images: + bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) + new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True) + new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32) + try: + new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission] + except Exception: + name = mat_slot.material.name + "_emission_replacement" + if name in bpy.data.images: + bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) + new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True) + new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32) + try: + new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion] + except Exception: + name = mat_slot.material.name + "_ambient_occlusion_replacement" + if name in bpy.data.images: + bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) + new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True) + new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32) + try: + new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height] + except Exception: + name = mat_slot.material.name + "_height_replacement" + if name in bpy.data.images: + bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) + new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True) + new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32) + try: + new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness] + except Exception: + name = mat_slot.material.name + "_roughness_replacement" + if name in bpy.data.images: + bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) + new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True) + new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32) + + new_mat_image_item.material = mat_slot.material + new_mat_image_item.parent_mesh = obj + material_image_list.append(new_mat_image_item) + 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: @@ -124,13 +141,16 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): def execute(self, context: Context) -> set: try: - mat_images: list[MaterialImageList] = prep_images_in_scene(context) + # 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] + + if not mat_images: + self.report({'WARNING'}, t("TextureAtlas.no_materials_selected")) + return {'CANCELLED'} packer: BinPacker = BinPacker(mat_images) - mat_images = packer.fit() - size: list[int] = [max([matimg.fit.w + matimg.albedo.size[0] for matimg in mat_images]), max([matimg.fit.h + matimg.albedo.size[1] for matimg in mat_images])] print([matimg.fit.w + matimg.albedo.size[1] for matimg in mat_images]) @@ -144,18 +164,17 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): h: int = int(mat.albedo.size[1]) for obj in bpy.data.objects: - mesh: Mesh = obj.data - - - for layer in mesh.polygons: - if obj.material_slots[layer.material_index].material: - if obj.material_slots[layer.material_index].material == mat.material: - for loop_idx in layer.loop_indices: - layer_loops: MeshUVLoopLayer - for layer_loops in mesh.uv_layers: - uv_item: Float2AttributeValue = layer_loops.uv[loop_idx] - uv_item.vector.x = (uv_item.vector.x*(w/size[0]))+(x/size[0]) - uv_item.vector.y = (uv_item.vector.y*(h/size[1]))+(y/size[1]) + if obj.type == 'MESH': + mesh: Mesh = obj.data + for layer in mesh.polygons: + if obj.material_slots[layer.material_index].material: + if obj.material_slots[layer.material_index].material == mat.material: + for loop_idx in layer.loop_indices: + layer_loops: MeshUVLoopLayer + for layer_loops in mesh.uv_layers: + uv_item: Float2AttributeValue = layer_loops.uv[loop_idx] + uv_item.vector.x = (uv_item.vector.x*(w/size[0]))+(x/size[0]) + uv_item.vector.y = (uv_item.vector.y*(h/size[1]))+(y/size[1]) for type in ["albedo","normal", "emission","ambient_occlusion","height", "roughness"]: new_image_name: str= "Atlas_"+type+"_"+context.scene.name+"_"+Path(bpy.data.filepath).stem @@ -167,7 +186,6 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): canvas: Image = bpy.data.images.new(name=new_image_name, width=int(size[0]),height=int(size[1]), alpha=True) c_w = canvas.size[0] - #c_h = canvas.size[1] canvas_pixels: list[float] = list(canvas.pixels[:]) for mat in mat_images: x: int = int(mat.fit.x) @@ -199,14 +217,12 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath),new_image_name+".png")) exec("atlased_mat."+type+" = canvas") - + #I am sorry for the amount of nodes I'm instanciating here and their values. + #This is so that the nodes look pretty in the UI, which I think looks kinda nice. - @989onan atlased_mat.material = bpy.data.materials.new(name="Atlas_Final_"+bpy.context.scene.name+"_"+Path(bpy.data.filepath).stem) atlased_mat.material.use_nodes = True atlased_mat.material.node_tree.nodes.clear() - - #I am sorry for the amount of nodes I'm instanciating here and their values. - #This is so that the nodes look pretty in the UI, which I think looks kinda nice. - @989onan principled_node: ShaderNodeBsdfPrincipled = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") principled_node.location.x = 7.29706335067749 principled_node.location.y = 298.918212890625 @@ -258,12 +274,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 for obj in context.scene.objects: - mesh: Mesh = obj.data - mesh.materials.clear() - - mesh.materials.append(atlased_mat.material) + 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) self.report({'INFO'}, t("TextureAtlas.atlas_completed")) return {"FINISHED"} @@ -271,5 +288,4 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): self.report({'ERROR'}, t("TextureAtlas.atlas_error")) raise e return {"FINISHED"} - \ No newline at end of file diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py index 285d3b5..c4f5def 100644 --- a/ui/atlas_materials.py +++ b/ui/atlas_materials.py @@ -40,23 +40,26 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' - def draw_item(self , context: Context, layout: UILayout, data: bpy.types.Object, item:SceneMatClass, icon, active_data, active_propname, index): + 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() - row.label(text=item.mat.name, icon = "MATERIAL") - col = box.row() - col.prop(item.mat, "texture_atlas_albedo") - col = box.row() - col.prop(item.mat, "texture_atlas_normal") - col = box.row() - col.prop(item.mat, "texture_atlas_emission") - col = box.row() - col.prop(item.mat, "texture_atlas_ambient_occlusion") - col = box.row() - col.prop(item.mat, "texture_atlas_height") - col = box.row() - col.prop(item.mat, "texture_atlas_roughness") + + # Draw material entry + 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="") + + if item.mat.material_expanded and item.mat.include_in_atlas: + 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") @register_wrap class AvatarToolKit_PT_TextureAtlasPanel(Panel): @@ -74,7 +77,6 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): if armature: layout.label(text=t("TextureAtlas.label"), icon='TEXTURE') - layout.separator(factor=0.5) box = layout.box() @@ -86,16 +88,22 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): if context.scene.texture_atlas_Has_Mat_List_Shown: row = box.row() - row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname, 'material_list', - context.scene, 'materials', context.scene, 'texture_atlas_material_index', - rows=12, type='DEFAULT') + row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname, + 'material_list', + context.scene, + 'materials', + context.scene, + 'texture_atlas_material_index', + rows=12, + type='DEFAULT') layout.separator(factor=1.0) row = layout.row() row.scale_y = 1.5 - row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname, text=t("TextureAtlas.atlas_materials"), icon='NODE_TEXTURE') - + row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname, + text=t("TextureAtlas.atlas_materials"), + icon='NODE_TEXTURE') else: layout.label(text=t("Tools.select_armature"), icon='ERROR')