使用qtpy制作插件实现材质导出、材质规范化与 Alembic 导出(含 2018+ 兼容方案)
找了比较多的教程,这个教程属于口齿比较清楚,思路也比较清楚的,但是不兼容maya2018,需要改动很多地方,我改的非常痛苦..但是公司只能装2018,命很苦了。他的教程版本是2022,换成2022以上会好很多。
文章中的git文件如果打开404,需要在git上申请,被邀请以后可以下载py测试
概述
之前插件一直由 TD 支持;最近打算自己动手做一套。调研下来发现不少坑(API、编码、reload、路径、wrapInstance 等)。
感觉和EUW有相似之处,挺好上手的,一天不到就搞定了..
这里有三个板块:
- Qt/UI 基础打通(Qt Designer、UI 加载、入口脚本、2018/2022+ 兼容)
- 工具面板设计与信号绑定(按钮/日志/右键帮助跳转)
- 三大实用功能落地
1) 导出 Maya 材质信息(SG 与贴图 Pattern) → CSV
2) 一键把分配材质刷成 lambert 并修正类型/命名
3) 带材质Alembic 导出(按 Time Slider,含插件检测与旗标降级回退)
有插件演示
1. 环境与版本差异
1.1 基本对照
| 版本 | Python | Qt / PySide | 兼容关注点 |
|---|---|---|---|
| Maya 2018 | 2.7 | Qt 5.6 / PySide2 | reload、wrapInstance(long)、编码(utf-8-sig)、字符串类型(basestring vs str) |
| Maya 2022+ | 3.x | Qt 5.15 / PySide2 | importlib.reload、wrapInstance(int)、路径字符串、API 细节兼容 |
注:2018 用 PySide2(而非旧 PySide),但 Python 还是 2.7;2022+ 统一到 Python 3.x。
1.2 推荐目录结构
F:\Test\QTtest\
├─ ui\
│ └─ mainWidget.ui # Qt Designer 做的 .ui
├─ icons\ # 可选
├─ testUI.py # UI 主模块(加载 .ui,绑定按钮)
├─ ops_mat_export.py # 功能1:导出 SG & 贴图 Pattern
├─ ops_mat_lambertize.py # 功能2:一键刷 lambert + 规范
├─ ops_abc_export.py # 功能3:Alembic 导出
└─ bootstrap_compat.py # 入口/兼容辅助(sys.path、reload 兼容等)2. Qt / UI 基础打通
2.1 工具:Qt Designer
- 你可用 Qt Designer 直接搭界面(按钮、文本框、日志区域等),存成
ui/mainWidget.ui。 - UI 中控件的
objectName后续会在代码里通过findChild找到并绑定信号。
2.2 模板(教程作者提供)
"""
Maya/QT UI template
Maya 2023
"""
import maya.cmds as cmds
import maya.mel as mel
from maya import OpenMayaUI as omui
from shiboken2 import wrapInstance
from PySide2 import QtUiTools, QtCore, QtGui, QtWidgets
from functools import partial # optional, for passing args during signal function calls
import sys
class MayaUITemplate(QtWidgets.QWidget):
"""
Create a default tool window.
"""
window = None
def __init__(self, parent = None):
"""
Initialize class.
"""
super(MayaUITemplate, self).__init__(parent = parent)
self.setWindowFlags(QtCore.Qt.Window)
self.widgetPath = ('C:\\')
self.widget = QtUiTools.QUiLoader().load(self.widgetPath + 'mainWidget.ui')
self.widget.setParent(self)
# set initial window size
self.resize(200, 100)
# locate UI widgets
self.btn_close = self.widget.findChild(QtWidgets.QPushButton, 'btn_close')
# assign functionality to buttons
self.btn_close.clicked.connect(self.close)
"""
Your code goes here
"""
def resizeEvent(self, event):
"""
Called on automatically generated resize event
"""
self.widget.resize(self.width(), self.height())
def closeWindow(self):
"""
Close window.
"""
print ('closing window')
self.destroy()
def openWindow():
"""
ID Maya and attach tool window.
"""
# Maya uses this so it should always return True
if QtWidgets.QApplication.instance():
# Id any current instances of tool and destroy
for win in (QtWidgets.QApplication.allWindows()):
if 'myToolWindowName' in win.objectName(): # update this name to match name below
win.destroy()
#QtWidgets.QApplication(sys.argv)
mayaMainWindowPtr = omui.MQtUtil.mainWindow()
mayaMainWindow = wrapInstance(int(mayaMainWindowPtr), QtWidgets.QWidget)
MayaUITemplate.window = MayaUITemplate(parent = mayaMainWindow)
MayaUITemplate.window.setObjectName('myToolWindowName') # code above uses this to ID any existing windows
MayaUITemplate.window.setWindowTitle('Maya UI Template')
MayaUITemplate.window.show()
openWindow()2.3 入口脚本(2022以上)
这里是我的路径和文件,需要的话自己改path和文件名字
import sys
sys.path.insert(0,'F:\Test\QTtest')
from importlib import reload
import testUI
reload(testUI)
testUI.openWindow()3. 界面设计
信号绑定模式:
self.btn_xxx.clicked.connect(self.on_xxx),UI 改名后只改一次objectName即可。(就是模板中的这一段)# locate UI widgets self.btn_close = self.widget.findChild(QtWidgets.QPushButton, 'btn_close') # assign functionality to buttons self.btn_close.clicked.connect(self.close)
- 日志区域:用
QPlainTextEdit更适合大量文本。 - 右键帮助:对选中文字做搜索(见上面
customContextMenuRequested)。 - 路径输入:给一个
QLineEdit,默认落地.../tmp。 - 与 UEW 的类比:UE Editor Utility Widget 与这里的 Qt 面板类似,核心是绑定入口与调用逻辑模块,尽量把“UI”和“业务逻辑”拆文件。

这里完成了教程中的关闭窗口,说明链路理解的是正确的

4. 功能实现

4.1 功能一:导出材质信息
设计目标
- 遍历所有
shadingEngine,过滤initialShadingGroup与initialParticleSE; - 找到与 SG 相连的
surfaceShader(材质) 与displacementShader; - 用
listHistory(..., future=False, pruneDagObjects=True)向上收集整个材质网络; - 在这些节点里查有无
computedFileTextureNamePattern属性;若有则读值; - 写出 CSV(UTF-8 with BOM,Excel 友好),表头:
SG,Materials,Node,Pattern; - 对没有 Pattern 的 SG 也写一行占位(Pattern 为空),保证后续映射表完整。
单功能python文件:功能1
4.2 功能二:一键刷成 lambert + 规范命名/类型
需求要点
- SG 规范:把
:替成_;若 引用的 SG 无法重命名,则新建本地 SG 并迁移成员; - 三大口(
surfaceShader/displacementShader/volumeShader)确保存在(SG 默认有,但历史脏数据可能异常); - 获取材质:找
SG.surfaceShader上连的材质; - 断开材质入向连接(不保留贴图),计数日志;
- 按类型替换:若材质类型在
CONVERT_TYPES,创建 同名(去冒号) lambert,把outColor -> SG.surfaceShader;否则保留原类型并记录; - 输出日志:创建/重命名了哪些 SG/材质;断开了多少连接;映射关系等。
单功能python文件:功能2
备查:检测所选模型是否还有非 lambert 材质:(应该是没问题的!但是我害怕!)
import maya.cmds as cmds
def print_non_lambert_materials_on_selection():
sels = cmds.ls(sl=True, dag=True, type='transform') or []
if not sels:
cmds.warning(u'请先选择至少一个模型(transform)。')
return []
shapes = cmds.listRelatives(sels, s=True, ni=True, f=True) or []
if not shapes:
cmds.warning(u'所选对象没有可渲染的 shape。')
return []
non_lambert = set()
checked = set()
for sh in shapes:
sgs = list(set(cmds.listConnections(sh, type='shadingEngine') or []))
for sg in sgs:
mats = cmds.listConnections(sg + '.surfaceShader', s=True, d=False) or []
for m in mats:
if m in checked:
continue
checked.add(m)
if cmds.nodeType(m) != 'lambert':
non_lambert.add(m)
if non_lambert:
print(u'以下材质的 type 不是 "lambert"(去重):')
for m in sorted(non_lambert):
print(m)
print(u'—— 共 {} 个非 lambert 材质。'.format(len(non_lambert)))
else:
print(u'✅ 所选对象的已分配材质全部为 "lambert"。')
return sorted(non_lambert)4.3 功能三:Alembic 导出
需求要点
- 设定默认导出目录;
- 确认
AbcExport插件已加载; - 从当前选择解析出 导出 root 列表(若选到 shape 取其 transform;若选 transform 则确认其子层级含非中间体 mesh);
- 去除祖先-后代冲突(否则报 “ancestor relationship” 错);
- 读取
Time Slider的min/max; - 组装 Job 字符串:
-frameRange start end -uvWrite -writeColorSets -writeFaceSets -worldSpace -writeUVSets -root <...> -file "..." - 若失败且判断
-writeUVSets不支持,则降级去掉该旗标再试。
单功能python文件:功能3
坑位说明(重要)
- “ancestor relationship” 报错:给 AbcExport 的
-root列表里包含了父与子两个层级(祖先/后代冲突),使用上面_gather_roots_from_selection()的去重逻辑即可规避。- 旗标不兼容:某些环境
-writeUVSets可能不被支持,上面做了自动降级。- 路径分隔符:Alembic 推荐统一
/,已做处理。
5. 插件使用录屏
6. 2018/2022+ 常见兼容点清单
reload:2018(Py2)可以直接reload(mod);2022+ 用importlib.reload(mod);文中统一封装在bootstrap_compat.py。wrapInstance:传int(ptr)在 Py2/3 都能用;极端情况下 Py2 也可用long(ptr)。- 编码:写 CSV 用
utf-8-sig,避免 Excel 中文乱码。 basestring:Py3 没这个;使用isinstance(x, str)即可,必要时自定义:try: string_types = (basestring,) # Py2 except NameError: string_types = (str,) # Py3- 路径:Windows 下用 原始字符串
r'F:\path\to\dir'或os.path.join,避免\t \n转义。 - 引用(reference)节点:很多时候不能重命名,要新建本地副本迁移成员(本文已处理 SG)。
- AbcExport:记得
loadPlugin('AbcExport');祖先/后代冲突需要去重。
7. 常见错误排查(Checklist)
AbcExport未加载 →loadPlugin('AbcExport');- 祖先/后代冲突 → 见
_gather_roots_from_selection()的去重。 NameError: basestring→ Py3 移除;使用str或本文的string_types模式。- 引用节点改名失败 → 新建本地 SG/材质,迁移成员。
- CSV 中文乱码 → 确保
utf-8-sig,且用 Excel 打开。 - .ui 加载失败 → 检查
UI_PATH、UI 控件objectName、以及QtUiTools.QUiLoader是否可用。 - 路径分隔符问题 → 优先
os.path.join;Alembic 输出路径换成/。 - 导出范围不对 → Time Slider 的
min/max是否设置正确。
8.可能迭代方向
- 等我看完ue的看看能不能直接通入ue,这样不需要手动拖一次abc
- 里面的路径和文件名没有暴露,之后可能加一个text框出去
附:(简 2018 兼容脚本)(仅入口)
import os
from maya import OpenMayaUI as omui
from PySide2 import QtUiTools, QtCore, QtWidgets
from shiboken2 import wrapInstance
class MayaUITemplate(QtWidgets.QWidget):
window = None
def __init__(self, parent=None):
super(MayaUITemplate, self).__init__(parent)
self.setWindowFlags(QtCore.Qt.Window)
ui_path = os.path.join(r"C:\", "mainWidget.ui")
self.widget = QtUiTools.QUiLoader().load(ui_path)
self.widget.setParent(self)
self.resize(200, 100)
self.btn_close = self.widget.findChild(QtWidgets.QPushButton, 'btn_close')
if self.btn_close:
self.btn_close.clicked.connect(self.close)
def resizeEvent(self, event):
self.widget.resize(self.width(), self.height())
super(MayaUITemplate, self).resizeEvent(event)
def openWindow():
app = QtWidgets.QApplication.instance()
if app:
for win in app.allWindows():
if win.objectName() == 'myToolWindowName':
win.close()
mw = wrapInstance(int(omui.MQtUtil.mainWindow()), QtWidgets.QWidget)
MayaUITemplate.window = MayaUITemplate(parent=mw)
MayaUITemplate.window.setObjectName('myToolWindowName')
MayaUITemplate.window.setWindowTitle('Maya UI Template')
MayaUITemplate.window.show() 