From 1d507ddaa01133ff268704c88eea50b827818d84 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 18 Jun 2024 03:54:10 +0100 Subject: [PATCH 1/2] Add some Basic Functions - Added functions folder. - Added Combine Materials (Basic and needs imporvements. - Added common file, this is where any common things that could be used by multiple functions will live. - Clean materials, basic at the minute it cleans up material names in the given mesh by removing the '.001' suffix. - Added fix UV Cords in which is in common, this should fix faulty uv coordinates, may need improvements as it's was the best way i could think of for the time being. - Added join all meshes and selected meshes functions, this will use the fix uv cords while joining mehses. This is pretty basic as blender is quite good at doing the mesh joining itself. We may want to expand it in the future though. --- __init__.py | 2 + core/common.py | 41 +++++++++++ functions/__init__.py | 18 +++++ functions/combine_materials.py | 124 +++++++++++++++++++++++++++++++++ functions/join_meshes.py | 80 +++++++++++++++++++++ ui/optimization.py | 10 +++ 6 files changed, 275 insertions(+) create mode 100644 core/common.py create mode 100644 functions/__init__.py create mode 100644 functions/combine_materials.py create mode 100644 functions/join_meshes.py diff --git a/__init__.py b/__init__.py index ab93ab7..4f93020 100644 --- a/__init__.py +++ b/__init__.py @@ -4,12 +4,14 @@ if "bpy" not in locals(): import bpy from . import ui from . import core + from . import functions from .core import register from .core.register import __bl_ordered_classes else: import importlib importlib.reload(ui) importlib.reload(core) + importlib.reload(functions) def register(): diff --git a/core/common.py b/core/common.py new file mode 100644 index 0000000..b73995b --- /dev/null +++ b/core/common.py @@ -0,0 +1,41 @@ +import bpy +import numpy as np + +from bpy.types import Object, ShapeKey +from functools import lru_cache + +### Clean up material names in the given mesh by removing the '.001' suffix. +def clean_material_names(mesh): + for j, mat in enumerate(mesh.material_slots): + if mat.name.endswith(('.0+', ' 0+')): + mesh.active_material_index = j + mesh.active_material.name = mat.name[:-len(mat.name.rstrip('0')) - 1] + + +# This will fix faulty uv coordinates, cats did this a other way which can have unintended consequences, +# this is the best way i could of think of doing this for the time being, however may need improvements. + +def fix_uv_coordinates(context): + obj = context.object + + # Check if the object is in Edit Mode + if obj.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + # Check if the object has any mesh data + if obj.type == 'MESH' and obj.data: + bpy.context.view_layer.objects.active = obj + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.average_islands_scale() + + # Switch back to Object Mode + bpy.ops.object.mode_set(mode='OBJECT') + else: + print("Object is not a valid mesh with UV data") + +def has_shapekeys(mesh_obj: Object) -> bool: + return mesh_obj.data.shape_keys is not None + +@lru_cache(maxsize=None) +def _get_shape_key_co(shape_key: ShapeKey) -> np.ndarray: + return np.array([v.co for v in shape_key.data]) diff --git a/functions/__init__.py b/functions/__init__.py new file mode 100644 index 0000000..afece68 --- /dev/null +++ b/functions/__init__.py @@ -0,0 +1,18 @@ +from ..core.register import register_wrap + +#to reload all things in this directory and import them properly - @989onan +if "bpy" not in locals(): + import bpy + import glob + import os + from os.path import dirname, basename, isfile, join + modules = glob.glob(join(dirname(__file__), "*.py")) + for module_name in [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]: + exec("from . import "+module_name) + print("importing " +module_name) +else: + import importlib + modules = glob.glob(join(dirname(__file__), "*.py")) + for module_name in [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]: + exec("importlib.reload("+module_name+")") + print("reloading " +module_name) diff --git a/functions/combine_materials.py b/functions/combine_materials.py new file mode 100644 index 0000000..1741fc8 --- /dev/null +++ b/functions/combine_materials.py @@ -0,0 +1,124 @@ +import bpy +import re +from ..core.common import clean_material_names +from ..core.register import register_wrap + +def textures_match(tex1, tex2): + return tex1.image == tex2.image and tex1.extension == tex2.extension + +def consolidate_nodes(node1, node2): + node2.color_space = node1.color_space + node2.coordinates = node1.coordinates + +def copy_tex_nodes(mat1, mat2): + for node1 in mat1.node_tree.nodes: + if node1.type == 'TEX_IMAGE': + node2 = mat2.node_tree.nodes.get(node1.name) + if node2: + node2.mapping = node1.mapping + node2.projection = node1.projection + +def consolidate_textures(mat1, mat2): + if mat1.node_tree and mat2.node_tree: + for node1 in mat1.node_tree.nodes: + if node1.type == 'TEX_IMAGE': + if node1.node_tree: + consolidate_textures(node1.node_tree, mat2.node_tree) + + for node2 in mat2.node_tree.nodes: + if (node2.type == 'TEX_IMAGE' and + node1.image == node2.image): + consolidate_nodes(node1, node2) + node2.image = node1.image + copy_tex_nodes(mat1, mat2) + +def color_match(col1, col2, tolerance=0.01): + return abs(col1[0] - col2[0]) < tolerance + +def materials_match(mat1, mat2, tolerance=0.01): + if not color_match(mat1.diffuse_color, mat2.diffuse_color, tolerance): + return False + + if mat1.roughness != mat2.roughness: + return False + + consolidate_textures(mat1, mat2) + + return True + +def get_base_name(name): + mat_match = re.match(r"^(.*)\.\d{3}$", name) + return mat_match.group(1) if mat_match else name + +def report_consolidated(self, num_combined): + self.report({'INFO'}, f"Combined {num_combined} materials") + +@register_wrap +class CombineMaterials(bpy.types.Operator): + bl_idname = "avatar_toolkit.combine_materials" + bl_label = "Combine Materials" + bl_description = "Combine similar materials to optimize the model" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + bpy.ops.object.mode_set(mode='OBJECT') + + armature = next((obj for obj in bpy.data.objects if obj.type == 'ARMATURE'), None) + if not armature: + return {'CANCELLED'} + + meshes = [obj for obj in bpy.data.objects if obj.type == 'MESH' and 'Armature' in obj.modifiers and obj.modifiers['Armature'].object == armature] + if not meshes: + return {'CANCELLED'} + + bpy.ops.object.mode_set(mode='OBJECT') + self.consolidate_materials(meshes) + self.remove_unused_materials() + self.cleanmatslots() + self.clean_material_names() + bpy.ops.object.mode_set(mode='OBJECT') + bpy.context.view_layer.objects.active = armature + + return {'FINISHED'} + + def consolidate_materials(self, objects): + mat_mapping = {} + num_combined = 0 + for ob in objects: + for slot in ob.material_slots: + mat = slot.material + if mat: + base_name = get_base_name(mat.name) + + if base_name in mat_mapping: + base_mat = mat_mapping[base_name] + if materials_match(base_mat, mat): + consolidate_textures(base_mat, mat) + num_combined += 1 + slot.material = base_mat + else: + mat_mapping[base_name] = mat + + report_consolidated(self, num_combined) + + def remove_unused_materials(self): + for mat in bpy.data.materials: + if not any(obj for obj in bpy.data.objects if obj.material_slots and mat.name in obj.material_slots): + bpy.data.materials.remove(mat, do_unlink=True) + + def cleanmatslots(self): + for obj in bpy.data.objects: + if obj.type == 'MESH': + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.material_slot_remove_unused() + obj.select_set(False) + + def clean_material_names(self): + for obj in bpy.data.objects: + if obj.type == 'MESH': + clean_material_names(obj) diff --git a/functions/join_meshes.py b/functions/join_meshes.py new file mode 100644 index 0000000..9e57789 --- /dev/null +++ b/functions/join_meshes.py @@ -0,0 +1,80 @@ +import bpy +from ..core.register import register_wrap +from ..core.common import fix_uv_coordinates + +@register_wrap +class JoinAllMeshes(bpy.types.Operator): + bl_idname = "avatar_toolkit.join_all_meshes" + bl_label = "Join All Meshes" + bl_description = "Join all meshes in the scene" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.mode == 'OBJECT' + + def execute(self, context): + self.join_all_meshes(context) + return {'FINISHED'} + + def join_all_meshes(self, context): + if not bpy.data.objects: + self.report({'INFO'}, "No objects in the scene") + return + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + + meshes = [obj for obj in bpy.data.objects if obj.type == 'MESH'] + for mesh in meshes: + mesh.select_set(True) + + if bpy.context.selected_objects: + bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] + bpy.ops.object.join() + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + fix_uv_coordinates(context) + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + self.report({'INFO'}, "Meshes joined successfully") + else: + self.report({'WARNING'}, "No mesh objects selected") + +@register_wrap +class JoinSelectedMeshes(bpy.types.Operator): + bl_idname = "avatar_toolkit.join_selected_meshes" + bl_label = "Join Selected Meshes" + bl_description = "Join selected meshes" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.mode == 'OBJECT' + + def execute(self, context): + self.join_selected_meshes(context) + return {'FINISHED'} + + def join_selected_meshes(self, context): + selected_objects = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH'] + + if not selected_objects: + self.report({'WARNING'}, "No mesh objects selected") + return + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + + for obj in selected_objects: + obj.select_set(True) + + if bpy.context.selected_objects: + bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] + bpy.ops.object.join() + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + fix_uv_coordinates(context) + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + self.report({'INFO'}, "Selected meshes joined successfully") + else: + self.report({'WARNING'}, "No mesh objects selected") diff --git a/ui/optimization.py b/ui/optimization.py index 54c5d0a..54e7ce2 100644 --- a/ui/optimization.py +++ b/ui/optimization.py @@ -14,4 +14,14 @@ class AvatarToolkitOptimizationPanel(bpy.types.Panel): def draw(self, context): layout = self.layout layout.label(text="Optimization Options") + + row = layout.row() + row.operator("avatar_toolkit.combine_materials", text="Combine Materials") + + row = layout.row() + row.operator("avatar_toolkit.join_all_meshes", text="Join All Meshes") + + row = layout.row() + row.operator("avatar_toolkit.join_selected_meshes", text="Join Selected Meshes") + # Add optimization options here From 81abc6ffd7fc5dc13fffdaace238516b83d8b819 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 18 Jun 2024 22:58:29 +0100 Subject: [PATCH 2/2] Some Baisc UI Improvements Nothing much, just improving the UI looks a little bit, we now have Import and Export in a popup and I made some changes to some of the buttons. That's about it. --- ui/optimization.py | 9 ++++++--- ui/panel.py | 5 ++++- ui/quick_access.py | 47 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/ui/optimization.py b/ui/optimization.py index 54e7ce2..2f6e69a 100644 --- a/ui/optimization.py +++ b/ui/optimization.py @@ -16,12 +16,15 @@ class AvatarToolkitOptimizationPanel(bpy.types.Panel): layout.label(text="Optimization Options") row = layout.row() + row.scale_y = 1.2 row.operator("avatar_toolkit.combine_materials", text="Combine Materials") - row = layout.row() + layout.separator(factor=0.5) + + row = layout.row(align=True) + row.scale_y = 1.2 row.operator("avatar_toolkit.join_all_meshes", text="Join All Meshes") - - row = layout.row() row.operator("avatar_toolkit.join_selected_meshes", text="Join Selected Meshes") # Add optimization options here + diff --git a/ui/panel.py b/ui/panel.py index 384c089..a987a28 100644 --- a/ui/panel.py +++ b/ui/panel.py @@ -11,6 +11,9 @@ class AvatarToolkitPanel(bpy.types.Panel): def draw(self, context): layout = self.layout - layout.label(text="Welcome to Avatar Toolkit!") + layout.label(text="Welcome to Avatar Toolkit, a tool for") + layout.label(text="creating and editing avatars in blender,") + layout.label(text="This is an early alpha version, so expect") + layout.label(text="bugs and issues.") #print("Avatar Toolkit Panel is being drawn") diff --git a/ui/quick_access.py b/ui/quick_access.py index 86f4bf7..80562b1 100644 --- a/ui/quick_access.py +++ b/ui/quick_access.py @@ -17,11 +17,50 @@ class AvatarToolkitQuickAccessPanel(bpy.types.Panel): def draw(self, context): layout = self.layout layout.label(text="Quick Access Options") - - # Add import buttons + row = layout.row() - row.operator("avatar_toolkit.import_pmx", text="Import PMX") - row.operator("avatar_toolkit.import_pmd", text="Import PMD") + row.label(text="Import/Export", icon='IMPORT') + + layout.separator(factor=0.5) + + row = layout.row(align=True) + row.scale_y = 1.5 + row.operator("avatar_toolkit.import_menu", text="Import") + row.operator("avatar_toolkit.export_menu", text="Export") + +@register_wrap +class AVATAR_TOOLKIT_OT_import_menu(bpy.types.Operator): + bl_idname = "avatar_toolkit.import_menu" + bl_label = "Import Menu" + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_popup(self, width=200) + + def draw(self, context): + layout = self.layout + layout.label(text="Select Import Method") + layout.operator("avatar_toolkit.import_pmx", text="Import PMX") + layout.operator("avatar_toolkit.import_pmd", text="Import PMD") + +@register_wrap +class AVATAR_TOOLKIT_OT_export_menu(bpy.types.Operator): + bl_idname = "avatar_toolkit.export_menu" + bl_label = "Export Menu" + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_popup(self, width=200) + + def draw(self, context): + layout = self.layout + layout.label(text="Export options will go here") @register_wrap class AVATAR_TOOLKIT_OT_import_pmx(bpy.types.Operator):