前言
上次我们讨论了怎么在 Maya 实现各种笔刷的姿势 Maya CurveBrush 笔刷开发 趁着最近比较有空,我又捡起了之前想要开发顶点颜色单通道笔刷, 仓库早在 1 年前就创建了,但是并没有好好开发出来。
https://github.com/FXTD-ODYSSEY/Maya-VertexColorPainter
关于单通道顶点色笔刷,其实是之前项目组给我提的需求,Maya 官方提供的 Paint Vertex Color Tool
挺好的
就是绘制的时候顶点色是混合在一起的。无法实现分通道绘制。 网上也可以找到有不少帖子抱怨 Maya 竟然没有实现这个功能的。
https://polycount.com/discussion/191918/single-channel-vertex-painting-in-maya-2018 https://www.reddit.com/r/Maya/comments/87znt2/paint_on_separate_channels_in_vertex_painting/
我当时做了一些研究,后来因为太忙了,就将需求转交给其他同事负责了。 那个同事解决了需求,只是解决方案比较复杂,需要用 OpenMaya 写一个节点,再加自定义笔刷实现。
经过我上次笔刷的折腾,我在想能否扩展原本 Maya Paint Vertex Color Tool 的功能
上面就是我最终实现的效果,在 Maya 的原生 UI 上进行修改,提供了额外的 UI 配置来进行单通道绘制。
笔刷选型
Maya CurveBrush 笔刷开发 我这篇文章已经覆盖了写笔刷的各种姿势。 用 Maya 开放的 MPxContext
写笔刷实最为自由的,但是很多功能都没有。 使用 Maya 内置的 artisan
笔刷,则已经实现了好多功能。
自带镜像
笔刷可以自定义笔刷图章实现渐变
内置序列化功能
artisan painting 扩展官方文档
所以如果不是复杂的笔刷,能用 artisan 就用 artisan 去实现。 只可惜 Maya 没有暴露 artisan 笔刷的 C++ 接口,所以如果用 C++ 开发就只能重新实现一遍 artisan
的功能,比较麻烦。 当然绘制顶点色我直接使用 artAttrPaintVertexCtx
即可。
实现原理 单通道 color set 拆分
利用 Maya 提供的 ColorSet
功能,将模型的主顶点色分拆成四个通道的 ColorSet
, 我这里就分别命名为 VertexColorR
VertexColorG
VertexColorB
VertexColorA
绘制的时候根据选择 UI 的选择激活相应的 ColorSet
。
这一步可以用 artAttrPaintVertexCtx 的 toolOnProc
和 toolOffProc
定义激活和关闭的回调。 激活 context 的时候创建 ColorSet
拆分,退出 Context 的时候删除冗余的 ColorSet
这个地方的 toolOnProc
toolOffProc
同样只接受 mel 函数,用 Python 解决的方案参考 Maya CurveBrush 笔刷开发 这篇文章。
颜色分解
那么上面拆分 ColorSet
的时候就需要将对应的 MainColorSet 的颜色按通道赋值给对应单通道的 ColorSet
.
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 import pyeml.core as pmfrom maya import OpenMayaPAINT_CTX = "artAttrColorPerVertexContext" color_set_representation = { "R" : "RGB" , "G" : "RGB" , "B" : "RGB" , "A" : "A" , } def get_color_sets (node ): color_sets = pm.polyColorSet(node, q=1 , allColorSets=1 ) return color_sets or pm.polyColorSet(node, create=1 ) def filter_color (color, index, source_color=None ): if index > 3 : return color is_color = isinstance (source_color, OpenMaya.MColor) color_list = list (source_color) if is_color else [0 , 0 , 0 , 1 ] color_list[index] = color[index] return OpenMaya.MColor(*color_list) for node in set (pm.artAttrPaintVertexCtx(PAINT_CTX, q=1 , pna=1 ).split()): node = pm.PyNode(node) node.displayColors.set (1 ) color_sets = get_color_sets(node) main_color_set = color_sets[0 ] mesh = node.__apimfn__() color_array = OpenMaya.MColorArray() mesh.getVertexColors(color_array, main_color_set) vtx_array = OpenMaya.MIntArray() for array_index in range (color_array.length()): vtx_array.append(array_index) final_colors = OpenMaya.MColorArray() for channel_index, color_channel in enumerate (cls.CHANNELS): color_set = "VertexColor{0}" .format (color_channel) if color_set not in color_sets: rpt = color_set_representation.get(color_channel) pm.polyColorSet(node, create=1 , rpt=rpt, colorSet=color_set) mesh.setCurrentColorSetName(color_set) final_colors.clear() for array_index in range (color_array.length()): full_color = color_array[array_index] color = filter_color(full_color, index=channel_index) final_colors.append(color) mesh.setVertexColors(final_colors, vtx_array) mesh.setCurrentColorSetName(main_color_set)
这里利用 pymel 提供的 __apimfn__
直接获取 MFnMesh 对象 利用 setVertexColors
API 批量设置顶点色,性能比起单点设置要好很多。
单通道 单颜色 绘制
下一步就是要实现绘制将颜色锁在对应通道上。 比如我在 UI 上设置为值绘制 R 通道的状态,绘制选择的颜色是 白色 [255,255,255],点击 Viewport 的时候会将颜色过滤成 红色 [255,0,0] ,这样勾选 R 的时候就只会刷出 红色 没有其他颜色。 这里可以监听 Viewport 的 press 和 release 触发,当点击 viewport 的时候根据 UI 勾选的通道过滤 Ctx 颜色配置。
1 2 3 4 5 6 7 8 9 10 rgb = pm.colorSliderGrp("colorPerVertexColor" , q=1 , rgb=1 ) alpha = pm.floatSliderGrp("colorPerVertexAlpha" , q=1 , value=1 ) rgb.append(alpha) sel = pm.radioButtonGrp(SINGLE_CONTROL, q=1 , sl=1 ) color = filter_color(rgb, index=sel) pm.artAttrPaintVertexCtx(PAINT_CTX, e=1 , cl4=tuple (color))
release 的时候恢复之前的 顶点色 颜色配置。
release 通道颜色同步
最后还需要实现将绘制完的通道同步到其他的 color set 上的功能。 因此 release 触发的时候要判断当前绘制的模式,如果绘制 rgb 就将颜色分解到对应的单通道上。 相反如果是单通道绘制就要将颜色反馈到 rgb 的主 color set 上。
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 def apply_color_channel (cls ): index = pm.radioButtonGrp(cls.SINGLE_CONTROL, q=1 , sl=1 ) mode = cls.OPTION_ITEMS[index + 1 ] is_rgb = mode == "RGB" for node in cls.get_paint_nodes(): dag_path = node.__apimdagpath__() mesh = OpenMaya.MFnMesh(dag_path) color_sets = cls.get_color_sets(node) main_color_set = color_sets[0 ] current_color_set = mesh.currentColorSetName() main_colors = OpenMaya.MColorArray() mesh.getVertexColors(main_colors, main_color_set) vtx_array = cls.vertex_color_data[node.fullPathName()] final_colors = OpenMaya.MColorArray() if is_rgb: for channel_index, color_channel in enumerate (cls.CHANNELS): final_colors.clear() color_set = "VertexColor{0}" .format (color_channel) mesh.setCurrentColorSetName(color_set) for vtx_index in vtx_array: main_color = main_colors[vtx_index] color = cls.filter_color(main_color, channel_index) final_colors.append(color) mesh.setVertexColors(final_colors, vtx_array) else : mode_index = cls.OPTION_ITEMS.index(mode) - 2 channel_colors = OpenMaya.MColorArray() fix_colors = OpenMaya.MColorArray() color_set = "VertexColor{0}" .format (mode) mesh.getVertexColors(channel_colors, color_set) for vtx_index in vtx_array: channel_color = channel_colors[vtx_index] main_color = main_colors[vtx_index] color = cls.filter_color(channel_color, mode_index, main_color) final_colors.append(color) fix_color = cls.filter_color(channel_color, mode_index) fix_colors.append(fix_color) mesh.setVertexColors(fix_colors, vtx_array) mesh.setCurrentColorSetName(main_color_set) mesh.setVertexColors(final_colors, vtx_array) mesh.setCurrentColorSetName(current_color_set)
通过上面的方式就可以每次绘制完之后同步顶点色到对应的 color set 上。
Maya UI 修改 & 扩展
Maya 有个非常好的设计是 UI 使用过 mel 脚本组装的,这样不需要编译就可以改动 UI,而且这部分的 mel 脚本都是开源的。 可以很清楚地知道 Maya 是如何组装出相应工具的界面。
C:\Program Files\Autodesk\Maya2018\scripts\others\artAttrColorPerVertexProperties.mel
Maya 的颜色笔刷是通过上面路径的 mel 脚本实现的。 这样可以找到对应 UI 的名字
如上图所示,可以找到 artAttrColorChannelChoices
的名字。 然后用 cmds 命令可以对这些 UI 进行二次修改。
1 2 from maya import cmdscmds.radioButtonGrp('artAttrColorChannelChoices' ,e=1 ,gbc=[255 ,0 ,0 ])
比如执行上面的代码可以修改相应 UI 的背景颜色。
上面已经展示了如何修改原生的 UI 这些操作需要学习 Mel 的 UI 构建方式,会有点复杂。
不过 Mel 的 example 都有案例,比如这里的UI 使用了 columnLayout 那我可以去到 columnLayout 的文档运行案例代码进行学习。
将代码放到代码编辑器执行。
查了一下 columnLayout 的 API ,发现它竟然没有 insert 功能。 于是我找了一下 Mel Tips大全的网站 MEL How-To (上古网站,但对学习Mel很有帮助)
可以找到一个 链接 如何实现UI的置顶插入。
方案一使用 frameLayout 比较繁琐 方案二则是使用一个新的 Layout 然后将旧 Layout 的 UI 删除掉。 这个方法删除 UI 对我想要的效果并不适用。
不过倒是启发了我,我想到了可以利用 childArray 可以拿到 Layout 下所有的 Control 名字。 然后对每个 Control 修改 parent 到新的 Layout 上。
使用 cmds 嵌入 UI 1 2 3 4 5 6 7 8 9 10 from maya import cmdsparent = cmds.radioButtonGrp('artAttrColorChannelChoices' ,q=1 ,parent=1 ) print (parent)window = cmds.window() column_layout = cmds.columnLayout() for control in cmds.layout(parent,q=1 ,childArray=1 ): cmds.control(control, e=1 , p=column_layout) cmds.showWindow(window)
上面的想法写成代码如上所示
直接实现了 UI 的乾坤大挪移 只是显示上有些不一样,主要原因是 mel 构建 UI 的时候使用 setUITemplate
1 2 3 4 5 6 7 8 9 10 11 from maya import cmdswindow = cmds.window() cmds.setUITemplate("OptionsTemplate" , pushTemplate=1 ) column_layout = cmds.columnLayout() parent = cmds.radioButtonGrp('artAttrColorChannelChoices' ,q=1 ,parent=1 ) for control in cmds.layout(parent,q=1 ,childArray=1 ): cmds.control(control, e=1 , p=column_layout) cmds.setUITemplate(popTemplate=1 ) cmds.showWindow(window)
加上了 OptionsTemplate
之后 UI 的显示就保持一致了 所以在 parent control 的过程中加入自己的 UI ,就可以实现对应位置的嵌入效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 from maya import cmdswindow = cmds.window() cmds.setUITemplate("OptionsTemplate" , pushTemplate=1 ) column_layout = cmds.columnLayout() parent = cmds.radioButtonGrp('artAttrColorChannelChoices' ,q=1 ,parent=1 ) for control in cmds.layout(parent,q=1 ,childArray=1 ): cmds.control(control, e=1 , p=column_layout) if control == "artAttrColorChannelChoices" : cmds.button(label="click me" ) cmds.setUITemplate(popTemplate=1 ) cmds.showWindow(window)
比如上面的效果,如此就可以在相应的位置嵌入任意的 UI
最后是怎么将 UI 嵌入到原本的位置,关键就是使用 setParent 命令
1 2 3 4 5 6 7 8 9 10 11 12 from maya import cmdsparent = cmds.radioButtonGrp('artAttrColorChannelChoices' ,q=1 ,parent=1 ) cmds.setParent(parent) cmds.setUITemplate("OptionsTemplate" , pushTemplate=1 ) column_layout = cmds.columnLayout() for control in cmds.layout(parent,q=1 ,childArray=1 ): cmds.control(control, e=1 , p=column_layout) if control == "artAttrColorChannelChoices" : cmds.button(label="click me" ) cmds.setUITemplate(popTemplate=1 )
如此就可以了, setParent 会将当前 UI 创建设置到之前的 Layout 下。
使用 Qt 嵌入 UI
既然 cmds 可以实现 UI 嵌入,那能否利用 Qt API 来实现这个效果呢?
我也想过将 Layout 转成 Qt Object 的方式进行调用。 但这个方式获取到的是 QLayout 无法使用 insertWidget
插入,倒是可以使用 addWidget
1 2 3 4 5 6 7 8 9 10 import pymel.core as pmfrom Qt import QtWidgetswidget = pm.uitypes.toQtObject("artAttrColorChannelChoices" ) parent = widget.parent() print (parent.objectName())layout = parent.layout() print (layout)layout.addWidget(QtWidgets.QPushButton("asd" ))
利用上面的方式就可以在 Layout 的最末端添加一个按钮。 上面使用了 toQtObject 的 pymel API 背后调用 OpenMayaUI 库通过 objectName 查找到对应的 Qt 组件,然后 wrapInstance 将组件转换为 QObject 类型。 用 pymel 的方式比较方便。
要实现 insert 的效果可以利用 takeAt
API 将 widget 提取出来再放回去。
1 2 3 4 5 6 7 8 9 10 import pymel.core as pmfrom Qt import QtWidgetswidget = pm.uitypes.toQtObject("artAttrColorChannelChoices" ) parent = widget.parent() layout = parent.layout() index = layout.indexOf(widget) widget_list = [layout.takeAt(0 ).widget() for _ in range (layout.count())] widget_list.insert(index,QtWidgets.QPushButton("click me" )) for widget in widget_list: layout.addWidget(widget)
如上所示,也完全实现了 cmds 库一样的效果,使用 Qt API 就比 cmds 要灵活很多。 可以嵌入 Designer 生成的 Widget 等等。
这里只是展望了一下,我的实现还是基于 cmds 的方式。
总结
我的工具已经做成了 Maya 插件,启用按照插件的方式加载即可。