前言
最近因为比较特殊的原因,工作上突然闲下来了,于是我就去研究了我们组做的 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 C++ 笔刷开发流程 Maya C++ MPxContext
什么是 Maya Context ? 官方文档说明 Maya Context 就是一个开放的接口,可以用于自定义 鼠标 在 Viewport 上执行的逻辑,实现 绘制 修改选择物体 等操作。
MPxContext 官方文档
上面的链接是一个 Maya Devkit 里面的案例 devkit\plug-ins\marqueeTool\marqueeTool.cpp
Maya CMake 构建 C++ 插件编译环境 我的这篇文章有提到如何将 devkit 的源码编译生成 mll
maya2020 - marqueeTool.mll
这里提供 Maya2020 windows 版本的 mll 插件 Maya 加载 mll 插件
1 2 3 import maya.cmds as cmdsctx = cmds.marqueeToolContext() cmds.setToolTo(ctx)
加载mll 插件后,可以使用上面的代码激活 Context
上面实现的效果和默认的 框选物体是一样的。 只是框的颜色变成了自定义的 黄色。
实现这个 context 需要继承实现两个类,一个是 MPxContext 另一个是 MPxContextCommand MPxContext 类定义了鼠标拖拽 移动 等逻辑的虚函数,MPxContextCommand 则是用来读取 MPxContext 数据的 Mel 命令。 通过 MPxContextCommand 就可以用 Mel 命令来修改 MPxContext 的变量(比如笔刷大小之类的)
MPxContextCommand 官方文档
MPxToolCommand 官方文档
上面提到的方案 Context 进行处理的时候是没有 Undo 功能的。 因此 Maya C++ 提供了 MPxToolCommand 这样将需要 undo 的逻辑放到 Command 当中实现,就可以 undo redo 操作了。 上面文档的案例来自于 devkit\plug-ins\helixTool\helixTool.cpp
maya2020 - helixTool.mll
这里照样提供 Maya2020 windows 版本的 mll 插件 Maya 加载 mll 插件
1 2 3 import maya.cmds as cmdsctx = cmds.helixToolContext() cmds.setToolTo(ctx)
加载mll 插件后,可以使用上面的代码激活 Context
需要注意这个插件只能在旧的 Viewport 生效 (我测了好久才明白过来)
这个工具可以在 Maya Viewport 拖拽一个 圆柱预览 ,这个圆柱最后生成 螺旋线。 通过 MPxToolCommand 的方式就可以让生成的 螺旋线 支持undo。
为何这个工具不能在 viewport2.0 下使用
从上面的 API 列表可以看到 doDrag
doPress
好几个 API 都有两个实现。 一个是只传入 event 的,这个方法只在 老 Viewport 下调用。 Viewport2.0 调用的是传入 MUIDrawManager 的方法。 helixTool 没有实现 MUIDrawManager 的方法,所以 Viewport2.0 下不起作用。
Tool Contexts 官方文档
官方文档被打散到 Viewport2.0 的目录下了,具体的说法可以参照上面
笔刷工具的 UI Tool property sheets 官方文档
<>Properties.mel
实现左侧的可修改界面<>Values.mel
获取笔刷数值 (更新到界面上)
Context 激活之后,双击可以看到工具界面
这个界面就是遵循上面两个 mel 的方法来实现的。
可以继续参考 helixTool 的源码目录,它提供了 helixProperties.mel
和 helixValues.mel
脚本 那么上面的命名 <>
是怎么决定的,为啥用 helixProperties
而不是 helixToolProperties
其实这是 getClassName
决定的。 mel脚本并不是重点,双击 Context 调用的是 helixProperties
helixValues
两个 mel 方法,如果找不到才会找同名脚本。
用 Python 生成 Mel Proc
如果要编写自定义的 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 模块之下会弹出警告
py2melProc 文档
pymel 库也提供了 py2mel 的方法 使用这个方法会比 Maya 内置的处理好一些 实现的原理基本一致,都是通过 Python 构建出 Mel 代码, Mel 代码本质就是用 python 关键字执行 Python 代码 (一会 Python 一会 Mel 的似乎挺绕的(:з」∠) )
pymel 还提供了 mel2pyStr
的方法可以直接将 mel 代码转成 Python 的版本。 这样就可以避免 python 和 mel 混写。
1 2 3 4 5 6 from pymel.tools import mel2pypath = r"C:\Program Files\Autodesk\Maya2018\scripts\others\customtoolPaint.mel" with open (path,'r' ) as rf: content = rf.read() py_str = mel2py.mel2pyStr(content, pymelNamespace="pm" ) print (py_str)
比如上面就可以将一些内置的 mel 案例转换成 python 版本。 pymelNamespace
可以给所有的调用加上相应的前缀。
利用上面的方法就可以将 helixTool 的 mel 脚本转为 Python 实现
转换完成之后需要注意 function 调用,要将 pm.mel
去掉 因为之前 proc 编程 Python function 用 pm.mel.helixSetCallbacks
是调用不了的。
另外一些变量名 mel 里面可能命名为了 set
,如果这些是 Python 的关键字或者内置命名需要注意。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import pymel.core as pmfrom pymel.tools import py2meldef helixProperties (): pm.setUITemplate("DefaultTemplate" , pushTemplate=1 ) parent = str (pm.toolPropertyWindow(q=1 , location=1 )) pm.setParent(parent) pm.columnLayout("helix" ) pm.tabLayout("helixTabs" , childResizable=True ) pm.columnLayout("helixTab" ) pm.frameLayout("helixFrame" , cll=True , l="Helix Options" , cl=False ) pm.columnLayout("helixOptions" ) pm.separator(style="none" ) pm.intSliderGrp( "numCVs" , field=1 , minValue=20 , maxValue=100 , value=1 , label="Number of CVs" ) pm.checkBoxGrp("upsideDownGrp" , numberOfCheckBoxes=1 , l1=" " , label="Upside Down" ) pm.setParent(".." ) pm.setParent(".." ) pm.setParent(".." ) pm.setParent(".." ) pm.setParent(".." ) pm.tabLayout("helixTabs" , tl=("helixTab" , "Tool Defaults" ), e=1 ) pm.setUITemplate(popTemplate=1 ) helixSetCallbacks(parent) def helixSetCallbacks (parent ): pm.setParent(parent) pm.checkBoxGrp( "upsideDownGrp" , e=1 , on1=lambda *args: pm.helixToolContext(pm.currentCtx(), upsideDown=True , e=1 ), of1=lambda *args: pm.helixToolContext(pm.currentCtx(), upsideDown=False , e=1 ), ) pm.intSliderGrp( "numCVs" , e=1 , cc=lambda *args: pm.helixToolContext(pm.currentCtx(), numCVs=args[0 ], e=1 ), ) def helixValues (toolName ): parent=(str (pm.toolPropertyWindow(q=1 , location=1 )) + "|helix|helixTabs|helixTab" ) pm.setParent(parent) icon="helixTool.xpm" help ="" pm.mel.toolPropertySetCommon(toolName, icon, help ) pm.frameLayout('helixFrame' , en=True , e=1 , cl=False ) helixOptionValues(toolName) pm.mel.toolPropertySelect('helix' ) def helixOptionValues (toolName ): cv_num = 0 cv_num=int (pm.mel.eval ("helixToolContext -q -numCVs " + toolName)) pm.intSliderGrp('numCVs' , e=1 , value=cv_num) cv_num=int (pm.mel.eval ("helixToolContext -q -upsideDown " + toolName)) if cv_num: pm.checkBoxGrp('upsideDownGrp' , e=1 , value1=1 ) else : pm.checkBoxGrp('upsideDownGrp' , e=1 , value1=0 ) py2mel.py2melProc(helixProperties, procName="helixProperties" ) py2mel.py2melProc(helixValues, procName="helixValues" )
经过一些修改之后,可以实现用 Python 的方式来编写 Mel Proc。 只是还是需要熟悉一下 mel UI 构建的语法。
Maya C++ CurveBrush
通过上面一番探讨之后,我们理清楚了做一个笔刷需要什么。
所以我在 C++ 代码层面拆分三个头文件,分别对应 MPxContext
MPxContextCommand
MPxContextToolCommand
的实现。 如何开发也可以参考 helixTool 的代码。
注册插件的时候需要同时注册 MPxContextCommand
和 MPxContextToolCommand
这样 Maya 就知道这两个命令是关联在一起的, MPxContext
里面调用 newToolCommand 方法就可以获取到 MPxContextToolCommand
笔刷属性调整
我先要让笔刷按住 B 键的时候可以实现 大小 调整。 默认 Maya API 没有提供键盘事件的监听。 于是查找官方的案例,找到了 devkit\plug-ins\grabUVMain.cpp
maya2020 - grabUV.mll
这里提供 Maya2020 windows 版本的 mll 插件 Maya 加载 mll 插件
1 2 3 import maya.cmds as cmdsctx = cmds.grabUVContext() cmds.setToolTo(ctx)
这个插件可以按住 B 键调整笔刷的大小。 原理是利用 Qt 的 eventFilter 监听全局的键盘响应,所以编译的 include 路径需要有 Qt 的头文件,默认的 include 路径只有 Qt 头文件压缩包,需要解压缩来索引。
所以我也是用同样的方式监听是否有按 B 键。 左键拖拽调整笔刷大小,中键拖拽调整笔刷强度。
曲线衰变颜色
笔刷覆盖的范围呈现颜色,这个是用 Viewport2.0
的 MUIDrawManager 实现的。
MUIDrawManager
MUIDrawManager 提供了 mesh 的 API 进行曲线模型等的绘制。 最重要的第一点是可以传入颜色数组,根据每个点自定义颜色,其他的 line API 无法实现这个功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 MStatus curveBrushContext::doPtrMoved (MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) { short x, y; event.getPosition (x, y); mBrushCenterScreenPoint = MPoint (x, y); auto radius = mBrushConfig.size (); drawMgr.beginDrawable (); if (bFalloffMode) { for (unsigned int index = 0 ; index < objDagPathArray.length (); ++index) { MPointArray pointArray; MColorArray colorArray; MFnNurbsCurve curveFn (objDagPathArray[index]) ; unsigned int segmentCount = 100 ; for (unsigned int pointIndex = 0 ; pointIndex < segmentCount; ++pointIndex) { MPoint point; auto param = curveFn.findParamFromLength (curveFn.length () * pointIndex / segmentCount); curveFn.getPointAtParam (param, point, MSpace::kWorld); pointArray.append (point); short x_pos, y_pos; view.worldToView (point, x_pos, y_pos); MPoint screenPoint (x_pos, y_pos) ; auto distance = (mBrushCenterScreenPoint - screenPoint).length (); auto field = 1 - distance / radius; colorArray.append (distance > radius ? MColor (0.f ) : MColor (field, field, field)); } drawMgr.setLineWidth (12.0f ); drawMgr.mesh (MHWRender::MUIDrawManager::kLineStrip, pointArray, NULL , &colorArray); } } drawMgr.setColor (MColor (1.f , 1.f , 1.f )); drawMgr.setLineWidth (2.0f ); drawMgr.circle2d (mBrushCenterScreenPoint, radius); drawMgr.endDrawable (); return MS::kSuccess; }
那么问题就变成怎么获取顶点上色了,如果曲线的顶点数量很少就很难有好的显示效果。
因此这里使用 findParamFromLength getPointAtParam 的方式重新采样曲线的顶点。 对采样的顶点再判断一下是否在笔刷的圆圈范围内,范围外的附上透明的颜色,范围内的根据距离附上黑白色。
曲线 CV 移动
首先要获取 drag 偏移的向量。 通过 doPress
方法可以获取到点击的时候的向量偏移。 再通过 doDrag
获取拖拽的时候鼠标的位置。 两个位置坐标就可以得到偏移的向量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 MStatus curveBrushContext::doPress (MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) { view = M3dView::active3dView (); event.getPosition (startPosX, startPosY); fStartBrushSize = mBrushConfig.size (); fStartBrushStrength = mBrushConfig.strength (); return MS::kSuccess; } MStatus curveBrushContext::doDrag (MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) { view.refresh (false , true ); short currentPosX, currentPosY; event.getPosition (currentPosX, currentPosY); auto currentPos = MPoint (currentPosX, currentPosY); MPoint start (startPosX, startPosY) ; MVector delta = MVector (currentPos - start); drawMgr.beginDrawable (); drawMgr.setColor (MColor (1.f , 1.f , 1.f )); drawMgr.setLineWidth (2.0f ); if (eDragMode == kBrushSize) { float deltaValue; char info[64 ]; if (event.mouseButton () == MEvent::kLeftMouse) { deltaValue = delta.x > 0 ? delta.length () : -delta.length (); mBrushConfig.setSize (fStartBrushSize + deltaValue); sprintf (info, "Brush Size: %.2f" , mBrushConfig.size ()); drawMgr.text2d (currentPos, info); } else if (event.mouseButton () == MEvent::kMiddleMouse) { deltaValue = delta.y > 0 ? delta.length () : -delta.length (); mBrushConfig.setStrength (fStartBrushStrength + deltaValue); sprintf (info, "Brush Strength: %.2f" , mBrushConfig.strength ()); drawMgr.text2d (currentPos, info); } drawMgr.line2d (start, MPoint (startPosX, startPosY + mBrushConfig.strength () * 2 )); } else { MPoint startNearPos, startFarPos, currNearPos, currFarPos; view.viewToWorld (currentPosX, currentPosY, currNearPos, currFarPos); view.viewToWorld (startPosX, startPosY, startFarPos, startFarPos); curveBrushTool *cmd = (curveBrushTool *)newToolCommand (); cmd->setStrength (mBrushConfig.strength ()); cmd->setRadius (mBrushConfig.size ()); cmd->setMoveVector ((currFarPos - startFarPos).normal ()); cmd->setStartPoint (start); cmd->setDagPathArray (objDagPathArray); cmd->redoIt (); cmd->finalize (); } drawMgr.circle2d (start, mBrushConfig.size ()); drawMgr.endDrawable (); return MS::kSuccess; }
doDrag
还会判断是否按住 B
键,按住的话就调整笔刷的大小。 反之则调用 newToolCommand 执行 CV 移动的逻辑
ToolCommand
会获取曲线上 CV 点的位置,将空间坐标转为屏幕坐标。 这样可以判断这些 CV 点是否在笔刷范围内。 如果在范围的 CV 点根据笔刷提供的方向进行偏移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 MStatus curveBrushTool::redoIt () { MVector offsetVector = moveVector * 0.002 * strength; M3dView view = M3dView::active3dView (); short x_pos, y_pos; for (unsigned int index = 0 ; index < dagPathArray.length (); ++index) { MFnNurbsCurve curveFn (dagPathArray[index]) ; std::map<int , MVector> offsetMap; for (MItCurveCV cvIter (dagPathArray[index]); !cvIter.isDone (); cvIter.next ()) { MPoint pos = cvIter.position (MSpace::kWorld); int cvIndex = cvIter.index (); curvePointMap[index][cvIndex] = pos; view.worldToView (pos, x_pos, y_pos); if ((startPoint - MPoint (x_pos, y_pos)).length () < radius) { offsetMap[cvIndex] = pos + offsetVector; } } for (const auto &it : offsetMap) { curveFn.setCV (it.first, it.second, MSpace::kWorld); } curveFn.updateCurve (); } return MStatus::kSuccess; }
通过 MItCurveCV 遍历曲线上所有的 CV 点。 利用 setCV 方法可以实现顶点的偏移 C++ 这边我发现不能在 MItCurveCV 的遍历过程中调用 setCV ,它会导致遍历中断。 但是用 MItCurveCV 提供的 setCVPosition 无法实现位置的刷新。 最后只好将 CV序号 和 位置通过 Map 保存起来,通过 setCV API 去偏移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 MStatus curveBrushTool::undoIt () { for (const auto &kv : curvePointMap) { MFnNurbsCurve curveFn (dagPathArray[kv.first]) ; for (const auto &it : kv.second) { int cvIndex = it.first; MPoint pos = it.second; curveFn.setCV (cvIndex, pos, MSpace::kWorld); } } return MStatus::kSuccess; }
通过 curvePointMap
变量保存了上一次所有 CV 点的位置,undo 只要遍历这个字典去重置 CV 位置即可。
OpenMaya 2.0 笔刷开发
既然 C++ 可以开发出如上看到的笔刷,理论上也可以通过 Python OpenMaya 库进行笔刷开发。 但是我发现 OpenMaya 1.0 不支持 Viewport 2.0 的 API,比如上面关键的 MUIDrawManager 在 OpenMaya1.0 下是不不存在的。
1 2 3 4 5 from maya import OpenMayaRenderOpenMayaRender.MUIDrawManager from maya.api import OpenMayaRenderOpenMayaRender.MUIDrawManager
可以看到 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
MPxContextCommand 缺失 syntax parser 方法
基于 OpenMaya 2.0 版本的插件我已经写完了,只是被它的不完整气得不轻。 首先 MPxContextCommand 缺失了 syntax
parser
方法 即便提供了 doQueryFlags
doEditFlags
的 API 但是没法和 C++ 一样进行调用,但是 OpenMaya 1.0 提供了 _syntax _parser 方法给 Python 调用。
registerContextCommand 不支持 MPxToolCommand 注册 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def initializePlugin (plugin ): pluginFn = om.MFnPlugin(plugin) try : pluginFn.registerContextCommand(CONTEXT_NAME, CurveBrushContextCmd.creator) except : sys.stderr.write("Failed to register command: %s\n" % CONTEXT_NAME) raise
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 2 3 4 5 6 7 8 9 10 class CurveBrushTool (omui.MPxToolCommand): def finalize (self ): command = om.MArgList() command.addArg(self.commandString) for flag, config in self.flags_data.items(): long_flag = config.get("long" ) command.addArg(flag) command.addArg(getattr (self, long_flag[1 :]))
虽然 registerContextCommand 无法注册 MPxToolCommand 导致 newToolCommand 没有正常的返回。 但我可以单独实例化 MPxToolCommand
从而实现 undo 可是还是不行,而且这个坑爹的情况明显是官方的问题。 doFinalize 明明可以接受一个 MArgList
类型的参数,但是这个 Python 函数却不接受任何参数(:з」∠)
OpenMaya 2.0 展示
虽然 2.0 有上述的诸多问题,笔刷的基础功能还是可以实现的。 只是 undo 功能解决不了,倒是可以将曲线的 tweak 操作转移到另一个 Command 上从而实现 undo 的。 不过我这里就点到为止,主要踩了 OpenMaya 2.0 的坑,对它好感度降低了不少(:з」∠)
Python Qt Overlay 实现自定义绘制
上面提到了 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 的绘制效果很接近。
Overlay 组件实现
组件叠加的方案我之前的文章也有过 Unreal Python 路径定位启动器 核心思路就是取消 Widget 的边框,忽略输入影响,透明化背景并且永远保持在最前面。
1 2 3 4 5 6 7 8 9 10 11 12 class CanvasOverlay (QtWidgets.QWidget): def __init__ (self, context ): super (CanvasOverlay, self).__init__() self.setWindowFlags( QtCore.Qt.FramelessWindowHint | QtCore.Qt.SplashScreen | QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.WindowTransparentForInput ) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setAttribute(QtCore.Qt.WA_NoSystemBackground)
这样就是一个无边框透明的窗口,如果不加上颜色用户是无感知的。
spore 参考
多个 Viewport 叠加支持 注: 这里的 Overlay 加上了大色块方便观察。
> 我添加了多个 Viewport 的 Overlay 支持,spore 默认是只对笔刷激活时的 Viewport 进行 Overlay 操作。
> 如果切换到多视图或者单独的 Viewport 窗口就会让 Overlay 显示不正常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class AppFilter (QtCore.QObject): def __init__ (self, canvas ): super (AppFilter, self).__init__() self.canvas = canvas def eventFilter (self, receiver, event ): if event.type () == QtCore.QEvent.MouseButtonPress: widget = QtWidgets.QApplication.widgetAt(QtGui.QCursor.pos()) panel = isinstance (widget, QtCore.QObject) and widget.parent() name = panel and panel.objectName() if name: is_model_editor = cmds.objectTypeUI(name, i="modelEditor" ) self.canvas.setVisible(is_model_editor) if is_model_editor: QtCore.QTimer.singleShot(0 , self.canvas.setup_active_viewport) return super (AppFilter, self).eventFilter(receiver, event) class CurveBrushContext (OpenMayaMPx.MPxContext): def toolOnSetup (self ): self.canvas = CanvasOverlay(self) app = QtWidgets.QApplication.instance() app_filter = AppFilter(self.canvas) app.installEventFilter(app_filter)
> 我这里的做法是利用
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import shiboken2import maya.OpenMaya as omimport maya.OpenMayaUI as omuifrom PySide2.QtWidgets import QWidgetfrom PySide2.QtCore import QObjectdef active_view (): """ return the active 3d view """ return omui.M3dView.active3dView() def active_view_wdg (): """ return the active 3d view wrapped in a QWidget """ view = active_view() active_view_widget = shiboken2.wrapInstance(long(view.widget()), QWidget) return active_view_widget
spore 参考 > 通过 OpenMaya 1.0 的 API 可以获取当前激活的 Viewport QWidget
> 拦截这个 Viewport QWidget 的事件可以实现鼠标点击拖拽等等的响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class MouseFilter (QtCore.QObject): wheel = QtCore.Signal(QtCore.QEvent) moved = QtCore.Signal(QtCore.QEvent) clicked = QtCore.Signal(QtCore.QEvent) dragged = QtCore.Signal(QtCore.QEvent) released = QtCore.Signal(QtCore.QEvent) entered = QtCore.Signal() leaved = QtCore.Signal() def __init__ (self, *args, **kwargs ): super (MouseFilter, self).__init__(*args, **kwargs) self.is_clicked = False def eventFilter (self, receiver, event ): event_type = event.type () if event_type == QtCore.QEvent.MouseMove: self.moved.emit(event) if self.is_clicked: self.dragged.emit(event) elif ( event_type == QtCore.QEvent.MouseButtonPress or event_type == QtCore.QEvent.MouseButtonDblClick ): self.is_clicked = True self.clicked.emit(event) elif event_type == QtCore.QEvent.MouseButtonRelease: self.is_clicked = False self.released.emit(event) elif event_type == QtCore.QEvent.Enter: self.entered.emit() elif event_type == QtCore.QEvent.Leave: self.leaved.emit() elif event_type == QtCore.QEvent.Wheel: self.wheel.emit(event) return super (MouseFilter, self).eventFilter(receiver, event) viewport = active_view_wdg() mouse_filter = MouseFilter() viewport.installEventFilter(mouse_filter)
> 通过上面的方式就可以拦截 viewport 的 event 通过
MouseFilter
的信号槽做相应的触发。
### 绘制实现
> 参考上图可以看到,Qt API 基本上和 Maya API 绘制的效果差不多。
> Maya API 的
MUIDrawManager
提供了 mesh API 来绘制复杂图形。
> Qt API 并没有类似的方法,不过 Qt 也有
QGradient > 通过
QLinearGradient 可以实现上面的效果。
> 同样地需要对曲线进行二次采样,提高分段数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 def paintEvent (self, event ): self.draw_shape(self.create_brush_cricle(), QtCore.Qt.white, 2 ) if self.is_press_B: self.draw_shape(self.create_brush_line(), QtCore.Qt.white, 2 ) self.draw_text(self._message_info) for curve, data in self.color_data.items(): self.draw_shape(data.get("points" ), data.get("colors" ), 10 ) return super (CanvasOverlay, self).paintEvent(event) def create_brush_cricle (self, count=60 ): shape = [] radius = self.radius pt = self.start_pos if self.is_press_B else self.current_pos for index in range (count + 1 ): theta = math.radians(360 * index / count) pos_x = pt.x() + radius * math.cos(theta) pos_y = pt.y() + radius * math.sin(theta) shape.append(QtCore.QPointF(pos_x, pos_y)) return shape def create_brush_line (self ): shape = [] start_pt = self.start_pos if self.is_press_B else self.current_pos shape.append(start_pt) shape.append(QtCore.QPoint(start_pt.x(), start_pt.y() - self.strength)) return shape def draw_shape (self, line_shapes, colors, width=1 ): if len (line_shapes) < 2 : return colors = colors or QtCore.Qt.white painter = QtGui.QPainter(self) painter.setRenderHint(painter.Antialiasing) painter.begin(self) if ( isinstance (colors, Iterable) and not isinstance (colors, six.string_types) and len (colors) == len (line_shapes) ): for index, point in enumerate (line_shapes[:-1 ]): start_point = point end_point = line_shapes[index + 1 ] grandient_color = QtGui.QLinearGradient(start_point, end_point) start_color = colors[index] end_color = colors[index + 1 ] grandient_color.setColorAt(0 , start_color) grandient_color.setColorAt(1 , end_color) pen = QtGui.QPen(grandient_color, width) pen.setCapStyle(QtCore.Qt.RoundCap) pen.setJoinStyle(QtCore.Qt.RoundJoin) painter.setPen(pen) painter.drawLine(start_point, end_point) else : path = QtGui.QPainterPath() path.moveTo(line_shapes[0 ]) [path.lineTo(point) for point in line_shapes] color = QtGui.QColor(colors) pen = QtGui.QPen(color, width) painter.setPen(pen) painter.drawPath(path) painter.end() def draw_text (self, text, pos=None , color=QtCore.Qt.white, width=1 ): if not text: return painter = QtGui.QPainter(self) pen = QtGui.QPen(color, width) painter.setPen(pen) pos = pos or self.current_pos + QtCore.QPoint(10 , 0 ) painter.drawText(pos, text) painter.end()
> 上面是绘制用到的 一些 API
> 核心就是
draw_shape
里面如果传入了多个 color ,获取color每个顶点画一条渐变的线
> 多条线组合成圆形,由此有了衰变颜色的圆形曲线。
> 其他的绘制比如 绘制文字,Qt 有
drawText
API
> 绘制圆圈可以利用
sin
cos
数学函数来生成圆形的顶点进行绘制。
### 踩坑注意
>
QtCore.QPoint
和
OpenMaya.MPoint
两者的 Y 轴坐标起始不一样,所以通过 M3dView 将世界坐标转换为屏幕坐标的时候需要额外的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def world_to_view (position, invertY=True ): """ convert the given 3d position to 2d viewport coordinates """ view = OpenMayaUI.M3dView.active3dView() arg_x = OpenMaya.MScriptUtil(0 ) arg_y = OpenMaya.MScriptUtil(0 ) arg_x_ptr = arg_x.asShortPtr() arg_y_ptr = arg_y.asShortPtr() view.worldToView(position, arg_x_ptr, arg_y_ptr) x_pos = arg_x.getShort(arg_x_ptr) y_pos = arg_y.getShort(arg_y_ptr) if invertY: y_pos = view.portHeight() - y_pos return (x_pos, y_pos)
spore 参考 ## 基于 draggerContext 笔刷
Curve paint and tweak tool
最后在 highend3d 里面也找到了一个直接 tweak CV 点的方案。 这个方案采用 draggerContext 实现 draggerContext 的案例就可以实现在 viewport 拖拽的时候实现回调。 highend3d
的 ysd 曲线工具集还结合软选择的范围作为笔刷移动的范围参数,这是非常聪明的做法。 也可以通过这个方式实现拖拽生成一条曲线。 结合 OpenMaya API 可以做更多的事情,比如散布物体等等,用这个的方案比起 从零构建一个 MPxContext 要简单许多。
绘制功能还是无法通过 draggerContext 解决,不过可以用上面 Qt Overlay 方案来解决。
ysv 工具优化
从 highend3d
下载的 ysv 曲线工具可以用,但是有几个问题
直接调用 PySide 导致不兼容
没有做 Python3 兼容
部分代码在新版本代码下运行有 BUG
PySide 兼容
将 PySide 的导入转成 Qt.py 的导入 Qt.py 库的引入则是采用 submodule 的方式放到 scripts 目录下。
Python3 兼容
Python3 兼容使用 Python内置的 lib2to3 库进行转换 参考链接
1 mayapy -m lib2to3 -w F:\repo\Maya-CurveBrush\scripts\ysv\ysvView.py
通过这个方式就可以自动将所有的 print 括号加上等操作。省去繁琐的人工操作。 我当时是写了一个脚本,批量执行,执行完之后调用 black 和 isort 风格化代码。
生成完之后会将之前的文件加上 .bak
后缀,如果没有问题就可以把 bak 删除。
BUG 修复
原代码获取当前摄像机是通过下面的方式
1 2 from pymel.core import *modelEditor(getPanel(wf=1 ), e=1 , nurbsCurves=1 )
但是如果当前 focus 的 panel 不是 modelEditor 就遭殃了。
1 2 3 4 5 from pymel.core import *for mp in getPanel(type ="modelPanel" ): if modelEditor(mp, q=1 , av=1 ): modelEditor(mp, e=1 , nurbsCurves=1 ) break
所以我把代码改成上面的效果。
1 2 3 4 5 6 7 for crv in self.inViewCurves: for cv in [crv.cv[0 ], crv.cv[-1 ]]: cv = str (cv) setAttr(cv + ".xv" , lock=1 ) setAttr(cv + ".yv" , lock=1 ) setAttr(cv + ".zv" , lock=1 )
用 pymel 获取 cv 点之后,直接将 NurbsCurveCV
与字符串相加 但是 NurbsCurveCV
有自己的相加逻辑,所以这里需要加上字符串转换可以修复 BUG。
artisan 笔刷
Maya 根据贴图在模型表面散列物体 以前也写过散列物体的文章,不过实现方式是非笔刷的。 利用 artisan 就可以实现笔刷的方式散布物体了。
官方文档: Overview of MEL script painting
官方提到有 spherePaint
geometryPaint
emitterPaint
几个案例。 具体的代码可以在 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 笔刷的关于顶点色编辑相关的内容。