@@ -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():
|
||||||
|
|||||||
@@ -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):
|
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
@@ -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")
|
||||||
|
|
||||||
|
|||||||
+43
-4
@@ -17,11 +17,50 @@ class AvatarToolkitQuickAccessPanel(bpy.types.Panel):
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
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):
|
||||||
|
|||||||
Reference in New Issue
Block a user