Banner image of the blog
0 0 分钟

头身分离角色衣服权重传递脚本

metahuman角色的头和身体是分离的两套骨架系统,传统的蒙皮复制工具,只能选择一个源物体和一个目标物体。这就造成了只能复制头的权重或者身体的权重给衣服。在一些高领衣服上,脖子处就会穿插衣服。再加上metahuman的脖子骨骼非常多,处理权重非常的难受。此脚本可以快速将头和身体的权重复制给衣服,完美解决高领衣服脖子穿插问题。

版本一

此版本只会在衣服的蒙皮列表上上添加身体的骨骼,脖子处还是会有一定的穿插

版本一.png

# -*- coding: utf-8 -*-
import maya.cmds as cmds

def find_skin_cluster(mesh_name):
    history = cmds.listHistory(mesh_name)
    if history:
        skin_clusters = [node for node in history if cmds.nodeType(node) == 'skinCluster']
        if skin_clusters:
            return skin_clusters[0]
    return None


def clothing_weight_py(head_mesh, body_mesh):
    """权重转移主逻辑"""
    if not cmds.objExists(head_mesh):
        cmds.warning("错误:找不到头部模型 '{}'".format(head_mesh))
        return
    if not cmds.objExists(body_mesh):
        cmds.warning("错误:找不到身体模型 '{}'".format(body_mesh))
        return

    clothing_items = cmds.ls(selection=True, type='transform')
    if not clothing_items:
        cmds.warning("请先选择要蒙皮的衣物模型。")
        return

    try:
        source_skin_cluster = find_skin_cluster(body_mesh)
        if not source_skin_cluster:
            raise RuntimeError("身体模型 '{}' 上没有找到 Skin Cluster".format(body_mesh))
        influences = cmds.skinCluster(source_skin_cluster, query=True, influence=True)
    except Exception as e:
        cmds.error("获取身体蒙皮信息时出错: {}".format(e))
        return

    print("开始处理 {} 件衣物...".format(len(clothing_items)))

    for item in clothing_items:
        print("正在处理: {}...".format(item))
        old_skin = find_skin_cluster(item)
        if old_skin:
            print("  - 删除旧的皮肤节点 '{}'".format(old_skin))
            cmds.delete(old_skin)

        cmds.skinCluster(influences, item, toSelectedBones=True,
                         bindMethod=0, skinMethod=0, normalizeWeights=1)

        cmds.select(head_mesh, body_mesh, item, replace=True)
        try:
            cmds.copySkinWeights(
                noMirror=True,
                surfaceAssociation='closestPoint',
                influenceAssociation='closestJoint'
            )
        except Exception as e:
            cmds.warning("  - '{}' 复制权重失败: {}".format(item, e))
            continue

    cmds.select(clothing_items, replace=True)
    print("--- 所有衣物权重转移完成! ---")


def clothing_weight_ui():
    """UI 界面"""
    win = "ClothingWeightUI"
    if cmds.window(win, exists=True):
        cmds.deleteUI(win)

    win = cmds.window(win, title="Clothing Weight Tool", widthHeight=(380, 220))
    cmds.columnLayout(adjustableColumn=True, rowSpacing=10)

    # --- 头部 ---
    cmds.text(label="1. 选中头部模型后点击右侧按钮加载:")
    head_row = cmds.rowLayout(numberOfColumns=2, adjustableColumn=1, columnAlign=(1, 'left'))
    head_field = cmds.textField(text="", editable=False)
    head_btn = cmds.button(label="加载头部", width=100, height=30)
    cmds.setParent("..")

    # --- 身体 ---
    cmds.text(label="2. 选中身体模型后点击右侧按钮加载:")
    body_row = cmds.rowLayout(numberOfColumns=2, adjustableColumn=1, columnAlign=(1, 'left'))
    body_field = cmds.textField(text="", editable=False)
    body_btn = cmds.button(label="加载身体", width=100, height=30)
    cmds.setParent("..")

    cmds.separator(height=10, style="in")

    cmds.text(label="3. 选中衣物后点击:")
    run_btn = cmds.button(label="开始权重转移", height=40, bgc=(0.3, 0.6, 0.3))

    # 存储变量
    state = {"head": None, "body": None}

    def set_head(*_):
        sel = cmds.ls(selection=True, type='transform')
        if not sel:
            cmds.warning("请先在场景中选中头部模型。")
            return
        state["head"] = sel[0]
        cmds.textField(head_field, edit=True, text=sel[0])
        print("已设置头部模型: {}".format(sel[0]))

    def set_body(*_):
        sel = cmds.ls(selection=True, type='transform')
        if not sel:
            cmds.warning("请先在场景中选中身体模型。")
            return
        state["body"] = sel[0]
        cmds.textField(body_field, edit=True, text=sel[0])
        print("已设置身体模型: {}".format(sel[0]))

    def run_transfer(*_):
        if not state["head"] or not state["body"]:
            cmds.warning("请先设置头部和身体模型。")
            return
        clothing_weight_py(state["head"], state["body"])

    # 绑定事件
    cmds.button(head_btn, edit=True, command=set_head)
    cmds.button(body_btn, edit=True, command=set_body)
    cmds.button(run_btn, edit=True, command=run_transfer)

    cmds.showWindow(win)


# 打开 UI
clothing_weight_ui()

版本二

此版本会在衣服上添加头和身体的所有骨骼,效果最好

版本二.png

# -*- coding: utf-8 -*-

import maya.cmds as cmds
from functools import partial

TOOL_NAME = "服装绑定工具"
TOOL_TITLE = "metahuman服装绑定工具_权重转移"

class SkinToolsUI_V11:
    def __init__(self):
        if cmds.window(TOOL_NAME, exists=True):
            cmds.deleteUI(TOOL_NAME, window=True)
            
        self.window = cmds.window(TOOL_NAME, title=TOOL_TITLE, widthHeight=(420, 600), sizeable=True)
        
        main_layout = cmds.columnLayout(adjustableColumn=True)
        tabs = cmds.tabLayout(innerMarginWidth=5, innerMarginHeight=5)
        
        clothing_tab = cmds.columnLayout(parent=tabs, adjustableColumn=True)
        self.create_clothing_tab(clothing_tab)
        
        transfer_tab = cmds.columnLayout(parent=tabs, adjustableColumn=True)
        self.create_transfer_tab(transfer_tab)
        
        cmds.tabLayout(tabs, edit=True, tabLabel=((clothing_tab, '服装权重复制'), (transfer_tab, '权重转移工具')))
        
        cmds.showWindow(self.window)

    def create_clothing_tab(self, parent_layout):
        cmds.text(label="说明: 为选中的衣物,从头部和身体模型复制权重。", align="left", ww=True, p=parent_layout)
        cmds.separator(height=10, style='in', p=parent_layout)
        self.head_mesh_field = cmds.textFieldButtonGrp(parent=parent_layout, label='头部模型:', text='character_head_geo', buttonLabel='加载选中', cw3=[80, 240, 80], buttonCommand=partial(self.load_selected_to_field, "head_mesh_field"))
        self.body_mesh_field = cmds.textFieldButtonGrp(parent=parent_layout, label='身体模型:', text='character_body_geo', buttonLabel='加载选中', cw3=[80, 240, 80], buttonCommand=partial(self.load_selected_to_field, "body_mesh_field"))
        cmds.separator(height=20, style='in', p=parent_layout)
        cmds.button(label='开始复制服装权重', height=40, command=self.run_clothing_weight_copy, p=parent_layout)

    def create_transfer_tab(self, parent_layout):
        cmds.frameLayout(label="权重转移 (Transfer Weights)", collapsable=False, marginWidth=10, marginHeight=5)
        cmds.text(label="说明: 将多个源骨骼的权重,精确地转移给一个目标骨骼,从权重工具中选择骨骼时,请先在模式中切换到选择模式。", align="left", ww=True)
        cmds.separator(height=10, style='none')
        cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 300), (2, 100)])
        self.source_joints_list = cmds.textScrollList(numberOfRows=8, allowMultiSelection=True)
        cmds.columnLayout(adjustableColumn=True)
        cmds.button(label='<< 添加源', height=40, command=self.load_source_joints)
        cmds.separator(height=10, style='none')
        cmds.button(label='清空列表', command=lambda *args: cmds.textScrollList(self.source_joints_list, e=True, removeAll=True))
        cmds.setParent('..'); cmds.setParent('..')
        cmds.separator(height=5, style='none')
        cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 300), (2, 100)])
        self.target_joint_field = cmds.textField(editable=False, placeholderText='(请从场景或权重工具加载...)')
        cmds.button(label='<< 加载目标', height=30, command=self.load_target_joint)
        cmds.setParent('..')
        cmds.separator(height=15, style='in')
        cmds.button(label='执行权重转移', height=40, command=self.run_weight_transfer_v11)
        cmds.setParent('..')
        cmds.separator(height=20, style='double')
        cmds.frameLayout(label="其他工具 (Utilities)", collapsable=True, collapse=False, marginWidth=10, marginHeight=5)
        cmds.text(label="说明: 选择物体和骨骼后,直接从蒙皮中移除影响。", align="left", ww=True)
        cmds.separator(height=10, style='none')
        cmds.button(label='从蒙皮列表中移除选中的骨骼', height=35, command=self.run_remove_influences)
        cmds.setParent('..')

    # --- 核心后端函数 ---

    def load_selected_to_field(self, field_name, *args):
        """ [BUG FIX] 恢复这个被误删的函数 """
        selection = cmds.ls(selection=True, head=1)
        if selection:
            field_map = {
                "head_mesh_field": self.head_mesh_field,
                "body_mesh_field": self.body_mesh_field
            }
            cmds.textFieldButtonGrp(field_map[field_name], edit=True, text=selection[0])
        else:
            cmds.warning("请先选择一个物体。")

    def get_selected_joints_from_anywhere(self):
        selection = []
        try:
            paint_tool_list = 'artSkinInflList'
            if cmds.textScrollList(paint_tool_list, exists=True):
                selection = cmds.textScrollList(paint_tool_list, q=True, selectItem=True)
        except Exception: pass
        if not selection:
            selection = cmds.ls(selection=True, type='joint')
        return selection

    def load_source_joints(self, *args):
        selection = self.get_selected_joints_from_anywhere()
        if not selection:
            cmds.warning("请先在场景或权重工具中选择一个或多个【源骨骼】。")
            return
        current_list = cmds.textScrollList(self.source_joints_list, q=True, allItems=True) or []
        target_joint = cmds.textField(self.target_joint_field, q=True, text=True)
        for jnt in selection:
            if jnt == target_joint:
                cmds.warning("不能将目标骨骼 '{}' 添加为源骨骼。".format(jnt))
                continue
            if jnt not in current_list:
                cmds.textScrollList(self.source_joints_list, edit=True, append=jnt)

    def load_target_joint(self, *args):
        selection = self.get_selected_joints_from_anywhere()
        if not selection:
            cmds.warning("请先在场景或权重工具中选择一个【目标骨骼】。")
            return
        if len(selection) > 1:
            cmds.warning("只能选择一个目标骨骼。将使用最后一个选择: '{}'".format(selection[-1]))
        target_joint = selection[-1]
        current_source_list = cmds.textScrollList(self.source_joints_list, q=True, allItems=True) or []
        if target_joint in current_source_list:
            cmds.warning("骨骼 '{}' 已在源列表中,将从中移除。".format(target_joint))
            cmds.textScrollList(self.source_joints_list, e=True, removeItem=target_joint)
        cmds.textField(self.target_joint_field, edit=True, text=target_joint)

    def find_skin_cluster(self, mesh_name):
        history = cmds.listHistory(mesh_name, future=False)
        if history:
            skin_clusters = [node for node in history if cmds.nodeType(node) == 'skinCluster']
            if skin_clusters: return skin_clusters[0]
        return None
        
    def run_clothing_weight_copy(self, *args):
        # ... 服装复制函数 ...
        head_mesh = cmds.textFieldButtonGrp(self.head_mesh_field, query=True, text=True)
        body_mesh = cmds.textFieldButtonGrp(self.body_mesh_field, query=True, text=True)
        if not all(cmds.objExists(name) for name in [head_mesh, body_mesh]):
            cmds.error("错误:头部或身体模型在场景中找不到。")
            return
        clothing_items = cmds.ls(selection=True, type='transform')
        if not clothing_items:
            cmds.warning("请先选择您想要蒙皮的衣物模型。")
            return
        try:
            skin_cluster_body = self.find_skin_cluster(body_mesh)
            skin_cluster_head = self.find_skin_cluster(head_mesh)
            influences_body = cmds.skinCluster(skin_cluster_body, q=True, influence=True)
            influences_head = cmds.skinCluster(skin_cluster_head, q=True, influence=True)
            combined_influences = list(set(influences_body + influences_head))
        except Exception as e:
            cmds.error("获取骨骼影响列表时出错: {}".format(e))
            return
        cmds.progressWindow(title='复制服装权重', progress=0, max=len(clothing_items), status='正在处理...', isInterruptable=True)
        for i, item in enumerate(clothing_items):
            if cmds.progressWindow(query=True, isCancelled=True): break
            cmds.progressWindow(edit=True, progress=i + 1, status="正在处理: {}".format(item))
            old_skin = self.find_skin_cluster(item)
            if old_skin: cmds.delete(old_skin)
            cmds.skinCluster(combined_influences, item, toSelectedBones=True, normalizeWeights=1)
            cmds.select(head_mesh, body_mesh, item, r=True)
            cmds.copySkinWeights(noMirror=True, surfaceAssociation='closestPoint', influenceAssociation='closestJoint')
        cmds.progressWindow(endProgress=1)
        cmds.select(clothing_items, r=True)
        print("--- 服装权重复制完成! ---")

    def run_weight_transfer_v11(self, *args):
        source_joints = cmds.textScrollList(self.source_joints_list, query=True, allItems=True)
        target_joint = cmds.textField(self.target_joint_field, query=True, text=True)
        if not source_joints or not target_joint:
            cmds.warning("源骨骼列表和目标骨骼不能为空。")
            return
        skinned_meshes = [obj for obj in cmds.ls(sl=True, type='transform') if self.find_skin_cluster(obj)]
        if not skinned_meshes:
            cmds.warning("请先选择一个或多个已经蒙皮的物体。")
            return
        
        total_operations = len(skinned_meshes) * len(source_joints)
        cmds.progressWindow(title='转移骨骼权重', progress=0, max=total_operations, status='准备开始...', isInterruptable=True)
        current_op = 0

        for mesh in skinned_meshes:
            skin_cluster = self.find_skin_cluster(mesh)
            if not skin_cluster: continue

            all_influences = cmds.skinCluster(skin_cluster, q=True, influence=True)
            if target_joint not in all_influences:
                cmds.skinCluster(skin_cluster, edit=True, addInfluence=target_joint, weight=0)
                # 重新获取,确保目标骨骼已包含
                all_influences = cmds.skinCluster(skin_cluster, q=True, influence=True)

            joints_to_lock = [jnt for jnt in all_influences if jnt != target_joint and jnt not in source_joints]
            num_verts = cmds.polyEvaluate(mesh, vertex=True)
            all_verts_string = "{}.vtx[0:{}]".format(mesh, num_verts - 1)

            try:
                # 1. 初始锁定: 锁定所有无关骨骼
                for jnt in joints_to_lock:
                    cmds.setAttr("{}.lockInfluenceWeights".format(jnt), True)
                
                # 2. 连锁操作
                for source_joint in source_joints:
                    if cmds.progressWindow(query=True, isCancelled=True): break
                    current_op += 1
                    cmds.progressWindow(edit=True, progress=current_op, status="处理 {}: {}".format(mesh, source_joint))

                    if source_joint in all_influences:
                        # 将当前源骨骼权重清零 (权重会流向其他未锁定的源骨骼+目标骨骼)
                        cmds.skinPercent(skin_cluster, all_verts_string, transformValue=[(source_joint, 0)])
                        # 立即锁定它,防止它接收后续的权重
                        cmds.setAttr("{}.lockInfluenceWeights".format(source_joint), True)
                
            except Exception as e:
                print("处理模型 {} 时发生错误: {}".format(mesh, e))
            finally:
                # 3. 终极解锁: 无论如何,解锁所有骨骼
                for jnt in all_influences:
                    if cmds.objExists(jnt) and cmds.attributeQuery('lockInfluenceWeights', node=jnt, exists=True):
                        cmds.setAttr("{}.lockInfluenceWeights".format(jnt), False)

            if cmds.progressWindow(query=True, isCancelled=True): break
            
        cmds.progressWindow(endProgress=1)
        print("--- 骨骼权重转移完成! ---")

    def run_remove_influences(self, *args):
        # ... 移除影响函数 ...
        joints_to_remove = self.get_selected_joints_from_anywhere()
        if not joints_to_remove:
            cmds.warning("请先选择要移除的骨骼。")
            return
        skinned_meshes = [obj for obj in cmds.ls(sl=True, type='transform') if self.find_skin_cluster(obj)]
        if not skinned_meshes:
            cmds.warning("请先选择一个或多个要操作的蒙皮物体。")
            return
        for mesh in skinned_meshes:
            skin_cluster = self.find_skin_cluster(mesh)
            if not skin_cluster: continue
            print("正在处理模型: {}".format(mesh))
            for jnt in joints_to_remove:
                try:
                    print("  - 正在移除影响: {}".format(jnt))
                    cmds.skinCluster(skin_cluster, edit=True, removeInfluence=jnt)
                except Exception as e:
                    print("    警告: 无法移除 '{}'. Error: {}".format(jnt, e))
        print("--- 移除影响操作完成! ---")


# --- 启动UI ---
if __name__ == "__main__":
    skin_tool_ui_v11 = SkinToolsUI_V11()

如何使用

  1. 头部模型设置:选中头部模型后点击 "加载头部" 按钮。
  2. 身体模型设置:选中身体模型后点击 "加载身体" 按钮。
  3. 衣物权重转移:选中需要处理的衣物模型后点击 "开始" 按钮,工具将自动执行权重复制流程