Files
Nexus/plug-ins/ARTv2/doc/TECHNICAL_DETAILS.md
2025-12-07 23:00:40 +08:00

12 KiB

ARTv2 技术细节文档

最后更新: 2024-12-07


📐 架构概述

模块结构

ARTv2/
├── Core/
│   ├── Scripts/
│   │   ├── System/          # 核心系统模块
│   │   ├── RigModules/      # 骨骼模块
│   │   ├── Interfaces/      # UI 界面
│   │   └── ThirdParty/      # 第三方库
│   ├── JointMover/          # 骨骼移动器
│   └── Pickers/             # 动画选择器
└── plug-ins/
    └── ARTv2.py             # Maya 插件入口

🔧 Python 3 迁移技术细节

1. 关键语法变更

reload() → importlib.reload()

# Python 2
reload(module)

# Python 3
import importlib
importlib.reload(module)

影响文件: 50+ 个模块
修复方法: 添加 import importlib 并替换所有 reload() 调用

print 语句 → print() 函数

# Python 2
print "message"
print "format %s" % value

# Python 3
print("message")
print(f"format {value}")  # 使用 f-string

影响文件: 40+ 个模块
修复方法: 统一使用 f-string 格式化

xrange() → range()

# Python 2
for i in xrange(100):
    pass

# Python 3
for i in range(100):
    pass

影响文件: riggingUtils.py 及相关模块
修复方法: 全局替换 xrangerange

except 语句

# Python 2
except:
    pass

# Python 3
except Exception:
    pass

影响文件: 100+ 处
修复方法: 明确捕获 Exception 类型


🏗️ 核心系统架构

1. ART_RigModule 基类

文件: System/ART_RigModule.py

职责:

  • 所有骨骼模块的基类
  • 提供通用的骨骼创建方法
  • 管理模块网络节点
  • 处理模块间的父子关系

关键方法:

class ART_RigModule:
    def __init__(self, moduleType, moduleName, userSpecifiedName)
    def addAttributes(self)
    def skeletonSettings_UI(self, name)
    def addJointMoverToOutliner(self)
    def updateSettingsUI(self)
    def applyModuleChanges(self, moduleInst)
    def resetSettings(self)
    def pinModule(self, state)
    def skinProxyGeo(self)
    def buildRig(self)  # 已修复异常变量作用域问题
    def aimMode_Setup(self, state)
    def returnNetworkNode(self)  # 已修复 UnboundLocalError

重要修复 1 - UnboundLocalError:

@property
def returnNetworkNode(self):
    networkNode = None  # 初始化变量
    networkNodes = cmds.ls(type="network")
    for node in networkNodes:
        attrs = cmds.listAttr(node)
        if "moduleName" in attrs:
            if cmds.getAttr(node + ".moduleName") == self.name:
                networkNode = node
                break  # 找到后退出
    return networkNode

重要修复 2 - 异常变量作用域问题:

def buildRig(self, textEdit, uiInst):
    """构建模块骨骼"""
    currentNodes = cmds.ls("*", long=True)
    successfulBuild = True
    errorMessage = ""
    buildException = None  # ✅ 初始化异常变量
    
    try:
        self.buildRigCustom(textEdit, uiInst)
    except Exception as e:
        successfulBuild = False
        buildException = e  # ✅ 保存异常
        errorMessage = str(traceback.format_exc())
    
    # ... 处理新节点 ...
    
    if not successfulBuild:
        print(f"Build Rig Failed: {str(buildException)}")  # ✅ 使用保存的异常
        print(errorMessage)
        if buildException:
            raise buildException  # ✅ 重新抛出供上层捕获

修复影响:

  • 修复了 Root 模块构建失败
  • 修复了 Torso 模块构建失败
  • 修复了"控制器消失"的问题
  • 确保异常能被 ART_BuildProgressUI 正确捕获和处理

---

### 2. 骨骼构建流程

**文件**: `Interfaces/ART_BuildProgressUI.py`

#### 构建阶段
  1. 预检查 (preflightCheck) ├─ 检查角色存在 ├─ 检查模块存在 ├─ 检查骨骼状态 └─ 检查场景状态

  2. 设置绑定姿态 (setRigPose) ├─ 导出皮肤权重 └─ 删除旧骨骼

  3. 重建骨骼 (rebuildSkeleton) ├─ 安全删除根骨骼 └─ 重新构建骨骼层级

  4. 构建绑定 (buildRigs) ├─ 创建驱动骨架 ├─ 逐个构建模块绑定 └─ 设置绑定层级

  5. 导入权重 (importWeights) ├─ 检查网格存在 ├─ 导入皮肤权重 └─ 生成导入摘要

  6. 后处理 (postScript) └─ 清理和优化


#### 预检查机制
```python
def preflightCheck(self):
    """构建前验证"""
    errors = []
    warnings = []
    
    # 检查 1: 角色存在
    if not cmds.objExists("ART_RIG_ROOT"):
        errors.append("No character found")
    
    # 检查 2: 模块存在
    modules = utils.returnRigModules()
    if not modules:
        errors.append("No modules found")
    
    # 检查 3: 骨骼存在
    if not cmds.objExists("root"):
        errors.append("Skeleton not built")
    
    # 检查 4: 场景状态
    state = cmds.getAttr("ART_RIG_ROOT.state")
    if state == 0:
        errors.append("Character in Skeleton Placement mode")
    
    return errors, warnings

错误恢复机制

def buildRigs(self):
    """构建绑定,支持错误恢复"""
    failed_modules = []
    
    for inst in self.mainUI.moduleInstances:
        try:
            inst.buildRig()
            self.infoText.append(f"✓ Built: {inst.name}")
        except Exception as e:
            failed_modules.append((inst.name, str(e)))
            self.infoText.setTextColor(QtGui.QColor(255, 100, 100))
            self.infoText.append(f"✗ Failed: {inst.name}")
            self.infoText.append(f"  Error: {e}")
            self.errors += 1
    
    # 显示摘要
    if failed_modules:
        self.infoText.append(f"\nBuild Summary:")
        self.infoText.append(f"  Success: {len(self.mainUI.moduleInstances) - len(failed_modules)}")
        self.infoText.append(f"  Failed: {len(failed_modules)}")

3. 权重管理系统

文件: System/riggingUtils.py

导出权重

def export_skin_weights(filePath, mesh):
    """导出皮肤权重到文件"""
    skinCluster = mel.eval(f'findRelatedSkinCluster("{mesh}")')
    if not skinCluster:
        return False
    
    # 获取权重数据
    weights = cmds.getAttr(f"{skinCluster}.weightList[*].weights[*]")
    
    # 保存到文件
    with open(filePath, 'w') as f:
        json.dump(weights, f)
    
    return True

导入权重(带错误处理)

def import_skin_weights(filePath, mesh, worldSpace=True):
    """导入皮肤权重,带完整错误处理"""
    # 检查网格存在
    if not cmds.objExists(mesh):
        raise RuntimeError(f"Mesh not found: {mesh}")
    
    # 检查文件存在
    if not os.path.exists(filePath):
        raise RuntimeError(f"Weight file not found: {filePath}")
    
    # 读取权重数据
    try:
        with open(filePath, 'r') as f:
            weights = json.load(f)
    except Exception as e:
        raise RuntimeError(f"Failed to read weight file: {e}")
    
    # 应用权重
    try:
        skinCluster = mel.eval(f'findRelatedSkinCluster("{mesh}")')
        if not skinCluster:
            raise RuntimeError("No skin cluster found")
        
        cmds.setAttr(f"{skinCluster}.weightList[*].weights[*]", *weights)
    except Exception as e:
        raise RuntimeError(f"Failed to apply weights: {e}")
    
    return True

🎨 UI 系统架构

Qt 兼容层

文件: ThirdParty/Qt/__init__.py

# 自动检测并导入正确的 Qt 版本
try:
    from PySide2 import QtCore, QtGui, QtWidgets
    from PySide2.QtCore import Signal, Slot
except ImportError:
    from PySide import QtCore, QtGui
    QtWidgets = QtGui
    from PySide.QtCore import Signal, Slot

主界面结构

文件: Interfaces/ART_RigCreatorUI.py

class ART_RigCreatorUI(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(ART_RigCreatorUI, self).__init__(parent)
        
        # 设置窗口属性
        self.setWindowTitle("ARTv2 - Rig Creator")
        self.setObjectName("ART_RigCreatorUI")
        
        # 构建 UI
        self.buildUI()
        
        # 连接信号
        self.connectSignals()
    
    def buildUI(self):
        """构建主界面"""
        # 创建中央部件
        centralWidget = QtWidgets.QWidget()
        self.setCentralWidget(centralWidget)
        
        # 创建布局
        mainLayout = QtWidgets.QVBoxLayout(centralWidget)
        
        # 添加工具栏
        self.createToolbar()
        
        # 添加模块列表
        self.createModuleList()
        
        # 添加属性编辑器
        self.createAttributeEditor()

🔒 安全机制

1. 节点锁定处理

def unlock_node_hierarchy(node):
    """递归解锁节点及其子节点"""
    if cmds.objExists(node):
        # 解锁节点
        if cmds.lockNode(node, q=True, lock=True)[0]:
            cmds.lockNode(node, lock=False)
        
        # 递归解锁子节点
        children = cmds.listRelatives(node, children=True, fullPath=True) or []
        for child in children:
            unlock_node_hierarchy(child)

2. 安全删除

def safe_delete(node):
    """安全删除节点"""
    try:
        # 解锁节点层级
        unlock_node_hierarchy(node)
        
        # 删除节点
        if cmds.objExists(node):
            cmds.delete(node)
            return True
    except Exception as e:
        cmds.warning(f"Failed to delete {node}: {e}")
        return False

📊 性能优化

1. 循环优化

# 优化前
for node in networkNodes:
    if condition:
        networkNode = node
# 继续循环所有节点

# 优化后
for node in networkNodes:
    if condition:
        networkNode = node
        break  # 找到后立即退出

2. 批量操作

# 优化前 - 逐个设置属性
for obj in objects:
    cmds.setAttr(f"{obj}.attr", value)

# 优化后 - 批量设置
cmds.setAttr([f"{obj}.attr" for obj in objects], value)

🧪 测试策略

单元测试示例

def test_returnNetworkNode():
    """测试 returnNetworkNode 方法"""
    # 创建测试模块
    module = ART_RigModule("test", "test", "test")
    
    # 测试:节点不存在
    result = module.returnNetworkNode
    assert result is None, "Should return None when node not found"
    
    # 创建网络节点
    node = cmds.createNode("network")
    cmds.addAttr(node, ln="moduleName", dt="string")
    cmds.setAttr(f"{node}.moduleName", "test", type="string")
    
    # 测试:节点存在
    result = module.returnNetworkNode
    assert result == node, "Should return the network node"
    
    # 清理
    cmds.delete(node)

🔍 调试技巧

1. 启用详细日志

import logging

# 配置日志
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger('ARTv2')
logger.debug("Debug message")

2. Maya 脚本编辑器输出

def debug_print(msg):
    """在 Maya 脚本编辑器中打印调试信息"""
    cmds.warning(f"[ARTv2 DEBUG] {msg}")

📚 API 参考

核心类

ART_RigModule

  • addAttributes() - 添加模块属性
  • buildRig() - 构建模块绑定
  • returnNetworkNode - 获取网络节点

ART_BuildProgressUI

  • preflightCheck() - 预检查
  • buildRigs() - 构建绑定
  • importWeights() - 导入权重

工具函数

utils.py

  • returnRigModules() - 获取所有模块
  • returnFriendlyPath() - 转换路径格式

riggingUtils.py

  • export_skin_weights() - 导出权重
  • import_skin_weights() - 导入权重

🔗 依赖关系

ARTv2.py (插件入口)
    ├─ System/utils.py
    ├─ System/interfaceUtils.py
    ├─ Interfaces/ART_RigCreatorUI.py
    │   ├─ System/ART_RigModule.py
    │   ├─ RigModules/ART_*.py
    │   └─ Interfaces/ART_BuildProgressUI.py
    └─ ThirdParty/Qt/

维护者: Cascade AI
技术支持: GitHub Issues