MetaFusion/scripts/utils/model_utils.py

599 lines
20 KiB
Python
Raw Normal View History

2025-02-06 04:46:41 +08:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#===================================== 1. Module Imports =====================================
2025-02-06 04:00:17 +08:00
import maya.cmds as cmds
import maya.mel as mel
2025-02-06 04:46:41 +08:00
import sys
import os
2025-02-06 04:00:17 +08:00
2025-02-06 04:46:41 +08:00
#===================================== 2. Model Utils =====================================
2025-02-06 04:00:17 +08:00
def load_model(lod_index, model_type, file_path):
"""加载模型
Args:
lod_index: LOD级别(0-7)
model_type: 模型类型(head/teeth/gums/eye_l/eye_r/iris/eyelash/eyelid/cartilage/body)
file_path: 模型文件路径
"""
try:
# 检查文件是否存在
if not os.path.exists(file_path):
raise FileNotFoundError(f"模型文件不存在: {file_path}")
# 导入模型
imported_nodes = cmds.file(
file_path,
i=True,
returnNewNodes=True,
namespace=f"LOD{lod_index}_{model_type}"
)
# 设置模型属性
for node in imported_nodes:
if cmds.objectType(node) == "transform":
# 添加自定义属性
cmds.addAttr(node, ln="LOD_INDEX", at="long", dv=lod_index)
cmds.addAttr(node, ln="MODEL_TYPE", dt="string")
cmds.setAttr(f"{node}.MODEL_TYPE", model_type, type="string")
print(f"成功加载LOD{lod_index}_{model_type}模型")
return imported_nodes
except Exception as e:
cmds.warning(f"加载模型失败: {str(e)}")
return None
def delete_lod(lod_index):
"""删除指定LOD级别的所有模型"""
try:
# 查找指定LOD的所有模型
lod_nodes = cmds.ls(f"LOD{lod_index}_*", long=True)
if lod_nodes:
cmds.delete(lod_nodes)
print(f"成功删除LOD{lod_index}的所有模型")
else:
print(f"未找到LOD{lod_index}的模型")
except Exception as e:
cmds.warning(f"删除LOD{lod_index}失败: {str(e)}")
# 模型工具功能
def split_model():
"""分离选中的模型"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择要分离的模型")
for obj in selection:
# 获取模型的面数
mesh = cmds.listRelatives(obj, shapes=True, type="mesh")
if not mesh:
continue
# 分离每个面为独立的模型
cmds.polySeparate(obj, ch=False)
print("模型分离完成")
except Exception as e:
cmds.warning(f"模型分离失败: {str(e)}")
def generate_facial_accessories():
"""生成面部配件"""
print("生成面部配件功能待实现")
def repair_normals():
"""修复法线"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择要修复法线的模型")
for obj in selection:
mesh = cmds.listRelatives(obj, shapes=True, type="mesh")
if not mesh:
continue
# 统一法线方向
cmds.polyNormal(obj, normalMode=2, userNormalMode=0)
# 解锁法线
cmds.polyNormalPerVertex(obj, ufn=True)
print("法线修复完成")
except Exception as e:
cmds.warning(f"修复法线失败: {str(e)}")
def repair_vertex_order():
"""修复点序"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择要修复点序的模型")
for obj in selection:
# 重新排序顶点
cmds.polyReorder(obj)
print("点序修复完成")
except Exception as e:
cmds.warning(f"修复点序失败: {str(e)}")
def repair_seams():
"""修复接缝"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择要修复接缝的模型")
for obj in selection:
# 合并重叠顶点
cmds.polyMergeVertex(obj, d=0.001)
# 删除重复面
cmds.polyRemoveFace(obj, removeDuplicateFaces=True)
print("接缝修复完成")
except Exception as e:
cmds.warning(f"修复接缝失败: {str(e)}")
# LOD功能
def load_custom_models():
"""自定义加载模型"""
print("自定义加载模型功能待实现")
def standardize_naming():
"""标准化命名"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择要标准化命名的模型")
for obj in selection:
# 获取LOD信息
if cmds.attributeQuery("LOD_INDEX", node=obj, exists=True):
lod_index = cmds.getAttr(f"{obj}.LOD_INDEX")
model_type = cmds.getAttr(f"{obj}.MODEL_TYPE")
# 重命名为标准格式
new_name = f"LOD{lod_index}_{model_type}"
cmds.rename(obj, new_name)
print("命名标准化完成")
except Exception as e:
cmds.warning(f"标准化命名失败: {str(e)}")
def auto_group():
"""自动分组"""
try:
# 创建LOD组
for i in range(8):
group_name = f"LOD{i}_GROUP"
if not cmds.objExists(group_name):
cmds.group(empty=True, name=group_name)
# 将模型移动到对应组
all_models = cmds.ls("LOD*_*", type="transform")
for model in all_models:
if cmds.attributeQuery("LOD_INDEX", node=model, exists=True):
lod_index = cmds.getAttr(f"{model}.LOD_INDEX")
group_name = f"LOD{lod_index}_GROUP"
cmds.parent(model, group_name)
print("自动分组完成")
except Exception as e:
cmds.warning(f"自动分组失败: {str(e)}")
# MetaHuman模型特定功能
def validate_metahuman_topology():
"""验证模型是否符合MetaHuman拓扑要求"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择要验证的模型")
results = []
for obj in selection:
mesh = cmds.listRelatives(obj, shapes=True, type="mesh")
if not mesh:
continue
# 获取模型信息
vertex_count = cmds.polyEvaluate(obj, vertex=True)
face_count = cmds.polyEvaluate(obj, face=True)
# 检查是否符合MetaHuman标准
is_valid = True
messages = []
# 获取LOD级别
lod_index = -1
if cmds.attributeQuery("LOD_INDEX", node=obj, exists=True):
lod_index = cmds.getAttr(f"{obj}.LOD_INDEX")
# 根据LOD级别验证顶点数和面数
if lod_index == 0:
if vertex_count != 7127:
messages.append(f"顶点数不符合LOD0标准(当前:{vertex_count}, 应为:7127)")
is_valid = False
elif lod_index == 1:
if vertex_count != 5127:
messages.append(f"顶点数不符合LOD1标准(当前:{vertex_count}, 应为:5127)")
is_valid = False
# ... 其他LOD级别的验证
results.append({
"object": obj,
"is_valid": is_valid,
"messages": messages
})
# 显示验证结果
for result in results:
status = "" if result["is_valid"] else ""
print(f"{status} {result['object']}:")
if not result["is_valid"]:
for msg in result["messages"]:
print(f" - {msg}")
return results
except Exception as e:
cmds.warning(f"拓扑验证失败: {str(e)}")
return None
def transfer_uvs():
"""传递UV到其他LOD级别"""
try:
# 获取源模型和目标模型
selection = cmds.ls(sl=True, type="transform")
if len(selection) != 2:
raise ValueError("请选择一个源模型和一个目标模型")
source = selection[0]
target = selection[1]
# 执行UV传递
cmds.transferAttributes(
source,
target,
transferUVs=2,
transferColors=0,
sampleSpace=4 # World space
)
# 删除构建历史
cmds.delete(target, ch=True)
print(f"UV传递完成: {source} -> {target}")
except Exception as e:
cmds.warning(f"UV传递失败: {str(e)}")
def generate_lod_chain():
"""生成完整的LOD链"""
try:
# 获取选中的LOD0模型
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择LOD0模型")
source = selection[0]
if not cmds.attributeQuery("LOD_INDEX", node=source, exists=True):
raise ValueError("请选择带有LOD属性的模型")
lod_index = cmds.getAttr(f"{source}.LOD_INDEX")
if lod_index != 0:
raise ValueError("请选择LOD0模型")
# 为每个LOD级别生成简化模型
for i in range(1, 8):
target_name = source.replace("LOD0", f"LOD{i}")
# 复制模型
duplicate = cmds.duplicate(source, name=target_name)[0]
# 设置LOD属性
cmds.setAttr(f"{duplicate}.LOD_INDEX", i)
# 简化模型
reduction_ratio = 1.0 - (i * 0.1) # 每级减少10%
cmds.polyReduce(
duplicate,
percentage=reduction_ratio * 100,
triangulate=False,
preserveTopology=True,
keepQuadsWeight=1.0,
keepBorder=True
)
print(f"生成LOD{i}完成: {target_name}")
print("LOD链生成完成")
except Exception as e:
cmds.warning(f"LOD链生成失败: {str(e)}")
def check_model_symmetry():
"""检查模型对称性"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择要检查的模型")
for obj in selection:
# 获取顶点位置
vertices = cmds.ls(f"{obj}.vtx[*]", flatten=True)
vertex_positions = {}
for vtx in vertices:
pos = cmds.pointPosition(vtx, world=True)
# 使用x坐标的绝对值作为键保存顶点信息
x_abs = abs(pos[0])
if x_abs not in vertex_positions:
vertex_positions[x_abs] = []
vertex_positions[x_abs].append((vtx, pos))
# 检查对称性
asymmetric_vertices = []
for x_abs, points in vertex_positions.items():
if len(points) == 2:
vtx1, pos1 = points[0]
vtx2, pos2 = points[1]
# 检查y和z坐标是否对称
if abs(pos1[1] - pos2[1]) > 0.001 or abs(pos1[2] - pos2[2]) > 0.001:
asymmetric_vertices.extend([vtx1, vtx2])
elif len(points) == 1 and x_abs > 0.001: # 忽略中心线上的点
asymmetric_vertices.append(points[0][0])
# 显示结果
if asymmetric_vertices:
cmds.select(asymmetric_vertices)
print(f"发现{len(asymmetric_vertices)}个不对称顶点")
else:
print(f"{obj}模型对称性检查通过")
except Exception as e:
cmds.warning(f"对称性检查失败: {str(e)}")
def mirror_geometry():
"""镜像几何体"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请先选择要镜像的模型")
for obj in selection:
# 创建镜像几何体
mirrored = cmds.duplicate(obj, name=f"{obj}_mirrored")[0]
# 执行镜像
cmds.scale(-1, 1, 1, mirrored)
# 反转法线
cmds.polyNormal(mirrored, normalMode=0)
print(f"模型镜像完成: {mirrored}")
except Exception as e:
cmds.warning(f"模型镜像失败: {str(e)}")
# 骨骼权重相关功能
def transfer_skin_weights():
"""传递蒙皮权重"""
try:
selection = cmds.ls(sl=True, type="transform")
if len(selection) < 2:
raise ValueError("请选择源模型和目标模型")
source = selection[0]
targets = selection[1:]
# 获取源模型的蒙皮变形器
skin_cluster = mel.eval(f'findRelatedSkinCluster("{source}")')
if not skin_cluster:
raise ValueError(f"源模型{source}没有蒙皮变形器")
# 获取骨骼列表
joints = cmds.skinCluster(skin_cluster, q=True, inf=True)
# 为每个目标模型创建蒙皮并传递权重
for target in targets:
# 创建新的蒙皮变形器
new_skin = cmds.skinCluster(
joints,
target,
toSelectedBones=True,
bindMethod=0,
skinMethod=0,
normalizeWeights=1
)[0]
# 复制权重
cmds.copySkinWeights(
ss=skin_cluster,
ds=new_skin,
noMirror=True,
surfaceAssociation="closestPoint",
influenceAssociation="oneToOne"
)
print(f"权重传递完成: {source} -> {target}")
except Exception as e:
cmds.warning(f"权重传递失败: {str(e)}")
def mirror_skin_weights():
"""镜像蒙皮权重"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请选择要镜像权重的模型")
for obj in selection:
# 获取蒙皮变形器
skin_cluster = mel.eval(f'findRelatedSkinCluster("{obj}")')
if not skin_cluster:
continue
# 执行镜像
cmds.copySkinWeights(
ss=skin_cluster,
ds=skin_cluster,
mirrorMode="YZ",
surfaceAssociation="closestPoint",
influenceAssociation="closestJoint"
)
print(f"权重镜像完成: {obj}")
except Exception as e:
cmds.warning(f"权重镜像失败: {str(e)}")
def clean_skin_weights():
"""清理蒙皮权重"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请选择要清理权重的模型")
for obj in selection:
# 获取蒙皮变形器
skin_cluster = mel.eval(f'findRelatedSkinCluster("{obj}")')
if not skin_cluster:
continue
# 移除小权重
cmds.skinPercent(
skin_cluster,
obj,
pruneWeights=0.01 # 移除小于1%的权重
)
# 规范化权重
cmds.skinPercent(
skin_cluster,
obj,
normalize=True
)
print(f"权重清理完成: {obj}")
except Exception as e:
cmds.warning(f"权重清理失败: {str(e)}")
# 变形器处理功能
def transfer_blendshapes():
"""传递变形器"""
try:
selection = cmds.ls(sl=True, type="transform")
if len(selection) < 2:
raise ValueError("请选择源模型和目标模型")
source = selection[0]
targets = selection[1:]
# 获取源模型的变形器
blendshapes = cmds.listConnections(source, type="blendShape")
if not blendshapes:
raise ValueError(f"源模型{source}没有变形器")
for bs in blendshapes:
# 获取所有目标形状
targets_count = cmds.blendShape(bs, q=True, target=True)
target_weights = []
target_names = []
# 保存当前权重
for i in range(targets_count):
weight = cmds.getAttr(f"{bs}.weight[{i}]")
name = cmds.aliasAttr(f"{bs}.weight[{i}]", q=True)
target_weights.append(weight)
target_names.append(name)
# 为每个目标模型创建变形器
for target in targets:
new_bs = cmds.blendShape(
target,
frontOfChain=True,
name=f"{target}_blendShape"
)[0]
# 设置权重
for i, (weight, name) in enumerate(zip(target_weights, target_names)):
cmds.setAttr(f"{new_bs}.{name}", weight)
print(f"变形器传递完成: {source} -> {targets}")
except Exception as e:
cmds.warning(f"变形器传递失败: {str(e)}")
def optimize_blendshapes():
"""优化变形器"""
try:
selection = cmds.ls(sl=True, type="transform")
if not selection:
raise ValueError("请选择要优化变形器的模型")
for obj in selection:
# 获取变形器
blendshapes = cmds.listConnections(obj, type="blendShape")
if not blendshapes:
continue
for bs in blendshapes:
# 获取所有目标形状
target_count = cmds.blendShape(bs, q=True, target=True)
# 删除未使用的目标形状
for i in range(target_count):
weight = cmds.getAttr(f"{bs}.weight[{i}]")
if abs(weight) < 0.001: # 权重接近0的目标形状
cmds.removeMultiInstance(f"{bs}.weight[{i}]", b=True)
# 重新排序索引
cmds.blendShape(bs, edit=True, resetTargetDelta=True)
print(f"变形器优化完成: {obj}")
except Exception as e:
cmds.warning(f"变形器优化失败: {str(e)}")
def create_corrective_blendshape():
"""创建修正变形器"""
try:
selection = cmds.ls(sl=True, type="transform")
if len(selection) != 2:
raise ValueError("请选择基础模型和目标形状")
base = selection[0]
target = selection[1]
# 创建修正变形器
corrective_bs = cmds.blendShape(
target,
base,
frontOfChain=True,
name=f"{base}_corrective"
)[0]
# 设置权重驱动
cmds.setDrivenKeyframe(
f"{corrective_bs}.{target}",
currentDriver="time",
driverValue=0,
value=0
)
cmds.setDrivenKeyframe(
f"{corrective_bs}.{target}",
currentDriver="time",
driverValue=1,
value=1
)
print(f"修正变形器创建完成: {corrective_bs}")
except Exception as e:
cmds.warning(f"创建修正变形器失败: {str(e)}")