Merge branch 'Alpha-2' into texture-atlas
This commit is contained in:
@@ -54,6 +54,28 @@ def process_vertex_merging(mesh_data: bpy.types.Mesh, vertices_original: dict[in
|
||||
|
||||
return merged_vertices
|
||||
|
||||
def vertex_moves(mesh_data: bpy.types.Mesh, vertex: int) -> bool:
|
||||
|
||||
for shapekey in mesh_data.shape_keys.key_blocks:
|
||||
data: bpy.types.ShapeKey = shapekey
|
||||
|
||||
if data.points[vertex].co.xyz != mesh_data.vertices[vertex].co.xyz:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def merge_vertex_at_index(mesh_data: bpy.types.Mesh, index: int, distance: float):
|
||||
|
||||
select_target_vertex = [False]*len(mesh_data.vertices)
|
||||
select_target_vertex[index] = 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 _ 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=distance, use_unselected=True, use_sharp_edge_from_normals=False)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
|
||||
bl_idname = "avatar_toolkit.remove_doubles_advanced"
|
||||
bl_label = t("Optimization.remove_doubles_advanced")
|
||||
@@ -168,7 +190,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
||||
except Exception as e:
|
||||
logger.error(f"Error in modify_mesh: {str(e)}")
|
||||
|
||||
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> bool:
|
||||
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> int:
|
||||
"""Advanced mesh modification with shape key handling"""
|
||||
try:
|
||||
final_merged_vertex_group = []
|
||||
@@ -179,26 +201,28 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
||||
duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name)
|
||||
vertices_original = {i: v.co.xyz for i, v in enumerate(duplicate.data.vertices)}
|
||||
|
||||
|
||||
merge_vertex_at_index(duplicate.data, mesh_entry["cur_vertex_pass"], merge_distance) #merge the vertex at our pass to find vertices that would merge to our vertex at this shapekey.
|
||||
|
||||
# Process merging
|
||||
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"])
|
||||
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"]) # find what vertices actually merged.
|
||||
|
||||
if not initialized_final:
|
||||
final_merged_vertex_group = merged_vertices.copy()
|
||||
initialized_final = True
|
||||
else:
|
||||
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices]
|
||||
|
||||
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices] # remove vertices that merged from the list if they didn't merge during this shapkey.
|
||||
bpy.ops.object.delete()
|
||||
|
||||
# Apply final merging
|
||||
if final_merged_vertex_group:
|
||||
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance)
|
||||
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance) # merge all vertices that merged on every shapekey no matter the shapekey during the loop.
|
||||
|
||||
return not (len(final_merged_vertex_group) > 1)
|
||||
return len(final_merged_vertex_group)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in modify_mesh_advanced: {str(e)}")
|
||||
return True
|
||||
return 1
|
||||
|
||||
def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None:
|
||||
"""Apply final vertex merging operations"""
|
||||
@@ -232,16 +256,14 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
||||
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
|
||||
"""Complete the mesh processing by performing final merge operations"""
|
||||
logger.debug("Finishing mesh processing")
|
||||
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=merge_distance, use_unselected=False)
|
||||
|
||||
if not 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=merge_distance, use_unselected=False)
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
mesh["mesh"].select_set(False)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
mesh["mesh"].select_set(False)
|
||||
|
||||
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
|
||||
"""Modal operator execution"""
|
||||
@@ -266,10 +288,21 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
||||
self.process_simple_mesh(context, mesh, merge_distance)
|
||||
self.objects_to_do.pop(0)
|
||||
|
||||
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced:
|
||||
if self.modify_mesh_advanced(context, mesh):
|
||||
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced: #advanced merging vertex by vertex
|
||||
if(mesh["cur_vertex_pass"] < 0): #make sure it doesn't go below 0 and explode when advancing backwards from a previous step
|
||||
mesh["cur_vertex_pass"] = 0
|
||||
|
||||
if vertex_moves(mesh["mesh"].data, mesh["cur_vertex_pass"]): # do not do advanced merging for vertices that don't move
|
||||
mesh["cur_vertex_pass"] -= self.modify_mesh_advanced(context, mesh)-2 #advance forward or backwards based on how many vertices actually got merged, changing the list size.
|
||||
#if above returns 1 (no vertices other than this one being merged to ourselves), advance by 1. else don't advance or go backwards. Makes sure all vertices get merged in the end.
|
||||
else:
|
||||
mesh["cur_vertex_pass"] += 1
|
||||
|
||||
|
||||
elif (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced and len(mesh['shapekeys']) > 0: #after advanced merging has gone past all the moving vertices, now we need to merge non moving vertices.
|
||||
shapekeyname = mesh['shapekeys'].pop(0)
|
||||
mesh["mesh"].active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekeyname)
|
||||
logger.debug(f"Processing shapekey {shapekeyname}")
|
||||
self.modify_mesh(context, mesh)
|
||||
else:
|
||||
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
|
||||
self.objects_to_do.pop(0)
|
||||
|
||||
@@ -134,17 +134,11 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
"""Execute the constraint removal operation"""
|
||||
|
||||
# Make sure we are in Object mode first or it will error
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
armature = get_active_armature(context)
|
||||
|
||||
# Select armature and make it active before changing mode
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
armature.select_set(True)
|
||||
context.view_layer.objects.active = armature
|
||||
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
constraints_removed = 0
|
||||
@@ -157,7 +151,6 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
||||
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||
"""Operator to remove bones with no vertex weights"""
|
||||
bl_idname = "avatar_toolkit.clean_weights"
|
||||
@@ -167,10 +160,37 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||
|
||||
def should_preserve_bone(self, bone_name: str, context: Context) -> bool:
|
||||
"""Check if bone should be preserved based on settings"""
|
||||
if context.scene.avatar_toolkit.merge_twist_bones:
|
||||
return "twist" in bone_name.lower()
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
bone = context.active_object.data.bones.get(bone_name)
|
||||
|
||||
if not bone:
|
||||
return False
|
||||
|
||||
if toolkit.preserve_parent_bones and bone.children:
|
||||
return True
|
||||
|
||||
if toolkit.target_bone_type == 'DEFORM' and not bone.use_deform:
|
||||
return True
|
||||
|
||||
if toolkit.target_bone_type == 'NON_DEFORM' and bone.use_deform:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def populate_bone_list(self, context: Context, zero_weight_bones: List[str]) -> None:
|
||||
"""Populate the zero weight bones list"""
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
toolkit.zero_weight_bones.clear()
|
||||
|
||||
armature = get_active_armature(context)
|
||||
for bone_name in zero_weight_bones:
|
||||
bone = armature.data.bones.get(bone_name)
|
||||
if bone:
|
||||
item = toolkit.zero_weight_bones.add()
|
||||
item.name = bone_name
|
||||
item.has_children = len(bone.children) > 0
|
||||
item.is_deform = bone.use_deform
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
"""Execute the zero weight bone removal operation"""
|
||||
armature = get_active_armature(context)
|
||||
@@ -192,6 +212,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||
# Get weighted bones
|
||||
weighted_bones: List[str] = []
|
||||
meshes = get_all_meshes(context)
|
||||
zero_weight_bones: List[str] = []
|
||||
|
||||
for mesh in meshes:
|
||||
mesh_data: Mesh = mesh.data
|
||||
@@ -209,6 +230,10 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||
if (bone.name not in weighted_bones and
|
||||
not self.should_preserve_bone(bone.name, context)):
|
||||
|
||||
if context.scene.avatar_toolkit.list_only_mode:
|
||||
zero_weight_bones.append(bone.name)
|
||||
continue
|
||||
|
||||
# Store children data
|
||||
children = bone.children
|
||||
children_data = {child.name: initial_transforms[child.name] for child in children}
|
||||
@@ -227,11 +252,38 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||
for child_name, data in children_data.items():
|
||||
if child_name in armature_data.edit_bones:
|
||||
child = armature_data.edit_bones[child_name]
|
||||
child.head = data['head']
|
||||
child.tail = data['tail']
|
||||
child.roll = data['roll']
|
||||
child.matrix = data['matrix']
|
||||
restore_bone_transforms(child, data)
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
if context.scene.avatar_toolkit.list_only_mode:
|
||||
self.populate_bone_list(context, zero_weight_bones)
|
||||
return {'FINISHED'}
|
||||
|
||||
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
||||
return {'FINISHED'}
|
||||
|
||||
class AvatarToolKit_OT_RemoveSelectedBones(Operator):
|
||||
"""Operator to remove selected bones from the zero weight bones list"""
|
||||
bl_idname = "avatar_toolkit.remove_selected_bones"
|
||||
bl_label = t("Tools.remove_selected_bones")
|
||||
bl_description = t("Tools.remove_selected_bones_desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
armature = get_active_armature(context)
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
|
||||
selected_bones = [item.name for item in toolkit.zero_weight_bones
|
||||
if item.selected]
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
for bone_name in selected_bones:
|
||||
if bone_name in armature.data.edit_bones:
|
||||
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
toolkit.zero_weight_bones.clear()
|
||||
|
||||
self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones)))
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,255 @@
|
||||
import bpy
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint
|
||||
from ...core.common import get_active_armature, validate_armature
|
||||
from ...core.logging_setup import logger
|
||||
from ...core.translations import t
|
||||
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
|
||||
|
||||
class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
||||
"""Convert Rigify armature to Unity-compatible format"""
|
||||
bl_idname = "avatar_toolkit.convert_rigify_to_unity"
|
||||
bl_label = t("Tools.convert_rigify_to_unity")
|
||||
bl_description = t("Tools.convert_rigify_to_unity_desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
return False
|
||||
return ("DEF-spine" in armature.data.bones or
|
||||
"spine" in armature.data.bones and "metarig" in armature.name.lower())
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
logger.info("Starting Rigify to Unity conversion")
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
logger.error("No armature found")
|
||||
self.report({'ERROR'}, t("Tools.no_armature"))
|
||||
return {'CANCELLED'}
|
||||
|
||||
logger.debug(f"Converting armature: {armature.name}")
|
||||
armature.name = "Armature"
|
||||
armature.data.name = "Armature"
|
||||
logger.debug("Renamed armature to 'Armature'")
|
||||
|
||||
if "DEF-spine" in armature.data.bones:
|
||||
logger.info("Processing DEF bones")
|
||||
self.move_def_bones(armature)
|
||||
self.rename_bones_for_unity(armature)
|
||||
else:
|
||||
logger.info("Processing basic bones")
|
||||
self.cleanup_extra_bones(armature)
|
||||
self.rename_basic_bones_for_unity(armature)
|
||||
|
||||
logger.debug("Cleaning up bone collections")
|
||||
self.cleanup_bone_collections(armature)
|
||||
|
||||
if context.scene.avatar_toolkit.merge_twist_bones:
|
||||
logger.info("Merging twist bones")
|
||||
self.handle_twist_bones(armature)
|
||||
|
||||
logger.info("Successfully converted Rigify armature to Unity format")
|
||||
self.report({'INFO'}, t("Tools.rigify_converted"))
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to convert Rigify: {str(e)}", exc_info=True)
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
def cleanup_extra_bones(self, armature: Object) -> None:
|
||||
"""Remove unnecessary bones and merge neck bones"""
|
||||
logger.debug("Starting cleanup of extra bones")
|
||||
|
||||
# Set armature as active object before mode switch
|
||||
bpy.context.view_layer.objects.active = armature
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
bones_to_remove: List[str] = []
|
||||
for bone in armature.data.edit_bones:
|
||||
if any(pattern in bone.name.lower() for pattern in rigify_unnecessary_bones):
|
||||
bones_to_remove.append(bone.name)
|
||||
|
||||
for bone_name in bones_to_remove:
|
||||
if bone_name in armature.data.edit_bones:
|
||||
logger.debug(f"Removing bone: {bone_name}")
|
||||
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
|
||||
|
||||
if 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones:
|
||||
logger.debug("Merging neck bones")
|
||||
neck_start = armature.data.edit_bones['spine.004']
|
||||
neck_end = armature.data.edit_bones['spine.005']
|
||||
neck_start.tail = neck_end.tail
|
||||
armature.data.edit_bones.remove(neck_end)
|
||||
neck_start.name = "Neck"
|
||||
|
||||
if 'spine.006' in armature.data.edit_bones:
|
||||
logger.debug("Renaming head bone")
|
||||
head_bone = armature.data.edit_bones['spine.006']
|
||||
head_bone.name = "Head"
|
||||
|
||||
def move_def_bones(self, armature: Object) -> None:
|
||||
"""Move DEF bones to their correct positions"""
|
||||
logger.debug("Moving DEF bones to correct positions")
|
||||
|
||||
# Set armature as active object
|
||||
bpy.context.view_layer.objects.active = armature
|
||||
remap: Dict[str, str] = self.get_org_remap(armature)
|
||||
remap.update(self.get_special_remap())
|
||||
|
||||
remove_bones_in_chain: List[str] = [
|
||||
'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: List[str] = self.get_transform_copies(armature)
|
||||
|
||||
logger.debug("Setting up transform copies")
|
||||
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)
|
||||
|
||||
logger.debug("Remapping bone 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]]
|
||||
|
||||
logger.debug("Processing bone chain removal")
|
||||
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: Object) -> None:
|
||||
"""Rename bones to Unity-compatible names"""
|
||||
logger.debug("Renaming bones to Unity format")
|
||||
for old_name, new_name in rigify_unity_names.items():
|
||||
bone = armature.pose.bones.get(old_name)
|
||||
if bone:
|
||||
logger.debug(f"Renaming bone: {old_name} -> {new_name}")
|
||||
bone.name = new_name
|
||||
|
||||
def rename_basic_bones_for_unity(self, armature: Object) -> None:
|
||||
"""Rename basic metarig bones to Unity-compatible names"""
|
||||
logger.debug("Renaming basic metarig bones")
|
||||
for old_name, new_name in rigify_basic_unity_names.items():
|
||||
bone = armature.pose.bones.get(old_name)
|
||||
if bone:
|
||||
logger.debug(f"Renaming basic bone: {old_name} -> {new_name}")
|
||||
bone.name = new_name
|
||||
|
||||
def cleanup_bone_collections(self, armature: Object) -> None:
|
||||
"""Remove all bone collections since they're not needed for Unity"""
|
||||
logger.debug("Cleaning up bone collections")
|
||||
if hasattr(armature.data, 'collections') and armature.data.collections:
|
||||
while len(armature.data.collections) > 0:
|
||||
collection = armature.data.collections[0]
|
||||
armature.data.collections.remove(collection)
|
||||
|
||||
while len(armature.data.collections) > 1:
|
||||
collection = armature.data.collections[1]
|
||||
armature.data.collections.remove(collection)
|
||||
|
||||
def handle_twist_bones(self, armature: Object) -> None:
|
||||
"""Handle twist bones during conversion"""
|
||||
logger.debug("Processing twist bones")
|
||||
twist_bones: List[Tuple[str, str]] = [
|
||||
("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:
|
||||
logger.debug(f"Merging twist bone: {twist_bone} into {parent_bone}")
|
||||
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: Object) -> Dict[str, str]:
|
||||
"""Get original bone remapping"""
|
||||
logger.debug("Getting original bone remapping")
|
||||
remap: Dict[str, str] = {}
|
||||
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) -> Dict[str, str]:
|
||||
"""Get special bone remapping cases"""
|
||||
logger.debug("Getting special bone remapping")
|
||||
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: Object) -> List[str]:
|
||||
"""Get bones that need transform copies"""
|
||||
logger.debug("Getting transform copy bones")
|
||||
result: List[str] = []
|
||||
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: PoseBone) -> bool:
|
||||
"""Check if bone has transform copy constraints"""
|
||||
return any(constraint.type == 'COPY_TRANSFORMS' for constraint in bone.constraints)
|
||||
|
||||
def is_def_bone(self, bone_name: str) -> bool:
|
||||
"""Check if bone is a DEF bone"""
|
||||
return bone_name.startswith('DEF-')
|
||||
|
||||
def is_org_bone(self, bone_name: str) -> bool:
|
||||
"""Check if bone is an ORG bone"""
|
||||
return bone_name.startswith('ORG-')
|
||||
|
||||
def get_proto_name(self, bone_name: str) -> str:
|
||||
"""Get the prototype name of a bone"""
|
||||
if self.is_def_bone(bone_name) or self.is_org_bone(bone_name):
|
||||
return bone_name[4:]
|
||||
return bone_name
|
||||
@@ -0,0 +1,253 @@
|
||||
from typing import TypedDict, Set, Dict, List, Optional, Any, Tuple
|
||||
import bpy
|
||||
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
|
||||
import bmesh
|
||||
import numpy as np
|
||||
import math
|
||||
from ...core.translations import t
|
||||
from ...core.logging_setup import logger
|
||||
|
||||
class GenerateLoopTreeResult(TypedDict):
|
||||
tree: Dict[str, Set[str]]
|
||||
selected_loops: Dict[str, List[int]]
|
||||
selected_verts: Dict[str, int]
|
||||
|
||||
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
|
||||
"""Operator to align selected UV edges to target edge"""
|
||||
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
|
||||
bl_label = t("UVTools.align_edges")
|
||||
bl_description = t("UVTools.align_edges_desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
#all selected objects need to be meshes for this to work - @989onan
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
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) -> Set[str]:
|
||||
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:
|
||||
logger.debug(f"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):
|
||||
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False):
|
||||
key = np.array(uv_lay.uv[k].vector[:])
|
||||
key = key.round(decimals=5)
|
||||
|
||||
if str(key) not in vert_target_loops:
|
||||
vert_target_loops[str(key)] = []
|
||||
vert_target_loops[str(key)].append(k)
|
||||
vert_target_verts[str(key)] = me.loops[k].vertex_index
|
||||
|
||||
if len(vert_target_loops) > 4000:
|
||||
self.report({'WARNING'}, t("UVTools.too_many_vertices"))
|
||||
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
|
||||
|
||||
logger.debug(f"Finding connections on line for {obj_name}")
|
||||
me.validate()
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(me)
|
||||
|
||||
tree: Dict[str, Set[str]] = {}
|
||||
selected_verts = np.hstack(list(vert_target_loops.values()))
|
||||
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.need_line", obj=obj_name))
|
||||
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
|
||||
|
||||
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()
|
||||
logger.debug(f"Found UV line connections for {obj_name}")
|
||||
|
||||
return {"tree": tree, "selected_loops": vert_target_loops, "selected_verts": vert_target_verts}
|
||||
|
||||
def sort_uv_tree(originaltree: Dict[str, Set[str]], obj_name: str) -> List[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.need_line", obj=obj_name))
|
||||
return []
|
||||
|
||||
uvcoords1 = [float(x) for x in startpoints[0].replace("[","").replace("]","").split()]
|
||||
uvcoords2 = [float(x) for x in startpoints[1].replace("[","").replace("]","").split()]
|
||||
|
||||
cursor = context.space_data.cursor_location
|
||||
|
||||
startpoint = startpoints[0] 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) else startpoints[1]
|
||||
|
||||
#Wew my first actual recursive sort! - @989onan
|
||||
def recursive_sort_uv_tree(point: str, sortedfinal: List[str]) -> 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 == "":
|
||||
logger.debug("Sorting complete, remaining tree:")
|
||||
logger.debug(sortedtree)
|
||||
return sortedfinal
|
||||
|
||||
return recursive_sort_uv_tree(new_point, sortedfinal)
|
||||
|
||||
sortedtree.pop(startpoint)
|
||||
return recursive_sort_uv_tree(startpoint, [])
|
||||
|
||||
def lerp(v0: float, v1: float, t: float) -> float:
|
||||
return v0 + t * (v1 - v0)
|
||||
|
||||
target_data = generate_loop_tree(target)
|
||||
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
|
||||
logger.debug("Sorted target tree")
|
||||
|
||||
for source in sources:
|
||||
if source == target:
|
||||
continue
|
||||
|
||||
try:
|
||||
source_data = generate_loop_tree(source)
|
||||
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
|
||||
logger.debug(f"Sorted source {source}")
|
||||
|
||||
vertex_factor = float(len(sorted_target_tree)-1) / float(len(sorted_source_tree)-1)
|
||||
logger.debug(f"Vertex factor: {vertex_factor}")
|
||||
|
||||
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.
|
||||
previous_point = [float(x) for x in sorted_target_tree[previous_vertex_index].replace("[","").replace("]","").split()]
|
||||
next_point = [float(x) for x in sorted_target_tree[next_vertex_index].replace("[","").replace("]","").split()]
|
||||
|
||||
#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]
|
||||
|
||||
me = bpy.data.objects[source].data
|
||||
me.validate()
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(me)
|
||||
uv_lay = me.uv_layers.active
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
for corner in uv_face_corners:
|
||||
uv_lay.uv[corner].vector = lerped_point
|
||||
|
||||
except:
|
||||
#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
|
||||
pass
|
||||
|
||||
logger.info(f"Finished mesh {source} for UV's")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing source {source}: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
bpy.ops.object.mode_set(mode=prev_mode)
|
||||
return {'FINISHED'}
|
||||
Reference in New Issue
Block a user