Merge pull request #9 from Yusarina/main

Add some Basic Functions
This commit is contained in:
Onan Chew
2024-06-18 18:21:09 -04:00
committed by GitHub
8 changed files with 325 additions and 5 deletions
+2
View File
@@ -4,12 +4,14 @@ if "bpy" not in locals():
import bpy import bpy
from . import ui from . import ui
from . import core from . import core
from . import functions
from .core import register from .core import register
from .core.register import __bl_ordered_classes from .core.register import __bl_ordered_classes
else: else:
import importlib import importlib
importlib.reload(ui) importlib.reload(ui)
importlib.reload(core) importlib.reload(core)
importlib.reload(functions)
def register(): def register():
+41
View File
@@ -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])
+18
View File
@@ -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)
+124
View File
@@ -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)
+80
View File
@@ -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")
+13
View File
@@ -14,4 +14,17 @@ class AvatarToolkitOptimizationPanel(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.label(text="Optimization Options") layout.label(text="Optimization Options")
row = layout.row()
row.scale_y = 1.2
row.operator("avatar_toolkit.combine_materials", text="Combine Materials")
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.operator("avatar_toolkit.join_selected_meshes", text="Join Selected Meshes")
# Add optimization options here # Add optimization options here
+4 -1
View File
@@ -11,6 +11,9 @@ class AvatarToolkitPanel(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout 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") #print("Avatar Toolkit Panel is being drawn")
+42 -3
View File
@@ -18,10 +18,49 @@ class AvatarToolkitQuickAccessPanel(bpy.types.Panel):
layout = self.layout layout = self.layout
layout.label(text="Quick Access Options") layout.label(text="Quick Access Options")
# Add import buttons
row = layout.row() row = layout.row()
row.operator("avatar_toolkit.import_pmx", text="Import PMX") row.label(text="Import/Export", icon='IMPORT')
row.operator("avatar_toolkit.import_pmd", text="Import PMD")
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 @register_wrap
class AVATAR_TOOLKIT_OT_import_pmx(bpy.types.Operator): class AVATAR_TOOLKIT_OT_import_pmx(bpy.types.Operator):