Merge branch 'Alpha-2' into texture-atlas
This commit is contained in:
@@ -354,3 +354,72 @@ resonite_translations = {
|
|||||||
'thumb_2_r': "thumb2.R",
|
'thumb_2_r': "thumb2.R",
|
||||||
'thumb_3_r': "thumb3.R"
|
'thumb_3_r': "thumb3.R"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rigify_unity_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"
|
||||||
|
}
|
||||||
|
|
||||||
|
rigify_basic_unity_names = {
|
||||||
|
"spine": "Hips",
|
||||||
|
"spine.001": "Spine",
|
||||||
|
"spine.002": "Chest",
|
||||||
|
"spine.003": "UpperChest",
|
||||||
|
"neck": "Neck",
|
||||||
|
"head": "Head",
|
||||||
|
"shoulder.L": "LeftShoulder",
|
||||||
|
"upper_arm.L": "LeftUpperArm",
|
||||||
|
"forearm.L": "LeftLowerArm",
|
||||||
|
"hand.L": "LeftHand",
|
||||||
|
"shoulder.R": "RightShoulder",
|
||||||
|
"upper_arm.R": "RightUpperArm",
|
||||||
|
"forearm.R": "RightLowerArm",
|
||||||
|
"hand.R": "RightHand",
|
||||||
|
"thigh.L": "LeftUpperLeg",
|
||||||
|
"shin.L": "LeftLowerLeg",
|
||||||
|
"foot.L": "LeftFoot",
|
||||||
|
"toe.L": "LeftToes",
|
||||||
|
"thigh.R": "RightUpperLeg",
|
||||||
|
"shin.R": "RightLowerLeg",
|
||||||
|
"foot.R": "RightFoot",
|
||||||
|
"toe.R": "RightToes"
|
||||||
|
}
|
||||||
|
|
||||||
|
rigify_unnecessary_bones = [
|
||||||
|
'face',
|
||||||
|
'ear.l', 'ear.r',
|
||||||
|
'forehead',
|
||||||
|
'cheek.t.l', 'cheek.t.r',
|
||||||
|
'cheek.b.l', 'cheek.b.r',
|
||||||
|
'brow.t.l', 'brow.t.r',
|
||||||
|
'brow.b.l', 'brow.b.r',
|
||||||
|
'jaw',
|
||||||
|
'chin',
|
||||||
|
'nose',
|
||||||
|
'temple.l', 'temple.r',
|
||||||
|
'teeth',
|
||||||
|
'lip',
|
||||||
|
'lid',
|
||||||
|
'heel',
|
||||||
|
'pelvis.'
|
||||||
|
]
|
||||||
@@ -4,6 +4,7 @@ from typing import Optional, Any
|
|||||||
from bpy.types import Context
|
from bpy.types import Context
|
||||||
|
|
||||||
logger = logging.getLogger('avatar_toolkit')
|
logger = logging.getLogger('avatar_toolkit')
|
||||||
|
_original_error = logger.error
|
||||||
|
|
||||||
def configure_logging(enabled: bool = False) -> None:
|
def configure_logging(enabled: bool = False) -> None:
|
||||||
"""Configure logging for Avatar Toolkit"""
|
"""Configure logging for Avatar Toolkit"""
|
||||||
@@ -16,15 +17,16 @@ def configure_logging(enabled: bool = False) -> None:
|
|||||||
if enabled:
|
if enabled:
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
handler.setLevel(logging.DEBUG)
|
handler.setLevel(logging.DEBUG)
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s\n%(exc_info)s' if enabled else '%(message)s')
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
# Override error logging to include traceback
|
|
||||||
def error_with_traceback(msg, *args, **kwargs):
|
def error_with_traceback(msg, *args, **kwargs):
|
||||||
if kwargs.get('exc_info', False):
|
if kwargs.get('exc_info', False) or isinstance(msg, Exception):
|
||||||
msg = f"{msg}\n{traceback.format_exc()}"
|
full_msg = f"{msg}\n{traceback.format_exc()}"
|
||||||
logger.error(msg, *args, **kwargs)
|
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
|
||||||
|
else:
|
||||||
|
_original_error(msg, *args, **kwargs)
|
||||||
|
|
||||||
logger.error = error_with_traceback
|
logger.error = error_with_traceback
|
||||||
|
|
||||||
|
|||||||
+47
-1
@@ -18,6 +18,13 @@ from .common import get_armature_list, get_active_armature, get_all_meshes, Scen
|
|||||||
from ..functions.visemes import VisemePreview
|
from ..functions.visemes import VisemePreview
|
||||||
from ..functions.eye_tracking import set_rotation
|
from ..functions.eye_tracking import set_rotation
|
||||||
|
|
||||||
|
class ZeroWeightBoneItem(PropertyGroup):
|
||||||
|
"""Property group for zero weight bone list items"""
|
||||||
|
name: StringProperty(name="Bone Name")
|
||||||
|
selected: BoolProperty(name="Selected", default=True)
|
||||||
|
has_children: BoolProperty(name="Has Children", default=False)
|
||||||
|
is_deform: BoolProperty(name="Is Deform Bone", default=False)
|
||||||
|
|
||||||
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Updates validation mode and saves preference"""
|
"""Updates validation mode and saves preference"""
|
||||||
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
||||||
@@ -361,6 +368,46 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
preserve_parent_bones: BoolProperty(
|
||||||
|
name=t("Tools.preserve_parent_bones"),
|
||||||
|
description=t("Tools.preserve_parent_bones_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
target_bone_type: EnumProperty(
|
||||||
|
name=t("Tools.target_bone_type"),
|
||||||
|
description=t("Tools.target_bone_type_desc"),
|
||||||
|
items=[
|
||||||
|
('ALL', t("Tools.target_all_bones"), ""),
|
||||||
|
('DEFORM', t("Tools.target_deform_bones"), ""),
|
||||||
|
('NON_DEFORM', t("Tools.target_non_deform_bones"), "")
|
||||||
|
],
|
||||||
|
default='ALL'
|
||||||
|
)
|
||||||
|
|
||||||
|
zero_weight_bones: CollectionProperty(
|
||||||
|
type=ZeroWeightBoneItem,
|
||||||
|
name="Zero Weight Bones",
|
||||||
|
description="List of bones with zero weights"
|
||||||
|
)
|
||||||
|
|
||||||
|
zero_weight_bones_index: IntProperty(
|
||||||
|
name="Zero Weight Bone Index",
|
||||||
|
default=0
|
||||||
|
)
|
||||||
|
|
||||||
|
merge_twist_bones: BoolProperty(
|
||||||
|
name=t("Tools.merge_twist_bones"),
|
||||||
|
description=t("Tools.merge_twist_bones_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
list_only_mode: BoolProperty(
|
||||||
|
name=t("Tools.list_only_mode"),
|
||||||
|
description=t("Tools.list_only_mode_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
cleanup_shape_keys: BoolProperty(
|
cleanup_shape_keys: BoolProperty(
|
||||||
name=t('MergeArmature.cleanup_shape_keys'),
|
name=t('MergeArmature.cleanup_shape_keys'),
|
||||||
description=t('MergeArmature.cleanup_shape_keys_desc'),
|
description=t('MergeArmature.cleanup_shape_keys_desc'),
|
||||||
@@ -464,7 +511,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
logger.info("Registering Avatar Toolkit properties")
|
||||||
|
|||||||
@@ -54,6 +54,28 @@ def process_vertex_merging(mesh_data: bpy.types.Mesh, vertices_original: dict[in
|
|||||||
|
|
||||||
return merged_vertices
|
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):
|
class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
|
||||||
bl_idname = "avatar_toolkit.remove_doubles_advanced"
|
bl_idname = "avatar_toolkit.remove_doubles_advanced"
|
||||||
bl_label = t("Optimization.remove_doubles_advanced")
|
bl_label = t("Optimization.remove_doubles_advanced")
|
||||||
@@ -168,7 +190,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in modify_mesh: {str(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"""
|
"""Advanced mesh modification with shape key handling"""
|
||||||
try:
|
try:
|
||||||
final_merged_vertex_group = []
|
final_merged_vertex_group = []
|
||||||
@@ -179,26 +201,28 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name)
|
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)}
|
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
|
# 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:
|
if not initialized_final:
|
||||||
final_merged_vertex_group = merged_vertices.copy()
|
final_merged_vertex_group = merged_vertices.copy()
|
||||||
initialized_final = True
|
initialized_final = True
|
||||||
else:
|
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()
|
bpy.ops.object.delete()
|
||||||
|
|
||||||
# Apply final merging
|
# Apply final merging
|
||||||
if final_merged_vertex_group:
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error in modify_mesh_advanced: {str(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:
|
def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None:
|
||||||
"""Apply final vertex merging operations"""
|
"""Apply final vertex merging operations"""
|
||||||
@@ -232,8 +256,6 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
|
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
|
||||||
"""Complete the mesh processing by performing final merge operations"""
|
"""Complete the mesh processing by performing final merge operations"""
|
||||||
logger.debug("Finishing mesh processing")
|
logger.debug("Finishing mesh processing")
|
||||||
|
|
||||||
if not advanced:
|
|
||||||
mesh["mesh"].select_set(True)
|
mesh["mesh"].select_set(True)
|
||||||
context.view_layer.objects.active = mesh["mesh"]
|
context.view_layer.objects.active = mesh["mesh"]
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
@@ -266,10 +288,21 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
self.process_simple_mesh(context, mesh, merge_distance)
|
self.process_simple_mesh(context, mesh, merge_distance)
|
||||||
self.objects_to_do.pop(0)
|
self.objects_to_do.pop(0)
|
||||||
|
|
||||||
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced:
|
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced: #advanced merging vertex by vertex
|
||||||
if self.modify_mesh_advanced(context, mesh):
|
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
|
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:
|
else:
|
||||||
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
|
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
|
||||||
self.objects_to_do.pop(0)
|
self.objects_to_do.pop(0)
|
||||||
|
|||||||
@@ -134,17 +134,11 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
|||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
"""Execute the constraint removal operation"""
|
"""Execute the constraint removal operation"""
|
||||||
|
|
||||||
# Make sure we are in Object mode first or it will error
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
|
|
||||||
# Select armature and make it active before changing mode
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
armature.select_set(True)
|
armature.select_set(True)
|
||||||
context.view_layer.objects.active = armature
|
context.view_layer.objects.active = armature
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
constraints_removed = 0
|
constraints_removed = 0
|
||||||
@@ -157,7 +151,6 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
|||||||
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
|
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||||
"""Operator to remove bones with no vertex weights"""
|
"""Operator to remove bones with no vertex weights"""
|
||||||
bl_idname = "avatar_toolkit.clean_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:
|
def should_preserve_bone(self, bone_name: str, context: Context) -> bool:
|
||||||
"""Check if bone should be preserved based on settings"""
|
"""Check if bone should be preserved based on settings"""
|
||||||
if context.scene.avatar_toolkit.merge_twist_bones:
|
toolkit = context.scene.avatar_toolkit
|
||||||
return "twist" in bone_name.lower()
|
bone = context.active_object.data.bones.get(bone_name)
|
||||||
|
|
||||||
|
if not bone:
|
||||||
return False
|
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]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
"""Execute the zero weight bone removal operation"""
|
"""Execute the zero weight bone removal operation"""
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
@@ -192,6 +212,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
# Get weighted bones
|
# Get weighted bones
|
||||||
weighted_bones: List[str] = []
|
weighted_bones: List[str] = []
|
||||||
meshes = get_all_meshes(context)
|
meshes = get_all_meshes(context)
|
||||||
|
zero_weight_bones: List[str] = []
|
||||||
|
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
mesh_data: Mesh = mesh.data
|
mesh_data: Mesh = mesh.data
|
||||||
@@ -209,6 +230,10 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
if (bone.name not in weighted_bones and
|
if (bone.name not in weighted_bones and
|
||||||
not self.should_preserve_bone(bone.name, context)):
|
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
|
# Store children data
|
||||||
children = bone.children
|
children = bone.children
|
||||||
children_data = {child.name: initial_transforms[child.name] for child in 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():
|
for child_name, data in children_data.items():
|
||||||
if child_name in armature_data.edit_bones:
|
if child_name in armature_data.edit_bones:
|
||||||
child = armature_data.edit_bones[child_name]
|
child = armature_data.edit_bones[child_name]
|
||||||
child.head = data['head']
|
restore_bone_transforms(child, data)
|
||||||
child.tail = data['tail']
|
|
||||||
child.roll = data['roll']
|
|
||||||
child.matrix = data['matrix']
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
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))
|
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
||||||
return {'FINISHED'}
|
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'}
|
||||||
@@ -149,6 +149,19 @@
|
|||||||
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
|
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
|
||||||
"Tools.clean_weights": "Remove Zero Weight Bones",
|
"Tools.clean_weights": "Remove Zero Weight Bones",
|
||||||
"Tools.clean_weights_desc": "Remove bones with no vertex weights",
|
"Tools.clean_weights_desc": "Remove bones with no vertex weights",
|
||||||
|
"Tools.preserve_parent_bones": "Preserve Parent Bones",
|
||||||
|
"Tools.preserve_parent_bones_desc": "Keep bones that have children even if they have no weights",
|
||||||
|
"Tools.target_bone_type": "Target Bone Type",
|
||||||
|
"Tools.target_bone_type_desc": "Filter which types of bones to process",
|
||||||
|
"Tools.target_all_bones": "All Bones",
|
||||||
|
"Tools.target_deform_bones": "Deform Bones Only",
|
||||||
|
"Tools.target_non_deform_bones": "Non-Deform Bones Only",
|
||||||
|
"Tools.list_only_mode": "List Mode Only",
|
||||||
|
"Tools.list_only_mode_desc": "List zero weight bones instead of removing them",
|
||||||
|
"Tools.zero_weight_bones_found": "Zero weight bones found: {bones}",
|
||||||
|
"Tools.remove_selected_bones": "Remove Selected Bones",
|
||||||
|
"Tools.remove_selected_bones_desc": "Remove selected zero weight bones from armature",
|
||||||
|
"Tools.bones_removed": "Removed {count} bones",
|
||||||
"Tools.clean_constraints": "Delete Bone Constraints",
|
"Tools.clean_constraints": "Delete Bone Constraints",
|
||||||
"Tools.clean_constraints_desc": "Remove all bone constraints from armature",
|
"Tools.clean_constraints_desc": "Remove all bone constraints from armature",
|
||||||
"Tools.clean_constraints_success": "Removed {count} bone constraints",
|
"Tools.clean_constraints_success": "Removed {count} bone constraints",
|
||||||
@@ -187,6 +200,16 @@
|
|||||||
"Tools.shapekey_tolerance": "Shape Key Tolerance",
|
"Tools.shapekey_tolerance": "Shape Key Tolerance",
|
||||||
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
|
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
|
||||||
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
|
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
|
||||||
|
"Tools.rigify_title": "Rigify Tools",
|
||||||
|
"Tools.convert_rigify_to_unity": "Convert Rigify to Unity",
|
||||||
|
"Tools.convert_rigify_to_unity_desc": "Convert Rigify armature to Unity-compatible format",
|
||||||
|
"Tools.rigify_converted": "Rigify armature converted successfully",
|
||||||
|
"Tools.no_armature": "No armature selected",
|
||||||
|
|
||||||
|
"UVTools.too_many_vertices": "Error! You have too much stuff selected. Are you sure you're selecting two edges?",
|
||||||
|
"UVTools.need_line": "You need one line of selected UV points per selected object. Object \"{obj}\" does not meet this requirement!",
|
||||||
|
"UVTools.align_edges": "Align UV Edges to Target",
|
||||||
|
"UVTools.align_edges_desc": "Aligns a selected line of UV points on each selected mesh to the line of selected UV points on the active mesh. Useful for kitbashing textures of one model onto another. Uses distance from the 2D cursor to identify the start of the line of UV points on each mesh.",
|
||||||
|
|
||||||
"MMD.label": "MMD Tools",
|
"MMD.label": "MMD Tools",
|
||||||
"MMD.bone_standardization": "Bone Standardization",
|
"MMD.bone_standardization": "Bone Standardization",
|
||||||
|
|||||||
@@ -149,6 +149,19 @@
|
|||||||
"Tools.merge_twist_bones_desc": "チェックすると、重みが0でもツイストボーンを保持します",
|
"Tools.merge_twist_bones_desc": "チェックすると、重みが0でもツイストボーンを保持します",
|
||||||
"Tools.clean_weights": "重みなしボーンを削除",
|
"Tools.clean_weights": "重みなしボーンを削除",
|
||||||
"Tools.clean_weights_desc": "頂点の重みがないボーンを削除",
|
"Tools.clean_weights_desc": "頂点の重みがないボーンを削除",
|
||||||
|
"Tools.preserve_parent_bones": "親ボーンを保持",
|
||||||
|
"Tools.preserve_parent_bones_desc": "ウェイトがなくても子ボーンを持つボーンを保持",
|
||||||
|
"Tools.target_bone_type": "対象ボーンタイプ",
|
||||||
|
"Tools.target_bone_type_desc": "処理するボーンタイプを選択",
|
||||||
|
"Tools.target_all_bones": "全てのボーン",
|
||||||
|
"Tools.target_deform_bones": "変形ボーンのみ",
|
||||||
|
"Tools.target_non_deform_bones": "非変形ボーンのみ",
|
||||||
|
"Tools.list_only_mode": "リストモードのみ",
|
||||||
|
"Tools.list_only_mode_desc": "ゼロウェイトボーンを削除せずにリスト表示",
|
||||||
|
"Tools.zero_weight_bones_found": "ゼロウェイトボーンが見つかりました: {bones}",
|
||||||
|
"Tools.remove_selected_bones": "選択したボーンを削除",
|
||||||
|
"Tools.remove_selected_bones_desc": "選択したゼロウェイトボーンをアーマチュアから削除",
|
||||||
|
"Tools.bones_removed": "{count}個のボーンを削除しました",
|
||||||
"Tools.clean_constraints": "ボーンのコンストレイントを削除",
|
"Tools.clean_constraints": "ボーンのコンストレイントを削除",
|
||||||
"Tools.clean_constraints_desc": "アーマチュアからすべてのボーンコンストレイントを削除",
|
"Tools.clean_constraints_desc": "アーマチュアからすべてのボーンコンストレイントを削除",
|
||||||
"Tools.clean_constraints_success": "{count}個のボーンコンストレイントを削除しました",
|
"Tools.clean_constraints_success": "{count}個のボーンコンストレイントを削除しました",
|
||||||
@@ -187,6 +200,10 @@
|
|||||||
"Tools.shapekey_tolerance": "シェイプキーの許容値",
|
"Tools.shapekey_tolerance": "シェイプキーの許容値",
|
||||||
"Tools.shapekey_tolerance_desc": "シェイプキーを使用済みと判断する最小差分",
|
"Tools.shapekey_tolerance_desc": "シェイプキーを使用済みと判断する最小差分",
|
||||||
"Tools.shapekeys_removed": "{count}個の未使用シェイプキーを削除しました",
|
"Tools.shapekeys_removed": "{count}個の未使用シェイプキーを削除しました",
|
||||||
|
"Tools.convert_rigify_to_unity": "RigifyをUnityに変換",
|
||||||
|
"Tools.convert_rigify_to_unity_desc": "RigifyアーマチュアをUnity互換フォーマットに変換",
|
||||||
|
"Tools.rigify_converted": "Rigifyアーマチュアの変換が完了しました",
|
||||||
|
"Tools.no_armature": "アーマチュアが選択されていません",
|
||||||
|
|
||||||
"MMD.label": "MMDツール",
|
"MMD.label": "MMDツール",
|
||||||
"MMD.bone_standardization": "ボーン標準化",
|
"MMD.bone_standardization": "ボーン標準化",
|
||||||
|
|||||||
@@ -149,6 +149,19 @@
|
|||||||
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이어도 트위스트 본 유지",
|
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이어도 트위스트 본 유지",
|
||||||
"Tools.clean_weights": "0 가중치 본 제거",
|
"Tools.clean_weights": "0 가중치 본 제거",
|
||||||
"Tools.clean_weights_desc": "버텍스 가중치가 없는 본 제거",
|
"Tools.clean_weights_desc": "버텍스 가중치가 없는 본 제거",
|
||||||
|
"Tools.preserve_parent_bones": "부모 본 보존",
|
||||||
|
"Tools.preserve_parent_bones_desc": "가중치가 없어도 자식 본이 있는 본 유지",
|
||||||
|
"Tools.target_bone_type": "대상 본 유형",
|
||||||
|
"Tools.target_bone_type_desc": "처리할 본 유형 필터링",
|
||||||
|
"Tools.target_all_bones": "모든 본",
|
||||||
|
"Tools.target_deform_bones": "변형 본만",
|
||||||
|
"Tools.target_non_deform_bones": "비변형 본만",
|
||||||
|
"Tools.list_only_mode": "목록 모드만",
|
||||||
|
"Tools.list_only_mode_desc": "제로 가중치 본을 제거하지 않고 목록으로 표시",
|
||||||
|
"Tools.zero_weight_bones_found": "제로 가중치 본 발견: {bones}",
|
||||||
|
"Tools.remove_selected_bones": "선택한 본 제거",
|
||||||
|
"Tools.remove_selected_bones_desc": "선택한 제로 가중치 본을 아마추어에서 제거",
|
||||||
|
"Tools.bones_removed": "{count}개의 본이 제거되었습니다",
|
||||||
"Tools.clean_constraints": "본 제약 조건 삭제",
|
"Tools.clean_constraints": "본 제약 조건 삭제",
|
||||||
"Tools.clean_constraints_desc": "아마추어에서 모든 본 제약 조건 제거",
|
"Tools.clean_constraints_desc": "아마추어에서 모든 본 제약 조건 제거",
|
||||||
"Tools.clean_constraints_success": "{count}개의 본 제약 조건 제거됨",
|
"Tools.clean_constraints_success": "{count}개의 본 제약 조건 제거됨",
|
||||||
@@ -187,6 +200,10 @@
|
|||||||
"Tools.shapekey_tolerance": "쉐이프 키 허용 오차",
|
"Tools.shapekey_tolerance": "쉐이프 키 허용 오차",
|
||||||
"Tools.shapekey_tolerance_desc": "쉐이프 키를 사용된 것으로 간주할 최소 차이",
|
"Tools.shapekey_tolerance_desc": "쉐이프 키를 사용된 것으로 간주할 최소 차이",
|
||||||
"Tools.shapekeys_removed": "{count}개의 미사용 쉐이프 키 제거됨",
|
"Tools.shapekeys_removed": "{count}개의 미사용 쉐이프 키 제거됨",
|
||||||
|
"Tools.convert_rigify_to_unity": "Rigify를 Unity로 변환",
|
||||||
|
"Tools.convert_rigify_to_unity_desc": "Rigify 아마추어를 Unity 호환 형식으로 변환",
|
||||||
|
"Tools.rigify_converted": "Rigify 아마추어 변환 완료",
|
||||||
|
"Tools.no_armature": "아마추어가 선택되지 않았습니다",
|
||||||
|
|
||||||
"MMD.label": "MMD 도구",
|
"MMD.label": "MMD 도구",
|
||||||
"MMD.bone_standardization": "본 표준화",
|
"MMD.bone_standardization": "본 표준화",
|
||||||
|
|||||||
+38
-2
@@ -1,9 +1,21 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from bpy.types import Panel, Context, UILayout, Operator
|
from bpy.types import Panel, Context, UILayout, Operator, UIList
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
|
|
||||||
|
class AVATAR_TOOLKIT_UL_ZeroWeightBones(UIList):
|
||||||
|
"""UI List for displaying zero weight bones with selection options"""
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
|
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.prop(item, "selected", text="")
|
||||||
|
row.label(text=item.name)
|
||||||
|
if item.has_children:
|
||||||
|
row.label(text="", icon='OUTLINER_OB_ARMATURE')
|
||||||
|
if item.is_deform:
|
||||||
|
row.label(text="", icon='MOD_ARMATURE')
|
||||||
|
|
||||||
class AvatarToolKit_PT_ToolsPanel(Panel):
|
class AvatarToolKit_PT_ToolsPanel(Panel):
|
||||||
"""Panel containing various tools for avatar customization and optimization"""
|
"""Panel containing various tools for avatar customization and optimization"""
|
||||||
bl_label: str = t("Tools.label")
|
bl_label: str = t("Tools.label")
|
||||||
@@ -18,6 +30,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the tools panel interface"""
|
"""Draw the tools panel interface"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# General Tools
|
# General Tools
|
||||||
tools_box: UILayout = layout.box()
|
tools_box: UILayout = layout.box()
|
||||||
@@ -45,7 +58,22 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
# Weight Tools
|
# Weight Tools
|
||||||
weight_box: UILayout = bone_box.box()
|
weight_box: UILayout = bone_box.box()
|
||||||
col = weight_box.column(align=True)
|
col = weight_box.column(align=True)
|
||||||
col.prop(context.scene.avatar_toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
||||||
|
col.prop(toolkit, "preserve_parent_bones")
|
||||||
|
col.prop(toolkit, "target_bone_type")
|
||||||
|
col.prop(toolkit, "list_only_mode")
|
||||||
|
|
||||||
|
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
|
||||||
|
box = weight_box.box()
|
||||||
|
row = box.row()
|
||||||
|
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
|
||||||
|
toolkit, "zero_weight_bones",
|
||||||
|
toolkit, "zero_weight_bones_index")
|
||||||
|
|
||||||
|
col = box.column(align=True)
|
||||||
|
col.operator("avatar_toolkit.remove_selected_bones",
|
||||||
|
text=t("Tools.remove_selected_bones"))
|
||||||
|
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.operator("avatar_toolkit.clean_weights", text=t("Tools.clean_weights"), icon='GROUP_BONE')
|
row.operator("avatar_toolkit.clean_weights", text=t("Tools.clean_weights"), icon='GROUP_BONE')
|
||||||
row.operator("avatar_toolkit.clean_constraints", text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
|
row.operator("avatar_toolkit.clean_constraints", text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
|
||||||
@@ -67,3 +95,11 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
col.operator("avatar_toolkit.apply_transforms", text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
|
col.operator("avatar_toolkit.apply_transforms", text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
|
||||||
col.operator("avatar_toolkit.clean_shapekeys", text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
|
col.operator("avatar_toolkit.clean_shapekeys", text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
|
||||||
|
|
||||||
|
# Rigify Tools
|
||||||
|
rigify_box: UILayout = layout.box()
|
||||||
|
col = rigify_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.rigify_title"), icon='ARMATURE_DATA')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.convert_rigify_to_unity", icon='ARMATURE_DATA')
|
||||||
|
col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Panel, Context, UILayout
|
||||||
|
from ..core.translations import t
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_UVPanel(Panel):
|
||||||
|
"""Main UV Tools panel for Avatar Toolkit"""
|
||||||
|
bl_label = t("AvatarToolkit.label")
|
||||||
|
bl_idname = "OBJECT_PT_avatar_toolkit_uv_main"
|
||||||
|
bl_space_type = 'IMAGE_EDITOR'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = "Avatar Toolkit"
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
# Add title section
|
||||||
|
box: UILayout = layout.box()
|
||||||
|
col: UILayout = box.column(align=True)
|
||||||
|
row: UILayout = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t("AvatarToolkit.label"), icon='UV')
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Panel, Context, UILayout
|
||||||
|
from ..core.translations import t
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_UVTools(Panel):
|
||||||
|
"""UV Tools panel containing UV manipulation operators"""
|
||||||
|
bl_label = t("Tools.label")
|
||||||
|
bl_idname = "OBJECT_PT_avatar_toolkit_uv_tools"
|
||||||
|
bl_space_type = 'IMAGE_EDITOR'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = "Avatar Toolkit"
|
||||||
|
bl_parent_id = "OBJECT_PT_avatar_toolkit_uv_main"
|
||||||
|
bl_order = 3
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
tools_box: UILayout = layout.box()
|
||||||
|
col: UILayout = tools_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.uv_title"), icon='UV')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
row: UILayout = col.row(align=True)
|
||||||
|
row.operator("avatar_toolkit.align_uv_edges_to_target",
|
||||||
|
text=t("UVTools.align_edges"),
|
||||||
|
icon='GP_MULTIFRAME_EDITING')
|
||||||
Reference in New Issue
Block a user