MetaFusion/scripts/utils/CloneBlendShape.py
2025-02-07 05:10:30 +08:00

476 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import maya.cmds as cmds
import maya.mel as mel
from Core import GetBlendShapeNodes, GetWrapNodes, FilterSingleBlendShapeDeformerTargets
def wrap_convert_blend():
"""
将选中的模型的混合变形转换为新的混合变形
"""
# 获取选中的对象
sel = cmds.ls(selection=True)
if len(sel) != 2:
cmds.warning("请选择两个模型对象")
return
# 获取混合变形节点
bs_nodes = GetBlendShapeNodes(sel[1])
if not bs_nodes:
cmds.warning("目标模型不包含混合变形,请确保选择正确的源和目标模型")
return
# 获取混合变形目标列表
bs_target_attrs = cmds.listAttr(f"{bs_nodes[0]}.w", multi=True) or []
if not bs_target_attrs:
cmds.warning("混合变形目标列表为空")
return
# 如果选择了wrap方法
if cmds.checkBox("wrapMethod", query=True, value=True):
cmds.select(sel[0], sel[1])
mel.eval('CreateWrap')
# 创建临时组
if cmds.objExists("WrapConvertTemp"):
cmds.delete("WrapConvertTemp")
temp_group = cmds.group(empty=True, name="WrapConvertTemp")
rb_mesh = []
# 复制每个混合变形目标
for bs_attr in bs_target_attrs:
bs_weight = cmds.getAttr(f"{bs_nodes[0]}.{bs_attr}")
is_connected = cmds.connectionInfo(f"{bs_nodes[0]}.{bs_attr}", isDestination=True)
if bs_weight != 1 and not is_connected:
# 设置权重为1并复制模型
cmds.setAttr(f"{bs_nodes[0]}.{bs_attr}", 1)
copy_mesh = cmds.duplicate(sel[0])[0]
cmds.parent(copy_mesh, temp_group)
copy_mesh = cmds.rename(copy_mesh, bs_attr)
rb_mesh.append(f":WrapConvertTemp|:{bs_attr}")
cmds.setAttr(f"{bs_nodes[0]}.{bs_attr}", 0)
# 处理wrap节点
if cmds.checkBox("wrapMethod", query=True, value=True):
wrap_nodes = GetWrapNodes(sel[0])
base_nodes = cmds.ls(f"{sel[1]}Bas*")
if wrap_nodes or base_nodes:
cmds.delete(wrap_nodes + base_nodes)
# 创建新的混合变形
if rb_mesh:
cmds.blendShape(rb_mesh, sel[0], frontOfChain=True)
# 处理DeltaMush
if cmds.objExists('deltaMush') and cmds.checkBox("deltaMushTarget", query=True, value=True):
delta_value = cmds.intSliderGrp("deltaIntensity", query=True, value=True)
_process_delta_mush(sel[0], delta_value)
# 清理
cmds.delete("WrapConvertTemp")
print("混合变形处理完成")
def _process_delta_mush(target, delta_value):
"""
处理DeltaMush相关的操作
参数:
target (str): 目标模型
delta_value (int): DeltaMush强度值
"""
# 创建DeltaMush变形器
dl_names = cmds.deltaMush(
target,
smoothingIterations=delta_value,
smoothingStep=1.0,
pinBorderVertices=1,
envelope=1
)
# 获取混合变形节点和目标
bs_nodes = GetBlendShapeNodes(target)
bs_target_attrs = cmds.listAttr(f"{bs_nodes[0]}.w", multi=True) or []
# 创建临时组
if cmds.objExists("WrapConvertTemp"):
cmds.delete("WrapConvertTemp")
temp_group = cmds.group(empty=True, name="WrapConvertTemp")
rb_mesh = []
# 处理每个混合变形目标
for bs_attr in bs_target_attrs:
bs_weight = cmds.getAttr(f"{bs_nodes[0]}.{bs_attr}")
if bs_weight != 1:
# 设置权重并创建关键帧
cmds.setAttr(f"{bs_nodes[0]}.{bs_attr}", 0)
cmds.currentTime(-10)
cmds.setAttr(f"{bs_nodes[0]}.{bs_attr}", 1)
cmds.currentTime(0)
# 复制模型并重命名
copy_mesh = cmds.duplicate(target)[0]
cmds.parent(copy_mesh, temp_group)
copy_mesh = cmds.rename(copy_mesh, bs_attr)
rb_mesh.append(f":WrapConvertTemp|:{bs_attr}")
# 重置权重
cmds.setAttr(f"{bs_nodes[0]}.{bs_attr}", 0)
cmds.currentTime(-10)
cmds.currentTime(0)
# 清理并创建新的混合变形
cmds.delete(dl_names + bs_nodes)
if rb_mesh:
cmds.blendShape(rb_mesh, target, frontOfChain=True)
cmds.delete("WrapConvertTemp")
print("DeltaMush优化完成")
def clone_blend_shape_ui():
"""
创建混合变形克隆工具的UI界面
"""
window_name = "wrapChangeBS"
if cmds.window(window_name, exists=True):
cmds.deleteUI(window_name)
# 创建主窗口
window = cmds.window(
window_name,
title="BlendShapeBake",
width=300,
height=300,
menuBar=True
)
# 创建主布局
main_layout = cmds.columnLayout(
adjustableColumn=1,
columnAttach=["both", 5],
rowSpacing=2,
columnWidth=150
)
cmds.separator(height=10, style="in")
# 创建Wrap方法选项
cmds.checkBox(
"wrapMethod",
label="Use Wrap Method",
value=1,
changeCommand="sparkly_wrap_convert_blend_check_return"
)
# 创建DeltaMush选项如果可用
if cmds.pluginInfo("deltaMush", query=True, loaded=True):
cmds.checkBox(
"deltaMushTarget",
label="DeltaMushTarget",
value=0,
changeCommand="sparkly_wrap_convert_blend_check_return"
)
cmds.intSliderGrp(
"deltaIntensity",
field=True,
minValue=0,
maxValue=100,
value=10,
width=50
)
else:
cmds.text(
label="低版本DeltaMush不可用",
font="boldLabelFont"
)
cmds.separator(height=10, style="in")
# 创建功能按钮
cmds.button(label="Cloning", command="wrap_convert_blend()")
cmds.button(label="Copy", command="sg_copy_blend()")
cmds.button(label="Mirror", command="mirror_blend()")
cmds.separator(height=10, style="in")
# 调整窗口大小
cmds.window(
window_name,
edit=True,
width=260,
height=85
)
cmds.showWindow()
def sparkly_wrap_convert_blend_check_return():
"""
检查框回调函数(保持为空以保持兼容性)
"""
pass
def sg_copy_blend():
"""
复制混合变形目标
"""
# 获取选中的混合变形目标
sel_bs_name_list = mel.eval('getShapeEditorTreeviewSelection(14)')
# 解析目标索引
tmp_a = sel_bs_name_list[0].split('.')
tmp_b = sel_bs_name_list[1].split('.')
target_a = int(tmp_a[1])
target_b = int(tmp_b[1])
# 重新生成目标并设置连接
mesh_a = cmds.sculptTarget(
tmp_a[0],
edit=True,
regenerate=True,
target=target_a
)
mesh_shape_a = cmds.listRelatives(mesh_a[0], shapes=True)
# 断开原有连接并建立新连接
cmds.disconnectAttr(
f"{mesh_shape_a[0]}.worldMesh[0]",
f"{tmp_a[0]}.inputTarget[0].inputTargetGroup[{target_a}].inputTargetItem[6000].inputGeomTarget"
)
cmds.connectAttr(
f"{mesh_shape_a[0]}.worldMesh[0]",
f"{tmp_a[0]}.inputTarget[0].inputTargetGroup[{target_b}].inputTargetItem[6000].inputGeomTarget",
force=True
)
# 删除临时网格
cmds.delete(mesh_a)
def mirror_blend():
"""
镜像混合变形目标
"""
# 获取当前对称设置
original_symmetry = cmds.symmetricModelling(query=True, symmetry=True)
symmetry_space = None
symmetry_axis = None
if original_symmetry:
symmetry_space = cmds.symmetricModelling(query=True, about=True)
if symmetry_space == "topo":
symmetry_axis = mel.eval('blendShapeGetTopoSymmetryEdge()')
else:
symmetry_axis = cmds.symmetricModelling(query=True, axis=True)
# 获取选中的混合变形目标
sel_bs_name_list = mel.eval('getShapeEditorTreeviewSelection(14)')
# 解析目标索引
tmp_a = sel_bs_name_list[0].split('.')
tmp_b = sel_bs_name_list[1].split('.')
target_a = int(tmp_a[1])
target_b = int(tmp_b[1])
# 重新生成目标并设置连接
mesh_a = cmds.sculptTarget(
tmp_a[0],
edit=True,
regenerate=True,
target=target_a
)
mesh_shape_a = cmds.listRelatives(mesh_a[0], shapes=True)
# 断开原有连接并建立新连接
cmds.disconnectAttr(
f"{mesh_shape_a[0]}.worldMesh[0]",
f"{tmp_a[0]}.inputTarget[0].inputTargetGroup[{target_a}].inputTargetItem[6000].inputGeomTarget"
)
cmds.connectAttr(
f"{mesh_shape_a[0]}.worldMesh[0]",
f"{tmp_a[0]}.inputTarget[0].inputTargetGroup[{target_b}].inputTargetItem[6000].inputGeomTarget",
force=True
)
# 删除临时网格
cmds.delete(mesh_a)
# 翻转目标
cmds.blendShape(
tmp_b[0],
edit=True,
flipTarget=True,
geometryIndex=0,
targetIndex=target_b,
symmetryAxis="x",
symmetrySpace=1
)
# 重置反射模式
mel.eval('reflectionSetMode none')
# 恢复对称设置
if not original_symmetry or not symmetry_axis:
mel.eval('symmetricModelling -s 0')
elif symmetry_space == "topo":
mel.eval(f'symmetricModelling -e -about {symmetry_space} -s 1 {symmetry_axis}')
else:
mel.eval(f'symmetricModelling -e -about {symmetry_space} -axis {symmetry_axis} -s 1')
def get_shape_editor_treeview_selection(scope):
"""
获取形状编辑器树视图中的选择
参数:
scope (int): 选择范围:
0-8: 简单范围
9: 单个BSD中的目标
10-16: 纯类型选择
20: 最后选择的项目
21,24: 纯类型及其组类型
30: 引用项目检查
31: 第一个选择项
返回:
list: 根据范围返回的字符串数组
"""
# 定义类型上限
type_roof = 6 # 注意:添加新树项类型时更新上限
# 检查选择变量是否存在
if not cmds.optionVar(exists="blendShapeEditorTreeViewSelection"):
return []
# 获取选择数据
ov = cmds.optionVar(query="blendShapeEditorTreeViewSelection")
# 处理不同范围的选择
if -1 < scope < type_roof + 3: # 范围 0-8
return ov[scope].split("/") if ov[scope] else []
if scope == 9: # 范围 9
return filter_single_bsd(ov[8].split("/") if ov[8] else [])
if 9 < scope < type_roof + 11: # 范围 10-16
selection = ov[scope-10].split("/") if ov[scope-10] else []
if not selection:
return []
# 跳过相关类型检查
type_array = list(range(type_roof))
new_type_array = [x for x in type_array if x not in [scope-10, 2, 5]]
if scope in [12, 15]:
new_type_array = [x for x in new_type_array if x != scope-12]
# 检查混合选择
for i in new_type_array:
if ov[i] and ov[i].split("/"):
return []
return selection
if scope == 20: # 范围 20
return ov[9].split("/") if ov[9] else []
if scope in [21, 24]: # 范围 21,24
selection_out = ov[scope-20].split("/") if ov[scope-20] else []
selection_in = ov[scope-19].split("/") if ov[scope-19] else []
selection = list(set(selection_out + selection_in))
if not selection:
return []
# 跳过相关类型检查
type_array = list(range(type_roof))
new_type_array = [x for x in type_array if x not in [scope-20, scope-19, scope-21]]
# 检查混合选择
for i in new_type_array:
if ov[i] and ov[i].split("/"):
return []
return selection
if scope == 30: # 范围 30
return ov[10].split("/") if ov[10] else []
if scope == 31: # 范围 31
return ov[11].split("/") if ov[11] else []
return [] # 无效范围
def filter_single_bsd(items):
"""
从单个混合变形变形器中返回选定的目标(组)
参数:
items (list): 要过滤的项目列表
返回:
list: 过滤后的项目列表如果来自多个BSD则返回空列表
"""
result = []
one_bsd = ""
for item in items:
sub_strings = item.split(".")
if not one_bsd:
one_bsd = sub_strings[0]
if one_bsd != sub_strings[0]:
return []
result.append(item)
return result
def is_shape_editor_reference_item_selected():
"""
检查是否在形状编辑器树视图中选择了引用项
返回:
bool: 如果选择了引用项则返回True否则返回False
"""
result = get_shape_editor_treeview_selection(30)
if len(result) == 1:
return result[0] == "1"
return False
def blend_shape_editor_selected_ancestor(bsd_name, tgt_index, selected_targets):
"""
获取选定的祖先(如果存在)
参数:
bsd_name (str): 混合变形变形器名称
tgt_index (int): 目标索引(>=0为目标<0为目标目录
selected_targets (list): 选定的目标列表
返回:
int: 最高选定的父目录索引如果没有选定的祖先则返回0
"""
highest_parent_directory = 0
parent_dir = -1
if tgt_index >= 0:
# 这是一个目标,获取父目录
temp_attr = f"{bsd_name}.parentDirectory[{tgt_index}]"
parent_dir = cmds.getAttr(temp_attr)
if parent_dir <= -1:
return 0 # 默认值
if parent_dir == 0:
return 0 # 目标项直接在混合变形节点下
else:
# 这是一个目标目录,获取父目录
temp_attr = f"{bsd_name}.targetDirectory[{-tgt_index}].parentIndex"
parent_dir = cmds.getAttr(temp_attr)
while parent_dir > 0:
# 检查此目录是否被选中
dir_name_encoded = f"{bsd_name}.{-parent_dir}"
if dir_name_encoded in selected_targets:
highest_parent_directory = parent_dir
# 获取父目录的父目录
temp_attr = f"{bsd_name}.targetDirectory[{parent_dir}].parentIndex"
parent_dir = cmds.getAttr(temp_attr)
return highest_parent_directory