Merge branch 'main' into RemoveUnusedShapekeys

This commit is contained in:
Yusarina
2024-09-11 01:43:15 +01:00
committed by GitHub
9 changed files with 548 additions and 7 deletions
+158
View File
@@ -0,0 +1,158 @@
import bpy
from ..core import common
from bpy.types import Operator, Context, Mesh, Armature, EditBone
from ..core.register import register_wrap
from .translations import t
@register_wrap
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')
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) #add bone name to list of bones that are greater than the weight threshold
bpy.ops.object.mode_set(mode='EDIT')
amature_data: Armature = armature.data
unweighted_bones: list[str] = []
#doing 2 loops to prevent modification of array during iteration
for bone in amature_data.edit_bones:
if bone.name not in weighted_bones:
unweighted_bones.append(bone.name) #add bones that arent in the list of bones that have weight into the list of bones that don't
for bone_name in unweighted_bones:
for edit_bone in amature_data.edit_bones[bone_name].children:
edit_bone.use_connect = False #to fix randomly moving bones
edit_bone.parent = amature_data.edit_bones[bone_name].parent #to fix unparented bones.
amature_data.edit_bones.remove(amature_data.edit_bones[bone_name]) #delete list of unweighted bones from the armature
self.report({'INFO'}, t("Tools.remove_zero_weight_bones.success"))
return {'FINISHED'}
@register_wrap
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'}
@register_wrap
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:
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) > 0
elif context.mode == "EDIT_ARMATURE":
return len(context.selected_bones) > 0
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')
armature_data: Armature = context.view_layer.objects.active.data
for obj in common.get_all_meshes(context):
for bone in [i.name for i in context.selected_bones]:
if armature_data.edit_bones[bone].parent != None:
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[bone].parent.name)
bpy.ops.object.mode_set(mode='EDIT')
for bone in [i.name for i in context.selected_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'}
+296
View File
@@ -0,0 +1,296 @@
from typing import TypedDict
import bpy
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
import bmesh
import numpy as np
import math
from ..functions.translations import t
from ..core.register import register_wrap
class GenerateLoopTreeResult(TypedDict):
tree: dict[str, set[str]]
selected_loops: dict[str,list[int]]
selected_verts: dict[str,int]
@register_wrap
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'}