@@ -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():
|
||||
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -14,4 +14,17 @@ class AvatarToolkitOptimizationPanel(bpy.types.Panel):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
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
|
||||
|
||||
|
||||
+4
-1
@@ -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")
|
||||
|
||||
|
||||
+43
-4
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user