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