Start of the Major Overhaul

I decided to go through each function and UI section one by one, improving and overhauling things. Each function and section is going to be fully tested and not rushed out.

This is the best way to catch things, but also include the code base as much as possible.
This commit is contained in:
Yusarina
2024-12-03 22:58:17 +00:00
parent 7f9dc20564
commit ff23d23cfc
38 changed files with 604 additions and 4765 deletions
View File
-161
View File
@@ -1,161 +0,0 @@
import bpy
import math
from bpy.types import Context, Operator
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes
from ..core.translations import t
class AvatarToolKit_OT_ApplyTransforms(Operator):
bl_idname = "avatar_toolkit.apply_transforms"
bl_label = t("Tools.apply_transforms.label")
bl_description = t("Tools.apply_transforms.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.apply_transforms.invalid_armature"))
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
meshes = get_all_meshes(context)
for mesh in meshes:
mesh.select_set(True)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
self.report({'INFO'}, t("Tools.apply_transforms.success"))
return {'FINISHED'}
class AvatarToolKit_OT_ConnectBones(Operator):
bl_idname = "avatar_toolkit.connect_bones"
bl_label = t("Tools.connect_bones.label")
bl_description = t("Tools.connect_bones.desc")
bl_options = {'REGISTER', 'UNDO'}
min_distance: bpy.props.FloatProperty(
name=t("Tools.connect_bones.min_distance.label"),
description=t("Tools.connect_bones.min_distance.desc"),
default=0.005,
min=0.001,
max=0.1
)
@classmethod
def poll(cls, context: Context) -> bool:
return get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.connect_bones.invalid_armature"))
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
bones_connected = 0
for bone in edit_bones:
if len(bone.children) == 1 and bone.name not in ['LeftEye', 'RightEye', 'Head', 'Hips']:
child = bone.children[0]
distance = math.dist(bone.head, child.head)
if distance > self.min_distance:
bone.tail = child.head
if bone.parent and len(bone.parent.children) == 1:
bone.use_connect = True
bones_connected += 1
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.connect_bones.success").format(bones_connected=bones_connected))
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "min_distance")
class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
bl_idname = "avatar_toolkit.delete_bone_constraints"
bl_label = t("Tools.delete_bone_constraints.label")
bl_description = t("Tools.delete_bone_constraints.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.delete_bone_constraints.invalid_armature"))
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='POSE')
constraints_removed = 0
for bone in armature.pose.bones:
while bone.constraints:
bone.constraints.remove(bone.constraints[0])
constraints_removed += 1
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.delete_bone_constraints.success").format(constraints_removed=constraints_removed))
return {'FINISHED'}
class AvatarToolKit_OT_SeparateByMaterials(Operator):
bl_idname = "avatar_toolkit.separate_by_materials"
bl_label = t("Tools.separate_by_materials.label")
bl_description = t("Tools.separate_by_materials.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.active_object and context.active_object.type == 'MESH'
def execute(self, context: Context) -> set[str]:
obj = context.active_object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.separate(type='MATERIAL')
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.separate_by_materials.success"))
return {'FINISHED'}
class AvatarToolKit_OT_SeparateByLooseParts(Operator):
bl_idname = "avatar_toolkit.separate_by_loose_parts"
bl_label = t("Tools.separate_by_loose_parts.label")
bl_description = t("Tools.separate_by_loose_parts.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.active_object and context.active_object.type == 'MESH'
def execute(self, context: Context) -> set[str]:
obj = context.active_object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.separate(type='LOOSE')
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.separate_by_loose_parts.success"))
return {'FINISHED'}
-461
View File
@@ -1,461 +0,0 @@
import bpy
from bpy.types import Context, Mesh, Panel, Operator, Armature, EditBone
from ..core.translations import t
from ..core.common import get_selected_armature, get_all_meshes
from ..core import common
from ..core.dictionaries import bone_names
from mathutils import Matrix
class AvatarToolkit_OT_StartPoseMode(Operator):
bl_idname = 'avatar_toolkit.start_pose_mode'
bl_label = t("Quick_Access.start_pose_mode.label")
bl_description = t("Quick_Access.start_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return get_selected_armature(context) != None and context.mode != "POSE"
def execute(self, context: Context) -> set[str]:
#give an active object so the next line doesn't throw an error.
context.view_layer.objects.active = get_selected_armature(context)
bpy.ops.object.mode_set(mode='OBJECT')
#deselect everything and select just our armature, then go into pose on just our selected armature. - @989onan
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = get_selected_armature(context)
context.view_layer.objects.active.select_set(True)
bpy.ops.object.mode_set(mode='POSE')
return {'FINISHED'}
class AvatarToolkit_OT_StopPoseMode(Operator):
bl_idname = 'avatar_toolkit.stop_pose_mode'
bl_label = t("Quick_Access.stop_pose_mode.label")
bl_description = t("Quick_Access.stop_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return get_selected_armature(context) != None and context.mode == "POSE"
def execute(self, context: Context) -> set[str]:
#this is done so that transforms are cleared but user selection is respected. - @989onan
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator):
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
bl_label = t("Quick_Access.apply_pose_as_shapekey.label")
bl_description = t("Quick_Access.apply_pose_as_shapekey.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = common.get_selected_armature(context)
return armature and context.mode == 'POSE'
def execute(self, context):
armature_obj = common.get_selected_armature(context)
mesh_objects = common.get_all_meshes(context)
for mesh_obj in mesh_objects:
if not mesh_obj.data:
continue
# Ensure basis exists
if not mesh_obj.data.shape_keys:
mesh_obj.shape_key_add(name='Basis')
# Store current pose as new shapekey
new_shape = mesh_obj.shape_key_add(name='Pose_Shapekey', from_mix=False)
# Evaluate mesh in current pose
depsgraph = context.evaluated_depsgraph_get()
eval_mesh = mesh_obj.evaluated_get(depsgraph)
# Apply evaluated vertices to new shapekey
for i, v in enumerate(eval_mesh.data.vertices):
new_shape.data[i].co = v.co.copy()
# Reset pose
bpy.ops.pose.select_all(action='SELECT')
bpy.ops.pose.transforms_clear()
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t('Tools.apply_pose_as_rest.success'))
return {'FINISHED'}
class AvatarToolkit_OT_ApplyPoseAsRest(Operator):
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
bl_label = t("Quick_Access.apply_pose_as_rest.label")
bl_description = t("Quick_Access.apply_pose_as_rest.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return get_selected_armature(context) != None and context.mode == "POSE"
def execute(self, context: Context):
if not common.apply_pose_as_rest(armature_obj=get_selected_armature(context),
meshes=get_all_meshes(context),
context=context):
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
return {'CANCELLED'}
return {'FINISHED'}
class AvatarToolkit_OT_RemoveZeroWeightBones(Operator):
bl_idname = "avatar_toolkit.remove_zero_weight_bones"
bl_label = t("Tools.remove_zero_weight_bones.label")
bl_description = t("Tools.remove_zero_weight_bones.desc")
bl_options = {'REGISTER', 'UNDO'}
threshold: bpy.props.FloatProperty(
default=0.01,
name=t("Tools.remove_zero_weight_bones.threshold.label"),
description=t("Tools.remove_zero_weight_bones.threshold.desc"),
min=0.0000001,
max=0.9999999)
@classmethod
def poll(cls, context: Context) -> bool:
return common.get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = common.get_selected_armature(context)
if not common.is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.apply_transforms.invalid_armature"))
return {'CANCELLED'}
weighted_bones: list[str] = []
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
# Modify the initial transforms collection section to include all bones:
initial_transforms = {}
bpy.ops.object.mode_set(mode='EDIT')
for bone in armature.data.edit_bones:
initial_transforms[bone.name] = {
'head': bone.head.copy(),
'tail': bone.tail.copy(),
'roll': bone.roll,
'matrix': bone.matrix.copy(),
'parent': bone.parent.name if bone.parent else None
}
# Handle any child bones including _end bones
for child in bone.children:
initial_transforms[child.name] = {
'head': child.head.copy(),
'tail': child.tail.copy(),
'roll': child.roll,
'matrix': child.matrix.copy(),
'parent': child.parent.name if child.parent else None
}
# Get weighted bones
armature.select_set(True)
context.view_layer.objects.active = armature
meshes = common.get_all_meshes(context)
for mesh in meshes:
mesh_data: Mesh = mesh.data
for vertex in mesh_data.vertices:
for group in vertex.groups:
if group.weight > self.threshold:
weighted_bones.append(mesh.vertex_groups[group.group].name)
bpy.ops.object.mode_set(mode='EDIT')
amature_data: Armature = armature.data
unweighted_bones: list[str] = []
# Identify unweighted bones
for bone in amature_data.edit_bones:
if bone.name not in weighted_bones:
unweighted_bones.append(bone.name)
# Process bone removal while preserving positions
for bone_name in unweighted_bones:
bone = amature_data.edit_bones[bone_name]
# Store children data
children = bone.children
children_data = {}
for child in children:
children_data[child.name] = initial_transforms[child.name]
# Reparent children
for child in children:
child.use_connect = False
if bone.parent:
child.parent = bone.parent
# Remove bone
amature_data.edit_bones.remove(bone)
# Restore children positions
for child_name, data in children_data.items():
if child_name in amature_data.edit_bones:
child = amature_data.edit_bones[child_name]
child.head = data['head']
child.tail = data['tail']
child.roll = data['roll']
child.matrix = data['matrix']
# Final position verification
for bone_name, transform in initial_transforms.items():
if bone_name in amature_data.edit_bones:
bone = amature_data.edit_bones[bone_name]
bone.matrix = transform['matrix']
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.remove_zero_weight_bones.success"))
return {'FINISHED'}
class AvatarToolkit_OT_MergeBonesToActive(Operator):
bl_idname = "avatar_toolkit.merge_bones_to_active"
bl_label = t("Tools.merge_bones_to_active.label")
bl_description = t("Tools.merge_bones_to_active.desc")
bl_options = {'REGISTER', 'UNDO'}
delete_old: bpy.props.BoolProperty(name=t("Tools.merge_bones_to_active.delete_old.label"), description=t("Tools.merge_bones_to_active.delete_old.desc"), default=False)
@classmethod
def poll(cls, context: Context) -> bool:
if common.get_selected_armature(context) is not None:
if common.get_selected_armature(context) == context.view_layer.objects.active:
if context.mode == "POSE":
return len(context.selected_pose_bones) > 1
elif context.mode == "EDIT_ARMATURE":
return len(context.selected_bones) > 1
return False
def execute(cls, context: Context) -> set[str]:
prev_mode: str = "EDIT"
if context.mode == "POSE":
prev_mode = "POSE"
#get active bone and a list of all other selected bones
bpy.ops.object.mode_set(mode='EDIT')
target_bone: str = context.active_bone.name
armature_data: Armature = context.view_layer.objects.active.data
bones: list[str] = [i.name for i in context.selected_bones]
bones.remove(target_bone)
for obj in common.get_all_meshes(context):
for bone in bones:
bone_name: str = armature_data.edit_bones[bone].name
common.transfer_vertex_weights(context=context,obj=obj,source_group=bone_name,target_group=armature_data.edit_bones[target_bone].name)
bpy.ops.object.mode_set(mode='EDIT')
for bone in bones:
if cls.delete_old:
for bone_child in armature_data.edit_bones[bone].children:
bone_child.parent = armature_data.edit_bones[bone].parent
armature_data.edit_bones.remove(armature_data.edit_bones[bone])
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
class AvatarToolkit_OT_MergeBonesToParents(Operator):
bl_idname = "avatar_toolkit.merge_bones_to_parents"
bl_label = t("Tools.merge_bones_to_parents.label")
bl_description = t("Tools.merge_bones_to_parents.desc")
bl_options = {'REGISTER', 'UNDO'}
delete_old: bpy.props.BoolProperty(
name=t("Tools.merge_bones_to_parents.delete_old.label"),
description=t("Tools.merge_bones_to_parents.delete_old.desc"),
default=False
)
@classmethod
def poll(cls, context: Context) -> bool:
armature = common.get_selected_armature(context)
if armature and armature == context.view_layer.objects.active:
if context.mode == "POSE":
return len(context.selected_pose_bones) > 0
elif context.mode == "EDIT_ARMATURE":
return len(context.selected_editable_bones) > 0
return False
def execute(self, context: Context) -> set[str]:
prev_mode = context.mode
armature = common.get_selected_armature(context)
# Map 'EDIT_ARMATURE' to 'EDIT' for bpy.ops.object.mode_set
if prev_mode == 'EDIT_ARMATURE':
prev_mode = 'EDIT'
# Set active object and mode
context.view_layer.objects.active = armature
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
armature_data: Armature = armature.data
# Get selected bones in Edit Mode
selected_bones = context.selected_editable_bones
selected_bone_names = [bone.name for bone in selected_bones]
if not selected_bone_names:
self.report({'ERROR'}, t("No bones selected"))
return {'CANCELLED'}
for obj in common.get_all_meshes(context):
for bone_name in selected_bone_names:
bone = armature_data.edit_bones.get(bone_name)
if bone and bone.parent:
# Transfer weights from bone to its parent
context.view_layer.objects.active = obj
common.transfer_vertex_weights(
context=context,
obj=obj,
source_group=bone_name,
target_group=bone.parent.name
)
# Return to armature edit mode
context.view_layer.objects.active = armature
armature.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
else:
self.report({'WARNING'}, f"Bone '{bone_name}' has no parent or not found; skipping")
# Optionally delete old bones
if self.delete_old:
for bone_name in selected_bone_names:
bone = armature_data.edit_bones.get(bone_name)
if bone:
# Reassign children to the parent of the bone being deleted
for child in bone.children:
child.parent = bone.parent
# Remove the bone
armature_data.edit_bones.remove(bone)
else:
self.report({'WARNING'}, f"Bone '{bone_name}' not found in armature; cannot delete")
# Return to previous mode
context.view_layer.objects.active = armature
armature.select_set(True)
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
class AvatarToolkit_OT_MergeArmatures(Operator):
bl_idname = "avatar_toolkit.merge_armatures"
bl_label = t("MergeArmature.merge_armatures.label")
bl_description = t("MergeArmature.merge_armatures.desc").format(selected_armature_label=t("MergeArmatures.selected_armature.label"))
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (common.get_selected_armature(context) is not None) and (common.get_merge_armature_source(context) is not None)
def make_active(self, obj: bpy.types.Object, context: Context):
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = obj
obj.select_set(True)
def execute(cls, context: Context) -> set[str]:
source_armature: bpy.types.Object = bpy.data.objects[context.scene.merge_armature_source]
source_armature_data: Armature = source_armature.data
target_armature: bpy.types.Object = common.get_selected_armature(context)
target_armature_data: Armature = target_armature.data
parent_dictionary: dict[str, list[str]] = {}
cls.make_active(obj=source_armature, context=context)
if context.scene.merge_armature_apply_transforms:
target_armature.select_set(True)
for obj in target_armature.children:
obj.select_set(True)
for obj in source_armature.children:
obj.select_set(True)
bpy.ops.object.transform_apply()
if context.scene.merge_armature_align_bones:
if not context.scene.merge_armature_apply_transforms:
source_armature.matrix_world = target_armature.matrix_world
def children_bone_recursive(parent_bone) -> list[bpy.types.PoseBone]:
child_bones = []
child_bones.append(parent_bone)
for child in parent_bone.children:
child_bones.extend(children_bone_recursive(child))
return child_bones
bpy.ops.object.mode_set(mode='POSE')
source_armature_bone_names = [j.name for j in children_bone_recursive(
source_armature.pose.bones[
next(bone.name for bone in source_armature.pose.bones if common.simplify_bonename(bone.name) in bone_names['hips']) #Find bone that matches dictionary for hips before continuing.
]
)] #bones are default in order of parent child.
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = target_armature
bpy.ops.object.mode_set(mode='EDIT')
for source_bone_name in source_armature_bone_names:
if source_bone_name in target_armature_data.edit_bones:
obj = source_armature
editbone = target_armature_data.edit_bones[source_bone_name]
bone = obj.pose.bones[source_bone_name]
bone.matrix = editbone.matrix
else:
continue
if not common.apply_pose_as_rest(armature_obj=source_armature,meshes=[i for i in source_armature.children if i.type == 'MESH'], context=context):
cls.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
return {'FINISHED'}
cls.make_active(obj=source_armature, context=context)
bpy.ops.object.mode_set(mode='EDIT')
source_armature_data: Armature = source_armature.data
for bone_name in [i.name for i in source_armature_data.edit_bones]:
if bone_name in target_armature_data.bones:
parent_dictionary[bone_name] = [i.name for i in source_armature_data.edit_bones[bone_name].children]
source_armature_data.edit_bones.remove(source_armature_data.edit_bones[bone_name])
bpy.ops.object.mode_set(mode='OBJECT')
cls.make_active(obj=target_armature, context=context)
source_armature.select_set(True)
bpy.ops.object.join()
target_armature: bpy.types.Object = common.get_selected_armature(context)
cls.make_active(obj=target_armature, context=context)
bpy.ops.object.mode_set(mode='EDIT')
for bone_name, bone_name_list in parent_dictionary.items():
if bone_name in target_armature_data.edit_bones:
for bone_child in bone_name_list:
target_armature_data.edit_bones[bone_child].parent = target_armature_data.edit_bones[bone_name]
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
-321
View File
@@ -1,321 +0,0 @@
from pathlib import Path
import numpy
import bpy
import os
from typing import List, Tuple, Optional
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeNormalMap
from ..core.common import SceneMatClass, MaterialListBool
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
from ..core.translations import t
class MaterialImageList:
def __init__(self):
self.albedo: Image = None
self.normal: Image = None
self.emission: Image = None
self.ambient_occlusion: Image = None
self.height: Image = None
self.roughness: Image = None
self.material: Material = None
self.parent_mesh: Object = None
self.w: int = 0
self.h: int = 0
self.fit = None
def scale_images_to_largest(images: list[Image]) -> tuple[int, int]:
try:
valid_images = []
for img in images:
if img and hasattr(img, 'name'):
image_data = bpy.data.images.get(img.name)
if image_data and image_data.has_data:
valid_images.append(image_data)
if not valid_images:
return 1, 1
max_width = max(img.size[0] for img in valid_images)
max_height = max(img.size[1] for img in valid_images)
return max_width, max_height
except:
return 1, 1
def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> list[Image]:
list_of_images: list[Image] = []
list_of_images.append(classitem.albedo)
list_of_images.append(classitem.normal)
list_of_images.append(classitem.emission)
list_of_images.append(classitem.ambient_occlusion)
list_of_images.append(classitem.height)
list_of_images.append(classitem.roughness)
return list_of_images
def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
material_image_list: list[MaterialImageList] = []
for obj in context.scene.objects:
if obj.type == 'MESH':
for mat_slot in obj.material_slots:
# Only process materials that are selected for atlas
if mat_slot.material and mat_slot.material.avatar_toolkit.include_in_atlas:
new_mat_image_item = MaterialImageList()
def get_or_create_image(image_name, replacement_name, default_color):
if image_name and image_name in bpy.data.images:
image = bpy.data.images[image_name]
else:
# Create a new image with the replacement name if it doesn't exist
if replacement_name in bpy.data.images:
image = bpy.data.images[replacement_name]
else:
image = bpy.data.images.new(
name=replacement_name, width=32, height=32, alpha=True
)
# Set the pixel data to the default color
num_pixels = 32 * 32
pixel_data = numpy.tile(numpy.array(default_color), num_pixels)
image.pixels[:] = pixel_data
# Set use_fake_user to True to prevent Blender from removing the image
image.use_fake_user = True
return image
# Albedo
albedo_name = getattr(mat_slot.material, 'texture_atlas_albedo', '')
new_mat_image_item.albedo = get_or_create_image(
albedo_name,
mat_slot.material.name + "_albedo_replacement",
[0.0, 0.0, 0.0, 1.0]
)
# Normal
normal_name = getattr(mat_slot.material, 'texture_atlas_normal', '')
new_mat_image_item.normal = get_or_create_image(
normal_name,
mat_slot.material.name + "_normal_replacement",
[0.5, 0.5, 1.0, 1.0]
)
# Emission
emission_name = getattr(mat_slot.material, 'texture_atlas_emission', '')
new_mat_image_item.emission = get_or_create_image(
emission_name,
mat_slot.material.name + "_emission_replacement",
[0.0, 0.0, 0.0, 1.0]
)
# Ambient Occlusion
ao_name = getattr(mat_slot.material, 'texture_atlas_ambient_occlusion', '')
new_mat_image_item.ambient_occlusion = get_or_create_image(
ao_name,
mat_slot.material.name + "_ambient_occlusion_replacement",
[1.0, 1.0, 1.0, 1.0]
)
# Height
height_name = getattr(mat_slot.material, 'texture_atlas_height', '')
new_mat_image_item.height = get_or_create_image(
height_name,
mat_slot.material.name + "_height_replacement",
[0.5, 0.5, 0.5, 1.0]
)
# Roughness
roughness_name = getattr(mat_slot.material, 'texture_atlas_roughness', '')
new_mat_image_item.roughness = get_or_create_image(
roughness_name,
mat_slot.material.name + "_roughness_replacement",
[1.0, 1.0, 1.0, 0.0]
)
new_mat_image_item.material = mat_slot.material
new_mat_image_item.parent_mesh = obj
material_image_list.append(new_mat_image_item)
return material_image_list
def prep_images_in_scene(context: Context) -> list[MaterialImageList]:
preped_images: list[MaterialImageList] = get_material_images_from_scene(context)
for MaterialImageClass in preped_images:
ImageList: list[Image] = MaterialImageList_to_Image_list(MaterialImageClass)
MaterialImageClass.w, MaterialImageClass.h = scale_images_to_largest(ImageList)
return preped_images
class AvatarToolKit_OT_AtlasMaterials(Operator):
bl_idname = "avatar_toolkit.atlas_materials"
bl_label = t("TextureAtlas.atlas_materials")
bl_description = t("TextureAtlas.atlas_materials_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
def execute(self, context: Context) -> set:
try:
# Get only materials that are explicitly marked for inclusion
selected_materials = [m for m in prep_images_in_scene(context)
if m.material and m.material.avatar_toolkit.include_in_atlas is True]
if not selected_materials:
self.report({'WARNING'}, t("TextureAtlas.no_materials_selected"))
return {'CANCELLED'}
packer: BinPacker = BinPacker(selected_materials)
mat_images = packer.fit()
size: list[int] = [
max([
matimg.fit.w + matimg.albedo.size[0]
for matimg in mat_images
if matimg.albedo and matimg.albedo.has_data
] or [1]),
max([
matimg.fit.h + matimg.albedo.size[1]
for matimg in mat_images
if matimg.albedo and matimg.albedo.has_data
] or [1])
]
print([matimg.fit.w + matimg.albedo.size[0] for matimg in mat_images if matimg.albedo and matimg.albedo.has_data])
atlased_mat: MaterialImageList = MaterialImageList()
for mat in mat_images:
if mat.albedo and mat.albedo.has_data:
x: int = int(mat.fit.x)
y: int = int(mat.fit.y)
w: int = int(mat.albedo.size[0])
h: int = int(mat.albedo.size[1])
for obj in bpy.data.objects:
if obj.type == 'MESH':
mesh: Mesh = obj.data
for layer in mesh.polygons:
if obj.material_slots[layer.material_index].material:
if obj.material_slots[layer.material_index].material == mat.material:
for loop_idx in layer.loop_indices:
layer_loops: MeshUVLoopLayer
for layer_loops in mesh.uv_layers:
uv_item: Float2AttributeValue = layer_loops.uv[loop_idx]
uv_item.vector.x = (uv_item.vector.x * (w / size[0])) + (x / size[0])
uv_item.vector.y = (uv_item.vector.y * (h / size[1])) + (y / size[1])
for texture_type in ["albedo", "normal", "emission", "ambient_occlusion", "height", "roughness"]:
new_image_name: str = f"Atlas_{texture_type}_{context.scene.name}_{Path(bpy.data.filepath).stem}"
print(f"Processing {texture_type} atlas image")
if new_image_name in bpy.data.images:
bpy.data.images.remove(bpy.data.images[new_image_name])
canvas: Image = bpy.data.images.new(name=new_image_name, width=int(size[0]), height=int(size[1]), alpha=True)
c_w = canvas.size[0]
canvas_pixels: list[float] = list(canvas.pixels[:])
for mat in mat_images:
image_var: Image = getattr(mat, texture_type, None)
if image_var and image_var.has_data:
x: int = int(mat.fit.x)
y: int = int(mat.fit.y)
w: int = int(image_var.size[0])
h: int = int(image_var.size[1])
image_pixels: list[float] = list(image_var.pixels[:])
print(f"Writing image \"{image_var.name}\" to canvas.")
print(f"x: \"{x}\" y: \"{y}\" w: \"{w}\" h: \"{h}\"")
for k in range(0, h):
for i in range(0, w):
for channel in range(0, 4):
canvas_index = (((k + y) * c_w) + (i + x)) * 4 + channel
image_index = ((k * w) + i) * 4 + channel
canvas_pixels[int(canvas_index)] = image_pixels[int(image_index)]
canvas.pixels[:] = canvas_pixels[:]
canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath), new_image_name + ".png"))
setattr(atlased_mat, texture_type, canvas)
#I am sorry for the amount of nodes I'm instanciating here and their values.
#This is so that the nodes look pretty in the UI, which I think looks kinda nice. - @989onan
atlased_mat.material = bpy.data.materials.new(name="Atlas_Final_"+bpy.context.scene.name+"_"+Path(bpy.data.filepath).stem)
atlased_mat.material.use_nodes = True
atlased_mat.material.node_tree.nodes.clear()
principled_node: ShaderNodeBsdfPrincipled = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
principled_node.location.x = 7.29706335067749
principled_node.location.y = 298.918212890625
output_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
output_node.location.x = 297.29705810546875
output_node.location.y = 298.918212890625
albedo_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
albedo_node.location.x = -588.6177978515625
albedo_node.location.y = 414.1948547363281
albedo_node.image = atlased_mat.albedo
emission_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
emission_node.location.x = -588.6177978515625
emission_node.location.y = -173.9259033203125
emission_node.image = atlased_mat.emission
normal_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
normal_node.location.x = -941.4189453125
normal_node.location.y = -20.8391780853271
normal_node.image = atlased_mat.normal
normal_map_node: ShaderNodeNormalMap = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeNormalMap")
normal_map_node.location.x = -545.550537109375
normal_map_node.location.y = -0.7543716430664062
roughness_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
roughness_node.location.x = -592.1703491210938
roughness_node.location.y = 206.74075317382812
roughness_node.image = atlased_mat.roughness
ambient_occlusion_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
ambient_occlusion_node.location.x = -906.4371337890625
ambient_occlusion_node.location.y = -389.9602355957031
ambient_occlusion_node.image = atlased_mat.ambient_occlusion
height_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
height_node.location.x = -1222.383056640625
height_node.location.y = -375.48406982421875
height_node.image = atlased_mat.height
atlased_mat.material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Metallic"], roughness_node.outputs["Alpha"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Roughness"], roughness_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Normal"], normal_map_node.outputs["Normal"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Emission Color"], emission_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"])
atlased_mat.material.node_tree.links.new(normal_map_node.inputs["Color"], normal_node.outputs["Color"])
# Only update selected materials for meshes
for obj in context.scene.objects:
if obj.type == 'MESH':
mesh: Mesh = obj.data
for i, mat_slot in enumerate(obj.material_slots):
if mat_slot.material and mat_slot.material.avatar_toolkit.include_in_atlas is True:
mesh.materials[i] = atlased_mat.material
self.report({'INFO'}, t("TextureAtlas.atlas_completed"))
return {"FINISHED"}
except Exception as e:
self.report({'ERROR'}, t("TextureAtlas.atlas_error"))
raise e
return {"FINISHED"}
-155
View File
@@ -1,155 +0,0 @@
import bpy
import re
from typing import List, Tuple, Optional, Set, Dict
from bpy.types import Material, Operator, Context, Object, NodeTree
from ..core.common import clean_material_names, get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
from ..core.translations import t
def textures_match(tex1: bpy.types.ImageTexture, tex2: bpy.types.ImageTexture) -> bool:
return tex1.image == tex2.image and tex1.extension == tex2.extension
def consolidate_nodes(node1: bpy.types.ShaderNodeTexImage, node2: bpy.types.ShaderNodeTexImage) -> None:
node2.color_space = node1.color_space
node2.coordinates = node1.coordinates
def copy_tex_nodes(mat1: Material, mat2: Material) -> None:
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(node_tree1: NodeTree, node_tree2: NodeTree) -> None:
for node1 in node_tree1.nodes:
if node1.type == 'TEX_IMAGE':
for node2 in node_tree2.nodes:
if (node2.type == 'TEX_IMAGE' and
node1.image == node2.image):
consolidate_nodes(node1, node2)
node2.image = node1.image
elif node1.type == 'GROUP':
if node1.node_tree and node2.node_tree:
consolidate_textures(node1.node_tree, node2.node_tree)
def color_match(col1: Tuple[float, float, float, float], col2: Tuple[float, float, float, float], tolerance: float = 0.01) -> bool:
return all(abs(c1 - c2) < tolerance for c1, c2 in zip(col1, col2))
def materials_match(mat1: Material, mat2: Material, tolerance: float = 0.01) -> bool:
if not color_match(mat1.diffuse_color, mat2.diffuse_color, tolerance):
return False
if abs(mat1.roughness - mat2.roughness) > tolerance:
return False
if mat1.node_tree and mat2.node_tree:
consolidate_textures(mat1.node_tree, mat2.node_tree)
return True
def get_base_name(name: str) -> str:
mat_match = re.match(r"^(.*)\.\d{3}$", name)
return mat_match.group(1) if mat_match else name
class AvatarToolKit_OT_CombineMaterials(Operator):
bl_idname = "avatar_toolkit.combine_materials"
bl_label = t("Optimization.combine_materials.label")
bl_description = t("Optimization.combine_materials.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def execute(self, context: Context) -> Set[str]:
armature = get_selected_armature(context)
if not armature:
self.report({'WARNING'}, t("Optimization.no_armature_selected"))
return {'CANCELLED'}
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='OBJECT')
meshes = get_all_meshes(context)
if not meshes:
self.report({'WARNING'}, t("Optimization.no_meshes_found"))
return {'CANCELLED'}
init_progress(context, 5) # 5 steps in total
update_progress(self, context, t("Optimization.consolidating_materials"))
num_combined = self.consolidate_materials(meshes)
update_progress(self, context, t("Optimization.cleaning_material_slots"))
cleaned_slots = self.clean_material_slots(meshes)
update_progress(self, context, t("Optimization.cleaning_material_names"))
cleaned_names = self.clean_material_names()
update_progress(self, context, t("Optimization.clearing_unused_data"))
removed_data_blocks = self.clear_unused_data_blocks()
update_progress(self, context, t("Optimization.finalizing"))
finish_progress(context)
self.report({'INFO'}, t("Optimization.materials_optimization_report").format(
num_combined=num_combined,
num_cleaned_slots=cleaned_slots,
num_cleaned_names=cleaned_names,
num_removed_data_blocks=removed_data_blocks
))
return {'FINISHED'}
def consolidate_materials(self, meshes: List[Object]) -> int:
mat_mapping: Dict[str, Material] = {}
num_combined: int = 0
for mesh in meshes:
for slot in mesh.material_slots:
mat: Optional[Material] = slot.material
if mat:
base_name: str = get_base_name(mat.name)
if base_name in mat_mapping:
base_mat: Material = mat_mapping[base_name]
try:
if materials_match(base_mat, mat):
consolidate_textures(base_mat.node_tree, mat.node_tree)
num_combined += 1
slot.material = base_mat
except AttributeError:
self.report({'WARNING'}, t("Optimization.material_attribute_mismatch").format(material_name=mat.name))
continue
else:
mat_mapping[base_name] = mat
return num_combined
def clean_material_slots(self, meshes: List[Object]) -> int:
cleaned_slots = 0
for obj in meshes:
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
initial_slots = len(obj.material_slots)
bpy.ops.object.material_slot_remove_unused()
cleaned_slots += initial_slots - len(obj.material_slots)
obj.select_set(False)
return cleaned_slots
def clean_material_names(self) -> int:
cleaned_names = 0
for obj in bpy.data.objects:
if obj.type == 'MESH':
result = clean_material_names(obj)
if result is not None:
cleaned_names += result
return cleaned_names
def clear_unused_data_blocks(self) -> int:
initial_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
final_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
return initial_count - final_count
-118
View File
@@ -1,118 +0,0 @@
import bpy
from ..core import common
from ..core.translations import t
import re
class AvatarToolKit_OT_CreateDigitigradeLegs(bpy.types.Operator):
bl_idname = "avatar_toolkit.create_digitigrade_legs"
bl_label = t('Tools.create_digitigrade_legs.label')
bl_description = t('Tools.create_digitigrade_legs.desc')
@classmethod
def poll(cls, context):
if(context.active_object is None):
return False
if(context.selected_editable_bones is not None):
if(len(context.selected_editable_bones) == 2):
return True
return False
def execute(self, context):
for digi0 in context.selected_editable_bones:
digi1: bpy.types.EditBone = None
digi2: bpy.types.EditBone = None
digi3: bpy.types.EditBone = None
try:
digi1 = digi0.children[0]
digi2 = digi1.children[0]
digi3 = digi2.children[0]
except:
self.report({'ERROR'}, t('Tools.digitigrade_legs.error.bone_format'))
return {'CANCELLED'}
digi4 = None
try:
digi4 = digi3.children[0]
except:
print("no toe bone. Continuing.")
digi0.select = True
digi1.select = True
digi2.select = True
digi3.select = True
if(digi4):
digi4.select = True
bpy.ops.armature.roll_clear()
bpy.ops.armature.select_all(action='DESELECT')
#creating transform for upper leg
digi0.select = True
bpy.ops.transform.create_orientation(name="Toolkit_digi0", overwrite=True)
bpy.ops.armature.select_all(action='DESELECT')
#duplicate digi0 and assign it to thigh
thigh = common.duplicatebone(digi0)
bpy.ops.armature.select_all(action='DESELECT')
#make digi2 parrallel to digi1
digi2.align_orientation(digi0)
#extrude thigh
thigh.select_tail = True
bpy.ops.armature.extrude_move(ARMATURE_OT_extrude={"forked":False},TRANSFORM_OT_translate=None)
#set new bone to calf varible
bpy.ops.armature.select_more()
calf = context.selected_bones[0]
bpy.ops.armature.select_all(action='DESELECT')
#set calf end to digi2 end
calf.tail = digi2.tail
#make copy of calf, flip it, and then align bone so that it's head is moved to match in align phase
flipedcalf = common.duplicatebone(calf)
bpy.ops.armature.select_all(action='DESELECT')
flipedcalf.select = True
bpy.ops.armature.switch_direction()
bpy.ops.armature.select_all(action='DESELECT')
flippeddigi1 = common.duplicatebone(digi1)
bpy.ops.armature.select_all(action='DESELECT')
flippeddigi1.select = True
bpy.ops.armature.switch_direction()
bpy.ops.armature.select_all(action='DESELECT')
#align flipped calf to flipped middle leg to move the head
flipedcalf.align_orientation(flippeddigi1)
flipedcalf.length = flippeddigi1.length
#assign calf tail to flipped calf head so it moves calf's tail to be out at the perfect parallelagram
calf.head = flipedcalf.tail
#delete helper bones
bpy.ops.armature.select_all(action='DESELECT')
flippeddigi1.select = True
bpy.ops.armature.delete()
bpy.ops.armature.select_all(action='DESELECT')
flipedcalf.select = True
bpy.ops.armature.delete()
bpy.ops.armature.select_all(action='DESELECT')
#reparent the foot to the new calf so it will be part of the new foot IK chain
digi3.parent = calf
#Tada! It's done! now to rename the old 3 segments that make up the old part to noik so resonite doesn't try to select them
digi0.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi0.name)+"<noik>"
digi1.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi1.name)+"<noik>"
digi2.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi2.name)+"<noik>"
#finally fully done!
self.report({'INFO'}, t('Tools.digitigrade_legs.success'))
return {'FINISHED'}
-188
View File
@@ -1,188 +0,0 @@
import bpy
from bpy.types import Operator
from bpy_extras.io_utils import ImportHelper
from ..core.importers.importer import imports, import_types
from ..core.common import remove_default_objects
from ..core.translations import t
import pathlib
import os
VRM_IMPORTER_URL = "https://github.com/saturday06/VRM_Addon_for_Blender"
class AvatarToolKit_OT_ImportAnyModel(Operator, ImportHelper):
bl_idname = 'avatar_toolkit.import_any_model'
bl_label = t('Tools.import_any_model.label')
bl_description = t('Tools.import_any_model.desc')
bl_options = {'REGISTER', 'UNDO'}
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
filter_glob: bpy.props.StringProperty(default=imports, options={'HIDDEN', 'SKIP_SAVE'})
directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
# since I wrote this myself, a bit more efficient than cats. mostly - @989onan
def execute(self, context: bpy.types.Context):
file_grouping_dict: dict[str, list[dict[str, str]]] = dict() # group our files so our importers can import them together. in the case of OBJ+MTL and others that need grouped files, this is extremely important.
remove_default_objects()
# check if we are importing multiple files
is_multi = len(self.files) > 0
if is_multi:
for file in self.files:
fullpath = os.path.join(self.directory, os.path.basename(file.name))
name = pathlib.Path(fullpath).suffix.replace(".", "")
# this makes sure our imports that should be grouped stay together.
# basically the method checks for if the first value has a lambda with the same bytecode as another lambda, then it will use that value's key (ex:"obj"<->"mtl" or "fbx"), keeping same importers together
if name not in file_grouping_dict:
file_grouping_dict[name] = []
file_grouping_dict[name].append({"name": os.path.basename(file.name)}) # emulate passing a list of files.
else:
fullpath: str = os.path.join(os.path.dirname(self.filepath), os.path.basename(self.filepath))
name = pathlib.Path(fullpath).suffix.replace(".", "")
if name not in file_grouping_dict:
file_grouping_dict[name] = []
file_grouping_dict[name].append({"name": fullpath}) # emulate passing a list of files.
# import the files together to make sure things like obj import together. This is important
for file_group_name, files in file_grouping_dict.items():
try:
# Check for VRM importer availability
if file_group_name == "vrm" and not hasattr(bpy.ops.import_scene, "vrm"):
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
return {'CANCELLED'}
if self.directory:
import_types[file_group_name](self.directory, files, self.filepath)
else:
import_types[file_group_name]("", files, self.filepath) # give an empty directory, works just fine for 90%
except AttributeError as e:
if file_group_name == "vrm":
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
else:
self.report({'ERROR'}, t('Importing.need_importer').format(extension=file_group_name))
print("Importer error:", e)
return {'CANCELLED'}
self.report({'INFO'}, t('Quick_Access.import_success'))
return {'FINISHED'}
class VRMImporterPopup(Operator):
bl_idname = "wm.vrm_importer_popup"
bl_label = "VRM Importer Not Installed"
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=300)
def draw(self, context):
layout = self.layout
layout.label(text="VRM importer plugin is not installed.")
layout.label(text="Please install it to import VRM files.")
layout.operator("wm.url_open", text="Get VRM Importer").url = VRM_IMPORTER_URL
#TODO: This needs to be done with our own MMD importer.
"""
#stolen from cats. Oh wait I made this code riiiiiiight - @989onan
class ImportMMDAnimation(bpy.types.Operator, ImportHelper):
bl_idname = 'avatar_toolkit.import_mmd_animation'
bl_label = t('Importer.mmd_anim_importer.label')
bl_description = t('Importer.mmd_anim_importer.desc')
bl_options = {'INTERNAL', 'UNDO'}
filter_glob: bpy.props.StringProperty(
default="*.vmd",
options={'HIDDEN'}
)
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
filepath: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
if common.get_armature(context) is None:
return False
return True
def execute(self, context):
# Make sure that the first layer is visible
if hasattr(context.scene, 'layers'):
context.scene.layers[0] = True
filename, extension = os.path.splitext(self.filepath)
if(extension == ".vmd"):
#A dictionary to change the current model to MMD importer compatable temporarily
bonedict = {
"chest":"UpperBody",
"neck":"Neck",
"head":"Head",
"hips":"Center",
"spine":"LowerBody",
"right_wrist":"Wrist_R",
"right_elbow":"Elbow_R",
"right_arm":"Arm_R",
"right_shoulder":"Shoulder_R",
"right_leg":"Leg_R",
"right_knee":"Knee_R",
"right_ankle":"Ankle_R",
"right_toe":"Toe_R",
"left_wrist":"Wrist_L",
"left_elbow":"Elbow_L",
"left_arm":"Arm_L",
"left_shoulder":"Shoulder_L",
"left_leg":"Leg_L",
"left_knee":"Knee_L",
"left_ankle":"Ankle_L",
"left_toe":"Toe_L"
}
armature = common.get_armature(context)
common.unselect_all()
common.Set_Mode(context, 'OBJECT')
common.unselect_all()
common.set_active(armature)
orig_names = dict()
reverse_bone_lookup = dict()
for (preferred_name, name_list) in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
for bone in armature.data.bones:
if common.simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[common.simplify_bonename(bone.name)] in bonedict:
orig_names[bonedict[reverse_bone_lookup[common.simplify_bonename(bone.name)]]] = bone.name
bone.name = bonedict[reverse_bone_lookup[common.simplify_bonename(bone.name)]]
try:
bpy.ops.mmd_tools.import_vmd(filepath=self.filepath,bone_mapper='RENAMED_BONES',use_underscore=True, dictionary='INTERNAL')
except AttributeError as e:
print("importer error was:")
print(e)
print(t('Importing.importer_search_term'))
common.open_web_after_delay_multi_threaded(delay=12, url=t('Importing.importer_search_term').format(extension = "MMD"))
self.report({'ERROR'},t('Importing.need_importer').format(extension = "MMD"))
return {'CANCELLED'}
#iterate through bones and put them back, therefore blender API will change the animation to be correct.
#this is because renaming bones fixes the animation targets in the data model.
for bone in armature.data.bones:
if common.simplify_bonename(bone.name) in orig_names:
bone.name = orig_names[common.simplify_bonename(bone.name)]
common.unselect_all()
common.Set_Mode(context, 'OBJECT')
common.unselect_all()
common.set_active(armature)
return {'FINISHED'} """
-212
View File
@@ -1,212 +0,0 @@
import numpy as np
import bpy
from typing import List, Optional, Set
from bpy.types import Operator, Context, Object
from ..core.common import fix_uv_coordinates, get_selected_armature, get_all_meshes, is_valid_armature, apply_shapekey_to_basis, has_shapekeys, select_current_armature, init_progress, update_progress, finish_progress
from ..core.translations import t
class AvatarToolkit_OT_RemoveUnusedShapekeys(bpy.types.Operator):
tolerance: bpy.props.FloatProperty(name=t("Tools.remove_unused_shapekeys.tolerance.label"), default=0.001, description=t("Tools.remove_unused_shapekeys.tolerance.desc"))
bl_idname = "avatar_toolkit.remove_unused_shapekeys"
bl_label = t("Tools.remove_unused_shapekeys.label")
bl_description = t("Tools.remove_unused_shapekeys.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and (len(get_all_meshes(context)) > 0) and (context.mode == "OBJECT")
def execute(self, context: Context) -> set[str]:
#Shamefully taken from: https://blender.stackexchange.com/a/237611
#at least I am crediting them - @989onan
for ob in get_all_meshes(context):
if not ob.data.shape_keys: continue
if not ob.data.shape_keys.use_relative: continue
kbs = ob.data.shape_keys.key_blocks
nverts = len(ob.data.vertices)
to_delete = []
# Cache locs for rel keys since many keys have the same rel key
cache = {}
locs = np.empty(3*nverts, dtype=np.float32)
for kb in kbs:
if kb == kb.relative_key: continue
kb.data.foreach_get("co", locs)
if kb.relative_key.name not in cache:
rel_locs = np.empty(3*nverts, dtype=np.float32)
kb.relative_key.data.foreach_get("co", rel_locs)
cache[kb.relative_key.name] = rel_locs
rel_locs = cache[kb.relative_key.name]
locs -= rel_locs
if (np.abs(locs) < self.tolerance).all():
to_delete.append(kb.name)
for kb_name in to_delete:
if ("-" in kb_name) or ("=" in kb_name) or ("~" in kb_name):
continue
ob.shape_key_remove(ob.data.shape_keys.key_blocks[kb_name])
return {'FINISHED'}
class AvatarToolkit_OT_ApplyShapeKey(bpy.types.Operator):
bl_idname = "avatar_toolkit.apply_shape_key"
bl_label = t("Tools.apply_shape_key.label")
bl_description = t("Tools.apply_shape_key.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and (len(get_all_meshes(context)) > 0) and (context.mode == "OBJECT") and context.view_layer.objects.active is not None and has_shapekeys(context.view_layer.objects.active)
def execute(self, context: Context) -> set[str]:
obj: bpy.types.Object = context.view_layer.objects.active
if (apply_shapekey_to_basis(context,obj,obj.active_shape_key.name,False)):
return {'FINISHED'}
else:
self.report({'ERROR'}, t("Tools.apply_shape_key.error"))
return {'FINISHED'}
class AvatarToolKit_OT_JoinAllMeshes(Operator):
bl_idname = "avatar_toolkit.join_all_meshes"
bl_label = t("Optimization.join_all_meshes.label")
bl_description = t("Optimization.join_all_meshes.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def execute(self, context: Context) -> Set[str]:
try:
self.join_all_meshes(context)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"{t('Optimization.join_error')}: {str(e)}")
return {'CANCELLED'}
def join_all_meshes(self, context: Context) -> None:
if not select_current_armature(context):
raise ValueError(t("Optimization.no_armature_selected"))
armature = get_selected_armature(context)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
meshes: List[Object] = get_all_meshes(context)
if not meshes:
raise ValueError(t("Optimization.no_meshes_found"))
init_progress(context, 5) # 5 steps in total
update_progress(self, context, t("Optimization.selecting_meshes"))
for mesh in meshes:
mesh.select_set(True)
if bpy.context.selected_objects:
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
update_progress(self, context, t("Optimization.joining_meshes"))
try:
bpy.ops.object.join()
except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.applying_transforms"))
try:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.fixing_uv_coordinates"))
bpy.ops.object.mode_set(mode='OBJECT')
fix_uv_coordinates(context)
update_progress(self, context, t("Optimization.finalizing"))
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
self.report({'INFO'}, t("Optimization.meshes_joined"))
else:
raise ValueError(t("Optimization.no_mesh_selected"))
context.view_layer.objects.active = armature
finish_progress(context)
class AvatarToolKit_OT_JoinSelectedMeshes(Operator):
bl_idname = "avatar_toolkit.join_selected_meshes"
bl_label = t("Optimization.join_selected_meshes.label")
bl_description = t("Optimization.join_selected_meshes.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.mode == 'OBJECT' and len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1
def execute(self, context: Context) -> Set[str]:
try:
self.join_selected_meshes(context)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"{t('Optimization.join_error')}: {str(e)}")
return {'CANCELLED'}
def join_selected_meshes(self, context: Context) -> None:
selected_objects: List[Object] = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
if len(selected_objects) < 2:
raise ValueError(t("Optimization.select_at_least_two_meshes"))
init_progress(context, 5) # 5 steps in total
update_progress(self, context, t("Optimization.preparing_meshes"))
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
update_progress(self, context, t("Optimization.selecting_meshes"))
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]
update_progress(self, context, t("Optimization.joining_meshes"))
try:
bpy.ops.object.join()
except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.applying_transforms"))
try:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.fixing_uv_coordinates"))
fix_uv_coordinates(context)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
self.report({'INFO'}, t("Optimization.selected_meshes_joined"))
else:
raise ValueError(t("Optimization.no_mesh_selected"))
finish_progress(context)
-396
View File
@@ -1,396 +0,0 @@
import bpy
import numpy as np
import re
from bpy.types import Operator, Context, Material, ShaderNodeTexImage, ShaderNodeGroup, Object
from ..core.translations import t
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
from ..functions.additional_tools import AvatarToolKit_OT_ConnectBones, AvatarToolKit_OT_DeleteBoneConstraints
from ..functions.armature_modifying import AvatarToolkit_OT_RemoveZeroWeightBones, AvatarToolkit_OT_MergeBonesToParents
class AvatarToolKit_OT_CleanupMesh(Operator):
bl_idname = "avatar_toolkit.cleanup_mesh"
bl_label = t("MMDOptions.cleanup_mesh.label")
bl_description = t("MMDOptions.cleanup_mesh.desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
init_progress(context, 4)
update_progress(self, context, t("MMDOptions.removing_empty_objects"))
bpy.ops.object.select_all(action='DESELECT')
for obj in context.scene.objects:
if obj.type == 'EMPTY':
obj.select_set(True)
bpy.ops.object.delete()
update_progress(self, context, t("MMDOptions.removing_unused_vertex_groups"))
for obj in get_all_meshes(context):
self.remove_unused_vertex_groups(obj)
update_progress(self, context, t("MMDOptions.removing_unused_vertices"))
for obj in get_all_meshes(context):
self.remove_unused_vertices(obj)
update_progress(self, context, t("MMDOptions.removing_empty_shape_keys"))
for obj in get_all_meshes(context):
self.remove_empty_shape_keys(obj)
finish_progress(context)
return {'FINISHED'}
def remove_unused_vertex_groups(self, obj):
vgroups = obj.vertex_groups
for vgroup in vgroups:
if not any(vgroup.index in [g.group for g in v.groups] for v in obj.data.vertices):
vgroups.remove(vgroup)
def remove_unused_vertices(self, obj):
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.delete_loose()
bpy.ops.object.mode_set(mode='OBJECT')
def remove_empty_shape_keys(self, obj):
if obj.data.shape_keys:
for key in obj.data.shape_keys.key_blocks:
if key.name != 'Basis' and all(abs(key.data[i].co[j] - obj.data.shape_keys.reference_key.data[i].co[j]) < 0.0001 for i in range(len(key.data)) for j in range(3)):
obj.shape_key_remove(key)
class AvatarToolKit_OT_OptimizeWeights(Operator):
bl_idname = "avatar_toolkit.optimize_weights"
bl_label = t("MMDOptions.optimize_weights.label")
bl_description = t("MMDOptions.optimize_weights.desc")
bl_options = {'REGISTER', 'UNDO'}
max_weights: bpy.props.IntProperty(
name=t("MMDOptions.max_weights.label"),
description=t("MMDOptions.max_weights.desc"),
default=4,
min=1,
max=8
)
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not armature:
self.report({'ERROR'}, t("MMDOptions.no_armature_selected"))
return {'CANCELLED'}
init_progress(context, 4)
update_progress(self, context, t("MMDOptions.merging_weights"))
for obj in get_all_meshes(context):
for modifier in obj.modifiers:
if modifier.type == 'ARMATURE' and modifier.object != armature:
bpy.ops.object.modifier_apply(modifier=modifier.name)
update_progress(self, context, t("MMDOptions.removing_zero_weight_bones"))
bpy.ops.avatar_toolkit.remove_zero_weight_bones('EXEC_DEFAULT')
update_progress(self, context, t("MMDOptions.limiting_vertex_weights"))
for obj in get_all_meshes(context):
self.limit_vertex_weights(obj)
update_progress(self, context, t("MMDOptions.weight_optimization_complete"))
finish_progress(context)
return {'FINISHED'}
def limit_vertex_weights(self, obj):
for v in obj.data.vertices:
if len(v.groups) > self.max_weights:
sorted_groups = sorted(v.groups, key=lambda g: g.weight, reverse=True)
for g in sorted_groups[self.max_weights:]:
obj.vertex_groups[g.group].remove([v.index])
class AvatarToolKit_OT_OptimizeArmature(Operator):
bl_idname = "avatar_toolkit.optimize_armature"
bl_label = t("MMDOptions.optimize_armature.label")
bl_description = t("MMDOptions.optimize_armature.desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not armature:
self.report({'ERROR'}, t("MMDOptions.no_armature_selected"))
return {'CANCELLED'}
init_progress(context, 9)
# Ensure proper object selection and mode
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
# Store initial transforms
bpy.ops.object.mode_set(mode='EDIT')
initial_transforms = {}
for bone in armature.data.edit_bones:
initial_transforms[bone.name] = {
'head': bone.head.copy(),
'tail': bone.tail.copy(),
'roll': bone.roll,
'matrix': bone.matrix.copy(),
'parent': bone.parent.name if bone.parent else None
}
update_progress(self, context, t("MMDOptions.deleting_bone_constraints"))
bpy.ops.avatar_toolkit.delete_bone_constraints('EXEC_DEFAULT')
update_progress(self, context, t("MMDOptions.merging_bones_to_parents"))
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
try:
bpy.ops.avatar_toolkit.merge_bones_to_parents('EXEC_DEFAULT')
except RuntimeError as e:
self.report({'WARNING'}, f"Failed to merge bones to parents: {str(e)}")
update_progress(self, context, t("MMDOptions.reordering_bones"))
self.reorder_bones(context, armature)
update_progress(self, context, t("MMDOptions.fixing_armature_names"))
self.fix_armature_names(armature)
update_progress(self, context, t("MMDOptions.renaming_bones"))
self.rename_bones(armature)
# Restore original bone transforms
bpy.ops.object.mode_set(mode='EDIT')
for bone_name, transform in initial_transforms.items():
if bone_name in armature.data.edit_bones:
bone = armature.data.edit_bones[bone_name]
bone.head = transform['head']
bone.tail = transform['tail']
bone.roll = transform['roll']
bone.matrix = transform['matrix']
update_progress(self, context, t("MMDOptions.armature_optimization_complete"))
# Ensure we end in object mode with proper selection
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
finish_progress(context)
return {'FINISHED'}
def reorder_bones(self, context: Context, armature: bpy.types.Object):
def sort_bones(bone):
children = sorted(bone.children, key=lambda b: b.name)
for child in children:
sort_bones(child)
bpy.ops.object.mode_set(mode='EDIT')
root_bones = [bone for bone in armature.data.edit_bones if not bone.parent]
for root_bone in sorted(root_bones, key=lambda b: b.name):
sort_bones(root_bone)
def fix_armature_names(self, armature):
for bone in armature.data.bones:
fixed_name = self.get_fixed_bone_name(bone.name)
if fixed_name != bone.name:
bone.name = fixed_name
def get_fixed_bone_name(self, name):
name = name.replace(' ', '_')
name = re.sub(r'[^\w]', '', name)
return name
def rename_bones(self, armature):
for bone in armature.data.bones:
new_name = self.get_standardized_bone_name(bone.name)
if new_name != bone.name:
bone.name = new_name
def get_standardized_bone_name(self, name):
if 'left' in name.lower():
return f"Left_{name}"
elif 'right' in name.lower():
return f"Right_{name}"
return name
def bake_mmd_colors(node_base_tex: ShaderNodeTexImage, node_mmd_shader: ShaderNodeGroup):
ambient_color_input = node_mmd_shader.inputs.get("Ambient Color")
diffuse_color_input = node_mmd_shader.inputs.get("Diffuse Color")
if not ambient_color_input or not diffuse_color_input:
return node_base_tex, None
ambient_color = np.array(ambient_color_input.default_value[:3])
diffuse_color = np.array(diffuse_color_input.default_value[:3])
mmd_color = np.clip(ambient_color + diffuse_color * 0.6, 0, 1)
if not node_base_tex or not node_base_tex.image:
principled_base_color = np.append(mmd_color, 1)
return None, principled_base_color
base_tex_image = node_base_tex.image
if not base_tex_image.pixels:
return node_base_tex, None
if base_tex_image.colorspace_settings.name == 'sRGB':
is_small_mask = mmd_color < 0.0031308
mmd_color[is_small_mask] = np.where(mmd_color[is_small_mask] < 0.0, 0, mmd_color[is_small_mask] * 12.92)
is_large_mask = np.invert(is_small_mask)
mmd_color[is_large_mask] = (mmd_color[is_large_mask] ** (1.0 / 2.4)) * 1.055 - 0.055
pixels = np.array(base_tex_image.pixels).reshape((-1, 4))
pixels[:, :3] *= mmd_color
baked_image = bpy.data.images.new(base_tex_image.name + "MMDCatsBaked",
width=base_tex_image.size[0],
height=base_tex_image.size[1],
alpha=True)
baked_image.filepath = bpy.path.abspath("//" + base_tex_image.name + ".png")
baked_image.file_format = 'PNG'
baked_image.colorspace_settings.name = base_tex_image.colorspace_settings.name
baked_image.pixels = pixels.flatten()
node_base_tex.image = baked_image
if bpy.data.is_saved:
baked_image.save()
return node_base_tex, None
def add_principled_shader(material: Material, bake_mmd=True):
node_tree = material.node_tree
nodes = node_tree.nodes
links = node_tree.links
principled_shader = nodes.new(type="ShaderNodeBsdfPrincipled")
principled_shader.label = "Cats Export Shader"
principled_shader.location = (501, -500)
output_shader = nodes.new(type="ShaderNodeOutputMaterial")
output_shader.label = "Cats Export"
output_shader.location = (801, -500)
links.new(principled_shader.outputs["BSDF"], output_shader.inputs["Surface"])
node_base_tex = nodes.get("mmd_base_tex") or next((n for n in nodes if n.type == 'TEX_IMAGE'), None)
node_mmd_shader = nodes.get("mmd_shader")
if node_mmd_shader and bake_mmd:
node_base_tex, principled_base_color = bake_mmd_colors(node_base_tex, node_mmd_shader)
else:
principled_base_color = None
if node_base_tex and node_base_tex.image:
links.new(node_base_tex.outputs["Color"], principled_shader.inputs["Base Color"])
links.new(node_base_tex.outputs["Alpha"], principled_shader.inputs["Alpha"])
elif principled_base_color is not None:
principled_shader.inputs["Base Color"].default_value = principled_base_color
principled_shader.inputs["Specular IOR Level"].default_value = 0
principled_shader.inputs["Roughness"].default_value = 0.9
principled_shader.inputs["Sheen Tint"].default_value = (1.0, 1.0, 1.0, 1.0)
principled_shader.inputs["Coat Roughness"].default_value = 0
principled_shader.inputs["IOR"].default_value = 1.45
# Handle transparency
if material.blend_method != 'OPAQUE':
principled_shader.inputs["Alpha"].default_value = material.alpha_threshold
material.blend_method = 'CLIP'
def fix_mmd_shader(material: Material):
mmd_shader_node = material.node_tree.nodes.get("mmd_shader")
if mmd_shader_node:
reflect_input = mmd_shader_node.inputs.get("Reflect")
if reflect_input:
reflect_input.default_value = 1
def fix_vrm_shader(material: Material):
nodes = material.node_tree.nodes
is_vrm_mat = False
for node in nodes:
if hasattr(node, 'node_tree') and 'MToon_unversioned' in node.node_tree.name:
node.location[0] = 200
node.inputs['ReceiveShadow_Texture_alpha'].default_value = -10000
node.inputs['ShadeTexture'].default_value = (1.0, 1.0, 1.0, 1.0)
node.inputs['Emission_Texture'].default_value = (0.0, 0.0, 0.0, 0.0)
node.inputs['SphereAddTexture'].default_value = (0.0, 0.0, 0.0, 0.0)
node_input = node.inputs.get('NomalmapTexture') or node.inputs.get('NormalmapTexture')
node_input.default_value = (1.0, 1.0, 1.0, 1.0)
is_vrm_mat = True
break
if is_vrm_mat:
nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture']
if 'HAIR' in material.name:
nodes_to_keep.append('SphereAddTexture')
for node in nodes:
if ('RGB' in node.name or 'Value' in node.name or 'Image Texture' in node.name or
'UV Map' in node.name or 'Mapping' in node.name):
if node.label not in nodes_to_keep:
material.node_tree.links = [link for link in material.node_tree.links
if not (link.from_node == node or link.to_node == node)]
class AvatarToolKit_OT_ConvertMaterials(Operator):
bl_idname = "avatar_toolkit.convert_materials"
bl_label = t("MMDOptions.convert_materials.label")
bl_description = t("MMDOptions.convert_materials.desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
meshes = get_all_meshes(context)
init_progress(context, len(meshes))
for obj in meshes:
update_progress(self, context, t("MMDOptions.converting_materials").format(name=obj.name))
self.convert_materials_for_mesh(obj)
finish_progress(context)
return {'FINISHED'}
def convert_materials_for_mesh(self, mesh: Object):
for mat_slot in mesh.material_slots:
if mat_slot.material:
mat = mat_slot.material
mat.use_nodes = True
# Add Principled BSDF shader
add_principled_shader(mat)
# Fix MMD shader if present
fix_mmd_shader(mat)
# Fix VRM shader if present
fix_vrm_shader(mat)
# Clean up unused nodes
self.clean_unused_nodes(mat)
def clean_unused_nodes(self, material: Material):
nodes = material.node_tree.nodes
links = material.node_tree.links
used_nodes = set()
output_node = next((n for n in nodes if n.type == 'OUTPUT_MATERIAL'), None)
if output_node:
self.traverse_node_tree(output_node, used_nodes)
for node in nodes:
if node not in used_nodes:
nodes.remove(node)
def traverse_node_tree(self, node, used_nodes):
used_nodes.add(node)
for input in node.inputs:
for link in input.links:
if link.from_node not in used_nodes:
self.traverse_node_tree(link.from_node, used_nodes)
+120
View File
@@ -0,0 +1,120 @@
import bpy
import numpy as np
from bpy.types import Operator, Context, Object
from typing import List
from ..core.translations import t
from ..core.common import (
get_active_armature,
get_all_meshes,
apply_pose_as_rest,
apply_armature_to_mesh,
apply_armature_to_mesh_with_shapekeys,
validate_armature
)
class AvatarToolkit_OT_StartPoseMode(Operator):
bl_idname = 'avatar_toolkit.start_pose_mode'
bl_label = t("Quick_Access.start_pose_mode.label")
bl_description = t("Quick_Access.start_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
if not armature or context.mode == "POSE":
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> set[str]:
armature = get_active_armature(context)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
bpy.ops.object.mode_set(mode='POSE')
return {'FINISHED'}
class AvatarToolkit_OT_StopPoseMode(Operator):
bl_idname = 'avatar_toolkit.stop_pose_mode'
bl_label = t("Quick_Access.stop_pose_mode.label")
bl_description = t("Quick_Access.stop_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return get_active_armature(context) and context.mode == "POSE"
def execute(self, context: Context) -> set[str]:
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator):
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
bl_label = t("Quick_Access.apply_pose_as_shapekey.label")
bl_description = t("Quick_Access.apply_pose_as_shapekey.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
if not armature or context.mode != 'POSE':
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context):
armature_obj = get_active_armature(context)
mesh_objects = get_all_meshes(context)
for mesh_obj in mesh_objects:
if not mesh_obj.data:
continue
if not mesh_obj.data.shape_keys:
mesh_obj.shape_key_add(name='Basis')
new_shape = mesh_obj.shape_key_add(name='Pose_Shapekey', from_mix=False)
depsgraph = context.evaluated_depsgraph_get()
eval_mesh = mesh_obj.evaluated_get(depsgraph)
for i, v in enumerate(eval_mesh.data.vertices):
new_shape.data[i].co = v.co.copy()
bpy.ops.pose.select_all(action='SELECT')
bpy.ops.pose.transforms_clear()
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t('Tools.apply_pose_as_rest.success'))
return {'FINISHED'}
class AvatarToolkit_OT_ApplyPoseAsRest(Operator):
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
bl_label = t("Quick_Access.apply_pose_as_rest.label")
bl_description = t("Quick_Access.apply_pose_as_rest.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
if not armature or context.mode != "POSE":
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context):
if not apply_pose_as_rest(
context=context,
armature_obj=get_active_armature(context),
meshes=get_all_meshes(context)
):
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
return {'CANCELLED'}
self.report({'INFO'}, t("Tools.apply_pose_as_rest.success"))
return {'FINISHED'}
-308
View File
@@ -1,308 +0,0 @@
import bpy
from typing import List, TypedDict, Any
from bpy.types import Operator, Context, Object
from ..core.common import get_selected_armature, is_valid_armature, select_current_armature, get_all_meshes
from ..core.translations import t
class meshEntry(TypedDict):
mesh: Object
shapekeys: list[str]
vertices: int
cur_vertex_pass: int
class AvatarToolKit_OT_RemoveDoublesSafelyAdvanced(Operator):
bl_idname = "avatar_toolkit.remove_doubles_safely_advanced"
bl_label = t("Optimization.remove_doubles_safely_advanced.label")
bl_description = t("Optimization.remove_doubles_safely_advanced.desc")
bl_options = {'REGISTER', 'UNDO'}
merge_distance: bpy.props.FloatProperty(default=0.0001)
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def draw(self, context):
layout = self.layout
layout.label(text="This process may take a long time.")
layout.label(text="Blender may seem unresponsive during this operation.")
layout.label(text="Please be patient and wait for it to complete.")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context):
bpy.ops.avatar_toolkit.remove_doubles_safely('INVOKE_DEFAULT', advanced=True, merge_distance=self.merge_distance)
return {'RUNNING_MODAL'}
class AvatarToolKit_OT_RemoveDoublesSafely(Operator):
bl_idname = "avatar_toolkit.remove_doubles_safely"
bl_label = t("Optimization.remove_doubles_safely.label")
bl_description = t("Optimization.remove_doubles_safely.desc")
bl_options = {'REGISTER', 'UNDO'}
objects_to_do: list[meshEntry] = []
merge_distance: bpy.props.FloatProperty(default=0.0001)
advanced: bpy.props.BoolProperty(default=False)
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def execute(self, context: Context) -> set:
if not select_current_armature(context):
self.report({'WARNING'}, t("Optimization.no_armature_selected"))
return {'CANCELLED'}
armature = get_selected_armature(context)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
objects: List[Object] = get_all_meshes(context)
self.objects_to_do = []
for mesh in objects:
if mesh.data.name not in [stored_object["mesh"].data.name for stored_object in self.objects_to_do]:
print("setting up data for object" + mesh.name)
mesh_shapekeys = {"mesh":mesh,"shapekeys":[],"vertices":0,"cur_vertex_pass":0}
mesh_data: bpy.types.Mesh = mesh.data
shape: bpy.types.ShapeKey = None
mesh_shapekeys["vertices"] = len(mesh_data.vertices)
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
if mesh_data.shape_keys:
for shape in mesh_data.shape_keys.key_blocks:
mesh_shapekeys["shapekeys"].append(shape.name)
if self.advanced:
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = mesh
bpy.ops.object.select_all(action='DESELECT')
print("queued data for "+mesh.name+" is: ")
print(mesh_shapekeys)
self.objects_to_do.append(mesh_shapekeys)
return {'FINISHED'}
def invoke(self, context: Context, event: bpy.types.Event) -> set:
print("==================")
print("==================")
print("==================")
print("==================")
print("starting modal execution of merge doubles safely.")
print("==================")
print("==================")
print("==================")
print("==================")
self.execute(context)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modify_mesh(self, context: Context, mesh: meshEntry):
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
mesh_data: bpy.types.Mesh = mesh["mesh"].data
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
for index, point in enumerate(mesh["mesh"].active_shape_key.points):
if point.co.xyz != mesh_data.shape_keys.key_blocks[0].points[index].co.xyz:
mesh_data.vertices[index].select = True
print("shapekey has a moved vertex at index \""+str(index)+"\", excluding from simple double merging!")
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
print("finished shapekey basic.")
def modify_mesh_advanced(self, context: Context, mesh_entry: meshEntry):
final_merged_vertex_group: list[int] = []
initialized_final: bool = False
for shapekey_name in mesh_entry["shapekeys"]:
mesh = mesh_entry["mesh"]
#make a copy to do double merge testing on for the current vertex
context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = mesh
mesh_data: bpy.types.Mesh = mesh.data
vertices_original: dict[int,Any] = {}
original_count: int = len(mesh_data.vertices)
mesh.select_set(True)
mesh.active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekey_name)
bpy.ops.object.duplicate()
bpy.ops.object.shape_key_move(type='TOP')
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.shape_key_remove(all=True, apply_mix=False)
mesh = context.view_layer.objects.active
mesh.name = shapekey_name+"_object_is_"+mesh.name
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
mesh.select_set(True)
context.view_layer.objects.active = mesh
mesh_data: bpy.types.Mesh = mesh.data
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
for index, merged_point in enumerate(mesh_data.vertices):
vertices_original[index] = merged_point.co.xyz
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
select_target_vertex = [False]*len(mesh_data.vertices)
try:
select_target_vertex[mesh_entry["cur_vertex_pass"]] = True
except:
bpy.ops.object.delete() #remove our double merge testing object for this shapekey, since we merged doubles on it, it will be useless.
return True
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",select_target_vertex)
bpy.ops.object.mode_set(mode='EDIT')
for i in range(0,20): #for some reason, if using merge to unselected on a vertex, the vertex will only merge to 1 other vertex. so we gotta spam it to fix it.
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance, use_unselected=True, use_sharp_edge_from_normals=False)
bpy.ops.object.mode_set(mode='OBJECT')
merged_vertices: list[int] = []
mesh_data_vertices: dict[int,Any] = {}
for idx,vertex in enumerate(mesh_data.vertices):
mesh_data_vertices[idx] = vertex.co.xyz
#I'm loosing my mind with indices because I cannot keep so many numbers in my head. I will have to use 2 pointers
# yes this can be simplified more, but the mountains of errors with using a normal for statement are making me
# loose my mind. This is hard. - @989onan
#Below is the magic that determines whether or not vertices were merged and then puts the vertices
#that were merged into a list. - @989onan
i = 0
j = 0
while(i<len(vertices_original)):
if j+1 > len(mesh_data.vertices):
merged_vertices.append(i)
j = j-1
elif mesh_data.vertices[j].co.xyz != vertices_original[i]:
merged_vertices.append(i)
j = j-1
elif vertices_original[i] == vertices_original[mesh_entry["cur_vertex_pass"]]:
merged_vertices.append(i)
i = i+1
j = j+1
#give our final set of points some inital data. we're looking for points that are merged on every shape key (and therefore appear in every version of merged_vertices).
# If we initialize the array with points from the first version of merged_vertices, then we can remove the vertices from final that don't get merged from
#every future version of merged_vertices with the "if merged_point not in merged_vertices:" code.
if initialized_final == False:
for point in merged_vertices:
final_merged_vertex_group.append(point)
initialized_final = True
#iterate through a copy of final vertex groups to prevent crash. If a vertex was merged before, but didn't merge in this vertex,
# then the vertex shouldn't be merged because it moves away from the vertex we are double merging now (ex: bottom of mouth moving away from top when opening on a shapekey) - @989onan
for merged_point in final_merged_vertex_group[:]:
if merged_point not in merged_vertices:
final_merged_vertex_group.remove(merged_point)
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.delete() #remove our double merge testing object for this shapekey, since we merged doubles on it, it will be useless.
context.view_layer.objects.active = mesh_entry["mesh"]
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = mesh_entry["mesh"]
mesh_entry["mesh"].select_set(True)
original_mesh_data: bpy.types.Mesh = mesh_entry["mesh"].data
select_target_group = [False]*len(original_mesh_data.vertices)
for vertex_index in final_merged_vertex_group:
select_target_group[vertex_index] = True
bpy.ops.object.mode_set(mode='OBJECT')
original_mesh_data.vertices.foreach_set("select",select_target_group)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance, use_unselected=False, use_sharp_edge_from_normals=False)
bpy.ops.object.mode_set(mode='OBJECT')
original_mesh_data.vertices.foreach_set("select",[False]*len(original_mesh_data.vertices))
print("finished advanced merge doubles for single vertex at index: "+str(mesh_entry["cur_vertex_pass"]))
return not (len(final_merged_vertex_group) > 1)
def modal(self, context: Context, event: bpy.types.Event) -> set:
if len(self.objects_to_do) > 0:
bpy.ops.object.select_all(action='DESELECT')
mesh: meshEntry = self.objects_to_do[0]
mesh_data: bpy.types.Mesh = mesh["mesh"].data
if (len(mesh['shapekeys']) > 0) and (not self.advanced):
shapekeyname: str = mesh['shapekeys'].pop(0)
target_shapekey: int = mesh_data.shape_keys.key_blocks.find(shapekeyname)
mesh["mesh"].active_shape_key_index = target_shapekey
print("doing shapekey \""+shapekeyname+"\" on mesh \""+mesh['mesh'].name+"\".")
self.modify_mesh(context, mesh)
elif not (mesh_data.shape_keys):
print("doing mesh with no shapekeys named \""+mesh['mesh'].name+"\".")
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance,use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
self.objects_to_do.pop(0)
elif (not (mesh["cur_vertex_pass"] > mesh["vertices"])) and self.advanced:
print("doing a merge by single vertex index at index "+str(mesh["cur_vertex_pass"]))
if self.modify_mesh_advanced(context, mesh):
mesh["cur_vertex_pass"] = mesh["cur_vertex_pass"]+1
else:
print("finishing double merge object.")
if not self.advanced:
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance,use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
self.objects_to_do.pop(0)
else:
self.report({'INFO'}, t("Optimization.remove_doubles_completed"))
print("finishing modal execution of merge doubles safely.")
return {'FINISHED'}
return {'RUNNING_MODAL'}
-114
View File
@@ -1,114 +0,0 @@
import bpy
from typing import List, Optional
import re
from bpy.types import Operator, Context, Object
from ..core.dictionaries import bone_names
from ..core.common import get_selected_armature, simplify_bonename, is_valid_armature
from ..core.translations import t
class AvatarToolKit_OT_ConvertToResonite(Operator):
bl_idname = 'avatar_toolkit.convert_to_resonite'
bl_label = t('Tools.convert_to_resonite.label')
bl_description = t('Tools.convert_to_resonite.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def execute(self, context: Context) -> set:
armature = get_selected_armature(context)
if not armature:
self.report({'WARNING'}, t("Tools.no_armature_selected"))
return {'CANCELLED'}
translate_bone_fails = 0
untranslated_bones = set()
reverse_bone_lookup = dict()
for (preferred_name, name_list) in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
resonite_translations = {
'hips': "Hips",
'spine': "Spine",
'chest': "Chest",
'neck': "Neck",
'head': "Head",
'left_eye': "Eye.L",
'right_eye': "Eye.R",
'right_leg': "UpperLeg.R",
'right_knee': "Calf.R",
'right_ankle': "Foot.R",
'right_toe': 'Toes.R',
'right_shoulder': "Shoulder.R",
'right_arm': "UpperArm.R",
'right_elbow': "ForeArm.R",
'right_wrist': "Hand.R",
'left_leg': "UpperLeg.L",
'left_knee': "Calf.L",
'left_ankle': "Foot.L",
'left_toe': "Toes.L",
'left_shoulder': "Shoulder.L",
'left_arm': "UpperArm.L",
'left_elbow': "ForeArm.L",
'left_wrist': "Hand.R",
'pinkie_1_l': "pinkie1.L",
'pinkie_2_l': "pinkie2.L",
'pinkie_3_l': "pinkie3.L",
'ring_1_l': "ring1.L",
'ring_2_l': "ring2.L",
'ring_3_l': "ring3.L",
'middle_1_l': "middle1.L",
'middle_2_l': "middle2.L",
'middle_3_l': "middle3.L",
'index_1_l': "index1.L",
'index_2_l': "index2.L",
'index_3_l': "index3.L",
'thumb_1_l': "thumb1.L",
'thumb_2_l': "thumb2.L",
'thumb_3_l': "thumb3.L",
'pinkie_1_r': "pinkie1.R",
'pinkie_2_r': "pinkie2.R",
'pinkie_3_r': "pinkie3.R",
'ring_1_r': "ring1.R",
'ring_2_r': "ring2.R",
'ring_3_r': "ring3.R",
'middle_1_r': "middle1.R",
'middle_2_r': "middle2.R",
'middle_3_r': "middle3.R",
'index_1_r': "index1.R",
'index_2_r': "index2.R",
'index_3_r': "index3.R",
'thumb_1_r': "thumb1.R",
'thumb_2_r': "thumb2.R",
'thumb_3_r': "thumb3.R"
}
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",bone.name) #remove "NOIK" from bones before translating again, in case an update was done that fixes a translation.
for bone in armature.data.bones:
if simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[simplify_bonename(bone.name)] in resonite_translations:
bone.name = resonite_translations[reverse_bone_lookup[simplify_bonename(bone.name)]]
else:
untranslated_bones.add(bone.name)
bone.name = bone.name+"<noik>"
translate_bone_fails += 1
bpy.ops.object.mode_set(mode='OBJECT')
if translate_bone_fails > 0:
self.report({'INFO'}, t("Tools.bones_translated_with_fails").format(translate_bone_fails=translate_bone_fails))
else:
self.report({'INFO'}, t("Tools.bones_translated_success"))
return {'FINISHED'}
-175
View File
@@ -1,175 +0,0 @@
# This code is heavily based on the Rigify-Move-DEF by NyankoNyan (https://github.com/NyankoNyan/Rigify-Move-DEF), which is licensed under the MIT License. We just heavily improve the code and add some new features.
import bpy
from ..core.common import get_selected_armature, is_valid_armature
from ..core.translations import t
from bpy.types import Operator, Context
class AvatarToolKit_OT_ConvertRigifyToUnity(Operator):
bl_idname = "avatar_toolkit.convert_rigify_to_unity"
bl_label = t("Tools.convert_rigify_to_unity.label")
bl_description = t("Tools.convert_rigify_to_unity.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and "DEF-spine" in armature.data.bones
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not armature:
self.report({'ERROR'}, t("Tools.no_armature_selected"))
return {'CANCELLED'}
self.move_def_bones(armature)
self.rename_bones_for_unity(armature)
if context.scene.merge_twist_bones:
self.handle_twist_bones(armature)
self.report({'INFO'}, t("Tools.convert_rigify_to_unity.success"))
return {'FINISHED'}
def move_def_bones(self, armature):
remap = self.get_org_remap(armature)
remap.update(self.get_special_remap())
remove_bones_in_chain = [
'DEF-upper_arm.L.001', 'DEF-forearm.L.001',
'DEF-upper_arm.R.001', 'DEF-forearm.R.001',
'DEF-thigh.L.001', 'DEF-shin.L.001',
'DEF-thigh.R.001', 'DEF-shin.R.001'
]
transform_copies = self.get_transform_copies(armature)
# Add missing constraints
bpy.ops.object.mode_set(mode='POSE')
for bone_name in transform_copies:
bone = armature.pose.bones[bone_name]
org_name = 'ORG-' + self.get_proto_name(bone_name)
if org_name in armature.pose.bones:
constraint = bone.constraints.new('COPY_TRANSFORMS')
constraint.target = armature
constraint.subtarget = org_name
constr_count = len(bone.constraints)
if constr_count > 1:
bone.constraints.move(constr_count-1, 0)
# Apply new parents
bpy.ops.object.mode_set(mode='EDIT')
for remap_key in remap:
if remap_key in armature.data.edit_bones and remap[remap_key] in armature.data.edit_bones:
armature.data.edit_bones[remap_key].parent = armature.data.edit_bones[remap[remap_key]]
# Remove extra bones in chains
bpy.ops.object.mode_set(mode='OBJECT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
armature.data.bones[bone_name].use_deform = False
bpy.ops.object.mode_set(mode='EDIT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
remove_bone = armature.data.edit_bones[bone_name]
parent_bone = remove_bone.parent
parent_bone.tail = remove_bone.tail
retarget_bones = list(remove_bone.children)
for bone in retarget_bones:
bone.parent = parent_bone
armature.data.edit_bones.remove(remove_bone)
def rename_bones_for_unity(self, armature):
unity_bone_names = {
"DEF-spine": "Hips",
"DEF-spine.001": "Spine",
"DEF-spine.002": "Chest",
"DEF-spine.003": "UpperChest",
"DEF-neck": "Neck",
"DEF-head": "Head",
"DEF-shoulder.L": "LeftShoulder",
"DEF-upper_arm.L": "LeftUpperArm",
"DEF-forearm.L": "LeftLowerArm",
"DEF-hand.L": "LeftHand",
"DEF-shoulder.R": "RightShoulder",
"DEF-upper_arm.R": "RightUpperArm",
"DEF-forearm.R": "RightLowerArm",
"DEF-hand.R": "RightHand",
"DEF-thigh.L": "LeftUpperLeg",
"DEF-shin.L": "LeftLowerLeg",
"DEF-foot.L": "LeftFoot",
"DEF-toe.L": "LeftToes",
"DEF-thigh.R": "RightUpperLeg",
"DEF-shin.R": "RightLowerLeg",
"DEF-foot.R": "RightFoot",
"DEF-toe.R": "RightToes"
}
for old_name, new_name in unity_bone_names.items():
bone = armature.pose.bones.get(old_name)
if bone:
bone.name = new_name
def handle_twist_bones(self, armature):
twist_bones = [
("DEF-upper_arm_twist.L", "DEF-upper_arm.L"),
("DEF-upper_arm_twist.R", "DEF-upper_arm.R"),
("DEF-forearm_twist.L", "DEF-forearm.L"),
("DEF-forearm_twist.R", "DEF-forearm.R"),
("DEF-thigh_twist.L", "DEF-thigh.L"),
("DEF-thigh_twist.R", "DEF-thigh.R")
]
bpy.ops.object.mode_set(mode='EDIT')
for twist_bone, parent_bone in twist_bones:
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
twist = armature.data.edit_bones[twist_bone]
parent = armature.data.edit_bones[parent_bone]
parent.tail = twist.tail
for child in twist.children:
child.parent = parent
armature.data.edit_bones.remove(twist)
bpy.ops.object.mode_set(mode='OBJECT')
def get_org_remap(self, armature):
remap = {}
for bone in armature.data.bones:
if self.is_def_bone(bone.name):
name = self.get_proto_name(bone.name)
parent = bone.parent
while parent:
parent_name = self.get_proto_name(parent.name)
if parent_name != name:
if ('DEF-' + parent_name) in armature.data.bones:
remap[bone.name] = 'DEF-' + parent_name
break
parent = parent.parent
return remap
def get_special_remap(self):
return {
'DEF-thigh.L': 'DEF-pelvis.L',
'DEF-thigh.R': 'DEF-pelvis.R',
'DEF-upper_arm.L': 'DEF-shoulder.L',
'DEF-upper_arm.R': 'DEF-shoulder.R',
}
def get_transform_copies(self, armature):
result = []
for bone in armature.pose.bones:
if self.is_def_bone(bone.name) and not self.has_transform_copies(bone):
result.append(bone.name)
return result
def has_transform_copies(self, bone):
return any(constraint.type == 'COPY_TRANSFORMS' for constraint in bone.constraints)
def is_def_bone(self, bone_name):
return bone_name.startswith('DEF-')
def is_org_bone(self, bone_name):
return bone_name.startswith('ORG-')
def get_proto_name(self, bone_name):
if self.is_def_bone(bone_name) or self.is_org_bone(bone_name):
return bone_name[4:]
return bone_name
-295
View File
@@ -1,295 +0,0 @@
from typing import TypedDict
import bpy
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
import bmesh
import numpy as np
import math
from ..core.translations import t
class GenerateLoopTreeResult(TypedDict):
tree: dict[str, set[str]]
selected_loops: dict[str,list[int]]
selected_verts: dict[str,int]
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
bl_label = t("avatar_toolkit.align_uv_edges_to_target.label")
bl_description = t("avatar_toolkit.align_uv_edges_to_target.desc")
bl_options = {'REGISTER', 'UNDO'}
#all selected objects need to be meshes for this to work - @989onan
@classmethod
def poll(cls, context: Context):
if not ((context.view_layer.objects.active is not None) and (len(context.view_layer.objects.selected) > 0)):
return False
if context.mode != "EDIT_MESH":
return False
for obj in context.view_layer.objects.selected:
if obj.type != "MESH":
return False
if not context.space_data:
return False
if not context.space_data.show_uvedit:
return False
if context.scene.tool_settings.use_uv_select_sync:
return False
return True
def execute(self, context: Context):
target: str = context.view_layer.objects.active.name #The object which we want to align every other selected object's selected UV vertex line to
sources: list[str] = [i.name for i in context.view_layer.objects.selected] #The objects which we want to align their selected UV lines to the target's UV line
prev_mode: str = bpy.context.object.mode
bpy.ops.object.mode_set(mode='OBJECT')
def generate_loop_tree(obj_name: str) -> GenerateLoopTreeResult:
print("Finding selected line for: \""+obj_name+"\"!")
vert_target_loops: dict[str,list[int]] = {}
vert_target_verts: dict[str,int] = {}
me: Mesh = bpy.data.objects[obj_name].data
uv_lay: MeshUVLoopLayer = me.uv_layers.active
bm: bmesh.types.BMesh = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
# To explain:
# So loops in UV maps are X polygons that make up a face (So a MeshLoop represent a face and each vertex on that face is in order)
#
# For some preknowledge:
# When a mesh is UV unwrapped, if a vertice is shared by two different faces on the model in the viewport and the vertice of both faces are in
# the same position on the UV map, then it considers it one point and the user can move it
# (is why the uv map doesn't split apart when you try to move a vertex because that would be annoying)
#
# The problem:
# The problem is that the data for whether the uv corners of two faces that share a vertex physically being connected and selected as one vertex on the uv map does not exist
# Though thankfully, blender forcibly (whether you like it or not) merges vertices of a uv map if the vertex of two different faces are actually shared in the UI,
# allowing for the moving of vertices of 4 faces connected by a single vertex. Behavior every normal blender user is familiar with.
#
# The solution
# We can use this to our advantage, by finding vertices on the uv map that share the same coridinate as another vertex that is also selected.
# that way we can group each pair shared in a line as the same vertex, and identify the line using these pairs and using the data that says for certain
# that two vertices share the same face loop, and therefore are connected.
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
for k,i in enumerate(uv_lay.vertex_selection): #go through the selected vertices on object.
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False): #filter out vertices that are hidden from UV port
key = np.array(uv_lay.uv[k].vector[:])
key = key.round(decimals=5) #make a key that is the position of a selected vertex
if str(key) not in vert_target_loops:
vert_target_loops[str(key)] = [] #if the vertex's position is not a list yet, add it.
vert_target_loops[str(key)].append(k) #Basically, group vertices based on their position on a UV map as a list.
vert_target_verts[str(key)] = me.loops[k].vertex_index #associate the index of the physical vertex in real space with the coordinate of the uv vertices that share a position (Basically associate UV vert with real vert)
if len(vert_target_loops) > 4000: #This usually indicates that the user has a bunch of crap selected.
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.too_much"))
return
print("Finding connections on line for \""+obj_name+"\"!")
me.validate()
bm = bmesh.new()
bm.from_mesh(me)
#print(vert_target_loops)
#print(vert_target_verts)
tree: dict[str, set[str]] = {}
selected_verts = np.hstack(list(vert_target_loops.values()))
#print(selected_verts)
bm.verts.ensure_lookup_table()
for uvcoordsstr in vert_target_loops:
uv_lay = me.uv_layers.active
#before this section, each vert_target_loops is just groupings of vertices that share coordinates.
# Using the data that determines UV face corners (uvloops) that are associated with the real vertex,
# and the uv face corners (loops) that are on the same faces as the vertices that share coordinates in
# vert_target_loops, we can now identify them
#TL;DR: pairs of vertices that share cooridinates (chain links) find their buddies (make chain connected)
# Someone explain this better than me if you can please - @989onan
extension_loops = []
loops = bm.verts[vert_target_verts[uvcoordsstr]].link_loops
loops_indexes = [i.index for i in loops]
for loop in vert_target_loops[uvcoordsstr]:
if loop in loops_indexes:
loop_obj = loops[loops_indexes.index(loop)]
extension_loops.append(loop_obj.link_loop_next.index)
extension_loops.append(loop_obj.link_loop_prev.index)
#make a tree out of the vertices we identified as sharing faces with the vertices in vert_target_loops, and then link them together in a dictionary.
#the order of this dictionary is unknown.
# Someone explain this better than me if you can please - @989onan
tree[uvcoordsstr] = set()
for i in extension_loops:
if i in selected_verts:
key = np.array(uv_lay.uv[i].vector[:])
key = key.round(decimals=5)
tree[uvcoordsstr].add(str(key))
if uvcoordsstr in tree:
if len(tree[uvcoordsstr]) > 2:
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name))
return {'FINISHED'}
uv_lay = me.uv_layers.active
for uvcoordstr in vert_target_loops:
for loop in vert_target_loops[uvcoordstr]:
uv_lay.vertex_selection[loop].value = True
bm.free()
me.validate()
print("found UV line connections for \""+obj_name+"\":")
#print(tree)
return {"tree":tree,"selected_loops":vert_target_loops,"selected_verts":vert_target_verts}
#This function uses the previous point to find the next point based on connected loops and faces.
def sort_uv_tree(originaltree: dict[str, set[str]], obj_name: str):
sortedtree: dict[str, set[str]] = originaltree.copy()
startpoints: list[str] = []
for i in sortedtree:
if len(sortedtree[i]) < 2:
startpoints.append(i)
if len(startpoints) != 2:
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name))
return
a_list1 = startpoints[0].replace(", "," ").replace("[","").replace("]","").split()
map_object1 = map(float, a_list1)
uvcoords1 = list(map_object1)
a_list2 = startpoints[1].replace(", "," ").replace("[","").replace("]","").split()
map_object2 = map(float, a_list2)
uvcoords2 = list(map_object2)
cursor = context.space_data.cursor_location
startpoint = None
if math.sqrt( (((uvcoords1[0]) - (cursor[0])) **2) + (((uvcoords1[1]) - (cursor[1])) **2) ) > math.sqrt( (((uvcoords2[0]) - (cursor[0])) **2) + (((uvcoords2[1]) - (cursor[1])) **2) ):
startpoint = startpoints[0]
else:
startpoint = startpoints[1]
#Wew my first actual recursive sort! - @989onan
def recursive_sort_uv_tree(point: str, sortedfinal: list[str]):
#print("appending "+point)
sortedfinal.append(point)
new_point: str = ""
for i in sortedtree:
if point in sortedtree[i]:
new_point = i
removed_value = sortedtree.pop(i)
#print(removed_value)
break
if new_point == "":
print("BROKE OUT OF SORTING, FINAL TREE (Should be empty, if not you errored here!):")
print(sortedtree)
return sortedfinal
return recursive_sort_uv_tree(new_point, sortedfinal)
array = []
sortedtree.pop(startpoint)
return recursive_sort_uv_tree(startpoint, array)
def lerp(v0, v1, t):
return v0 + t * (v1 - v0)
target_data: GenerateLoopTreeResult = generate_loop_tree(target)
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
print("sorted target.")
#print(sorted_target_tree)
for source in sources:
if source == target:
continue
#create our list of points that is a chain. then sort the chain into the correct order based on connections of vertices and the faces that the vertices make up in the UV map.
try:
source_data = generate_loop_tree(source)
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
print("Sorted source "+source)
print(sorted_source_tree)
vertex_factor = float(len(sorted_target_tree)-1) / (float(len(sorted_source_tree)-1))
print(str(vertex_factor)+" = "+str(float(len(sorted_target_tree)-1)) + " / " + str((float(len(sorted_source_tree)-1)))+")")
except Exception as e:
print(e)
return {'FINISHED'}
for k,i in enumerate(sorted_source_tree):
try:
#find where we are on the target edges, to interpolate the current point we're placing along the target point's line.
progress_along_edge = (float(k)*vertex_factor)
previous_vertex_index = math.floor(progress_along_edge)
next_vertex_index = math.ceil(progress_along_edge)
#find the uv coordinates of the previous and next points on the target uv line.
a_list1 = sorted_target_tree[previous_vertex_index].replace(", "," ").replace("[","").replace("]","").split()
map_object1 = map(float, a_list1)
previous_point = list(map_object1)
a_list2 = sorted_target_tree[next_vertex_index].replace(", "," ").replace("[","").replace("]","").split()
map_object2 = map(float, a_list2)
next_point = list(map_object2)
#create a point between these two values that represents a decimal 0-1 going where we are to where we are going between the two current points on the edge we are targeting this whole shebang with.
progress_between_points = progress_along_edge - int(progress_along_edge)
lerped_point = [lerp(previous_point[0],next_point[0],progress_between_points),lerp(previous_point[1],next_point[1],progress_between_points)]
#grab our uv face corners for each uv coord that we saved.
#Since each face is considered separate internally, we have to treat each connected face to a vertex in a uv map as separate entities/vertexes.
#basically pretend they are split apart.
uv_face_corners = source_data["selected_loops"][i]
#print("doing from vertex "+str(previous_vertex_index)+" to "+str(next_vertex_index)+" total progress: "+str(progress_along_edge))
me: Mesh = bpy.data.objects[source].data
me.validate()
bm: bmesh.types.BMesh = bmesh.new()
bm.from_mesh(me)
uv_lay: MeshUVLoopLayer = me.uv_layers.active
bm.verts.ensure_lookup_table()
for corner in uv_face_corners:
uv_lay.uv[corner].vector = lerped_point #put the vertcies at the point we calculated.
except:
print("This is probably fine? - @989onan") #TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan
print("Finished mesh \""+source+"\" for UV's")
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
-93
View File
@@ -1,93 +0,0 @@
import bpy
from ..core import common
from ..core.translations import t
from typing import List, Tuple
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
class AvatarToolKit_OT_AutoVisemeButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.create_visemes'
bl_label = t('AutoVisemeButton.label')
bl_description = t('AutoVisemeButton.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and get_all_meshes(context)
def execute(self, context: bpy.types.Context) -> set:
try:
self.create_visemes(context)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def create_visemes(self, context: bpy.types.Context) -> None:
init_progress(context, 5) # 5 main steps
update_progress(self, context, t("VisemePanel.start_viseme_creation"))
mesh = bpy.data.objects.get(context.scene.avatar_toolkit.selected_mesh)
if not mesh or not common.has_shapekeys(mesh):
raise ValueError(t('AutoVisemeButton.error.noShapekeys'))
update_progress(self, context, t("VisemePanel.removing_existing_visemes"))
self.remove_existing_vrc_shapekeys(mesh)
shape_a = context.scene.avatar_toolkit.mouth_a
shape_o = context.scene.avatar_toolkit.mouth_o
shape_ch = context.scene.avatar_toolkit.mouth_ch
if shape_a == "Basis" or shape_o == "Basis" or shape_ch == "Basis":
raise ValueError(t('AutoVisemeButton.error.selectShapekeys'))
update_progress(self, context, t("VisemePanel.creating_visemes"))
visemes: List[Tuple[str, List[Tuple[str, float]]]] = [
('vrc.v_aa', [(shape_a, 0.9998)]),
('vrc.v_ch', [(shape_ch, 0.9996)]),
('vrc.v_dd', [(shape_a, 0.3), (shape_ch, 0.7)]),
('vrc.v_e', [(shape_a, 0.5), (shape_ch, 0.2)]),
('vrc.v_ff', [(shape_a, 0.2), (shape_ch, 0.4)]),
('vrc.v_ih', [(shape_ch, 0.7), (shape_o, 0.3)]),
('vrc.v_kk', [(shape_a, 0.7), (shape_ch, 0.4)]),
('vrc.v_nn', [(shape_a, 0.2), (shape_ch, 0.7)]),
('vrc.v_oh', [(shape_a, 0.2), (shape_o, 0.8)]),
('vrc.v_ou', [(shape_o, 0.9994)]),
('vrc.v_pp', [(shape_a, 0.0004), (shape_o, 0.0004)]),
('vrc.v_rr', [(shape_ch, 0.5), (shape_o, 0.3)]),
('vrc.v_sil', [(shape_a, 0.0002), (shape_ch, 0.0002)]),
('vrc.v_ss', [(shape_ch, 0.8)]),
('vrc.v_th', [(shape_a, 0.4), (shape_o, 0.15)])
]
for viseme_name, shape_mix in visemes:
self.create_viseme(mesh, viseme_name, shape_mix, context.scene.avatar_toolkit.shape_intensity)
update_progress(self, context, t("VisemePanel.sorting_shapekeys"))
common.sort_shape_keys(mesh)
update_progress(self, context, t("VisemePanel.viseme_creation_completed"))
finish_progress(context)
def create_viseme(self, mesh: bpy.types.Object, viseme_name: str, shape_mix: List[Tuple[str, float]], intensity: float) -> None:
shape_keys = mesh.data.shape_keys.key_blocks
if viseme_name in shape_keys:
mesh.shape_key_remove(shape_keys[viseme_name])
new_key = mesh.shape_key_add(name=viseme_name, from_mix=False)
new_key.value = 0.0
for shape_name, value in shape_mix:
if shape_name in shape_keys:
source_shape = shape_keys[shape_name]
for i, vert in enumerate(new_key.data):
vert.co += (source_shape.data[i].co - shape_keys['Basis'].data[i].co) * value * intensity
def remove_existing_vrc_shapekeys(self, mesh: bpy.types.Object) -> None:
vrc_prefixes = ['vrc.v_', 'vrc.blink_', 'vrc.lowerlid_']
shape_keys = mesh.data.shape_keys.key_blocks
for key in reversed(shape_keys):
if any(key.name.startswith(prefix) for prefix in vrc_prefixes):
mesh.shape_key_remove(key)