534 lines
12 KiB
Markdown
534 lines
12 KiB
Markdown
# 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
|
|
# Python 2
|
|
reload(module)
|
|
|
|
# Python 3
|
|
import importlib
|
|
importlib.reload(module)
|
|
```
|
|
|
|
**影响文件**: 50+ 个模块
|
|
**修复方法**: 添加 `import importlib` 并替换所有 `reload()` 调用
|
|
|
|
#### print 语句 → print() 函数
|
|
```python
|
|
# Python 2
|
|
print "message"
|
|
print "format %s" % value
|
|
|
|
# Python 3
|
|
print("message")
|
|
print(f"format {value}") # 使用 f-string
|
|
```
|
|
|
|
**影响文件**: 40+ 个模块
|
|
**修复方法**: 统一使用 f-string 格式化
|
|
|
|
#### xrange() → range()
|
|
```python
|
|
# Python 2
|
|
for i in xrange(100):
|
|
pass
|
|
|
|
# Python 3
|
|
for i in range(100):
|
|
pass
|
|
```
|
|
|
|
**影响文件**: riggingUtils.py 及相关模块
|
|
**修复方法**: 全局替换 `xrange` 为 `range`
|
|
|
|
#### except 语句
|
|
```python
|
|
# Python 2
|
|
except:
|
|
pass
|
|
|
|
# Python 3
|
|
except Exception:
|
|
pass
|
|
```
|
|
|
|
**影响文件**: 100+ 处
|
|
**修复方法**: 明确捕获 Exception 类型
|
|
|
|
---
|
|
|
|
## 🏗️ 核心系统架构
|
|
|
|
### 1. ART_RigModule 基类
|
|
|
|
**文件**: `System/ART_RigModule.py`
|
|
|
|
**职责**:
|
|
- 所有骨骼模块的基类
|
|
- 提供通用的骨骼创建方法
|
|
- 管理模块网络节点
|
|
- 处理模块间的父子关系
|
|
|
|
**关键方法**:
|
|
```python
|
|
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**:
|
|
```python
|
|
@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 - 异常变量作用域问题**:
|
|
```python
|
|
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
|
|
```
|
|
|
|
#### 错误恢复机制
|
|
```python
|
|
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`
|
|
|
|
#### 导出权重
|
|
```python
|
|
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
|
|
```
|
|
|
|
#### 导入权重(带错误处理)
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
# 自动检测并导入正确的 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`
|
|
|
|
```python
|
|
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. 节点锁定处理
|
|
|
|
```python
|
|
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. 安全删除
|
|
|
|
```python
|
|
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. 循环优化
|
|
|
|
```python
|
|
# 优化前
|
|
for node in networkNodes:
|
|
if condition:
|
|
networkNode = node
|
|
# 继续循环所有节点
|
|
|
|
# 优化后
|
|
for node in networkNodes:
|
|
if condition:
|
|
networkNode = node
|
|
break # 找到后立即退出
|
|
```
|
|
|
|
### 2. 批量操作
|
|
|
|
```python
|
|
# 优化前 - 逐个设置属性
|
|
for obj in objects:
|
|
cmds.setAttr(f"{obj}.attr", value)
|
|
|
|
# 优化后 - 批量设置
|
|
cmds.setAttr([f"{obj}.attr" for obj in objects], value)
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 测试策略
|
|
|
|
### 单元测试示例
|
|
|
|
```python
|
|
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. 启用详细日志
|
|
|
|
```python
|
|
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 脚本编辑器输出
|
|
|
|
```python
|
|
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
|