From bfdbac8412313a834e124ff40695ab1d7c8dffe1 Mon Sep 17 00:00:00 2001 From: 989onan Date: Sun, 7 Jul 2024 19:11:50 -0400 Subject: [PATCH 1/8] start work on texture atlas structures start of something big --- .vscode/settings.json | 11 +++++++++++ core/properties.py | 25 +++++++++++++++++++++++++ functions/texture_atlas.py | 23 +++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 core/properties.py create mode 100644 functions/texture_atlas.py diff --git a/.vscode/settings.json b/.vscode/settings.json index e69de29..e685f8d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.analysis.extraPaths": [ + "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.3\\scripts\\addons", + "C:\\Users\\Onan\\AppData\\Roaming\\Blender Foundation\\Blender\\4.3\\extensions\\user_default\\",//C:/Users/Onan/AppData/Roaming/Blender Foundation/Blender/4.0/scripts/addons + "D:\\blender stuff\\blendercodestuff\\4.3" + ], + "python.analysis.diagnosticSeverityOverrides": { + "reportInvalidTypeForm": "none" + }, + "python.REPL.enableREPLSmartSend": false, +} \ No newline at end of file diff --git a/core/properties.py b/core/properties.py new file mode 100644 index 0000000..530a53b --- /dev/null +++ b/core/properties.py @@ -0,0 +1,25 @@ +from bpy.types import Scene, PropertyGroup, Object, Material, TextureNode +from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty +from bpy.utils import register_class + + + +def register_properties(): + class Material_Texture_Atlas_PropertyGroup(PropertyGroup): + normal: PointerProperty(type=TextureNode) + albedo: PointerProperty(type=TextureNode) + emission: PointerProperty(type=TextureNode) + ambient_occlusion: PointerProperty(type=TextureNode) + height: PointerProperty(type=TextureNode) + + + register_class(Material_Texture_Atlas_PropertyGroup) + Material.texture_atlas = PointerProperty(type=Material_Texture_Atlas_PropertyGroup) + + class Texture_Atlas_PropertyGroup(PropertyGroup): + materials: CollectionProperty(type=Material) + + Scene.texture_atlas_properties = PointerProperty(type=Texture_Atlas_PropertyGroup) + + + \ No newline at end of file diff --git a/functions/texture_atlas.py b/functions/texture_atlas.py new file mode 100644 index 0000000..e303838 --- /dev/null +++ b/functions/texture_atlas.py @@ -0,0 +1,23 @@ +import bpy +from typing import List, Optional +from bpy.types import Operator, Context, Object, TextureNode +from ..core.register import register_wrap +from ..core.common import get_armature, simplify_bonename + +@register_wrap +class Atlas_Textures(Operator): + bl_idname = "avatar_toolkit.atlas_textures" + bl_label = "Atlas Textures" + bl_description = """Combines materials and their textures to optimize the model. +Although this combines materials, it may not reduce your VRAM usage. Other tools +like Tuxedo can vastly reduce your VRAM usage as well as many other optimizations, +rather than just duct taping the textures together like material combiner and this tool. +""" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context: Context) -> set: + + + return {'FINISHED'} + + From 23b4656859ef46dc9a3d23a3a67e572c08e382fb Mon Sep 17 00:00:00 2001 From: 989onan Date: Sun, 7 Jul 2024 22:36:10 -0400 Subject: [PATCH 2/8] Added part of Texture Atlas UI -added dynamic list for texture atlas ui - ui isn't the most intuitive but it will do for now --- __init__.py | 2 ++ core/properties.py | 38 +++++++++++++++-------- ui/atlas_materials.py | 71 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 ui/atlas_materials.py diff --git a/__init__.py b/__init__.py index 4f93020..da7389e 100644 --- a/__init__.py +++ b/__init__.py @@ -7,6 +7,7 @@ if "bpy" not in locals(): from . import functions from .core import register from .core.register import __bl_ordered_classes + from .core.properties import register_properties else: import importlib importlib.reload(ui) @@ -18,6 +19,7 @@ def register(): print("Registering Avatar Toolkit") # Order the classes before registration core.register.order_classes() + register_properties() # Register the UI classes # Iterate over the classes to register and register them diff --git a/core/properties.py b/core/properties.py index 530a53b..2773ca4 100644 --- a/core/properties.py +++ b/core/properties.py @@ -1,25 +1,37 @@ -from bpy.types import Scene, PropertyGroup, Object, Material, TextureNode +import bpy +from bpy.types import Scene, PropertyGroup, Object, Material, TextureNode, Context from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty from bpy.utils import register_class def register_properties(): - class Material_Texture_Atlas_PropertyGroup(PropertyGroup): - normal: PointerProperty(type=TextureNode) - albedo: PointerProperty(type=TextureNode) - emission: PointerProperty(type=TextureNode) - ambient_occlusion: PointerProperty(type=TextureNode) - height: PointerProperty(type=TextureNode) + + #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.name+"_image",(i.image.name if i.image else "node with no image..."),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 + + 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) + 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) + 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) + 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) + 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) + + Scene.texture_atlas_material_index = IntProperty()#default=-1, get=(lambda self : -1), set=(lambda self,context : None) - register_class(Material_Texture_Atlas_PropertyGroup) - Material.texture_atlas = PointerProperty(type=Material_Texture_Atlas_PropertyGroup) + #class Texture_Atlas_PropertyGroup(PropertyGroup): + # materials: CollectionProperty(type=Material) + #register_class(Texture_Atlas_PropertyGroup) - class Texture_Atlas_PropertyGroup(PropertyGroup): - materials: CollectionProperty(type=Material) - - Scene.texture_atlas_properties = PointerProperty(type=Texture_Atlas_PropertyGroup) + #Scene.texture_atlas_properties = PointerProperty(type=Texture_Atlas_PropertyGroup) \ No newline at end of file diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py new file mode 100644 index 0000000..78735a5 --- /dev/null +++ b/ui/atlas_materials.py @@ -0,0 +1,71 @@ +from bpy.types import UIList, Panel, UILayout, Object,Context,MaterialSlot +import bpy +from ..core.register import register_wrap +from .panel import AvatarToolkitPanel + +@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:MaterialSlot, icon, active_data, active_propname, index): + + if item.material: + box = layout.box() + col = box.row() + col.label(text="Material: \""+item.material.name+"\"") + if data.active_material_index == index: + col = box.row() + col.prop(item.material, "texture_atlas_albedo") + col = box.row() + col.prop(item.material, "texture_atlas_normal") + col = box.row() + col.prop(item.material, "texture_atlas_emission") + col = box.row() + col.prop(item.material, "texture_atlas_ambient_occlusion") + col = box.row() + col.prop(item.material, "texture_atlas_height") + else: + box = layout.box() + col = box.row() + col.label(text="Empty Material Slot.") + +@register_wrap +class MaterialListPanel(UIList): + bl_label = "Texture Atlas Material List" + bl_idname = "Material_UL_avatar_toolkit_texture_atlas_mat_list" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + + def draw_item(self, context: Context, layout: UILayout, data, item:Object, icon, active_data, active_propname, index): + custom_icon = "OBJECT_DATAMODE" + box = layout.box() + row = box.row() + row.label(text=item.name, icon = custom_icon) + if context.scene.texture_atlas_material_index == index: + row = box.row() + box = row.box() + + box.template_list("Material_UL_avatar_toolkit_texture_atlas_mat_list_mat", "The_Texture_Atlas_List_mat_"+item.name, item, "material_slots", item, "active_material_index") + + +@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() + row = boxoutter.row() + row.label(text=MaterialListPanel.bl_label) + row = boxoutter.row() + row.template_list("Material_UL_avatar_toolkit_texture_atlas_mat_list", "The_Texture_Atlas_List", context.scene, "objects", context.scene, "texture_atlas_material_index") From e875f9192a638634aa8107f4b0a4aa2b221537ea Mon Sep 17 00:00:00 2001 From: 989onan Date: Sun, 14 Jul 2024 15:36:01 -0400 Subject: [PATCH 3/8] Fixed the UI to be much better - ui for materials is now a list with no duplicates - auto detects that materials have changed and prompts the user to reload - due to context limitations in code, user is needed to reload the materials, but the ui is made so the user is forced to reload the materials to see them - later on, we should prevent user from atlasing if the material list is not up to date. --- core/properties.py | 51 ++++++++++++++++++++--- ui/atlas_materials.py | 97 +++++++++++++++++++++++++------------------ 2 files changed, 103 insertions(+), 45 deletions(-) diff --git a/core/properties.py b/core/properties.py index 2773ca4..abe3e17 100644 --- a/core/properties.py +++ b/core/properties.py @@ -1,12 +1,50 @@ import bpy -from bpy.types import Scene, PropertyGroup, Object, Material, TextureNode, Context +from bpy.types import Scene, PropertyGroup, Object, Material, TextureNode, Context, SceneObjects from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty from bpy.utils import register_class +class material_list_bool: + old_list: list[Material] = [] + bool_material_list_expand: bool = False + + def set_bool(self, value: bool) -> None: + material_list_bool.bool_material_list_expand = value + if value == False: + material_list_bool.old_list = [] + + 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 + for item in newlist: + if item not in material_list_bool.old_list: + still_the_same = False + break + for item in material_list_bool.old_list: + if item not in newlist: + still_the_same = False + break + + material_list_bool.bool_material_list_expand = still_the_same + + return material_list_bool.bool_material_list_expand + +class SceneMatClass(PropertyGroup): + mat: PointerProperty(type=Material) + + def register_properties(): + register_class(SceneMatClass) + #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: @@ -25,11 +63,14 @@ def register_properties(): 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) 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) - Scene.texture_atlas_material_index = IntProperty()#default=-1, get=(lambda self : -1), set=(lambda self,context : None) + Scene.texture_atlas_material_index = IntProperty(default=-1, get=(lambda self : -1), set=(lambda self,context : None)) - #class Texture_Atlas_PropertyGroup(PropertyGroup): - # materials: CollectionProperty(type=Material) - #register_class(Texture_Atlas_PropertyGroup) + + + + Scene.materials = CollectionProperty(type=SceneMatClass) + + Scene.texture_atlas_Has_Mat_List_Shown = BoolProperty(default=False, get=material_list_bool.get_bool, set=material_list_bool.set_bool) #Scene.texture_atlas_properties = PointerProperty(type=Texture_Atlas_PropertyGroup) diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py index 78735a5..f0c8018 100644 --- a/ui/atlas_materials.py +++ b/ui/atlas_materials.py @@ -1,7 +1,38 @@ -from bpy.types import UIList, Panel, UILayout, Object,Context,MaterialSlot +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.properties import SceneMatClass, material_list_bool + + +@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 = newlist + else: + context.scene.texture_atlas_Has_Mat_List_Shown = False + return {'FINISHED'} @register_wrap class MaterialTextureAtlasProperties(UIList): @@ -11,46 +42,25 @@ class MaterialTextureAtlasProperties(UIList): bl_region_type = 'UI' - def draw_item(self, context: Context, layout: UILayout, data: bpy.types.Object, item:MaterialSlot, icon, active_data, active_propname, index): + def draw_item(self , context: Context, layout: UILayout, data: bpy.types.Object, item:SceneMatClass, icon, active_data, active_propname, index): - if item.material: + if context.scene.texture_atlas_Has_Mat_List_Shown: box = layout.box() - col = box.row() - col.label(text="Material: \""+item.material.name+"\"") - if data.active_material_index == index: - col = box.row() - col.prop(item.material, "texture_atlas_albedo") - col = box.row() - col.prop(item.material, "texture_atlas_normal") - col = box.row() - col.prop(item.material, "texture_atlas_emission") - col = box.row() - col.prop(item.material, "texture_atlas_ambient_occlusion") - col = box.row() - col.prop(item.material, "texture_atlas_height") - else: - box = layout.box() - col = box.row() - col.label(text="Empty Material Slot.") - -@register_wrap -class MaterialListPanel(UIList): - bl_label = "Texture Atlas Material List" - bl_idname = "Material_UL_avatar_toolkit_texture_atlas_mat_list" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - - def draw_item(self, context: Context, layout: UILayout, data, item:Object, icon, active_data, active_propname, index): - custom_icon = "OBJECT_DATAMODE" - box = layout.box() - row = box.row() - row.label(text=item.name, icon = custom_icon) - if context.scene.texture_atlas_material_index == index: row = box.row() - box = row.box() + 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") - box.template_list("Material_UL_avatar_toolkit_texture_atlas_mat_list_mat", "The_Texture_Atlas_List_mat_"+item.name, item, "material_slots", item, "active_material_index") - + + @register_wrap class TextureAtlasPanel(Panel): @@ -65,7 +75,14 @@ class TextureAtlasPanel(Panel): 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.label(text=MaterialListPanel.bl_label) - row = boxoutter.row() - row.template_list("Material_UL_avatar_toolkit_texture_atlas_mat_list", "The_Texture_Atlas_List", context.scene, "objects", context.scene, "texture_atlas_material_index") + 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') + \ No newline at end of file From 942e7e286800fb6a50d40c343141d9a104fa38b7 Mon Sep 17 00:00:00 2001 From: 989onan Date: Sun, 14 Jul 2024 23:55:20 -0400 Subject: [PATCH 4/8] Got images working - does not do UVs yet - is able to pack images using a split algorithm. I think I broke the size finding though for the output canvas. - does not combine materials after packing --- core/packer/rectangle_packer.py | 151 +++++++++++++++++++++++++++ core/properties.py | 42 +++++--- core/register.py | 2 +- functions/atlas_materials.py | 178 ++++++++++++++++++++++++++++++++ functions/texture_atlas.py | 2 +- ui/atlas_materials.py | 6 +- 6 files changed, 360 insertions(+), 21 deletions(-) create mode 100644 core/packer/rectangle_packer.py create mode 100644 functions/atlas_materials.py diff --git a/core/packer/rectangle_packer.py b/core/packer/rectangle_packer.py new file mode 100644 index 0000000..b8b8b0a --- /dev/null +++ b/core/packer/rectangle_packer.py @@ -0,0 +1,151 @@ + +# thank you https://stackoverflow.com/a/71432759 +from __future__ import annotations + + +from typing import Optional +from bpy.types import Image + + +# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +class Rectangle_Obj: + x: int = 0 + y: int = 0 + w: int = 0 + h: int = 0 + down: Rectangle_Obj = None + used: bool = False + right: Rectangle_Obj = None + + def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None): + self.x = x + self.y = y + self.w = w + self.h = h + self.down = down + self.used = used + self.right = right + + def split(self, w, h) -> Rectangle_Obj: + self.used = True + self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h) + self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h) + return self + + def find(self, w, h) -> Optional[Rectangle_Obj]: + if self.used: + return self.right.find(w, h) or self.down.find(w, h) + elif (w <= self.w) and (h <= self.h): + return self + return None + +class MaterialImageList: + albedo: Image + normal: Image + emission: Image + ambient_occlusion: Image + height: Image + fit: Rectangle_Obj + + def __init__(self): + pass + + x: int = 0 + y: int = 0 + w: int = 0 + h: int = 0 + + + + + +class BinPacker(object): + root: Rectangle_Obj + bin: list[MaterialImageList] = [] + def __init__(self, structure: list[MaterialImageList]): + self.root = None + self.bin = structure + + def fit(self): + structure = self.bin + structure_len = len(self.bin) + w: int = 0 + h: int = 0 + if structure_len > 0: + w = structure[0].w + h = structure[0].h + self.root = Rectangle_Obj(x=0, y=0, w=w, h=h) + for img in structure: + w = img.w + h = img.h + node = self.root.find(w, h) + if node: + img.fit = node.split(w, h) + else: + img.fit = self.grow_node(w, h) + return structure + + def grow_node(self, w, h) -> Optional[Rectangle_Obj]: + can_grow_right = (h <= self.root.h) + can_grow_down = (w <= self.root.w) + + should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w)) + should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h)) + + if should_grow_right: + return self.grow_right(w, h) + elif should_grow_down: + return self.grow_down(w, h) + elif can_grow_right: + return self.grow_right(w, h) + elif can_grow_down: + return self.grow_down(w, h) + return None + + def grow_right(self, w, h) -> Optional[Rectangle_Obj]: + self.root = Rectangle_Obj( + used=True, + x=0, + y=0, + w=self.root.w + w, + h=self.root.h, + down=self.root, + right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h)) + node = self.root.find(w, h) + if node: + return node.split(w, h) + return None + + def grow_down(self, w, h) -> Optional[Rectangle_Obj]: + self.root = Rectangle_Obj( + used=True, + x=0, + y=0, + w=self.root.w, + h=self.root.h + h, + down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h), + right=self.root + ) + node = self.root.find(w, h) + if node: + return node.split(w, h) + return None \ No newline at end of file diff --git a/core/properties.py b/core/properties.py index abe3e17..5cb1141 100644 --- a/core/properties.py +++ b/core/properties.py @@ -6,13 +6,18 @@ from bpy.utils import register_class class material_list_bool: - old_list: list[Material] = [] - bool_material_list_expand: bool = False + #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 = value + material_list_bool.bool_material_list_expand[bpy.context.scene.name] = value if value == False: - material_list_bool.old_list = [] + material_list_bool.old_list[bpy.context.scene.name] = [] def get_bool(self) -> bool: newlist: list[Material] = [] @@ -24,18 +29,20 @@ class material_list_bool: newlist.append(mat_slot.material) still_the_same: bool = True - for item in newlist: - if item not in material_list_bool.old_list: - still_the_same = False - break - for item in material_list_bool.old_list: - if item not in newlist: - still_the_same = False - break - - material_list_bool.bool_material_list_expand = still_the_same + 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 + return material_list_bool.bool_material_list_expand[bpy.context.scene.name] class SceneMatClass(PropertyGroup): mat: PointerProperty(type=Material) @@ -48,7 +55,7 @@ def register_properties(): #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.name+"_image",(i.image.name if i.image else "node with no image..."),i.name,index+1) for index,i in enumerate(self.node_tree.nodes) if i.bl_idname == "ShaderNodeTexImage"] + 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: @@ -57,8 +64,9 @@ def register_properties(): return Object.Enum - 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) + 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) + 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) 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) 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) 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) diff --git a/core/register.py b/core/register.py index 0d91a8a..2c20729 100644 --- a/core/register.py +++ b/core/register.py @@ -32,7 +32,7 @@ def toposort(deps_dict): unsorted.append(value) deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted} - sort_order(sorted_list) #to sort by 'bl_order' so we can choose how things may appear in the ui + #sort_order(sorted_list) #to sort by 'bl_order' so we can choose how things may appear in the ui return sorted_list diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py new file mode 100644 index 0000000..39f3aa3 --- /dev/null +++ b/functions/atlas_materials.py @@ -0,0 +1,178 @@ +from pathlib import Path +import bpy +import re +import os +from typing import List, Tuple, Optional +from bpy.types import Material, Operator, Context, Object, Image +from ..core.register import register_wrap +from ..core.properties import material_list_bool, SceneMatClass +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) + + 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) + 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) + 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) + + 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) + 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) + 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]) + + for type in ["albedo","normal", "emission","ambient_occlusion","height"]: + new_image_name: str= "Atlas_"+type+"_"+bpy.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")) + return {"FINISHED"} + except Exception as e: + raise e + return {"FINISHED"} + \ No newline at end of file diff --git a/functions/texture_atlas.py b/functions/texture_atlas.py index e303838..90a4aec 100644 --- a/functions/texture_atlas.py +++ b/functions/texture_atlas.py @@ -16,8 +16,8 @@ rather than just duct taping the textures together like material combiner and th bl_options = {'REGISTER', 'UNDO'} def execute(self, context: Context) -> set: - + return {'FINISHED'} diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py index f0c8018..4a9b245 100644 --- a/ui/atlas_materials.py +++ b/ui/atlas_materials.py @@ -3,6 +3,7 @@ import bpy from ..core.register import register_wrap from .panel import AvatarToolkitPanel from ..core.properties import SceneMatClass, material_list_bool +from ..functions.atlas_materials import Atlas_Materials @register_wrap @@ -29,7 +30,7 @@ class ExpandSection_Materials(Operator): newlist.append(mat_slot.material) newitem: SceneMatClass = context.scene.materials.add() newitem.mat = mat_slot.material - material_list_bool.old_list = newlist + material_list_bool.old_list[context.scene.name] = newlist else: context.scene.texture_atlas_Has_Mat_List_Shown = False return {'FINISHED'} @@ -85,4 +86,5 @@ class TextureAtlasPanel(Panel): row = boxoutter.row() row.template_list(MaterialTextureAtlasProperties.bl_idname, 'material_list', context.scene, 'materials', context.scene, 'texture_atlas_material_index', rows=12, type='DEFAULT') - \ No newline at end of file + row = layout.row() + row.operator(Atlas_Materials.bl_idname, text="Atlas Materials!") \ No newline at end of file From 5a3cc5a087291178a65ef58f10a0e100e46c8cad Mon Sep 17 00:00:00 2001 From: 989onan Date: Mon, 15 Jul 2024 01:51:59 -0400 Subject: [PATCH 5/8] Finally works This is a good first start to material combining It may need a few tweaks from here, but for now it should be good --- core/packer/rectangle_packer.py | 4 +- core/properties.py | 1 + functions/atlas_materials.py | 126 ++++++++++++++++++++++++++++---- ui/atlas_materials.py | 2 + 4 files changed, 117 insertions(+), 16 deletions(-) diff --git a/core/packer/rectangle_packer.py b/core/packer/rectangle_packer.py index b8b8b0a..fc147ce 100644 --- a/core/packer/rectangle_packer.py +++ b/core/packer/rectangle_packer.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Optional -from bpy.types import Image +from bpy.types import Image, Material # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors @@ -64,7 +64,9 @@ class MaterialImageList: emission: Image ambient_occlusion: Image height: Image + roughness: Image fit: Rectangle_Obj + material: Material def __init__(self): pass diff --git a/core/properties.py b/core/properties.py index 5cb1141..9a15d2f 100644 --- a/core/properties.py +++ b/core/properties.py @@ -70,6 +70,7 @@ def register_properties(): 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) 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) 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) + 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) Scene.texture_atlas_material_index = IntProperty(default=-1, get=(lambda self : -1), set=(lambda self,context : None)) diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py index 39f3aa3..60a871d 100644 --- a/functions/atlas_materials.py +++ b/functions/atlas_materials.py @@ -1,9 +1,12 @@ from pathlib import Path + +import numpy import bpy import re import os from typing import List, Tuple, Optional -from bpy.types import Material, Operator, Context, Object, Image +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.properties import material_list_bool, SceneMatClass from ..core.packer.rectangle_packer import MaterialImageList, BinPacker @@ -34,6 +37,7 @@ def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> list[Image] 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 @@ -64,6 +68,7 @@ def get_material_images_from_scene(context: Context) -> list[MaterialImageList]: 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] @@ -72,6 +77,7 @@ def get_material_images_from_scene(context: Context) -> list[MaterialImageList]: 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: @@ -79,6 +85,17 @@ def get_material_images_from_scene(context: Context) -> list[MaterialImageList]: 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 @@ -95,17 +112,6 @@ def prep_images_in_scene(context: Context) -> list[MaterialImageList]: return preped_images - - - - - - - - - - - @register_wrap class Atlas_Materials(Operator): @@ -131,8 +137,30 @@ class Atlas_Materials(Operator): 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]) - for type in ["albedo","normal", "emission","ambient_occlusion","height"]: - new_image_name: str= "Atlas_"+type+"_"+bpy.context.scene.name+"_"+Path(bpy.data.filepath).stem + 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") @@ -141,7 +169,7 @@ class Atlas_Materials(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] + #c_h = canvas.size[1] canvas_pixels: list[float] = list(canvas.pixels[:]) for mat in mat_images: x: int = int(mat.fit.x) @@ -171,6 +199,74 @@ class Atlas_Materials(Operator): 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 diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py index 4a9b245..0596846 100644 --- a/ui/atlas_materials.py +++ b/ui/atlas_materials.py @@ -59,6 +59,8 @@ class MaterialTextureAtlasProperties(UIList): 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") From ce7c6aa664a1756ae796d7f9ffcfadea0019a0a7 Mon Sep 17 00:00:00 2001 From: 989onan Date: Wed, 24 Jul 2024 17:41:17 -0400 Subject: [PATCH 6/8] Add digitgrade legs tool --- core/common.py | 11 ++++ functions/digitigrade_legs.py | 117 ++++++++++++++++++++++++++++++++++ functions/translations.py | 2 +- ui/tools.py | 4 +- 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 functions/digitigrade_legs.py diff --git a/core/common.py b/core/common.py index 6e08f93..2c7ce75 100644 --- a/core/common.py +++ b/core/common.py @@ -57,3 +57,14 @@ def get_armature(context, armature_name=None) -> Optional[Object]: if obj.type == "ARMATURE": return obj return next((obj for obj in context.view_layer.objects if obj.type == 'ARMATURE'), None) + + +def duplicatebone(b: bpy.types.EditBone) -> bpy.types.EditBone: + arm = bpy.context.object.data + cb = arm.edit_bones.new(b.name) + + cb.head = b.head + cb.tail = b.tail + cb.matrix = b.matrix + cb.parent = b.parent + return cb \ No newline at end of file diff --git a/functions/digitigrade_legs.py b/functions/digitigrade_legs.py new file mode 100644 index 0000000..27ea3e5 --- /dev/null +++ b/functions/digitigrade_legs.py @@ -0,0 +1,117 @@ +import bpy +from ..core import common +from ..core import register_wrap +from .translations import t +import re + + +@register_wrap +class CreateDigitigradeLegs(bpy.types.Operator): + bl_idname = "avatar_toolkit.createdigitigradelegs" + bl_label = t('Tools.create_digitigrade_legs.label') + bl_description = t('Tools.create_digitigrade_legs.desc') + + @classmethod + def poll(cls, context): + if(context.active_object is None): + return False + if(context.selected_editable_bones is not None): + if(len(context.selected_editable_bones) == 2): + return True + return False + + def execute(self, context): + + for digi0 in context.selected_editable_bones: + digi1: bpy.types.EditBone = None + digi2: bpy.types.EditBone = None + digi3: bpy.types.EditBone = None + + try: + digi1 = digi0.children[0] + digi2 = digi1.children[0] + digi3 = digi2.children[0] + except: + print("bone format incorrect! Please select a chain of 4 continious bones!") #TODO: Show this to user. this is an error. + return {'CANCELLED'} + digi4 = None + try: + digi4 = digi3.children[0] + + except: + print("no toe bone. Continuing.") + digi0.select = True + digi1.select = True + digi2.select = True + digi3.select = True + if(digi4): + digi4.select = True + bpy.ops.armature.roll_clear() + bpy.ops.armature.select_all(action='DESELECT') + + #creating transform for upper leg + digi0.select = True + bpy.ops.transform.create_orientation(name="Toolkit_digi0", overwrite=True) + bpy.ops.armature.select_all(action='DESELECT') + + + #duplicate digi0 and assign it to thigh + thigh = common.duplicatebone(digi0) + bpy.ops.armature.select_all(action='DESELECT') + + #make digi2 parrallel to digi1 + digi2.align_orientation(digi0) + + #extrude thigh + thigh.select_tail = True + bpy.ops.armature.extrude_move(ARMATURE_OT_extrude={"forked":False},TRANSFORM_OT_translate=None) + #set new bone to calf varible + bpy.ops.armature.select_more() + calf = context.selected_bones[0] + bpy.ops.armature.select_all(action='DESELECT') + + #set calf end to digi2 end + calf.tail = digi2.tail + + #make copy of calf, flip it, and then align bone so that it's head is moved to match in align phase + flipedcalf = common.duplicatebone(calf) + bpy.ops.armature.select_all(action='DESELECT') + flipedcalf.select = True + bpy.ops.armature.switch_direction() + bpy.ops.armature.select_all(action='DESELECT') + flippeddigi1 = common.duplicatebone(digi1) + bpy.ops.armature.select_all(action='DESELECT') + flippeddigi1.select = True + bpy.ops.armature.switch_direction() + bpy.ops.armature.select_all(action='DESELECT') + + + + #align flipped calf to flipped middle leg to move the head + flipedcalf.align_orientation(flippeddigi1) + + flipedcalf.length = flippeddigi1.length + + #assign calf tail to flipped calf head so it moves calf's tail to be out at the perfect parallelagram + calf.head = flipedcalf.tail + + #delete helper bones + bpy.ops.armature.select_all(action='DESELECT') + flippeddigi1.select = True + bpy.ops.armature.delete() + bpy.ops.armature.select_all(action='DESELECT') + flipedcalf.select = True + bpy.ops.armature.delete() + bpy.ops.armature.select_all(action='DESELECT') + + + + #reparent the foot to the new calf so it will be part of the new foot IK chain + digi3.parent = calf + #Tada! It's done! now to rename the old 3 segments that make up the old part to noik so resonite doesn't try to select them + + digi0.name = re.compile(re.escape(""), re.IGNORECASE).sub("",digi0.name)+"" + digi1.name = re.compile(re.escape(""), re.IGNORECASE).sub("",digi1.name)+"" + digi2.name = re.compile(re.escape(""), re.IGNORECASE).sub("",digi2.name)+"" + #finally fully done! + return {'FINISHED'} \ No newline at end of file diff --git a/functions/translations.py b/functions/translations.py index 4f84a1d..a265c81 100644 --- a/functions/translations.py +++ b/functions/translations.py @@ -44,7 +44,7 @@ def load_translations() -> None: print("Default translation file 'en_US.json' not found.") def t(phrase: str, *args, **kwargs) -> str: - output: str = dictionary.get(phrase) + output: str = dictionary.get(phrase, None) if output is None: if verbose: print('Warning: Unknown phrase: ' + phrase) diff --git a/ui/tools.py b/ui/tools.py index 408a95d..0593bcf 100644 --- a/ui/tools.py +++ b/ui/tools.py @@ -2,6 +2,7 @@ import bpy from ..core.register import register_wrap from .panel import AvatarToolkitPanel from bpy.types import Context +from ..functions.digitigrade_legs import CreateDigitigradeLegs @register_wrap class AvatarToolkitToolsPanel(bpy.types.Panel): @@ -19,4 +20,5 @@ class AvatarToolkitToolsPanel(bpy.types.Panel): row = layout.row(align=True) row.scale_y = 1.5 - row.operator("avatar_toolkit.convert_to_resonite", text="Translate to Resonite") \ No newline at end of file + row.operator("avatar_toolkit.convert_to_resonite", text="Translate to Resonite") + row.operator(CreateDigitigradeLegs.bl_idname, text="Create Digitigrade Legs") \ No newline at end of file From 6d4b11585503475470f62c23a473eb6e66bc7117 Mon Sep 17 00:00:00 2001 From: 989onan Date: Wed, 24 Jul 2024 17:49:26 -0400 Subject: [PATCH 7/8] oops took changes from another branch, this should yeet --- core/packer/rectangle_packer.py | 153 ------------------ core/properties.py | 2 + functions/atlas_materials.py | 274 -------------------------------- functions/texture_atlas.py | 23 --- functions/translations.py | 2 +- 5 files changed, 3 insertions(+), 451 deletions(-) delete mode 100644 core/packer/rectangle_packer.py delete mode 100644 functions/atlas_materials.py delete mode 100644 functions/texture_atlas.py diff --git a/core/packer/rectangle_packer.py b/core/packer/rectangle_packer.py deleted file mode 100644 index fc147ce..0000000 --- a/core/packer/rectangle_packer.py +++ /dev/null @@ -1,153 +0,0 @@ - -# thank you https://stackoverflow.com/a/71432759 -from __future__ import annotations - - -from typing import Optional -from bpy.types import Image, Material - - -# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -class Rectangle_Obj: - x: int = 0 - y: int = 0 - w: int = 0 - h: int = 0 - down: Rectangle_Obj = None - used: bool = False - right: Rectangle_Obj = None - - def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None): - self.x = x - self.y = y - self.w = w - self.h = h - self.down = down - self.used = used - self.right = right - - def split(self, w, h) -> Rectangle_Obj: - self.used = True - self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h) - self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h) - return self - - def find(self, w, h) -> Optional[Rectangle_Obj]: - if self.used: - return self.right.find(w, h) or self.down.find(w, h) - elif (w <= self.w) and (h <= self.h): - return self - return None - -class MaterialImageList: - albedo: Image - normal: Image - emission: Image - ambient_occlusion: Image - height: Image - roughness: Image - fit: Rectangle_Obj - material: Material - - def __init__(self): - pass - - x: int = 0 - y: int = 0 - w: int = 0 - h: int = 0 - - - - - -class BinPacker(object): - root: Rectangle_Obj - bin: list[MaterialImageList] = [] - def __init__(self, structure: list[MaterialImageList]): - self.root = None - self.bin = structure - - def fit(self): - structure = self.bin - structure_len = len(self.bin) - w: int = 0 - h: int = 0 - if structure_len > 0: - w = structure[0].w - h = structure[0].h - self.root = Rectangle_Obj(x=0, y=0, w=w, h=h) - for img in structure: - w = img.w - h = img.h - node = self.root.find(w, h) - if node: - img.fit = node.split(w, h) - else: - img.fit = self.grow_node(w, h) - return structure - - def grow_node(self, w, h) -> Optional[Rectangle_Obj]: - can_grow_right = (h <= self.root.h) - can_grow_down = (w <= self.root.w) - - should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w)) - should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h)) - - if should_grow_right: - return self.grow_right(w, h) - elif should_grow_down: - return self.grow_down(w, h) - elif can_grow_right: - return self.grow_right(w, h) - elif can_grow_down: - return self.grow_down(w, h) - return None - - def grow_right(self, w, h) -> Optional[Rectangle_Obj]: - self.root = Rectangle_Obj( - used=True, - x=0, - y=0, - w=self.root.w + w, - h=self.root.h, - down=self.root, - right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h)) - node = self.root.find(w, h) - if node: - return node.split(w, h) - return None - - def grow_down(self, w, h) -> Optional[Rectangle_Obj]: - self.root = Rectangle_Obj( - used=True, - x=0, - y=0, - w=self.root.w, - h=self.root.h + h, - down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h), - right=self.root - ) - node = self.root.find(w, h) - if node: - return node.split(w, h) - return None \ No newline at end of file diff --git a/core/properties.py b/core/properties.py index f8fb79d..37da322 100644 --- a/core/properties.py +++ b/core/properties.py @@ -5,6 +5,8 @@ from typing import Tuple from bpy.types import Scene, PropertyGroup, Object, Material, TextureNode, Context, SceneObjects from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty from bpy.utils import register_class +from ..functions.translations import t, get_languages_list, update_language +from ..core.addon_preferences import get_preference class material_list_bool: diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py deleted file mode 100644 index 60a871d..0000000 --- a/functions/atlas_materials.py +++ /dev/null @@ -1,274 +0,0 @@ -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.properties import material_list_bool, SceneMatClass -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) - 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) - 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/functions/texture_atlas.py b/functions/texture_atlas.py deleted file mode 100644 index 90a4aec..0000000 --- a/functions/texture_atlas.py +++ /dev/null @@ -1,23 +0,0 @@ -import bpy -from typing import List, Optional -from bpy.types import Operator, Context, Object, TextureNode -from ..core.register import register_wrap -from ..core.common import get_armature, simplify_bonename - -@register_wrap -class Atlas_Textures(Operator): - bl_idname = "avatar_toolkit.atlas_textures" - bl_label = "Atlas Textures" - bl_description = """Combines materials and their textures to optimize the model. -Although this combines materials, it may not reduce your VRAM usage. Other tools -like Tuxedo can vastly reduce your VRAM usage as well as many other optimizations, -rather than just duct taping the textures together like material combiner and this tool. -""" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context: Context) -> set: - - - return {'FINISHED'} - - diff --git a/functions/translations.py b/functions/translations.py index a265c81..4f84a1d 100644 --- a/functions/translations.py +++ b/functions/translations.py @@ -44,7 +44,7 @@ def load_translations() -> None: print("Default translation file 'en_US.json' not found.") def t(phrase: str, *args, **kwargs) -> str: - output: str = dictionary.get(phrase, None) + output: str = dictionary.get(phrase) if output is None: if verbose: print('Warning: Unknown phrase: ' + phrase) From ce1cc796640a58e39cc519cd80538dad05c1ec6e Mon Sep 17 00:00:00 2001 From: 989onan Date: Wed, 24 Jul 2024 17:53:02 -0400 Subject: [PATCH 8/8] yuck oml fix! --- core/properties.py | 3 ++ functions/translations.py | 3 +- ui/atlas_materials.py | 92 --------------------------------------- 3 files changed, 5 insertions(+), 93 deletions(-) delete mode 100644 ui/atlas_materials.py diff --git a/core/properties.py b/core/properties.py index 7370b75..9841212 100644 --- a/core/properties.py +++ b/core/properties.py @@ -1,4 +1,6 @@ import bpy +from ..functions.translations import t, get_languages_list, update_language +from ..core.addon_preferences import get_preference def register(): default_language = get_preference("language", 0) @@ -12,6 +14,7 @@ def register(): ) bpy.types.Scene.avatar_toolkit_language_changed = bpy.props.BoolProperty(default=False) + def unregister(): if hasattr(bpy.types.Scene, "avatar_toolkit_language"): del bpy.types.Scene.avatar_toolkit_language diff --git a/functions/translations.py b/functions/translations.py index 225920c..9e2a742 100644 --- a/functions/translations.py +++ b/functions/translations.py @@ -63,9 +63,10 @@ def load_translations() -> bool: print(f"Loaded default translations: {dictionary}") # Debug print else: print("Default translation file 'en_US.json' not found.") + return dictionary != old_dictionary -def t(phrase: str, *args, **kwargs) -> str: +def t(phrase: str, default: str = None) -> str: output: str = dictionary.get(phrase) if output is None: if verbose: diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py deleted file mode 100644 index 0596846..0000000 --- a/ui/atlas_materials.py +++ /dev/null @@ -1,92 +0,0 @@ -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.properties 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