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.
This commit is contained in:
Yusarina
2024-06-18 03:54:10 +01:00
parent 7d60d285ea
commit 1d507ddaa0
6 changed files with 275 additions and 0 deletions
+2
View File
@@ -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():
+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")
+10
View File
@@ -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