.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}准备概览目标：在 Maya 中用 Pencil+ 4 做测试静态线稿渲染（不涉骨骼动画），快速得到“外轮廓 + 结构线”的干净结果。测试模型：使用《绝区零》官方发布模型（来源见下）。模型格式：原始为 MMD（.pmx），为静帧演示转换为 OBJ。渲染思路：以 Line Set 分类出线（外轮廓 / 对象轮廓 / 交叉 / 材质边 / 硬边等），按需给不同类线指定独立笔刷，并用遮罩与焊接确保线条干净不重影。进阶：测试插件主要功能，排查风险下载与安装官网：Pencil+ 4 for Maya – Downloads – | PSOFT WEBSITE✅ 请购买或授权试用，遵守 EULA 与版权法规。神秘网站：PSOFT Pencil+ 4.2.3 For Maya 2017 – 2025 Win⚠️ 为合规与版权安全，本文仅供个人学习素材与格式转换（MMD→OBJ）适合只做静态渲染预览；若要绑定/动画，建议保留骨骼信息并在 DCC 内做更规范的导入流程。来源《绝区零》官方发布的模型下载汇总：绝区零 UP主激励计划 模型 汇总下载遵守使用条款 / 二创规范，避免商用侵权。转换（PMX → OBJ）在线：AnyConv导出建议：仅静帧 → OBJ 即可（无骨骼、无动画）。导入 Maya 后的整理Modify > Freeze Transforms、Center PivotMesh Display > Set to Face 再 Soften/Harden Edges（按结构设置硬/软边）合并或分离网格（按线条策略决定是否需要单体或多对象）统一材质（可先赋一个纯白 Lambert 以便观察线）Pencil+ 4 基础上手名称以 Pencil+ 4 for Maya 常见面板为参考，菜单/UI 可能随版本略有差异。加载插件Windows > Settings/Preferences > Plug-in Manager勾选 Pencil+4 相关插件（如 Line / Material 等）。创建 Line Set（线集）新建一个 Line Set（可按「角色」「道具」「背景」分层管理）。将要出线的对象拖入或用过滤规则指定。勾选出线类别（核心）见下「Line Set 详解」。先只开 Outline（轮廓），确认外轮廓正确。为不同线类指定独立笔刷各类别旁的 Specific Brush Settings 打勾后，会出现一个可指派笔刷资源的槽（例如 PencilBrushSettings_VisibleX）。将 Brush/Stroke 预设拖入，使该类线有独立的粗细 / 纹理 / 虚线 / 端点等。渲染预览切到目标相机，设置输出分辨率。渲染（需要用pencil自己的渲染器）。如需前后景压线逻辑，使用多 Line Set + 遮罩（见下）。这里有一个比较通俗易懂的视频：Line Set 详解与推荐搭配一些功能解释和理解1) Outline（轮廓）Open Edge（开放边）：把模型边界/洞口也当作轮廓线（衣服开口、眼洞等）。Merge Groups（合并组）：同组多个物体合并计算外轮廓，内遮挡不重复出线。Specific Brush Settings：给外轮廓单独用更粗/更黑的笔刷。搭配：角色主体（身体+衣物+饰品）常勾 Merge Groups，得到一圈干净大外轮廓；需要服装开口强调时再开 Open Edge。2) Object（对象轮廓）Open Edge：与 Outline 同义，但按每个对象独立计算。Specific Brush Settings：仅覆盖对象轮廓的笔刷。用途：同一组内的零件各保一圈外轮廓的画风（更“零件化”）。3) Intersection（交叉线）Self-Intersection（自相交）：自身表面互相穿插/折叠处出线（衣褶、厚面回折）。Specific Brush Settings：仅覆盖交叉线。用途：强调接触/压痕（袖口压在手臂、鞋底接地）。技术插画常用。4) Smoothing Boundary（光滑组/硬边边界）Specific Brush Settings用途：在硬边/法线断开处出线。非常适合硬表面倒角线/板块转折。5) Material ID Boundary（材质 ID 边界）Specific Brush Settings用途：同网格内不同材质 ID的分界出线 → 配色块分隔、面板缝，无需额外切线。6) Selected Edges（选定边）Specific Brush Settings用途：仅对你手工选择的边出线，最适合“艺术指导式定点勾线”。 提示：用 Create > Sets > Quick Select Set 建组件选择集，便于复用与版本管理。7) Normal Angle（法线夹角）Min / Max：只在法线夹角落在该范围的边上出线。Specific Brush Settings用途：用角度阈值自动抓锐利/转折边。 建议：硬表面把 Min 提高只抓很锐的折边；想更细腻就放宽范围。8) Wireframe（线框）Specific Brush Settings用途：直接按拓扑线框出线。适合蓝图/教学/拓扑展示，或与其他线型叠加做质感。9) 辅助管理Weld Edges Between Objects（跨对象焊接边）：将紧贴的不同物体在边界上视作连在一起，避免接缝双线/断裂。Mask Hidden Lines of Other Line Sets（遮罩其它线集的隐藏线）：当前线集会遮蔽被其它线集物体挡住的线。典型：人物线集压住背景线集，防止背景线穿出人物。笔刷 / 线型预设速览（可直接套用，官方内置的）这些是常见命名的示意与适用性总结。不同版本可能略有差异，请以你的安装包为准；你也可在此基础上做二次定制。Brush（笔刷纹理类）P_Brush_Bubble（泡泡）：柔软圆斑，可爱/软糯风外轮廓或填充式线条。P_Brush_Circle（圆头）：线宽均匀干净，卡通/技术图的基础款。P_Brush_Default（默认）：通用起点，便于继续自定义。P_Brush_Flat（扁平/排刷）：椭圆笔尖，转向有“压扁/起收锋”感，带一点书法味。P_Brush_Flower（花瓣）：转角有装饰纹理，适合特效/装饰线。P_Brush_Rough（粗糙）：颗粒断续，模拟蜡笔/干粉，有旧纸质感。P_Brush_Soft（软边）：边缘羽化，适合次级结构线/弱化存在感。Stroke（线型类）P_Stroke_DashedLine1/2/3（虚线）：不同间隔/端形；用于隐线/分区线/运动轨迹。P_Stroke_Default（默认线型）：平滑连续，标准。P_Stroke_Fuzzy1/2/3/4（毛边/抖动）：噪声递增；用于手绘颤动/远景弱化/粗糙材质。P_Stroke_Pen / Pen2 / Pen3（记号笔系）：硬/刷/压感各异；漫画分镜/产品线稿常用。P_Stroke_Spray（喷点）：粒子感强；喷涂/尘土/速度尾迹或衰减末端。P_Stroke_Wave_Large / Wave_Small（波浪线）：震动/热气/水波/毛绒边等卡通化强调。📌 在 Specific Brush Settings 的灰色槽中指派上述笔刷/线型资源，即可让该类线独立控制粗细/纹理/噪声/端头等。fuzzy真的很像手绘，可以做很多有意思的效果渲染输出与合成建议分层管理：人物（粗外轮廓）、人物（结构线）、道具 / 背景 分成多个 Line Set，利于遮罩与独立调线。人物线集放在最上层，勾选 Mask Hidden Lines of Other Line Sets。厚薄与缩放：线宽通常与场景尺度/相机距离相关；建立相机-线宽标尺：近景外轮廓：2.0~4.0（示意）中景外轮廓：1.2~2.2远景外轮廓：0.6~1.2（或用 Fuzzy 线弱化）输出与合成：建议输出带透明通道（如 PNG/TIF）便于后期与底色/纹理合成。可分多通道：外轮廓（粗）、结构线（细）、隐线/虚线；在 PS/AE/Nuke 中按线型分组做二次调色。常见问题排错清单接缝处双线/破碎勾选 Weld Edges Between Objects；检查是否存在重叠面/重合壳。开口/洞口没出线在 Outline/Object 内勾 Open Edge。结构线过多、画面脏先只开外轮廓，逐一加开 Intersection / Normal Angle / Material ID。控制 Normal Angle 的 Min/Max，收窄阈值。硬面折线抓不住使用 Smoothing Boundary 或提升 Normal Angle Min。材质分块不清检查模型是否有清晰的材质 ID；必要时在 Maya 中分配多材质。笔刷不生效确认勾选了 Specific Brush Settings 并正确指派资源。线宽随镜头飘统一场景单位与镜头距离；必要时按镜头远近做多版本线宽或合成端做指数衰减。法线异常导致漏线Mesh Display > Conform / Set to Face 后再 Soften/Harden；清理反法线与非流形。*渲染设置（补丁）理论上基本和正常的render是一致的，这里可以改渲染器但是，我调取不到batch render官方说明中的-pl:port <int>是不可用的..所以修修补补写了个临时的batch插件（仅供参考，学习交流）（2022以上）： from __future__ import annotations import traceback from maya import cmds, mel FORCE_OUTPUT_DIR = "" def _ensure_plugin_loaded(plugin_name): try: if not cmds.pluginInfo(plugin_name, q=True, loaded=True): cmds.loadPlugin(plugin_name) return True except Exception: return False def _get_current_renderer(): try: return cmds.getAttr("defaultRenderGlobals.currentRenderer") except Exception: return "unknown" def _get_frame_range_from_settings(): s = int(cmds.getAttr("defaultRenderGlobals.startFrame")) e = int(cmds.getAttr("defaultRenderGlobals.endFrame")) by = int(round(cmds.getAttr("defaultRenderGlobals.byFrameStep"))) return s, e, max(1, by) def _set_frame_range(s, e, by): cmds.setAttr("defaultRenderGlobals.startFrame", s) cmds.setAttr("defaultRenderGlobals.endFrame", e) cmds.setAttr("defaultRenderGlobals.byFrameStep", by) def _ensure_animation_on(): try: if cmds.getAttr("defaultRenderGlobals.animation") != 1: cmds.setAttr("defaultRenderGlobals.animation", 1) except Exception: pass def _list_render_layers(): try: from maya.app.renderSetup.model import renderSetup rs = renderSetup.instance() layers = rs.getRenderLayers() active = [l for l in layers if getattr(l, "isRenderable", None) and l.isRenderable()] \ or [l for l in layers if getattr(l, "isVisible", None) and l.isVisible()] \ or list(layers) return active except Exception: return [] def _switch_to_layer(layer_obj): try: layer_obj.switchTo() return True except Exception: return False def _list_renderable_cameras(): cams = [] for cam_shape in cmds.ls(type="camera", long=True) or []: try: if cmds.getAttr(cam_shape + ".renderable"): tr = cmds.listRelatives(cam_shape, p=True, f=True) or [] if tr: cams.append(tr[0]) except Exception: pass seen, out = set(), [] for c in cams: if c not in seen: out.append(c); seen.add(c) return out def _maybe_override_output_dir(dir_path): if not dir_path: return try: if not cmds.objExists("defaultRenderGlobals.imageFilePrefix"): return prefix = cmds.getAttr("defaultRenderGlobals.imageFilePrefix") or "<Scene>/<RenderLayer>/<Camera>" if dir_path.endswith("/") or dir_path.endswith("\\"): dir_path = dir_path[:-1] new_prefix = dir_path.replace("\\", "/") + "/" + prefix cmds.setAttr("defaultRenderGlobals.imageFilePrefix", new_prefix, type="string") print("[BatchRender] Override output dir ->", new_prefix) except Exception: print("[BatchRender] Failed to override output dir") traceback.print_exc() def _camera_arg_for_mel(camera_transform): return camera_transform.replace('"', '\\"') def _renderer_entry(renderer_name): r = (renderer_name or "").lower() if r in ("arnold", "mtoa"): return "arnold" return "generic" def _render_sequence_with_arnold(camera, start, end, by): if not _ensure_plugin_loaded("mtoa"): raise RuntimeError("无法加载 Arnold 插件（mtoa）。") shapes = cmds.listRelatives(camera, s=True, ni=True, f=True) or [] cam_shape = (shapes[0] if shapes else camera).replace('"','\\"') mel.eval( 'arnoldRender -seq -cam "{cam}" -start {s} -end {e} -by {by};'.format( cam=cam_shape, s=start, e=end, by=by ) ) def _render_sequence_generic(camera, start, end, by): cam_arg = _camera_arg_for_mel(camera) for f in range(start, end + 1, by): print("[BatchRender] Rendering frame {f} @ {cam}".format(f=f, cam=camera)) cmds.currentTime(f, e=True) mel.eval('render "{cam}";'.format(cam=cam_arg)) def run_batch(cameras=None, layers=None, start=None, end=None, by=None, override_output_dir=FORCE_OUTPUT_DIR): try: cur_renderer = _get_current_renderer() entry = _renderer_entry(cur_renderer) print("[BatchRender] Current Renderer:", cur_renderer, "->", entry) s0, e0, by0 = _get_frame_range_from_settings() s = int(start) if start is not None else s0 e = int(end) if end is not None else e0 step = int(by) if by is not None else by0 _set_frame_range(s, e, step) _ensure_animation_on() _maybe_override_output_dir(override_output_dir) layer_objs = layers if layers is not None else _list_render_layers() if not layer_objs: print("[BatchRender] 未找到 Render Setup 层，可能场景未使用 Render Setup。将在当前层渲染。") cams = cameras if cameras is not None else _list_renderable_cameras() if not cams: raise RuntimeError("未发现可渲染相机（CameraShape.renderable==True）。请在需要的相机上勾选 Renderable。") def _do_render_for_current_layer(): for cam in cams: if entry == "arnold": _render_sequence_with_arnold(cam, s, e, step) else: _render_sequence_generic(cam, s, e, step) if layer_objs: for lyr in layer_objs: lname = getattr(lyr, "name", lambda: str(lyr))() print("\n[BatchRender] ===== Render Layer:", lname, "=====") _switch_to_layer(lyr) _do_render_for_current_layer() else: _do_render_for_current_layer() print("\n[BatchRender] ✅ 完成。请查看输出目录。") except Exception as ex: print("\n[BatchRender] ❌ 失败：", ex) traceback.print_exc() def _build_ui(): if cmds.window("BatchSeqRenderUI", exists=True): cmds.deleteUI("BatchSeqRenderUI") win = cmds.window("BatchSeqRenderUI", title="Batch Sequence Render (Maya 2024)", sizeable=False) col = cmds.columnLayout(adj=True, rs=6, cw=440) s0, e0, by0 = _get_frame_range_from_settings() cmds.text(l="Frame Range（读取自 Render Settings）") fr = cmds.rowLayout(nc=6, cw6=[70,90,50,90,50,90], adj=2) cmds.text(l="Start") f_s = cmds.intField(v=s0) cmds.text(l="End") f_e = cmds.intField(v=e0) cmds.text(l="Step") f_by = cmds.intField(v=by0) cmds.setParent("..") cmds.text(l="Override Output Directory（可选，留空沿用 Render Settings）") f_dir = cmds.textField(tx=FORCE_OUTPUT_DIR, cc=lambda *_: None) cams = _list_renderable_cameras() cmds.text(l="Renderable Cameras（如不选择则使用全部）") f_cam = cmds.textScrollList(ams=True, h=120) for c in cams: cmds.textScrollList(f_cam, e=True, a=c) lyr_names = [] try: from maya.app.renderSetup.model import renderSetup rs = renderSetup.instance() for l in rs.getRenderLayers(): lyr_names.append(l.name()) except Exception: pass cmds.text(l="Render Setup Layers（如不选择则按可渲染/可见层自动）") f_layer = cmds.textScrollList(ams=True, h=120) for n in lyr_names: cmds.textScrollList(f_layer, e=True, a=n) def _on_run(*_): s = cmds.intField(f_s, q=True, v=True) e = cmds.intField(f_e, q=True, v=True) by = cmds.intField(f_by, q=True, v=True) out_dir = cmds.textField(f_dir, q=True, tx=True).strip() sel_cams = cmds.textScrollList(f_cam, q=True, sii=True) or [] use_cams = [cams[i-1] for i in sel_cams] if sel_cams else cams sel_layers = cmds.textScrollList(f_layer, q=True, sii=True) or [] use_layers = None if sel_layers: from maya.app.renderSetup.model import renderSetup rs = renderSetup.instance() name_to_obj = {l.name(): l for l in rs.getRenderLayers()} use_layers = [name_to_obj[lyr_names[i-1]] for i in sel_layers] run_batch(cameras=use_cams, layers=use_layers, start=s, end=e, by=by, override_output_dir=out_dir) cmds.separator(h=5, st="none") cmds.button(l="Run", h=34, c=_on_run) cmds.separator(h=6, st="none") cmds.button(l="Close", c=lambda *_: cmds.deleteUI(win)) cmds.showWindow(win) # 执行即弹 UI；也可在 Script Editor 里手动调用 run_batch(...) try: _build_ui() except Exception: traceback.print_exc() print("提示：你也可以在 Script Editor 里手动调用： run_batch()")
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}使用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 基本对照版本PythonQt / PySide兼容关注点Maya 20182.7Qt 5.6 / PySide2reload、wrapInstance(long)、编码（utf-8-sig）、字符串类型（basestring vs str）Maya 2022+3.xQt 5.15 / PySide2importlib.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文件:功能14.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()