From 54f1800eadaee45025c9a94e83466d20283e4e94 Mon Sep 17 00:00:00 2001 From: 989onan Date: Wed, 24 Jul 2024 20:35:49 -0400 Subject: [PATCH] Hot Fix fix texture atlas having registry problems, and the normal maps not looking good after being atlased. --- __init__.py | 12 +- core/common.py | 52 ++++++- core/properties.py | 44 +++++- functions/atlas_materials.py | 278 +++++++++++++++++++++++++++++++++++ ui/atlas_materials.py | 92 ++++++++++++ 5 files changed, 470 insertions(+), 8 deletions(-) create mode 100644 functions/atlas_materials.py create mode 100644 ui/atlas_materials.py diff --git a/__init__.py b/__init__.py index f49f0c7..2e9aa4d 100644 --- a/__init__.py +++ b/__init__.py @@ -25,12 +25,20 @@ def register(): # Order the classes before registration core.register.order_classes() - # Register the properties - core.register.register_properties() # Register the UI classes for cls in __bl_ordered_classes: print("registering" + str(cls)) bpy.utils.register_class(cls) + + + + # Register the properties + for cls in core.register.__bl_ordered_classes: + print("registering " + str(cls)) + bpy.utils.register_class(cls) + + #finally register properties that may use some classes. + core.register.register_properties() def unregister(): print("Unregistering Avatar Toolkit") diff --git a/core/common.py b/core/common.py index 1664b47..6b48c8a 100644 --- a/core/common.py +++ b/core/common.py @@ -7,8 +7,58 @@ import webbrowser import typing from typing import List, Optional, Tuple -from bpy.types import Object, ShapeKey, Mesh, Context +from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup from functools import lru_cache +from bpy.props import PointerProperty +from bpy.utils import register_class + + + + +class SceneMatClass(PropertyGroup): + mat: PointerProperty(type=Material) + +register_class(SceneMatClass) + +class material_list_bool: + #For the love that is holy do not ever touch these. If this was java I would make these private + #They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown + #This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up. + + #The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading. + old_list: dict[str,list[Material]] = {} + bool_material_list_expand: dict[str,bool] = {} + + def set_bool(self, value: bool) -> None: + material_list_bool.bool_material_list_expand[bpy.context.scene.name] = value + if value == False: + material_list_bool.old_list[bpy.context.scene.name] = [] + + def get_bool(self) -> bool: + newlist: list[Material] = [] + for obj in bpy.context.scene.objects: + if len(obj.material_slots)>0: + for mat_slot in obj.material_slots: + if mat_slot.material: + if mat_slot.material not in newlist: + newlist.append(mat_slot.material) + + still_the_same: bool = True + if bpy.context.scene.name in material_list_bool.old_list: + for item in newlist: + if item not in material_list_bool.old_list[bpy.context.scene.name]: + still_the_same = False + break + for item in material_list_bool.old_list[bpy.context.scene.name]: + if item not in newlist: + still_the_same = False + break + else: + still_the_same = False + material_list_bool.bool_material_list_expand[bpy.context.scene.name] = still_the_same + + return material_list_bool.bool_material_list_expand[bpy.context.scene.name] + ### Clean up material names in the given mesh by removing the '.001' suffix. def clean_material_names(mesh: Mesh) -> None: diff --git a/core/properties.py b/core/properties.py index 801356e..261f607 100644 --- a/core/properties.py +++ b/core/properties.py @@ -1,11 +1,19 @@ import bpy -from ..functions.translations import t, get_languages_list, update_language +from ..functions.translations import t, get_languages_list, update_ui +from ..core.register import register_property +from bpy.types import Scene, Object, Material, TextureNode, Context, SceneObjects, PropertyGroup +from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty +from bpy.utils import register_class +from ..core.register import register_wrap from ..core.addon_preferences import get_preference -from .common import get_armatures - -def register(): - default_language = get_preference("language", 0) +from ..core.common import SceneMatClass, material_list_bool, get_armatures + + + + +def register() -> None: + default_language = get_preference("language", 0) bpy.types.Scene.avatar_toolkit_language = bpy.props.EnumProperty( name=t("Settings.language.label", "Language"), description=t("Settings.language.desc", "Select the language for the addon"), @@ -21,6 +29,31 @@ def register(): name="Selected Armature", description="The currently selected armature for Avatar Toolkit operations" ) + + #happy with how compressed this get_texture_node_list method is - @989onan + def get_texture_node_list(self: Material, context: Context) -> list[set[3]]: + if self.use_nodes: + Object.Enum = [((i.image.name if i.image else i.name+"_image"),(i.image.name if i.image else "node with no image..."),(i.image.name if i.image else i.name),index+1) for index,i in enumerate(self.node_tree.nodes) if i.bl_idname == "ShaderNodeTexImage"] + if not len(Object.Enum): + Object.Enum = [("ERROR", "THIS MATERIAL HAS NO IMAGES!", "ERROR", 0)] + else: + Object.Enum = [("ERROR", "THIS MATERIAL DOES NOT USE NODES!", "ERROR", 0)] + Object.Enum.append(("None", "None", "None", 0)) + return Object.Enum + + register_property((Material, "texture_atlas_albedo", EnumProperty(name="Albedo", description="The texture that will be used for the albedo map atlas", default=0, items=get_texture_node_list))) + register_property((Material, "texture_atlas_normal", EnumProperty(name="Normal", description="The texture that will be used for the normal map atlas", default=0, items=get_texture_node_list))) + register_property((Material, "texture_atlas_emission", EnumProperty(name="Emission", description="The texture that will be used for the emission map atlas", default=0, items=get_texture_node_list))) + register_property((Material, "texture_atlas_ambient_occlusion", EnumProperty(name="Ambient Occlusion", description="The texture that will be used for the ambient occlusion map atlas", default=0, items=get_texture_node_list))) + register_property((Material, "texture_atlas_height", EnumProperty(name="Height", description="The texture that will be used for the height map atlas", default=0, items=get_texture_node_list))) + register_property((Material, "texture_atlas_roughness", EnumProperty(name="Roughness", description="The texture that will be used for the roughness map atlas", default=0, items=get_texture_node_list))) + + register_property((Scene, "texture_atlas_material_index", IntProperty(default=-1, get=(lambda self : -1), set=(lambda self,context : None)))) + + register_property((Scene, "materials", CollectionProperty(type=SceneMatClass))) + + register_property((Scene, "texture_atlas_Has_Mat_List_Shown", BoolProperty(default=False, get=material_list_bool.get_bool, set=material_list_bool.set_bool))) + def unregister(): if hasattr(bpy.types.Scene, "avatar_toolkit_language"): @@ -31,3 +64,4 @@ def unregister(): if hasattr(bpy.types.Scene, "selected_armature"): del bpy.types.Scene.selected_armature + diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py new file mode 100644 index 0000000..c0e537a --- /dev/null +++ b/functions/atlas_materials.py @@ -0,0 +1,278 @@ +from pathlib import Path + +import numpy +import bpy +import re +import os +from typing import List, Tuple, Optional +from mathutils import Vector +from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeNormalMap +from ..core.register import register_wrap +from ..core.common import SceneMatClass, material_list_bool +from ..core.packer.rectangle_packer import MaterialImageList, BinPacker + + + + + +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) + + for image in images: + image.scale(width=int(x), height=int(y)) + + return x,y + +def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> list[Image]: + list_of_images: list[Image] = [] + + list_of_images.append(classitem.albedo) + list_of_images.append(classitem.normal) + list_of_images.append(classitem.emission) + list_of_images.append(classitem.ambient_occlusion) + list_of_images.append(classitem.height) + list_of_images.append(classitem.roughness) + + return list_of_images + + +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) + 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: + ImageList: list[Image] = MaterialImageList_to_Image_list(MaterialImageClass) + + MaterialImageClass.w, MaterialImageClass.h = scale_images_to_largest(ImageList) + + + + return preped_images + + + +@register_wrap +class Atlas_Materials(Operator): + + bl_idname = "avatar_toolkit.atlas_materials" + bl_label = "Atlas Materials" + bl_description = "Atlas materials to optimize the model" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + return context.scene.texture_atlas_Has_Mat_List_Shown + + def execute(self, context: Context) -> set: + try: + mat_images: list[MaterialImageList] = prep_images_in_scene(context) + + 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]) + + atlased_mat: MaterialImageList = MaterialImageList() + + for mat in mat_images: + x: int = int(mat.fit.x) + y: int = int(mat.fit.y) + w: int = int(mat.albedo.size[0]) + 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]) + + for type in ["albedo","normal", "emission","ambient_occlusion","height", "roughness"]: + new_image_name: str= "Atlas_"+type+"_"+context.scene.name+"_"+Path(bpy.data.filepath).stem + + print("Processing "+type+" atlas image") + + if new_image_name in bpy.data.images: + bpy.data.images.remove(bpy.data.images[new_image_name]) + + 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) + y: int = int(mat.fit.y) + w: int = int(mat.albedo.size[0]) + h: int = int(mat.albedo.size[1]) + + image_var: Image = eval("mat."+type) + + image_pixels: list[float] = list(image_var.pixels[:]) + + print("writing image \""+image_var.name+"\" to canvas.") + print("x: \""+str(x)+"\" "+"y: \""+str(y)+"\" "+"w: \""+str(w)+"\" "+"h: \""+str(h)+"\" ") + for k in range(0,h): + for i in range(0, w): + for channel in range(0,4): + canvas_pixels[ + int((((k+y)*c_w) + + + (i+x))*4) + +int(channel) + ] = image_pixels[ + int(( + (k*w) + +i)*4) + +int(channel)] + + canvas.pixels[:] = canvas_pixels[:] + canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath),new_image_name+".png")) + exec("atlased_mat."+type+" = canvas") + + + 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 + + output_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeOutputMaterial") + output_node.location.x = 297.29705810546875 + output_node.location.y = 298.918212890625 + + albedo_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage") + albedo_node.location.x = -588.6177978515625 + albedo_node.location.y = 414.1948547363281 + albedo_node.image = atlased_mat.albedo + + emission_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage") + emission_node.location.x = -588.6177978515625 + emission_node.location.y = -173.9259033203125 + emission_node.image = atlased_mat.emission + + normal_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage") + normal_node.location.x = -941.4189453125 + normal_node.location.y = -20.8391780853271 + normal_node.image = atlased_mat.normal + + normal_map_node: ShaderNodeNormalMap = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeNormalMap") + normal_map_node.location.x = -545.550537109375 + normal_map_node.location.y = -0.7543716430664062 + + roughness_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage") + roughness_node.location.x = -592.1703491210938 + roughness_node.location.y = 206.74075317382812 + roughness_node.image = atlased_mat.roughness + + ambient_occlusion_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage") + ambient_occlusion_node.location.x = -906.4371337890625 + ambient_occlusion_node.location.y = -389.9602355957031 + ambient_occlusion_node.image = atlased_mat.ambient_occlusion + + height_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage") + height_node.location.x = -1222.383056640625 + height_node.location.y = -375.48406982421875 + height_node.image = atlased_mat.height + + atlased_mat.material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"]) + atlased_mat.material.node_tree.links.new(principled_node.inputs["Metallic"], roughness_node.outputs["Alpha"]) + atlased_mat.material.node_tree.links.new(principled_node.inputs["Roughness"], roughness_node.outputs["Color"]) + atlased_mat.material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"]) + atlased_mat.material.node_tree.links.new(principled_node.inputs["Normal"], normal_map_node.outputs["Normal"]) + atlased_mat.material.node_tree.links.new(principled_node.inputs["Emission Color"], emission_node.outputs["Color"]) + 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"]) + + + for obj in context.scene.objects: + mesh: Mesh = obj.data + mesh.materials.clear() + + mesh.materials.append(atlased_mat.material) + + return {"FINISHED"} + except Exception as e: + raise e + return {"FINISHED"} + + \ No newline at end of file diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py new file mode 100644 index 0000000..4487718 --- /dev/null +++ b/ui/atlas_materials.py @@ -0,0 +1,92 @@ +from bpy.types import UIList, Panel, UILayout, Object, Context,Material, Operator +import bpy +from ..core.register import register_wrap +from .panel import AvatarToolkitPanel +from ..core.common import SceneMatClass, material_list_bool +from ..functions.atlas_materials import Atlas_Materials + + +@register_wrap +class ExpandSection_Materials(Operator): + bl_idname = 'avatar_toolkit.expand_section_materials' + bl_label = "" + bl_description = "" + + @classmethod + def poll(cls, context: Context) -> bool: + return True + + + def execute(self, context: Context) -> set: + + if not context.scene.texture_atlas_Has_Mat_List_Shown: + context.scene.materials.clear() + newlist: list[Material] = [] + for obj in bpy.context.scene.objects: + if len(obj.material_slots)>0: + for mat_slot in obj.material_slots: + if mat_slot.material: + if mat_slot.material not in newlist: + newlist.append(mat_slot.material) + newitem: SceneMatClass = context.scene.materials.add() + newitem.mat = mat_slot.material + material_list_bool.old_list[context.scene.name] = newlist + else: + context.scene.texture_atlas_Has_Mat_List_Shown = False + return {'FINISHED'} + +@register_wrap +class MaterialTextureAtlasProperties(UIList): + bl_label = "Texture Atlas Material List Material" + bl_idname = "Material_UL_avatar_toolkit_texture_atlas_mat_list_mat" + 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): + + 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") + + + + +@register_wrap +class TextureAtlasPanel(Panel): + bl_label = "Texture Atlasing" + bl_idname = "OBJECT_PT_avatar_toolkit_texture_atlas" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "Avatar Toolkit" + bl_parent_id = "OBJECT_PT_avatar_toolkit" + + def draw(self, context: Context): + layout = self.layout + row = layout.row() + boxoutter = row.box() + direction_icon = 'RIGHTARROW' if not context.scene.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT' + row = boxoutter.row() + row.operator(ExpandSection_Materials.bl_idname, text=("Reload Texture Atlas Material List" if not context.scene.texture_atlas_Has_Mat_List_Shown else "Loaded Texture Atlas Material List"), icon=direction_icon) + if context.scene.texture_atlas_Has_Mat_List_Shown: + + #get_texture_node_list(bpy.context) + + row = boxoutter.row() + row.template_list(MaterialTextureAtlasProperties.bl_idname, 'material_list', context.scene, 'materials', + context.scene, 'texture_atlas_material_index', rows=12, type='DEFAULT') + row = layout.row() + row.operator(Atlas_Materials.bl_idname, text="Atlas Materials!") \ No newline at end of file