最近因为比较特殊的原因,工作上突然闲下来了,于是我就去研究了我们组做的 Maya 毛发工具。
学习一下 Maya 做笔刷有哪些坑。
这次我的主要目的是模仿
XGen的毛发笔刷效果,通过最小案例的实现,探讨不同的实现方案。
官方文档: XGen Interactive Grooming
上面的视频就是 XGen 实现的笔刷效果,对于毛发制作非常丝滑好用。
只可惜这个笔刷不能对曲线直接生效。
上面是我用 C++ 写的曲线笔刷,下面我也会来探讨如何用 Python OpenMaya 结合 Qt 开发笔刷的流程。
具体代码已经开源到 https://github.com/FXTD-ODYSSEY/Maya-CurveBrush
C++ 插件提供了 2020 - 2023 支持
Python 插件有om1_curve_brush.py和om2_curve_brush.py
什么是 Maya Context ? 官方文档说明
Maya Context 就是一个开放的接口,可以用于自定义 鼠标 在 Viewport 上执行的逻辑,实现 绘制 修改选择物体 等操作。
上面的链接是一个 Maya Devkit 里面的案例
devkit\plug-ins\marqueeTool\marqueeTool.cpp
Maya CMake 构建 C++ 插件编译环境 我的这篇文章有提到如何将 devkit 的源码编译生成 mll
这里提供 Maya2020 windows 版本的 mll 插件
Maya 加载 mll 插件
1 | import maya.cmds as cmds |
加载mll 插件后,可以使用上面的代码激活 Context
上面实现的效果和默认的 框选物体是一样的。
只是框的颜色变成了自定义的 黄色。
实现这个 context 需要继承实现两个类,一个是 MPxContext 另一个是 MPxContextCommand
MPxContext 类定义了鼠标拖拽 移动 等逻辑的虚函数,MPxContextCommand 则是用来读取 MPxContext 数据的 Mel 命令。
通过 MPxContextCommand 就可以用 Mel 命令来修改 MPxContext 的变量(比如笔刷大小之类的)
上面提到的方案 Context 进行处理的时候是没有 Undo 功能的。
因此 Maya C++ 提供了 MPxToolCommand 这样将需要 undo 的逻辑放到 Command 当中实现,就可以 undo redo 操作了。
上面文档的案例来自于devkit\plug-ins\helixTool\helixTool.cpp
这里照样提供 Maya2020 windows 版本的 mll 插件
Maya 加载 mll 插件
1 | import maya.cmds as cmds |
加载mll 插件后,可以使用上面的代码激活 Context

需要注意这个插件只能在旧的 Viewport 生效 (我测了好久才明白过来)
这个工具可以在 Maya Viewport 拖拽一个 圆柱预览 ,这个圆柱最后生成 螺旋线。
通过 MPxToolCommand 的方式就可以让生成的 螺旋线 支持undo。
为何这个工具不能在 viewport2.0 下使用

从上面的 API 列表可以看到
doDragdoPress好几个 API 都有两个实现。
一个是只传入 event 的,这个方法只在 老 Viewport 下调用。
Viewport2.0 调用的是传入 MUIDrawManager 的方法。
helixTool 没有实现 MUIDrawManager 的方法,所以 Viewport2.0 下不起作用。
官方文档被打散到 Viewport2.0 的目录下了,具体的说法可以参照上面
<>Properties.mel 实现左侧的可修改界面<>Values.mel 获取笔刷数值 (更新到界面上)
Context 激活之后,双击可以看到工具界面

这个界面就是遵循上面两个 mel 的方法来实现的。

可以继续参考 helixTool 的源码目录,它提供了
helixProperties.mel和helixValues.mel脚本
那么上面的命名<>是怎么决定的,为啥用helixProperties而不是helixToolProperties

其实这是
getClassName决定的。
mel脚本并不是重点,双击 Context 调用的是helixPropertieshelixValues两个 mel 方法,如果找不到才会找同名脚本。
如果要编写自定义的 UI,一定要用 mel 才能编写吗?
能否用 Python 解决问题呢?
Python function as a MEL procedure 官方文档

如果嫌弃使用 mel 确实可以参考上面的链接用 Python 创建的 Mel Proc
C:\Program Files\Autodesk\Maya2020\Python\Lib\site-packages\maya\mel\melutils.py
具体的代码实现可以通过上面的路径找到。

我尝试了一下,默认
returnCmd是 False 会打开文件窗口生成出 mel 脚本。
可以设置returnCmd=True这样就返回 mel 代码了。
后面可以用mel.eval来执行返回的代码
就是传入的Python
function如果不在 Python 模块之下会弹出警告
pymel 库也提供了 py2mel 的方法
使用这个方法会比 Maya 内置的处理好一些
实现的原理基本一致,都是通过 Python 构建出 Mel 代码,
Mel 代码本质就是用 python 关键字执行 Python 代码 (一会 Python 一会 Mel 的似乎挺绕的(:з」∠))
pymel 还提供了
mel2pyStr的方法可以直接将 mel 代码转成 Python 的版本。
这样就可以避免 python 和 mel 混写。
1 | from pymel.tools import mel2py |
比如上面就可以将一些内置的 mel 案例转换成 python 版本。
pymelNamespace可以给所有的调用加上相应的前缀。
利用上面的方法就可以将 helixTool 的 mel 脚本转为 Python 实现

转换完成之后需要注意 function 调用,要将
pm.mel去掉
因为之前 proc 编程 Python function 用pm.mel.helixSetCallbacks是调用不了的。

另外一些变量名 mel 里面可能命名为了
set,如果这些是 Python 的关键字或者内置命名需要注意。
1 | import pymel.core as pm |
经过一些修改之后,可以实现用 Python 的方式来编写 Mel Proc。
只是还是需要熟悉一下 mel UI 构建的语法。
通过上面一番探讨之后,我们理清楚了做一个笔刷需要什么。

所以我在 C++ 代码层面拆分三个头文件,分别对应
MPxContextMPxContextCommandMPxContextToolCommand的实现。
如何开发也可以参考 helixTool 的代码。

注册插件的时候需要同时注册
MPxContextCommand和MPxContextToolCommand
这样 Maya 就知道这两个命令是关联在一起的,MPxContext里面调用 newToolCommand 方法就可以获取到MPxContextToolCommand
我先要让笔刷按住 B 键的时候可以实现 大小 调整。
默认 Maya API 没有提供键盘事件的监听。
于是查找官方的案例,找到了devkit\plug-ins\grabUVMain.cpp
这里提供 Maya2020 windows 版本的 mll 插件
Maya 加载 mll 插件
1 | import maya.cmds as cmds |
这个插件可以按住 B 键调整笔刷的大小。
原理是利用 Qt 的 eventFilter 监听全局的键盘响应,所以编译的 include 路径需要有 Qt 的头文件,默认的 include 路径只有 Qt 头文件压缩包,需要解压缩来索引。
所以我也是用同样的方式监听是否有按 B 键。
左键拖拽调整笔刷大小,中键拖拽调整笔刷强度。

笔刷覆盖的范围呈现颜色,这个是用
Viewport2.0的 MUIDrawManager 实现的。
MUIDrawManager 提供了 mesh 的 API 进行曲线模型等的绘制。
最重要的第一点是可以传入颜色数组,根据每个点自定义颜色,其他的 line API 无法实现这个功能
1 | MStatus curveBrushContext::doPtrMoved(MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) |
那么问题就变成怎么获取顶点上色了,如果曲线的顶点数量很少就很难有好的显示效果。
因此这里使用 findParamFromLength getPointAtParam 的方式重新采样曲线的顶点。
对采样的顶点再判断一下是否在笔刷的圆圈范围内,范围外的附上透明的颜色,范围内的根据距离附上黑白色。
首先要获取 drag 偏移的向量。
通过doPress方法可以获取到点击的时候的向量偏移。
再通过doDrag获取拖拽的时候鼠标的位置。
两个位置坐标就可以得到偏移的向量。
1 | MStatus curveBrushContext::doPress(MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) |
doDrag还会判断是否按住B键,按住的话就调整笔刷的大小。
反之则调用 newToolCommand 执行 CV 移动的逻辑
ToolCommand会获取曲线上 CV 点的位置,将空间坐标转为屏幕坐标。
这样可以判断这些 CV 点是否在笔刷范围内。
如果在范围的 CV 点根据笔刷提供的方向进行偏移。
1 | MStatus curveBrushTool::redoIt() |
通过 MItCurveCV 遍历曲线上所有的 CV 点。
利用 setCV 方法可以实现顶点的偏移
C++ 这边我发现不能在 MItCurveCV 的遍历过程中调用 setCV ,它会导致遍历中断。
但是用 MItCurveCV 提供的 setCVPosition 无法实现位置的刷新。
最后只好将 CV序号 和 位置通过 Map 保存起来,通过 setCV API 去偏移。
1 | MStatus curveBrushTool::undoIt() |
通过
curvePointMap变量保存了上一次所有 CV 点的位置,undo 只要遍历这个字典去重置 CV 位置即可。
既然 C++ 可以开发出如上看到的笔刷,理论上也可以通过 Python OpenMaya 库进行笔刷开发。
但是我发现 OpenMaya 1.0 不支持 Viewport 2.0 的 API,比如上面关键的 MUIDrawManager
在 OpenMaya1.0 下是不不存在的。
1 | from maya import OpenMayaRender |
可以看到 OpenMaya 2.0 才有 MUIDrawManager
https://matiascodesal.com/blog/maya-python-api-20-it-ready-yet/
以前 18 年的时候还看到有人了文章介绍 OpenMaya 2.0 到底是否可以已经完善了。
OpenMaya 2.0 与 OpenMaya 1.0 相比还缺了挺多的 C++ 类的。
而且 OpenMaya2.0 的案例都有一些代码错误,比如plug-ins\python\api2\py2LassoTool.py(已经是 2023 的最新版本了)
这实在是令人失望,脚本的第 224 行有明显true使用不当,并且MItCurveCV这个类 OpenMaya 2.0 不支持的。
我启用这个脚本框选 CV 点直接给我报错(:з」∠)
也因为 OpenMaya 2.0 各种不完善, 👨💻mottosso 大佬才会做自己的 Pyd wrapper 封装 C++ API cmdc ,只是目前的进度还需要更多人加入支持开发。
那是用 OpenMaya 2.0 能否完成我上面的 C++ 曲线笔刷的复刻呢?
我查了一下,发现 Maya 2020 之后添加了 MPxToolCommand 命令,似乎可以实现和 C++ 一样的 undo 命令。
然而我的实测却让我非常失望。
https://github.com/FXTD-ODYSSEY/Maya-CurveBrush/blob/main/plug-ins/om2_curve_brush.py
基于 OpenMaya 2.0 版本的插件我已经写完了,只是被它的不完整气得不轻。
首先 MPxContextCommand 缺失了syntaxparser方法
即便提供了doQueryFlagsdoEditFlags的 API 但是没法和 C++ 一样进行调用,但是 OpenMaya 1.0 提供了 _syntax _parser 方法给 Python 调用。
1 | def initializePlugin(plugin): |
OpenMaya 2.0 终于在 Maya 2020 提供了 MPxToolCommand 的接口。
但是 MPxToolCommand 需要通过 registerContextCommand 来注册进去。
但是它目前不支持 5 个参数的调用,导致 MPxToolCommand 无法注册。
1 | # Error: TypeError: file F:/repo/CMakeMaya/modules/Maya-CurveBrush/plug-ins/om2_curve_brush.py line 448: function takes exactly 2 arguments (5 given) # |
注册的时候会提示 registerContextCommand 只接受两个参数。
1 | class CurveBrushTool(omui.MPxToolCommand): |
虽然 registerContextCommand 无法注册 MPxToolCommand 导致 newToolCommand 没有正常的返回。
但我可以单独实例化MPxToolCommand从而实现 undo
可是还是不行,而且这个坑爹的情况明显是官方的问题。
doFinalize 明明可以接受一个MArgList类型的参数,但是这个 Python 函数却不接受任何参数(:з」∠)
虽然 2.0 有上述的诸多问题,笔刷的基础功能还是可以实现的。
只是 undo 功能解决不了,倒是可以将曲线的 tweak 操作转移到另一个 Command 上从而实现 undo 的。
不过我这里就点到为止,主要踩了 OpenMaya 2.0 的坑,对它好感度降低了不少(:з」∠)
上面提到了 OpenMaya 1.0 缺失了
MUIDrawManager所以无法在 Viewport 2.0 下进行图像绘制。

C++ 文档也注明了带
MUIDrawManager是无法在 Python 下使用的。
我也测试了不传入MUIDrawManager的几个方法,他们只能在 Legacy Viewport 下响应触发。
那还有什么方法不用 C++ 也可以实现 Python 的绘制呢?
这就可以参考一个非常棒的 Maya Python 工具 spore
spore 也实现了自己的笔刷工具,并且对低版本 Maya 兼容。
它的做法不是通过 Maya API 实现,而是利用 Qt 的 API 进行绘制。
首先对 Maya 的 Viewport 叠加一层透明的 QWidget 层,通过 paintEvent 的实现,绘制自定义图形叠加到 Viewport 上。
实现效果如上图,基本和 Maya API 的绘制效果很接近。
组件叠加的方案我之前的文章也有过 Unreal Python 路径定位启动器
核心思路就是取消 Widget 的边框,忽略输入影响,透明化背景并且永远保持在最前面。
1 | class CanvasOverlay(QtWidgets.QWidget): |
这样就是一个无边框透明的窗口,如果不加上颜色用户是无感知的。
注: 这里的 Overlay 加上了大色块方便观察。
> 我添加了多个 Viewport 的 Overlay 支持,spore 默认是只对笔刷激活时的 Viewport 进行 Overlay 操作。
> 如果切换到多视图或者单独的 Viewport 窗口就会让 Overlay 显示不正常。
1 | class AppFilter(QtCore.QObject): |
> 我这里的做法是利用 toolOnSetup API ,激活笔刷的时候监听 Maya QApplication 全局的点击事件
> 如果点击的 Widget 是 modelEditor 就将 overlay 同步过去。
> Qt 的 objectName 就是 Maya 的 UI control Name ,所以从 objectName() 获取的 API 可以直接用 objectTypeUI 判断类型
> 利用这个方法任何 Viewport 点击都可以直接 resize Overlay 上去。
> 本来不想搞得那么复杂的,但是 Maya 原生的监听方案不起作用 stackoverflow
> stackoverflow 的回答是使用 timer 定时触发,也不是很理想,所以还是借助 Qt API 监听鼠标按键的方法最好。
### 监听 Viewport 事件
> 正如上面所说的 doDrag doPress 等一系列 API 在 Viewport 2.0 下是失效的。
> 通过 Qt 的 installEventFilter 可以实现对 Viewport 的事件监听。
1 | import shiboken2 |
> 通过 OpenMaya 1.0 的 API 可以获取当前激活的 Viewport QWidget
> 拦截这个 Viewport QWidget 的事件可以实现鼠标点击拖拽等等的响应。
1 |
|
> 通过上面的方式就可以拦截 viewport 的 event 通过 MouseFilter 的信号槽做相应的触发。
### 绘制实现

> 参考上图可以看到,Qt API 基本上和 Maya API 绘制的效果差不多。
> Maya API 的 MUIDrawManager 提供了 mesh API 来绘制复杂图形。
> Qt API 并没有类似的方法,不过 Qt 也有 QGradient
> 通过 QLinearGradient 可以实现上面的效果。
> 同样地需要对曲线进行二次采样,提高分段数。
1 | def paintEvent(self, event): |
> 上面是绘制用到的 一些 API
> 核心就是 draw_shape 里面如果传入了多个 color ,获取color每个顶点画一条渐变的线
> 多条线组合成圆形,由此有了衰变颜色的圆形曲线。
> 其他的绘制比如 绘制文字,Qt 有 drawText API
> 绘制圆圈可以利用 sin cos 数学函数来生成圆形的顶点进行绘制。
### 踩坑注意
> QtCore.QPoint 和 OpenMaya.MPoint 两者的 Y 轴坐标起始不一样,所以通过 M3dView 将世界坐标转换为屏幕坐标的时候需要额外的处理。
1 | def world_to_view(position, invertY=True): |
## 基于 draggerContext 笔刷
最后在 highend3d 里面也找到了一个直接 tweak CV 点的方案。
这个方案采用 draggerContext 实现
draggerContext 的案例就可以实现在 viewport 拖拽的时候实现回调。
highend3d的 ysd 曲线工具集还结合软选择的范围作为笔刷移动的范围参数,这是非常聪明的做法。
也可以通过这个方式实现拖拽生成一条曲线。
结合 OpenMaya API 可以做更多的事情,比如散布物体等等,用这个的方案比起 从零构建一个 MPxContext 要简单许多。
绘制功能还是无法通过 draggerContext 解决,不过可以用上面 Qt Overlay 方案来解决。
从
highend3d下载的 ysv 曲线工具可以用,但是有几个问题
- 直接调用 PySide 导致不兼容
- 没有做 Python3 兼容
- 部分代码在新版本代码下运行有 BUG
将 PySide 的导入转成 Qt.py 的导入
Qt.py 库的引入则是采用 submodule 的方式放到 scripts 目录下。
Python3 兼容使用 Python内置的 lib2to3 库进行转换 参考链接
1 | mayapy -m lib2to3 -w F:\repo\Maya-CurveBrush\scripts\ysv\ysvView.py |
通过这个方式就可以自动将所有的 print 括号加上等操作。省去繁琐的人工操作。
我当时是写了一个脚本,批量执行,执行完之后调用 black 和 isort 风格化代码。
生成完之后会将之前的文件加上
.bak后缀,如果没有问题就可以把 bak 删除。
原代码获取当前摄像机是通过下面的方式
1 | from pymel.core import * |
但是如果当前 focus 的 panel 不是 modelEditor 就遭殃了。
1 | from pymel.core import * |
所以我把代码改成上面的效果。
1 | for crv in self.inViewCurves: |
用 pymel 获取 cv 点之后,直接将
NurbsCurveCV与字符串相加
但是NurbsCurveCV有自己的相加逻辑,所以这里需要加上字符串转换可以修复 BUG。
Maya 根据贴图在模型表面散列物体 以前也写过散列物体的文章,不过实现方式是非笔刷的。
利用 artisan 就可以实现笔刷的方式散布物体了。
官方文档: Overview of MEL script painting
官方提到有
spherePaintgeometryPaintemitterPaint几个案例。
具体的代码可以在 mel 脚本库里面找到 eg:C:\Program Files\Autodesk\Maya2018\scripts\others\spherePaint.mel
从最简单的spherePaint介绍

在
Modify页面下找到Paint Scripts Tool工具
打开工具属性面板,在Setup标签页的Tool setup cmd输入spherePaint就可以激活笔刷
激活工具之后就可以模型上刷 Sphere
只可惜 artisan 笔刷它不响应
NurbsCurve,只支持 mesh。
所以无法实现上面探讨的 曲线笔刷 的功能。
artisan 方案更适合颜色绘制或者是物体散布。
以上就是我发现的 Maya 笔刷的多种使用姿势。
后面有机会可以再探讨一下 artisan 笔刷的关于顶点色编辑相关的内容。
最近因为比较特殊的原因,工作上突然闲下来了,于是我就去研究了我们组做的 Maya 毛发工具。
学习一下 Maya 做笔刷有哪些坑。
这次我的主要目的是模仿
XGen的毛发笔刷效果,通过最小案例的实现,探讨不同的实现方案。
官方文档: XGen Interactive Grooming
上面的视频就是 XGen 实现的笔刷效果,对于毛发制作非常丝滑好用。
只可惜这个笔刷不能对曲线直接生效。
上面是我用 C++ 写的曲线笔刷,下面我也会来探讨如何用 Python OpenMaya 结合 Qt 开发笔刷的流程。
具体代码已经开源到 https://github.com/FXTD-ODYSSEY/Maya-CurveBrush
C++ 插件提供了 2020 - 2023 支持
Python 插件有om1_curve_brush.py和om2_curve_brush.py
什么是 Maya Context ? 官方文档说明
Maya Context 就是一个开放的接口,可以用于自定义 鼠标 在 Viewport 上执行的逻辑,实现 绘制 修改选择物体 等操作。
上面的链接是一个 Maya Devkit 里面的案例
devkit\plug-ins\marqueeTool\marqueeTool.cpp
Maya CMake 构建 C++ 插件编译环境 我的这篇文章有提到如何将 devkit 的源码编译生成 mll
这里提供 Maya2020 windows 版本的 mll 插件
Maya 加载 mll 插件
1 | import maya.cmds as cmds |
加载mll 插件后,可以使用上面的代码激活 Context
上面实现的效果和默认的 框选物体是一样的。
只是框的颜色变成了自定义的 黄色。
实现这个 context 需要继承实现两个类,一个是 MPxContext 另一个是 MPxContextCommand
MPxContext 类定义了鼠标拖拽 移动 等逻辑的虚函数,MPxContextCommand 则是用来读取 MPxContext 数据的 Mel 命令。
通过 MPxContextCommand 就可以用 Mel 命令来修改 MPxContext 的变量(比如笔刷大小之类的)
上面提到的方案 Context 进行处理的时候是没有 Undo 功能的。
因此 Maya C++ 提供了 MPxToolCommand 这样将需要 undo 的逻辑放到 Command 当中实现,就可以 undo redo 操作了。
上面文档的案例来自于devkit\plug-ins\helixTool\helixTool.cpp
这里照样提供 Maya2020 windows 版本的 mll 插件
Maya 加载 mll 插件
1 | import maya.cmds as cmds |
加载mll 插件后,可以使用上面的代码激活 Context

需要注意这个插件只能在旧的 Viewport 生效 (我测了好久才明白过来)
这个工具可以在 Maya Viewport 拖拽一个 圆柱预览 ,这个圆柱最后生成 螺旋线。
通过 MPxToolCommand 的方式就可以让生成的 螺旋线 支持undo。
为何这个工具不能在 viewport2.0 下使用

从上面的 API 列表可以看到
doDragdoPress好几个 API 都有两个实现。
一个是只传入 event 的,这个方法只在 老 Viewport 下调用。
Viewport2.0 调用的是传入 MUIDrawManager 的方法。
helixTool 没有实现 MUIDrawManager 的方法,所以 Viewport2.0 下不起作用。
官方文档被打散到 Viewport2.0 的目录下了,具体的说法可以参照上面
<>Properties.mel 实现左侧的可修改界面<>Values.mel 获取笔刷数值 (更新到界面上)
Context 激活之后,双击可以看到工具界面

这个界面就是遵循上面两个 mel 的方法来实现的。

可以继续参考 helixTool 的源码目录,它提供了
helixProperties.mel和helixValues.mel脚本
那么上面的命名<>是怎么决定的,为啥用helixProperties而不是helixToolProperties

其实这是
getClassName决定的。
mel脚本并不是重点,双击 Context 调用的是helixPropertieshelixValues两个 mel 方法,如果找不到才会找同名脚本。
如果要编写自定义的 UI,一定要用 mel 才能编写吗?
能否用 Python 解决问题呢?
Python function as a MEL procedure 官方文档

如果嫌弃使用 mel 确实可以参考上面的链接用 Python 创建的 Mel Proc
C:\Program Files\Autodesk\Maya2020\Python\Lib\site-packages\maya\mel\melutils.py
具体的代码实现可以通过上面的路径找到。

我尝试了一下,默认
returnCmd是 False 会打开文件窗口生成出 mel 脚本。
可以设置returnCmd=True这样就返回 mel 代码了。
后面可以用mel.eval来执行返回的代码
就是传入的Python
function如果不在 Python 模块之下会弹出警告
pymel 库也提供了 py2mel 的方法
使用这个方法会比 Maya 内置的处理好一些
实现的原理基本一致,都是通过 Python 构建出 Mel 代码,
Mel 代码本质就是用 python 关键字执行 Python 代码 (一会 Python 一会 Mel 的似乎挺绕的(:з」∠))
pymel 还提供了
mel2pyStr的方法可以直接将 mel 代码转成 Python 的版本。
这样就可以避免 python 和 mel 混写。
1 | from pymel.tools import mel2py |
比如上面就可以将一些内置的 mel 案例转换成 python 版本。
pymelNamespace可以给所有的调用加上相应的前缀。
利用上面的方法就可以将 helixTool 的 mel 脚本转为 Python 实现

转换完成之后需要注意 function 调用,要将
pm.mel去掉
因为之前 proc 编程 Python function 用pm.mel.helixSetCallbacks是调用不了的。

另外一些变量名 mel 里面可能命名为了
set,如果这些是 Python 的关键字或者内置命名需要注意。
1 | import pymel.core as pm |
经过一些修改之后,可以实现用 Python 的方式来编写 Mel Proc。
只是还是需要熟悉一下 mel UI 构建的语法。
通过上面一番探讨之后,我们理清楚了做一个笔刷需要什么。

所以我在 C++ 代码层面拆分三个头文件,分别对应
MPxContextMPxContextCommandMPxContextToolCommand的实现。
如何开发也可以参考 helixTool 的代码。

注册插件的时候需要同时注册
MPxContextCommand和MPxContextToolCommand
这样 Maya 就知道这两个命令是关联在一起的,MPxContext里面调用 newToolCommand 方法就可以获取到MPxContextToolCommand
我先要让笔刷按住 B 键的时候可以实现 大小 调整。
默认 Maya API 没有提供键盘事件的监听。
于是查找官方的案例,找到了devkit\plug-ins\grabUVMain.cpp
这里提供 Maya2020 windows 版本的 mll 插件
Maya 加载 mll 插件
1 | import maya.cmds as cmds |
这个插件可以按住 B 键调整笔刷的大小。
原理是利用 Qt 的 eventFilter 监听全局的键盘响应,所以编译的 include 路径需要有 Qt 的头文件,默认的 include 路径只有 Qt 头文件压缩包,需要解压缩来索引。
所以我也是用同样的方式监听是否有按 B 键。
左键拖拽调整笔刷大小,中键拖拽调整笔刷强度。

笔刷覆盖的范围呈现颜色,这个是用
Viewport2.0的 MUIDrawManager 实现的。
MUIDrawManager 提供了 mesh 的 API 进行曲线模型等的绘制。
最重要的第一点是可以传入颜色数组,根据每个点自定义颜色,其他的 line API 无法实现这个功能
1 | MStatus curveBrushContext::doPtrMoved(MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) |
那么问题就变成怎么获取顶点上色了,如果曲线的顶点数量很少就很难有好的显示效果。
因此这里使用 findParamFromLength getPointAtParam 的方式重新采样曲线的顶点。
对采样的顶点再判断一下是否在笔刷的圆圈范围内,范围外的附上透明的颜色,范围内的根据距离附上黑白色。
首先要获取 drag 偏移的向量。
通过doPress方法可以获取到点击的时候的向量偏移。
再通过doDrag获取拖拽的时候鼠标的位置。
两个位置坐标就可以得到偏移的向量。
1 | MStatus curveBrushContext::doPress(MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) |
doDrag还会判断是否按住B键,按住的话就调整笔刷的大小。
反之则调用 newToolCommand 执行 CV 移动的逻辑
ToolCommand会获取曲线上 CV 点的位置,将空间坐标转为屏幕坐标。
这样可以判断这些 CV 点是否在笔刷范围内。
如果在范围的 CV 点根据笔刷提供的方向进行偏移。
1 | MStatus curveBrushTool::redoIt() |
通过 MItCurveCV 遍历曲线上所有的 CV 点。
利用 setCV 方法可以实现顶点的偏移
C++ 这边我发现不能在 MItCurveCV 的遍历过程中调用 setCV ,它会导致遍历中断。
但是用 MItCurveCV 提供的 setCVPosition 无法实现位置的刷新。
最后只好将 CV序号 和 位置通过 Map 保存起来,通过 setCV API 去偏移。
1 | MStatus curveBrushTool::undoIt() |
通过
curvePointMap变量保存了上一次所有 CV 点的位置,undo 只要遍历这个字典去重置 CV 位置即可。
既然 C++ 可以开发出如上看到的笔刷,理论上也可以通过 Python OpenMaya 库进行笔刷开发。
但是我发现 OpenMaya 1.0 不支持 Viewport 2.0 的 API,比如上面关键的 MUIDrawManager
在 OpenMaya1.0 下是不不存在的。
1 | from maya import OpenMayaRender |
可以看到 OpenMaya 2.0 才有 MUIDrawManager
https://matiascodesal.com/blog/maya-python-api-20-it-ready-yet/
以前 18 年的时候还看到有人了文章介绍 OpenMaya 2.0 到底是否可以已经完善了。
OpenMaya 2.0 与 OpenMaya 1.0 相比还缺了挺多的 C++ 类的。
而且 OpenMaya2.0 的案例都有一些代码错误,比如plug-ins\python\api2\py2LassoTool.py(已经是 2023 的最新版本了)
这实在是令人失望,脚本的第 224 行有明显true使用不当,并且MItCurveCV这个类 OpenMaya 2.0 不支持的。
我启用这个脚本框选 CV 点直接给我报错(:з」∠)
也因为 OpenMaya 2.0 各种不完善, 👨💻mottosso 大佬才会做自己的 Pyd wrapper 封装 C++ API cmdc ,只是目前的进度还需要更多人加入支持开发。
那是用 OpenMaya 2.0 能否完成我上面的 C++ 曲线笔刷的复刻呢?
我查了一下,发现 Maya 2020 之后添加了 MPxToolCommand 命令,似乎可以实现和 C++ 一样的 undo 命令。
然而我的实测却让我非常失望。
https://github.com/FXTD-ODYSSEY/Maya-CurveBrush/blob/main/plug-ins/om2_curve_brush.py
基于 OpenMaya 2.0 版本的插件我已经写完了,只是被它的不完整气得不轻。
首先 MPxContextCommand 缺失了syntaxparser方法
即便提供了doQueryFlagsdoEditFlags的 API 但是没法和 C++ 一样进行调用,但是 OpenMaya 1.0 提供了 _syntax _parser 方法给 Python 调用。
1 | def initializePlugin(plugin): |
OpenMaya 2.0 终于在 Maya 2020 提供了 MPxToolCommand 的接口。
但是 MPxToolCommand 需要通过 registerContextCommand 来注册进去。
但是它目前不支持 5 个参数的调用,导致 MPxToolCommand 无法注册。
1 | # Error: TypeError: file F:/repo/CMakeMaya/modules/Maya-CurveBrush/plug-ins/om2_curve_brush.py line 448: function takes exactly 2 arguments (5 given) # |
注册的时候会提示 registerContextCommand 只接受两个参数。
1 | class CurveBrushTool(omui.MPxToolCommand): |
虽然 registerContextCommand 无法注册 MPxToolCommand 导致 newToolCommand 没有正常的返回。
但我可以单独实例化MPxToolCommand从而实现 undo
可是还是不行,而且这个坑爹的情况明显是官方的问题。
doFinalize 明明可以接受一个MArgList类型的参数,但是这个 Python 函数却不接受任何参数(:з」∠)
虽然 2.0 有上述的诸多问题,笔刷的基础功能还是可以实现的。
只是 undo 功能解决不了,倒是可以将曲线的 tweak 操作转移到另一个 Command 上从而实现 undo 的。
不过我这里就点到为止,主要踩了 OpenMaya 2.0 的坑,对它好感度降低了不少(:з」∠)
上面提到了 OpenMaya 1.0 缺失了
MUIDrawManager所以无法在 Viewport 2.0 下进行图像绘制。

C++ 文档也注明了带
MUIDrawManager是无法在 Python 下使用的。
我也测试了不传入MUIDrawManager的几个方法,他们只能在 Legacy Viewport 下响应触发。
那还有什么方法不用 C++ 也可以实现 Python 的绘制呢?
这就可以参考一个非常棒的 Maya Python 工具 spore
spore 也实现了自己的笔刷工具,并且对低版本 Maya 兼容。
它的做法不是通过 Maya API 实现,而是利用 Qt 的 API 进行绘制。
首先对 Maya 的 Viewport 叠加一层透明的 QWidget 层,通过 paintEvent 的实现,绘制自定义图形叠加到 Viewport 上。
实现效果如上图,基本和 Maya API 的绘制效果很接近。
组件叠加的方案我之前的文章也有过 Unreal Python 路径定位启动器
核心思路就是取消 Widget 的边框,忽略输入影响,透明化背景并且永远保持在最前面。
1 | class CanvasOverlay(QtWidgets.QWidget): |
这样就是一个无边框透明的窗口,如果不加上颜色用户是无感知的。
注: 这里的 Overlay 加上了大色块方便观察。
> 我添加了多个 Viewport 的 Overlay 支持,spore 默认是只对笔刷激活时的 Viewport 进行 Overlay 操作。
> 如果切换到多视图或者单独的 Viewport 窗口就会让 Overlay 显示不正常。
1 | class AppFilter(QtCore.QObject): |
> 我这里的做法是利用 toolOnSetup API ,激活笔刷的时候监听 Maya QApplication 全局的点击事件
> 如果点击的 Widget 是 modelEditor 就将 overlay 同步过去。
> Qt 的 objectName 就是 Maya 的 UI control Name ,所以从 objectName() 获取的 API 可以直接用 objectTypeUI 判断类型
> 利用这个方法任何 Viewport 点击都可以直接 resize Overlay 上去。
> 本来不想搞得那么复杂的,但是 Maya 原生的监听方案不起作用 stackoverflow
> stackoverflow 的回答是使用 timer 定时触发,也不是很理想,所以还是借助 Qt API 监听鼠标按键的方法最好。
### 监听 Viewport 事件
> 正如上面所说的 doDrag doPress 等一系列 API 在 Viewport 2.0 下是失效的。
> 通过 Qt 的 installEventFilter 可以实现对 Viewport 的事件监听。
1 | import shiboken2 |
> 通过 OpenMaya 1.0 的 API 可以获取当前激活的 Viewport QWidget
> 拦截这个 Viewport QWidget 的事件可以实现鼠标点击拖拽等等的响应。
1 |
|
> 通过上面的方式就可以拦截 viewport 的 event 通过 MouseFilter 的信号槽做相应的触发。
### 绘制实现

> 参考上图可以看到,Qt API 基本上和 Maya API 绘制的效果差不多。
> Maya API 的 MUIDrawManager 提供了 mesh API 来绘制复杂图形。
> Qt API 并没有类似的方法,不过 Qt 也有 QGradient
> 通过 QLinearGradient 可以实现上面的效果。
> 同样地需要对曲线进行二次采样,提高分段数。
1 | def paintEvent(self, event): |
> 上面是绘制用到的 一些 API
> 核心就是 draw_shape 里面如果传入了多个 color ,获取color每个顶点画一条渐变的线
> 多条线组合成圆形,由此有了衰变颜色的圆形曲线。
> 其他的绘制比如 绘制文字,Qt 有 drawText API
> 绘制圆圈可以利用 sin cos 数学函数来生成圆形的顶点进行绘制。
### 踩坑注意
> QtCore.QPoint 和 OpenMaya.MPoint 两者的 Y 轴坐标起始不一样,所以通过 M3dView 将世界坐标转换为屏幕坐标的时候需要额外的处理。
1 | def world_to_view(position, invertY=True): |
## 基于 draggerContext 笔刷
最后在 highend3d 里面也找到了一个直接 tweak CV 点的方案。
这个方案采用 draggerContext 实现
draggerContext 的案例就可以实现在 viewport 拖拽的时候实现回调。
highend3d的 ysd 曲线工具集还结合软选择的范围作为笔刷移动的范围参数,这是非常聪明的做法。
也可以通过这个方式实现拖拽生成一条曲线。
结合 OpenMaya API 可以做更多的事情,比如散布物体等等,用这个的方案比起 从零构建一个 MPxContext 要简单许多。
绘制功能还是无法通过 draggerContext 解决,不过可以用上面 Qt Overlay 方案来解决。
从
highend3d下载的 ysv 曲线工具可以用,但是有几个问题
- 直接调用 PySide 导致不兼容
- 没有做 Python3 兼容
- 部分代码在新版本代码下运行有 BUG
将 PySide 的导入转成 Qt.py 的导入
Qt.py 库的引入则是采用 submodule 的方式放到 scripts 目录下。
Python3 兼容使用 Python内置的 lib2to3 库进行转换 参考链接
1 | mayapy -m lib2to3 -w F:\repo\Maya-CurveBrush\scripts\ysv\ysvView.py |
通过这个方式就可以自动将所有的 print 括号加上等操作。省去繁琐的人工操作。
我当时是写了一个脚本,批量执行,执行完之后调用 black 和 isort 风格化代码。
生成完之后会将之前的文件加上
.bak后缀,如果没有问题就可以把 bak 删除。
原代码获取当前摄像机是通过下面的方式
1 | from pymel.core import * |
但是如果当前 focus 的 panel 不是 modelEditor 就遭殃了。
1 | from pymel.core import * |
所以我把代码改成上面的效果。
1 | for crv in self.inViewCurves: |
用 pymel 获取 cv 点之后,直接将
NurbsCurveCV与字符串相加
但是NurbsCurveCV有自己的相加逻辑,所以这里需要加上字符串转换可以修复 BUG。
Maya 根据贴图在模型表面散列物体 以前也写过散列物体的文章,不过实现方式是非笔刷的。
利用 artisan 就可以实现笔刷的方式散布物体了。
官方文档: Overview of MEL script painting
官方提到有
spherePaintgeometryPaintemitterPaint几个案例。
具体的代码可以在 mel 脚本库里面找到 eg:C:\Program Files\Autodesk\Maya2018\scripts\others\spherePaint.mel
从最简单的spherePaint介绍

在
Modify页面下找到Paint Scripts Tool工具
打开工具属性面板,在Setup标签页的Tool setup cmd输入spherePaint就可以激活笔刷
激活工具之后就可以模型上刷 Sphere
只可惜 artisan 笔刷它不响应
NurbsCurve,只支持 mesh。
所以无法实现上面探讨的 曲线笔刷 的功能。
artisan 方案更适合颜色绘制或者是物体散布。
以上就是我发现的 Maya 笔刷的多种使用姿势。
后面有机会可以再探讨一下 artisan 笔刷的关于顶点色编辑相关的内容。